返回 DeepSeek-Reasonix
runtime_plan_test.go
根目录 / internal / boot / runtime_plan_test.go
1 package boot
2
3 import (
4 "context"
5 "testing"
6
7 "reasonix/internal/extension"
8 "reasonix/internal/extension/protocol"
9 "reasonix/internal/extension/sidecar"
10 "reasonix/internal/extensioncontract"
11 "reasonix/internal/provider"
12 )
13
14 func TestRebuildPlanNoOpCacheGuard(t *testing.T) {
15 isolateConfigHome(t)
16 oldRes, err := BuildRuntime(context.Background(), Options{})
17 if err != nil {
18 t.Fatalf("BuildRuntime: %v", err)
19 }
20 t.Cleanup(func() {
21 if oldRes.Controller != nil {
22 oldRes.Controller.Close()
23 }
24 })
25 res, err := RebuildFrom(context.Background(), oldRes, Options{})
26 if err != nil {
27 t.Fatalf("RebuildFrom: %v", err)
28 }
29 t.Cleanup(func() {
30 if res.Controller != nil {
31 res.Controller.Close()
32 }
33 })
34 if res.Plan == nil {
35 t.Fatal("expected plan")
36 }
37 if res.Plan.IsNoOp() && res.Plan.PrefixChanged {
38 t.Fatal("no-op plan marked PrefixChanged")
39 }
40 if oldRes.Snapshot != nil && res.Snapshot != nil && res.Plan.IsNoOp() {
41 if oldRes.Snapshot.CacheHash() != res.Snapshot.CacheHash() {
42 t.Fatalf("no-op rebuild changed CacheHash: %s -> %s", oldRes.Snapshot.CacheHash(), res.Snapshot.CacheHash())
43 }
44 }
45 }
46
47 func TestAttachPlanAndStatusObservesActualPrefixHash(t *testing.T) {
48 empty := buildPlanGraph(t, nil)
49 uiFrom := buildPlanGraph(t, []extension.ComponentDescriptor{{
50 ID: "plugin/ui", Provides: []extensioncontract.Capability{planCapability("plugin/ui", "ui", "panel", "1.0.0", "sha256:a")},
51 }})
52 uiTo := buildPlanGraph(t, []extension.ComponentDescriptor{{
53 ID: "plugin/ui", Provides: []extensioncontract.Capability{planCapability("plugin/ui", "ui", "panel", "1.0.1", "sha256:b")},
54 }})
55 providerFrom := buildPlanGraph(t, []extension.ComponentDescriptor{{
56 ID: "plugin/provider", Provides: []extensioncontract.Capability{planCapability("plugin/provider", "provider", "main", "1.0.0", "sha256:a")},
57 }})
58 providerTo := buildPlanGraph(t, []extension.ComponentDescriptor{{
59 ID: "plugin/provider", Provides: []extensioncontract.Capability{planCapability("plugin/provider", "provider", "main", "1.0.1", "sha256:b")},
60 }})
61 mcpFrom := buildPlanGraph(t, []extension.ComponentDescriptor{{
62 ID: "plugin/mcp",
63 Source: extension.ContributionSource{Scope: extension.ScopePlugin, PluginID: "mcp", Version: "1.0.0"},
64 Provides: []extensioncontract.Capability{planCapability("plugin/mcp", "mcp", "server", "1.0.0", "sha256:stable")},
65 }})
66 mcpTo := buildPlanGraph(t, []extension.ComponentDescriptor{{
67 ID: "plugin/mcp",
68 Source: extension.ContributionSource{Scope: extension.ScopePlugin, PluginID: "mcp", Version: "1.0.1"},
69 Provides: []extensioncontract.Capability{planCapability("plugin/mcp", "mcp", "server", "1.0.0", "sha256:stable")},
70 }})
71 stable := buildPlanSnapshot(t, "system-a", provider.ToolSchema{Name: "bash", Description: "stable"})
72 changedPrompt := buildPlanSnapshot(t, "system-b", provider.ToolSchema{Name: "bash", Description: "stable"})
73 changedTool := buildPlanSnapshot(t, "system-a", provider.ToolSchema{Name: "bash", Description: "changed"})
74
75 tests := []struct {
76 name string
77 from *extension.DependencyGraph
78 to *extension.DependencyGraph
79 old *extension.RuntimeSnapshot
80 neo *extension.RuntimeSnapshot
81 wantPrefix bool
82 wantProvider bool
83 }{
84 {name: "no-op", from: empty, to: empty, old: stable, neo: stable.WithGeneration(2)},
85 {name: "UI-only", from: uiFrom, to: uiTo, old: stable, neo: stable.WithGeneration(2)},
86 {name: "provider-only", from: providerFrom, to: providerTo, old: stable, neo: stable.WithGeneration(2), wantProvider: true},
87 {name: "MCP backend only", from: mcpFrom, to: mcpTo, old: stable, neo: stable.WithGeneration(2)},
88 {name: "system prompt", from: empty, to: empty, old: stable, neo: changedPrompt, wantPrefix: true},
89 {name: "tool schema", from: empty, to: empty, old: stable, neo: changedTool, wantPrefix: true},
90 }
91 for _, tc := range tests {
92 t.Run(tc.name, func(t *testing.T) {
93 res := &BuildResult{Snapshot: tc.neo}
94 attachPlanAndStatus(res, tc.from, tc.to, 1, tc.old)
95 if res.Plan == nil || res.Plan.PrefixChanged != tc.wantPrefix || res.Plan.ProviderChanged != tc.wantProvider {
96 t.Fatalf("plan = %+v, want PrefixChanged=%v ProviderChanged=%v", res.Plan, tc.wantPrefix, tc.wantProvider)
97 }
98 if res.Status == nil || res.Status.Plan == nil || res.Status.Plan.PrefixChanged != tc.wantPrefix || res.Status.Plan.ProviderChanged != tc.wantProvider {
99 t.Fatalf("status plan = %+v, want PrefixChanged=%v ProviderChanged=%v", res.Status, tc.wantPrefix, tc.wantProvider)
100 }
101 })
102 }
103 }
104
105 func buildPlanGraph(t *testing.T, components []extension.ComponentDescriptor) *extension.DependencyGraph {
106 t.Helper()
107 graph, err := extension.BuildDependencyGraph(components)
108 if err != nil {
109 t.Fatal(err)
110 }
111 return graph
112 }
113
114 func planCapability(namespace, kind, id, version, schemaHash string) extensioncontract.Capability {
115 return extensioncontract.Capability{
116 Key: extensioncontract.CapabilityKey{Namespace: namespace, Kind: kind, ID: id},
117 Version: version, SchemaHash: schemaHash,
118 }
119 }
120
121 func buildPlanSnapshot(t *testing.T, prompt string, schema provider.ToolSchema) *extension.RuntimeSnapshot {
122 t.Helper()
123 builder := extension.NewBuilder().WithSystemPrompt(prompt).WithGeneration(2)
124 builder.AddContributor(extension.ContributorFunc{
125 ContributorName: "plan-test-tool",
126 Fn: func(context.Context) ([]extension.Contribution, error) {
127 return []extension.Contribution{{
128 Kind: extension.KindTool,
129 ID: schema.Name,
130 Source: extension.ContributionSource{Scope: extension.ScopeBuiltin, Origin: "plan-test"},
131 Payload: schema,
132 }}, nil
133 },
134 })
135 snapshot, runtimeSet, err := builder.Build(context.Background())
136 if err != nil {
137 t.Fatal(err)
138 }
139 if runtimeSet != nil {
140 t.Cleanup(func() { _ = runtimeSet.Close() })
141 }
142 return snapshot
143 }
144
145 func TestStartPackagesWithPlanEmptyHome(t *testing.T) {
146 plan := &extension.RuntimePlan{
147 Unchanged: []extension.ComponentID{"plugin/demo"},
148 }
149 session := protocol.SessionContext{SessionID: "s", WorkspaceRoot: t.TempDir(), Generation: 1}
150 m, warnings, err := sidecar.StartPackagesWithPlan(context.Background(), t.TempDir(), session, nil, nil, plan)
151 if err != nil {
152 t.Fatal(err)
153 }
154 if len(warnings) != 0 {
155 t.Fatalf("warnings = %v", warnings)
156 }
157 if m == nil {
158 t.Fatal("nil manager")
159 }
160 if len(m.Clients()) != 0 {
161 t.Fatalf("clients = %d, want 0", len(m.Clients()))
162 }
163 _ = m.Close()
164 }
165
165 lines GO