fix(opencode): avoid sync panic on model metadata

This commit is contained in:
apale7 2026-04-17 18:03:57 +08:00
parent 14061b020f
commit 887eb14eca
3 changed files with 94 additions and 21 deletions

View File

@ -1,6 +1,7 @@
package cli package cli
import ( import (
"bytes"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -165,3 +166,57 @@ func TestProviderEnableDisableCommands(t *testing.T) {
t.Fatalf("provider after enable = %#v, want disabled=false", provider) t.Fatalf("provider after enable = %#v, want disabled=false", provider)
} }
} }
func TestOpencodeSyncDoesNotPanicOnSliceModelMetadata(t *testing.T) {
t.Setenv("OLPX_CONFIG", filepath.Join(t.TempDir(), "olpx.json"))
configPath = ""
cfg, err := loadCfg()
if err != nil {
t.Fatalf("loadCfg: %v", err)
}
cfg.UpsertProvider(config.Provider{
ID: "p1",
BaseURL: "https://example.com/v1",
})
cfg.UpsertAlias(config.Alias{
Alias: "gpt-5.4",
Enabled: true,
Targets: []config.Target{{Provider: "p1", Model: "up-1", Enabled: true}},
})
if err := cfg.Save(); err != nil {
t.Fatalf("save config: %v", err)
}
target := filepath.Join(t.TempDir(), "opencode.jsonc")
seed := []byte("{\n \"$schema\": \"https://opencode.ai/config.json\",\n \"provider\": {\n \"olpx\": {\n \"npm\": \"@ai-sdk/openai\",\n \"name\": \"OpenCode LocalProxy CLI\",\n \"options\": {\n \"baseURL\": \"http://127.0.0.1:9982/v1\",\n \"apiKey\": \"olpx-local\",\n \"setCacheKey\": true\n },\n \"models\": {\n \"gpt-5.4\": {\n \"name\": \"custom-display-name\",\n \"tags\": [\"reasoning\", \"priority\"],\n \"variants\": [\n {\"name\": \"high\", \"effort\": \"high\"}\n ]\n }\n }\n }\n }\n}\n")
if err := os.WriteFile(target, seed, 0o600); err != nil {
t.Fatalf("write target config: %v", err)
}
cmd := newOpencodeSyncCmd()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(&stdout)
cmd.SetArgs([]string{"--target", target})
defer func() {
if r := recover(); r != nil {
t.Fatalf("opencode sync panicked with slice metadata: %v", r)
}
}()
if err := cmd.Execute(); err != nil {
t.Fatalf("execute opencode sync: %v", err)
}
if got := stdout.String(); got != "✓ no changes required at "+target+"\n" {
t.Fatalf("stdout = %q", got)
}
data, err := os.ReadFile(target)
if err != nil {
t.Fatalf("read target config: %v", err)
}
if !bytes.Equal(data, seed) {
t.Fatalf("sync rewrote unchanged config:\n%s", string(data))
}
}

View File

@ -10,6 +10,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect"
"sort" "sort"
"github.com/tidwall/jsonc" "github.com/tidwall/jsonc"
@ -601,25 +602,5 @@ func ImportCustomProviders(raw Raw) []ImportableProvider {
// mapsEqualShallow compares two string-keyed maps for structural equality. // mapsEqualShallow compares two string-keyed maps for structural equality.
func mapsEqualShallow(a, b map[string]any) bool { func mapsEqualShallow(a, b map[string]any) bool {
if len(a) != len(b) { return reflect.DeepEqual(a, b)
return false
}
for k, v := range a {
bv, ok := b[k]
if !ok {
return false
}
am, amOk := v.(map[string]any)
bm, bmOk := bv.(map[string]any)
if amOk && bmOk {
if !mapsEqualShallow(am, bm) {
return false
}
continue
}
if v != bv {
return false
}
}
return true
} }

View File

@ -142,6 +142,43 @@ func TestEnsureOLPXProviderPreservesExistingModelMetadata(t *testing.T) {
} }
} }
func TestEnsureOLPXProviderDoesNotPanicOnComparableMetadata(t *testing.T) {
raw := Raw{
"$schema": "https://opencode.ai/config.json",
"provider": map[string]any{
"olpx": map[string]any{
"npm": "@ai-sdk/openai",
"name": "OpenCode LocalProxy CLI",
"options": map[string]any{
"baseURL": "http://127.0.0.1:9982/v1",
"apiKey": "olpx-local",
"setCacheKey": true,
},
"models": map[string]any{
"gpt-5.4": map[string]any{
"name": "custom-display-name",
"tags": []any{"reasoning", "priority"},
"variants": []any{
map[string]any{"name": "high", "effort": "high"},
},
},
},
},
},
}
defer func() {
if r := recover(); r != nil {
t.Fatalf("EnsureOLPXProvider() panicked with slice metadata: %v", r)
}
}()
changed := EnsureOLPXProvider(raw, "http://127.0.0.1:9982/v1", "olpx-local", []string{"gpt-5.4"})
if changed {
t.Fatal("EnsureOLPXProvider() reported change for unchanged alias metadata with slices")
}
}
func TestValidateOLPXProviderAllowsCustomModelMetadata(t *testing.T) { func TestValidateOLPXProviderAllowsCustomModelMetadata(t *testing.T) {
raw := Raw{ raw := Raw{
"provider": map[string]any{ "provider": map[string]any{