Files
multica/server/pkg/agent/opencode_models_test.go

272 lines
9.0 KiB
Go

package agent
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
)
func TestDiscoverOpenCodeV2Models(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX shell fixture")
}
// Schema-shaped fixture for OpenCode 2.0.32 (843a6c514e60):
// packages/schema/src/model.ts and Location.response in location.ts.
fixture, err := os.ReadFile("testdata/opencode_v2_models.json")
if err != nil {
t.Fatal(err)
}
fake := filepath.Join(t.TempDir(), "opencode")
writeTestExecutable(t, fake, []byte(fmt.Sprintf(`#!/bin/sh
case "$1" in
--version) echo 'opencode v2.0.32' ;;
api) cat <<'JSON'
%s
JSON
;;
models)
if [ "$2" = "--verbose" ]; then
echo 'Unrecognized flag: --verbose in command opencode models' >&2
exit 1
fi
printf 'opencode-go/deepseek-v4.1-flash\nopencode-go/plain\n'
;;
*) exit 1 ;;
esac
`, fixture)))
models, err := discoverOpenCodeModels(context.Background(), Command{Path: fake})
if err != nil {
t.Fatal(err)
}
if len(models) != 2 {
t.Fatalf("models = %+v", models)
}
if models[0].Thinking == nil {
t.Fatal("structured discovery lost reasoning variants")
}
if models[1].Thinking != nil {
t.Fatalf("plain model acquired thinking: %+v", models[1])
}
}
func TestParseOpenCodeV2Models(t *testing.T) {
fixture, err := os.ReadFile("testdata/opencode_v2_models.json")
if err != nil {
t.Fatal(err)
}
models := parseOpenCodeV2Models(fixture)
want := []Model{
{ID: "opencode-go/deepseek-v4.1-flash", Label: "opencode-go/deepseek-v4.1-flash", Provider: "opencode-go",
Thinking: &ModelThinking{SupportedLevels: []ThinkingLevel{
{Value: "low", Label: "Low"}, {Value: "high", Label: "High"}, {Value: "fast-mode", Label: "Fast Mode"},
}}},
{ID: "opencode-go/plain", Label: "opencode-go/plain", Provider: "opencode-go"},
}
if !reflect.DeepEqual(models, want) {
t.Fatalf("models = %#v, want %#v", models, want)
}
// No default is advertised by the API. Preserve runtime-native variant IDs
// through the daemon/UI wire format and the existing v2 argument helper.
raw, err := json.Marshal(models)
if err != nil {
t.Fatal(err)
}
var roundTrip []Model
if err := json.Unmarshal(raw, &roundTrip); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(roundTrip, want) {
t.Fatalf("catalog wire round trip = %+v", roundTrip)
}
for _, level := range roundTrip[0].Thinking.SupportedLevels {
if !IsKnownThinkingValue("opencode", level.Value) {
t.Fatalf("API would reject variant %q for persistence", level.Value)
}
valid, err := ValidateThinkingLevelWith(func() (Catalog, error) {
return Catalog{Models: roundTrip}, nil
}, "opencode", roundTrip[0].ID, level.Value)
if err != nil || !valid {
t.Fatalf("execution guard dropped variant %q: valid=%v, err=%v", level.Value, valid, err)
}
arg, ok := opencodeModelArg(roundTrip[0].ID, level.Value)
if !ok || arg != roundTrip[0].ID+"#"+level.Value {
t.Fatalf("variant %q did not reach model argument: %q", level.Value, arg)
}
}
}
func TestParseOpenCodeV2ModelsInvalidAndEmpty(t *testing.T) {
for _, input := range []string{
``, `not json`, `{"data":`, `[]`, `{}`, `null`,
`{"data":null}`, `{"data":[]}`, `{"error":"unavailable"}`,
`{"data":[{"id":"missing-provider"},{"providerID":"missing-model"}]}`,
`{"data":[{"id":"m","providerID":"p","variants":{"high":{}}}]}`,
} {
t.Run(input, func(t *testing.T) {
if got := parseOpenCodeV2Models([]byte(input)); len(got) != 0 {
t.Fatalf("invalid/empty response produced models: %+v", got)
}
})
}
models := parseOpenCodeV2Models([]byte(`{"data":[
{"providerID":"p","id":"m","variants":[{"id":""},{"id":"custom"},{"id":"custom"}]},
{"providerID":"p","id":"m","variants":[]},
{"providerID":"p","id":"plain","variants":[{"id":""}]}
]}`))
if len(models) != 2 || models[0].Thinking == nil ||
!reflect.DeepEqual(models[0].Thinking.SupportedLevels, []ThinkingLevel{{Value: "custom", Label: "Custom"}}) ||
models[1].Thinking != nil {
t.Fatalf("duplicate/empty variants: %+v", models)
}
}
func TestDiscoverOpenCodeModelContractsAndFallback(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX shell fixture")
}
for _, tc := range []struct {
name, version, api, apiExit, verbose, wantCalls string
wantThinking bool
}{
{"v1 metadata", "1.18.31", "", "1", "valid", "--version\nmodels --verbose\n", true},
{"unknown version keeps v1", "nightly", "", "1", "valid", "--version\nmodels --verbose\n", true},
{"version probe fails keeps v1", "", "", "1", "valid", "--version\nmodels --verbose\n", true},
{"v1 partial output", "1.18.31", "", "1", "partial", "--version\nmodels --verbose\n", true},
{"v2 malformed", "opencode v2.0.32", "{broken", "0", "", "--version\napi\nmodels\n", false},
{"v2 empty", "opencode v2.0.32", `{"location":{"directory":"/tmp"},"data":[]}`, "0", "", "--version\napi\nmodels\n", false},
{"v2 failure", "opencode v2.0.32", `{"data":[{"providerID":"p","id":"m","variants":[{"id":"high"}]}]}`, "1", "", "--version\napi\nmodels\n", false},
} {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
trace := filepath.Join(dir, "trace")
t.Setenv("OPENCODE_TEST_TRACE", trace)
fake := filepath.Join(dir, "opencode")
writeTestExecutable(t, fake, []byte(fmt.Sprintf(`#!/bin/sh
case "$1" in
--version)
echo '--version' >> "$OPENCODE_TEST_TRACE"
version='%s'
[ -n "$version" ] || exit 1
echo "$version"
;;
api)
echo api >> "$OPENCODE_TEST_TRACE"
echo '%s'
exit %s
;;
models)
echo "$*" >> "$OPENCODE_TEST_TRACE"
if [ "$2" = "--verbose" ]; then
[ '%s' = '' ] && exit 1
printf 'p/m\n{"reasoning":true,"variants":{"high":{}}}\n'
[ '%s' = partial ] && exit 1
else
echo p/m
fi
exit 0
;;
*) exit 1 ;;
esac
`, tc.version, tc.api, tc.apiExit, tc.verbose, tc.verbose)))
models, err := discoverOpenCodeModels(context.Background(), Command{Path: fake})
if err != nil || len(models) != 1 || models[0].ID != "p/m" ||
(models[0].Thinking != nil) != tc.wantThinking {
t.Fatalf("models = %+v, err = %v", models, err)
}
raw, err := os.ReadFile(trace)
if err != nil || string(raw) != tc.wantCalls {
t.Fatalf("calls = %q, want %q (err=%v)", raw, tc.wantCalls, err)
}
})
}
}
func TestDiscoverOpenCodeV2ModelsPreservesProfileAndLocation(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX shell fixture")
}
for _, connection := range [][]string{nil, {"--server", "http://localhost:4567"}, {"--server=http://localhost:4567"}, {"--standalone"}} {
t.Run(strings.Join(connection, " "), func(t *testing.T) {
dir := t.TempDir()
trace := filepath.Join(dir, "trace")
t.Setenv("OPENCODE_TEST_TRACE", trace)
t.Setenv("OPENCODE_TEST_PROFILE", "profile-environment")
fake := filepath.Join(dir, "wrapper")
writeTestExecutable(t, fake, []byte(`#!/bin/sh
[ "$1" = profile ] && [ "$2" = test ] || exit 1
shift 2
case "$1" in
--server) shift 2 ;;
--server=*|--standalone) shift ;;
esac
[ "$OPENCODE_TEST_PROFILE" = profile-environment ] || exit 1
if [ "$1" = "--version" ]; then echo 'opencode v2.0.32'; exit 0; fi
[ "$1" = api ] || exit 1
printf '%s\n' "$@" > "$OPENCODE_TEST_TRACE"
echo '{"location":{"directory":"/unused"},"data":[{"providerID":"p","id":"m","variants":[{"id":"high"}]}]}'
`))
prefix := append([]string{"profile", "test"}, connection...)
models, err := discoverOpenCodeModels(context.Background(), NewCommand(fake, prefix))
if err != nil || len(models) != 1 || models[0].Thinking == nil {
t.Fatalf("profile discovery = %+v, %v", models, err)
}
raw, err := os.ReadFile(trace)
if err != nil {
t.Fatal(err)
}
args := strings.Split(strings.TrimSpace(string(raw)), "\n")
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
want := []string{"api", "GET", "/api/model?" + url.Values{"location[directory]": {cwd}}.Encode()}
if len(connection) == 0 {
want = append(want, "--standalone")
}
if !reflect.DeepEqual(args, want) {
t.Fatalf("API args = %q, want %q", args, want)
}
if !reflect.DeepEqual(prefix, append([]string{"profile", "test"}, connection...)) {
t.Fatal("discovery mutated runtime prefix")
}
})
}
}
func TestDiscoverOpenCodeV2ModelsDeadline(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("POSIX shell fixture")
}
for _, hangAt := range []string{"version", "api"} {
t.Run(hangAt, func(t *testing.T) {
fake := filepath.Join(t.TempDir(), "opencode")
writeTestExecutable(t, fake, []byte(fmt.Sprintf(`#!/bin/sh
if [ "$1" = "--version" ]; then
[ '%s' = version ] && exec sleep 30
echo 'opencode v2.0.32'
exit 0
fi
exec sleep 30
`, hangAt)))
ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
defer cancel()
start := time.Now()
models, err := discoverOpenCodeModels(ctx, Command{Path: fake})
if err != nil || len(models) != 0 {
t.Fatalf("timed-out discovery = %+v, %v", models, err)
}
if elapsed := time.Since(start); elapsed > 3*time.Second {
t.Fatalf("discovery exceeded caller deadline: %v", elapsed)
}
})
}
}