返回 DeepSeek-Reasonix
integration_plan_test.go
根目录 / internal / boot / integration_plan_test.go
1 package boot
2
3 import (
4 "context"
5 "strings"
6 "testing"
7
8 "reasonix/internal/extension"
9 "reasonix/internal/extensioncontract"
10 )
11
12 func TestRebuildFromNoOpPreservesCacheHash(t *testing.T) {
13 isolateConfigHome(t)
14 oldRes, err := BuildRuntime(context.Background(), Options{})
15 if err != nil {
16 t.Fatalf("BuildRuntime: %v", err)
17 }
18 t.Cleanup(func() {
19 if oldRes.Controller != nil {
20 oldRes.Controller.Close()
21 }
22 })
23 if oldRes.Assembly == nil {
24 t.Fatal("expected Assembly retained for reuse")
25 }
26 res, err := RebuildFrom(context.Background(), oldRes, Options{})
27 if err != nil {
28 t.Fatalf("RebuildFrom: %v", err)
29 }
30 t.Cleanup(func() {
31 if res.Controller != nil {
32 res.Controller.Close()
33 }
34 })
35 if oldRes.Snapshot != nil && res.Snapshot != nil {
36 if oldRes.Snapshot.CacheHash() != res.Snapshot.CacheHash() {
37 t.Fatalf("cache hash churned on no-op: %s -> %s", oldRes.Snapshot.CacheHash(), res.Snapshot.CacheHash())
38 }
39 }
40 if res.Plan != nil && res.Plan.PrefixChanged {
41 t.Fatal("no-op plan should not set PrefixChanged")
42 }
43 }
44
45 func TestPublishGateAdmissionAfterRebuild(t *testing.T) {
46 isolateConfigHome(t)
47 oldRes, err := BuildRuntime(context.Background(), Options{})
48 if err != nil {
49 t.Fatal(err)
50 }
51 t.Cleanup(func() {
52 if oldRes.Controller != nil {
53 oldRes.Controller.Close()
54 }
55 })
56 oldGen := uint64(0)
57 if oldRes.Snapshot != nil {
58 oldGen = oldRes.Snapshot.Generation()
59 oldRes.Controller.SetRuntimeGeneration(oldGen)
60 }
61 res, err := RebuildFrom(context.Background(), oldRes, Options{})
62 if err != nil {
63 t.Fatal(err)
64 }
65 t.Cleanup(func() {
66 if res.Controller != nil {
67 res.Controller.Close()
68 }
69 })
70 // Old generation must not admit new work after publish of the new one.
71 if oldGen != 0 && oldRes.Owner.Gate.AdmitNewWork(oldGen) {
72 // Only fails if new gen was published and differs.
73 if res.Snapshot != nil && res.Snapshot.Generation() != oldGen {
74 t.Fatal("old generation still admits after rebuild publish")
75 }
76 }
77 if res.Controller.RuntimePhase() != "Active" && res.Controller.RuntimePhase() != "Unknown" {
78 // Active when generation is published.
79 if res.Snapshot != nil && res.Snapshot.Generation() != 0 && res.Controller.RuntimePhase() != "Active" {
80 t.Fatalf("new controller phase = %s", res.Controller.RuntimePhase())
81 }
82 }
83 }
84
85 func TestDoctorRuntimeReport(t *testing.T) {
86 before := extension.RuntimeOwnerFallbackCount()
87 _ = extension.RuntimeOwnerFromContext(context.Background())
88 report := CollectRuntimeDoctor(nil)
89 if report.RuntimeOwnerFallbacks <= before {
90 t.Fatalf("runtime owner fallbacks = %d, want greater than %d", report.RuntimeOwnerFallbacks, before)
91 }
92 text := RenderRuntimeDoctorText(report)
93 if !strings.Contains(text, "runtime owner fallbacks:") || (!strings.Contains(text, "metrics:") && !strings.Contains(text, "recoverability")) {
94 t.Fatalf("unexpected doctor text:\n%s", text)
95 }
96 if _, err := RenderRuntimeDoctorJSON(report); err != nil {
97 t.Fatal(err)
98 }
99 }
100
101 func TestPlanClassifyIntegration(t *testing.T) {
102 from, err := extension.BuildDependencyGraph([]extension.ComponentDescriptor{
103 {ID: "plugin/a", Provides: []extensioncontract.Capability{{
104 Key: extensioncontract.CapabilityKey{Namespace: "plugin/a", Kind: "ui", ID: "x"},
105 Version: "1.0.0", SchemaHash: "sha256:a",
106 }}},
107 })
108 if err != nil {
109 t.Fatal(err)
110 }
111 to, err := extension.BuildDependencyGraph([]extension.ComponentDescriptor{
112 {ID: "plugin/a", Provides: []extensioncontract.Capability{{
113 Key: extensioncontract.CapabilityKey{Namespace: "plugin/a", Kind: "ui", ID: "x"},
114 Version: "1.0.1", SchemaHash: "sha256:b",
115 }}},
116 })
117 if err != nil {
118 t.Fatal(err)
119 }
120 plan := extension.DiffRuntimePlan(from, to, 1, 2)
121 if plan.Kind != extension.SubgraphUIOnly {
122 t.Fatalf("kind = %v", plan.Kind)
123 }
124 if plan.MayChangePrefix() {
125 t.Fatal("UI-only must not affect cache")
126 }
127 }
128
129 func TestRapidReloadPreservesAdmission(t *testing.T) {
130 isolateConfigHome(t)
131 var prev *BuildResult
132 for i := range 3 {
133 var res *BuildResult
134 var err error
135 if prev == nil {
136 res, err = BuildRuntime(context.Background(), Options{})
137 } else {
138 res, err = RebuildFrom(context.Background(), prev, Options{})
139 }
140 if err != nil {
141 t.Fatalf("reload %d: %v", i, err)
142 }
143 t.Cleanup(func() {
144 if res.Controller != nil {
145 res.Controller.Close()
146 }
147 })
148 if res.Snapshot != nil {
149 gen := res.Snapshot.Generation()
150 if !res.Owner.Gate.AdmitNewWork(gen) {
151 t.Fatalf("reload %d: gen %d not admitted", i, gen)
152 }
153 }
154 if prev != nil && prev.Snapshot != nil && res.Snapshot != nil {
155 if prev.Snapshot.Generation() != res.Snapshot.Generation() {
156 if res.Owner.Gate.AdmitNewWork(prev.Snapshot.Generation()) {
157 t.Fatalf("reload %d: old gen still admits", i)
158 }
159 }
160 }
161 prev = res
162 }
163 }
164
165 func TestProviderDrainReloadClassify(t *testing.T) {
166 from, err := extension.BuildDependencyGraph([]extension.ComponentDescriptor{
167 {ID: "plugin/p", Provides: []extensioncontract.Capability{{
168 Key: extensioncontract.CapabilityKey{Namespace: "plugin/p", Kind: "provider", ID: "main"},
169 Version: "1.0.0",
170 SchemaHash: "sha256:p1",
171 }}},
172 })
173 if err != nil {
174 t.Fatal(err)
175 }
176 to, err := extension.BuildDependencyGraph([]extension.ComponentDescriptor{
177 {ID: "plugin/p", Provides: []extensioncontract.Capability{{
178 Key: extensioncontract.CapabilityKey{Namespace: "plugin/p", Kind: "provider", ID: "main"},
179 Version: "1.0.1",
180 SchemaHash: "sha256:p2",
181 }}},
182 })
183 if err != nil {
184 t.Fatal(err)
185 }
186 plan := extension.DiffRuntimePlan(from, to, 1, 2)
187 if plan.Kind != extension.SubgraphProviderOnly {
188 t.Fatalf("kind = %v want provider-only", plan.Kind)
189 }
190 if !plan.MayChangePrefix() {
191 t.Fatal("provider change must conservatively check the prefix")
192 }
193 if !plan.ProviderChanged || plan.PrefixChanged {
194 t.Fatalf("provider plan flags = provider:%v prefix:%v", plan.ProviderChanged, plan.PrefixChanged)
195 }
196 if !shouldReuseDiscovery(plan) {
197 t.Fatal("provider-only should reuse skill/command/hook discovery")
198 }
199 }
200
201 func TestActivationFailureDoesNotPublish(t *testing.T) {
202 // Synthetic: BeginDrain without Publish must leave published gate unchanged
203 // and never admit the failed generation.
204 gate := extension.NewPublishGate()
205 gate.Publish(1)
206 gate.BeginDrain(99) // failed activation of gen 99
207 if gate.Published() != 1 {
208 t.Fatalf("published = %d", gate.Published())
209 }
210 if gate.AdmitNewWork(99) {
211 t.Fatal("failed activation gen must not admit")
212 }
213 if !gate.IsDraining(99) {
214 t.Fatal("failed gen should be marked draining for cleanup")
215 }
216 }
217
218 func TestResumePolicyInDoctor(t *testing.T) {
219 extension.RecordMessageSent(42, "m1", "test")
220 report := CollectRuntimeDoctor(nil)
221 // Force assess on gen 42 via DecideResume
222 d := extension.DecideResumeDefault(42)
223 if d.CleanRollback {
224 t.Fatal("message send must block clean rollback")
225 }
226 if !d.AllowResume {
227 t.Fatal("must still allow resume")
228 }
229 _ = report
230 }
231
231 lines GO