返回 DeepSeek-Reasonix
session_transition_commit_test.go
根目录 / internal / serve / session_transition_commit_test.go
1 package serve
2
3 import (
4 "encoding/json"
5 "path/filepath"
6 "testing"
7
8 "reasonix/internal/agent"
9 "reasonix/internal/config"
10 "reasonix/internal/control"
11 "reasonix/internal/event"
12 "reasonix/internal/eventwire"
13 "reasonix/internal/provider"
14 )
15
16 func TestSessionTransitionPublishesRouteOnlyAfterControllerCommit(t *testing.T) {
17 bc := NewBroadcaster()
18 tag := newSessionTagSink(bc)
19 dir := t.TempDir()
20 initialPath := filepath.Join(dir, "old.jsonl")
21 sess := agent.NewSession("sys")
22 exec := agent.New(nil, nil, sess, agent.Options{}, tag)
23 ctrl := control.New(control.Options{Runner: exec, Executor: exec, Sink: tag, SessionDir: dir, SessionPath: initialPath})
24 oldPath := agent.CanonicalSessionPath(ctrl.SessionPath())
25 tag.SetPath(oldPath)
26 bc.SetCurrentSession(oldPath)
27
28 srv := New(ctrl, bc, config.ServeConfig{})
29 defer srv.Close()
30 srv.RegisterSessionTag(ctrl, tag)
31 handler := srv.sessionTransitionHandler(ctrl, nil)
32 ctrl.SetOnSessionTransition(func(info control.SessionTransitionInfo) error {
33 if err := handler(info); err != nil {
34 return err
35 }
36 // This models output produced after transition preparation but before
37 // ClearSession swaps its executor and session path.
38 tag.Emit(event.Event{Kind: event.Notice, Text: "pre-commit hook output"})
39 return nil
40 })
41
42 all, stop := bc.SubscribeAll()
43 defer stop()
44 if err := ctrl.ClearSession(); err != nil {
45 t.Fatal(err)
46 }
47 newPath := agent.CanonicalSessionPath(ctrl.SessionPath())
48 if newPath == oldPath {
49 t.Fatal("clear did not rotate the session path")
50 }
51 tag.Emit(event.Event{Kind: event.Notice, Text: "post-commit output"})
52
53 var before, after eventwire.Event
54 if err := json.Unmarshal(<-all, &before); err != nil {
55 t.Fatal(err)
56 }
57 if err := json.Unmarshal(<-all, &after); err != nil {
58 t.Fatal(err)
59 }
60 if before.SessionPath != oldPath || !before.SessionCurrent {
61 t.Fatalf("pre-commit frame route = %q current=%v, want old foreground %q", before.SessionPath, before.SessionCurrent, oldPath)
62 }
63 if after.SessionPath != newPath || !after.SessionCurrent {
64 t.Fatalf("post-commit frame route = %q current=%v, want new foreground %q", after.SessionPath, after.SessionCurrent, newPath)
65 }
66 }
67
68 func TestBranchTransitionPublishesMustDeliverRoute(t *testing.T) {
69 bc := NewBroadcaster()
70 tag := newSessionTagSink(bc)
71 dir := t.TempDir()
72 initialPath := filepath.Join(dir, "old.jsonl")
73 sess := agent.NewSession("")
74 sess.Add(provider.Message{Role: provider.RoleUser, Content: "branch me"})
75 exec := agent.New(nil, nil, sess, agent.Options{}, tag)
76 ctrl := control.New(control.Options{Runner: exec, Executor: exec, Sink: tag, SessionDir: dir, SessionPath: initialPath})
77 defer ctrl.Close()
78 tag.SetPath(initialPath)
79 bc.SetCurrentSession(initialPath)
80
81 srv := New(ctrl, bc, config.ServeConfig{})
82 srv.RegisterSessionTag(ctrl, tag)
83 ctrl.SetOnSessionTransition(srv.sessionTransitionHandler(ctrl, nil))
84 all, stop := bc.SubscribeAll()
85 defer stop()
86 for range subscriberBufferSize - subscriberPriorityReserve {
87 bc.Emit(event.Event{Kind: event.Text, Text: "delta", SessionPath: initialPath})
88 }
89 for range subscriberPriorityReserve {
90 bc.Emit(event.Event{Kind: event.Notice, Text: "priority", SessionPath: initialPath})
91 }
92 if got := len(all); got != subscriberBufferSize {
93 t.Fatalf("saturated subscriber length = %d, want %d", got, subscriberBufferSize)
94 }
95
96 newPath, err := ctrl.Branch("child")
97 if err != nil {
98 t.Fatal(err)
99 }
100 newPath = agent.CanonicalSessionPath(newPath)
101 found := false
102 for len(all) > 0 {
103 var frame eventwire.Event
104 if err := json.Unmarshal(<-all, &frame); err != nil {
105 t.Fatal(err)
106 }
107 if frame.Kind == "session_changed" && frame.SessionPath == newPath && frame.SessionCurrent {
108 found = true
109 }
110 }
111 if !found {
112 t.Fatalf("saturated subscriber lost branch route to %q", newPath)
113 }
114 }
115
116 func TestForegroundRecoveryPublishesMustDeliverRoute(t *testing.T) {
117 bc := NewBroadcaster()
118 tag := newSessionTagSink(bc)
119 dir := t.TempDir()
120 initialPath := filepath.Join(dir, "old.jsonl")
121 recoveryPath := filepath.Join(dir, "old-recovery.jsonl")
122 ctrl := control.New(control.Options{SessionPath: initialPath})
123 defer ctrl.Close()
124 tag.SetPath(initialPath)
125 bc.SetCurrentSession(initialPath)
126
127 srv := New(ctrl, bc, config.ServeConfig{})
128 srv.RegisterSessionTag(ctrl, tag)
129 all, stop := bc.SubscribeAll()
130 defer stop()
131 for range subscriberBufferSize - subscriberPriorityReserve {
132 bc.Emit(event.Event{Kind: event.Text, Text: "delta", SessionPath: initialPath})
133 }
134 for range subscriberPriorityReserve {
135 bc.Emit(event.Event{Kind: event.Notice, Text: "priority", SessionPath: initialPath})
136 }
137 if got := len(all); got != subscriberBufferSize {
138 t.Fatalf("saturated subscriber length = %d, want %d", got, subscriberBufferSize)
139 }
140
141 srv.publishRecoveredControllerRoute(ctrl, recoveryPath)
142 recoveryPath = agent.CanonicalSessionPath(recoveryPath)
143 found := false
144 for len(all) > 0 {
145 var frame eventwire.Event
146 if err := json.Unmarshal(<-all, &frame); err != nil {
147 t.Fatal(err)
148 }
149 if frame.Kind == "session_changed" && frame.SessionPath == recoveryPath && frame.SessionCurrent {
150 found = true
151 }
152 }
153 if !found {
154 t.Fatalf("saturated subscriber lost foreground recovery route to %q", recoveryPath)
155 }
156 }
157
157 lines GO