fix(opencode): avoid sync panic on model metadata
This commit is contained in:
parent
14061b020f
commit
887eb14eca
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user