返回 DeepSeek-Reasonix
runtime_state_regression_test.go
根目录 / desktop / runtime_state_regression_test.go
1 package main
2
3 import (
4 "context"
5 "reflect"
6 "sync"
7 "testing"
8 "time"
9
10 "reasonix/internal/control"
11 )
12
13 // A real controller delivers TurnDone while its admission gate is still held.
14 // The project tree must subsequently receive the settled state, without the
15 // user opening another tab or causing a catalog reload.
16 func TestRuntimeStateCompletedTurnPublishesIdleProjectTree(t *testing.T) {
17 isolateDesktopUserDirs(t)
18 app := NewApp()
19 ctx, cancel := context.WithCancel(t.Context())
20 defer cancel()
21 app.ctx = ctx
22 snapshots := make(chan ProjectTreeRuntimeSnapshot, 64)
23 app.runtimeEvents.emit = func(_ context.Context, name string, payload ...any) {
24 if name == "project-tree:runtime-changed" {
25 snapshots <- payload[0].(ProjectTreeRuntimeSnapshot)
26 }
27 }
28 sink := &tabEventSink{tabID: "runtime-regression", app: app}
29 runner := &blockingRunner{started: make(chan struct{}), release: make(chan struct{})}
30 releaseRunner := sync.OnceFunc(func() { close(runner.release) })
31 ctrl := control.New(control.Options{
32 Runner: runner, SessionDir: t.TempDir(), Label: "runtime regression", Sink: sink,
33 })
34 defer ctrl.Close()
35 defer releaseRunner()
36 tab := &WorkspaceTab{
37 ID: sink.tabID, Scope: "global", TopicID: "runtime-regression-topic",
38 Ctrl: ctrl, Ready: true, sink: sink, disabledMCP: map[string]ServerView{},
39 }
40 // Hold the independent metadata-save lane busy: runtime completion must
41 // not depend on an autosave/catalog refresh happening to repair its state.
42 // scheduleTabSnapshot will only mark saveAgain while this lane is occupied.
43 tab.saving = true
44 app.tabs[tab.ID] = tab
45 app.tabOrder = []string{tab.ID}
46 app.activeTabID = tab.ID
47 ctrl.Submit("complete the isolated test turn")
48 select {
49 case <-runner.started:
50 case <-time.After(5 * time.Second):
51 t.Fatal("isolated runner did not start")
52 }
53 // Observe execution before allowing completion, so a publisher that
54 // intentionally coalesces intermediate snapshots cannot hide the start.
55 startDeadline := time.NewTimer(5 * time.Second)
56 defer startDeadline.Stop()
57 started:
58 for {
59 select {
60 case snapshot := <-snapshots:
61 if len(snapshot.Topics) == 1 && snapshot.Topics[0].Node.Running {
62 break started
63 }
64 case <-startDeadline.C:
65 t.Fatal("running controller never published its activity")
66 }
67 }
68 releaseRunner()
69 waitNotRunning(t, ctrl)
70
71 deadline := time.NewTimer(5 * time.Second)
72 defer deadline.Stop()
73 var last ProjectTreeRuntimeSnapshot
74 for {
75 select {
76 case last = <-snapshots:
77 if len(last.Topics) != 1 {
78 continue
79 }
80 node := last.Topics[0].Node
81 if node.Running || node.Status != "" {
82 continue
83 }
84 fresh := app.GetProjectTreeRuntimeSnapshot()
85 if last.Revision == fresh.Revision && !reflect.DeepEqual(last, fresh) {
86 t.Fatalf("one revision identifies different runtime snapshots: published=%+v read=%+v", last, fresh)
87 }
88 status := ctrl.RuntimeStatus()
89 if status.Running || status.PendingPrompt || status.BackgroundJobs != 0 {
90 t.Fatalf("idle publication disagrees with controller: %+v", status)
91 }
92 return
93 case <-deadline.C:
94 t.Fatalf("completed controller never published idle: controller=%+v last=%+v fresh=%+v", ctrl.RuntimeStatus(), last, app.GetProjectTreeRuntimeSnapshot())
95 }
96 }
97 }
98
98 lines GO