mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
272 lines
9.0 KiB
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)
|
|
}
|
|
})
|
|
}
|
|
}
|