返回 DeepSeek-Reasonix
extension_models_test.go
根目录 / desktop / extension_models_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "strings"
7 "testing"
8
9 "reasonix/internal/agent"
10 "reasonix/internal/boot"
11 "reasonix/internal/control"
12 "reasonix/internal/event"
13 "reasonix/internal/provider"
14 )
15
16 // extensionCatalogCtrl stubs only what the model discovery/switch paths read
17 // from a tab controller; everything else falls through the embedded nil
18 // SessionAPI and must not be called.
19 type extensionCatalogCtrl struct {
20 control.SessionAPI
21 catalog []provider.Descriptor
22 }
23
24 func (c *extensionCatalogCtrl) ProviderCatalog() []provider.Descriptor { return c.catalog }
25
26 // TestModelsForTabIncludesExtensionProviders: the desktop model switcher
27 // merges the tab controller's extension catalog into the config-backed list —
28 // plugin refs arrive fully namespaced, duplicates collapse, and the active
29 // plugin ref is marked current.
30 func TestModelsForTabIncludesExtensionProviders(t *testing.T) {
31 reloadRuntimeFixture(t) // one configured provider "old" with model old-model
32
33 app := NewApp()
34 tab := &WorkspaceTab{
35 ID: "tab_a",
36 Scope: "global",
37 Ready: true,
38 model: "plugin/demo/fake/x",
39 Ctrl: &extensionCatalogCtrl{catalog: []provider.Descriptor{
40 {Ref: "plugin/demo/fake/x", Model: "x"},
41 {Ref: "plugin/demo/fake/y", Model: "y"},
42 {Ref: "old/old-model"}, // claim-replaced duplicate must not repeat
43 }},
44 disabledMCP: map[string]ServerView{},
45 }
46 app.tabs = map[string]*WorkspaceTab{tab.ID: tab}
47 app.tabOrder = []string{tab.ID}
48 app.activeTabID = tab.ID
49
50 models := app.ModelsForTab(tab.ID)
51 var pluginX, pluginY, oldCount *ModelInfo
52 oldSeen := 0
53 for i := range models {
54 switch models[i].Ref {
55 case "plugin/demo/fake/x":
56 pluginX = &models[i]
57 case "plugin/demo/fake/y":
58 pluginY = &models[i]
59 case "old/old-model":
60 oldSeen++
61 oldCount = &models[i]
62 }
63 }
64 if pluginX == nil || pluginY == nil {
65 t.Fatalf("ModelsForTab = %+v, want both plugin refs", models)
66 }
67 if pluginX.Provider != "plugin/demo" || pluginX.Model != "fake/x" {
68 t.Fatalf("plugin ref display = %+v, want provider plugin/demo model fake/x", pluginX)
69 }
70 if !pluginX.Current {
71 t.Fatalf("active plugin ref not marked current: %+v", pluginX)
72 }
73 if oldSeen != 1 || oldCount == nil {
74 t.Fatalf("config ref repeated %d times, want exactly one", oldSeen)
75 }
76
77 // A tab whose controller has no extension catalog (nil → no sidecar
78 // declared providers) enumerates config only, as before.
79 tab.Ctrl = &extensionCatalogCtrl{}
80 for _, info := range app.ModelsForTab(tab.ID) {
81 if strings.HasPrefix(info.Ref, "plugin/") {
82 t.Fatalf("empty catalog produced plugin ref %+v", info)
83 }
84 }
85 }
86
87 // TestSetModelForTabPluginRefReachesBoot: with a controller whose merged
88 // catalog declares the plugin ref, SetModelForTab's config gate accepts the
89 // ref and hands it to boot — which is the resolver of record (here it fails
90 // only because no sidecar is installed in this test's REASONIX_HOME). A
91 // non-plugin unknown ref must still fail at the desktop gate, before boot.
92 func TestSetModelForTabPluginRefReachesBoot(t *testing.T) {
93 dir := reloadRuntimeFixture(t)
94
95 oldExec := agent.New(nil, nil, agent.NewSession("old system prompt"), agent.Options{}, event.Discard)
96 app := NewApp()
97 app.ctx = context.Background()
98 app.readyHook = func() {}
99 oldCtrl := control.New(control.Options{
100 Executor: oldExec,
101 SessionDir: dir,
102 Label: "old",
103 Sink: event.Discard,
104 // The merged catalog this controller would expose with the demo
105 // sidecar live: plugin/demo/fake/x is a valid switch target.
106 ProviderResolver: &provider.StaticResolver{Descriptors: []provider.Descriptor{
107 {Ref: "plugin/demo/fake/x", Model: "x"},
108 }},
109 })
110 reloadRuntimeTab(t, app, dir, oldCtrl)
111
112 err := app.SetModelForTab("tab_a", "plugin/demo/fake/x")
113 if err == nil {
114 t.Fatal("SetModelForTab succeeded without an installed sidecar; expected boot to gate the plugin ref")
115 }
116 if !errors.Is(err, boot.ErrUnknownModel) {
117 t.Fatalf("SetModelForTab error = %v, want boot.ErrUnknownModel (the desktop gate must pass plugin refs through to boot)", err)
118 }
119
120 // The desktop config gate still rejects unknown NON-plugin refs itself.
121 err = app.SetModelForTab("tab_a", "ghost/never-existed")
122 if err == nil || !strings.Contains(err.Error(), `unknown model "ghost/never-existed"`) {
123 t.Fatalf("SetModelForTab non-plugin error = %v, want the desktop unknown-model gate", err)
124 }
125 if errors.Is(err, boot.ErrUnknownModel) {
126 t.Fatalf("non-plugin unknown ref reached boot: %v", err)
127 }
128 }
129
129 lines GO