返回 DeepSeek-Reasonix
replay_pending_test.go
根目录 / internal / control / replay_pending_test.go
1 package control
2
3 import (
4 "context"
5 "encoding/json"
6 "reflect"
7 "testing"
8
9 "reasonix/internal/event"
10 )
11
12 // TestReplayPendingPromptsReEmitsBlockedApproval proves a tool approval that is
13 // still blocking the gate is re-emitted on demand, so a frontend that reloaded
14 // after the original ApprovalRequest can rebuild its modal instead of leaving the
15 // gate stuck (#3844).
16 func TestReplayPendingPromptsReEmitsBlockedApproval(t *testing.T) {
17 reqs := make(chan event.Approval, 8)
18 c := New(Options{Sink: event.FuncSink(func(e event.Event) {
19 if e.Kind == event.ApprovalRequest {
20 reqs <- e.Approval
21 }
22 })})
23
24 done := make(chan struct{})
25 go func() {
26 defer close(done)
27 _, _, _ = gateApprover{c}.Approve(context.Background(), "bash", "go test ./...", json.RawMessage(`{"command":"go test ./..."}`))
28 }()
29
30 first := <-reqs
31 if first.Tool != "bash" || first.Subject != "go test ./..." {
32 t.Fatalf("first request = %+v, want bash / go test ./...", first)
33 }
34
35 c.ReplayPendingPrompts()
36
37 replayed := <-reqs
38 if !reflect.DeepEqual(replayed, first) {
39 t.Fatalf("replayed = %+v, want identical re-emit of %+v", replayed, first)
40 }
41
42 c.Approve(first.ID, true, false, false)
43 <-done
44 }
45
46 // TestReplayPendingPromptsReEmitsBlockedAsk proves the same for a blocked `ask`
47 // question, including its question payload (which the controller now retains).
48 func TestReplayPendingPromptsReEmitsBlockedAsk(t *testing.T) {
49 asks := make(chan event.Ask, 8)
50 c := New(Options{Sink: event.FuncSink(func(e event.Event) {
51 if e.Kind == event.AskRequest {
52 asks <- e.Ask
53 }
54 })})
55
56 questions := []event.AskQuestion{{
57 ID: "q1",
58 Header: "Pick",
59 Prompt: "Which option?",
60 Options: []event.AskOption{{Label: "A"}, {Label: "B"}},
61 }}
62 done := make(chan struct{})
63 go func() {
64 defer close(done)
65 _, _ = c.Ask(context.Background(), questions)
66 }()
67
68 first := <-asks
69 c.ReplayPendingPrompts()
70 replayed := <-asks
71
72 if replayed.ID != first.ID || len(replayed.Questions) != 1 || replayed.Questions[0].Prompt != "Which option?" {
73 t.Fatalf("replayed ask = %+v, want same id and questions as %+v", replayed, first)
74 }
75
76 c.AnswerQuestion(first.ID, []event.AskAnswer{{QuestionID: "q1", Selected: []string{"A"}}})
77 <-done
78 }
79
80 // TestReplayPendingPromptsNoOpWhenIdle proves replay emits nothing when no prompt
81 // is outstanding, so a frontend (re)connect on an idle session is silent.
82 func TestReplayPendingPromptsNoOpWhenIdle(t *testing.T) {
83 var count int
84 c := New(Options{Sink: event.FuncSink(func(e event.Event) {
85 if e.Kind == event.ApprovalRequest || e.Kind == event.AskRequest {
86 count++
87 }
88 })})
89
90 c.ReplayPendingPrompts()
91 if count != 0 {
92 t.Fatalf("emitted %d prompts with nothing pending, want 0", count)
93 }
94 }
95
95 lines GO