返回 DeepSeek-Reasonix
ask_queue_test.go
根目录 / internal / control / ask_queue_test.go
1 package control
2
3 import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "reasonix/internal/event"
10 )
11
12 func askProbeQuestions() []event.AskQuestion {
13 return []event.AskQuestion{{
14 ID: "q1", Header: "Fix", Prompt: "Which fix?",
15 Options: []event.AskOption{{Label: "A"}, {Label: "B"}},
16 }}
17 }
18
19 type askProbeSink struct {
20 mu sync.Mutex
21 asks []event.Ask
22 }
23
24 func (s *askProbeSink) Emit(e event.Event) {
25 if e.Kind != event.AskRequest {
26 return
27 }
28 s.mu.Lock()
29 s.asks = append(s.asks, e.Ask)
30 s.mu.Unlock()
31 }
32
33 func (s *askProbeSink) snapshot() []event.Ask {
34 s.mu.Lock()
35 defer s.mu.Unlock()
36 return append([]event.Ask(nil), s.asks...)
37 }
38
39 // Independent interactions must all become visible without waiting for an
40 // earlier answer. The frontend decides which card to put on top; the registry
41 // retains every pending request and each waiter has its own reply channel.
42 func TestConcurrentAsksArePublishedAndResolvedIndependently(t *testing.T) {
43 sink := &askProbeSink{}
44 c := newOwnedTestController(t, Options{Sink: sink, SessionDir: t.TempDir()})
45
46 ctx := t.Context()
47 type result struct {
48 answers []event.AskAnswer
49 err error
50 }
51 results := make(chan result, 2)
52 for range 2 {
53 go func() {
54 answers, err := c.Ask(ctx, askProbeQuestions())
55 results <- result{answers: answers, err: err}
56 }()
57 }
58
59 deadline := time.After(2 * time.Second)
60 var asks []event.Ask
61 for len(asks) != 2 {
62 select {
63 case <-deadline:
64 t.Fatalf("published asks = %d, want 2", len(asks))
65 case <-time.After(5 * time.Millisecond):
66 asks = sink.snapshot()
67 }
68 }
69 if asks[0].ID == asks[1].ID {
70 t.Fatalf("concurrent asks share id %q", asks[0].ID)
71 }
72 if _, pending := c.approval.snapshotPrompts(); len(pending) != 2 {
73 t.Fatalf("pending asks = %d, want 2", len(pending))
74 }
75
76 for _, ask := range asks {
77 c.AnswerQuestion(ask.ID, []event.AskAnswer{{QuestionID: "q1", Selected: []string{ask.ID}}})
78 }
79 for range 2 {
80 select {
81 case got := <-results:
82 if got.err != nil || len(got.answers) != 1 {
83 t.Fatalf("Ask result = %#v, %v", got.answers, got.err)
84 }
85 case <-time.After(2 * time.Second):
86 t.Fatal("concurrent Ask stayed blocked after its answer")
87 }
88 }
89 }
90
91 // Ask has no timeout of its own: approvalTimeout defaults to zero, so a
92 // question nobody answers blocks its turn until the user cancels.
93 func TestAskWithoutTimeoutBlocksUntilCancelled(t *testing.T) {
94 c := newOwnedTestController(t, Options{Sink: event.Discard, SessionDir: t.TempDir()})
95 if c.approval.approvalTimeout != 0 {
96 t.Skipf("approvalTimeout is %v; this test pins the unbounded default", c.approval.approvalTimeout)
97 }
98
99 ctx, cancel := context.WithCancel(context.Background())
100 errc := make(chan error, 1)
101 go func() {
102 _, err := c.Ask(ctx, askProbeQuestions())
103 errc <- err
104 }()
105
106 select {
107 case err := <-errc:
108 t.Fatalf("Ask returned %v without an answer; it is expected to block", err)
109 case <-time.After(200 * time.Millisecond):
110 }
111
112 cancel()
113 select {
114 case err := <-errc:
115 if err == nil {
116 t.Fatal("cancelled Ask returned a nil error")
117 }
118 case <-time.After(2 * time.Second):
119 t.Fatal("Ask did not unblock after cancellation")
120 }
121 }
122
122 lines GO