返回 DeepSeek-Reasonix
inbox_drain_test.go
根目录 / internal / acp / inbox_drain_test.go
1 package acp
2
3 import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8
9 "reasonix/internal/control"
10 "reasonix/internal/event"
11 )
12
13 type durableQueueFactory struct {
14 dir string
15 behavior func(ctx context.Context, sink event.Sink, input string) error
16 }
17
18 func (f *durableQueueFactory) SessionDir() string { return f.dir }
19
20 func (f *durableQueueFactory) NewSession(_ context.Context, p SessionParams) (*control.Controller, error) {
21 runner := &fakeRunner{sink: p.Sink, behavior: f.behavior}
22 return control.New(control.Options{Runner: runner, Sink: p.Sink, SessionDir: f.dir}), nil
23 }
24
25 func TestSessionPromptDrainsDurableFollowupBeforeResponding(t *testing.T) {
26 started := make(chan struct{})
27 release := make(chan struct{})
28 inputs := make(chan string, 2)
29 factory := &durableQueueFactory{
30 dir: t.TempDir(),
31 behavior: func(ctx context.Context, _ event.Sink, input string) error {
32 inputs <- input
33 if input == "start" {
34 close(started)
35 select {
36 case <-release:
37 case <-ctx.Done():
38 return ctx.Err()
39 }
40 }
41 return nil
42 },
43 }
44 client, stop := startServer(t, factory)
45 defer stop()
46 sessionID := openSession(t, client)
47 promptCh := client.callAsync("session/prompt", SessionPromptParams{
48 SessionID: sessionID,
49 Prompt: []ContentBlock{{Type: "text", Text: "start"}},
50 })
51 select {
52 case <-started:
53 case <-time.After(2 * time.Second):
54 t.Fatal("first ACP prompt did not start")
55 }
56 enqueue := client.call(t, sessionInboxEnqueueMethod, SessionInboxEnqueueParams{
57 SessionID: sessionID, Text: "queued followup", Intent: "followup", IdempotencyKey: "acp-msg-1",
58 })
59 if enqueue.Error != nil {
60 t.Fatalf("durable enqueue failed: %+v", enqueue.Error)
61 }
62 close(release)
63 for _, want := range []string{"start", "queued followup"} {
64 select {
65 case got := <-inputs:
66 if got != want {
67 t.Fatalf("ACP input = %q, want %q", got, want)
68 }
69 case <-time.After(2 * time.Second):
70 t.Fatalf("missing ACP input %q", want)
71 }
72 }
73 _, promptResp := drainPrompt(t, client, promptCh)
74 if promptResp.Error != nil {
75 t.Fatalf("session/prompt errored: %+v", promptResp.Error)
76 }
77 listed := client.call(t, sessionInboxListMethod, map[string]string{"sessionId": sessionID})
78 if listed.Error != nil {
79 t.Fatalf("inbox list errored: %+v", listed.Error)
80 }
81 var snapshot struct {
82 Items []json.RawMessage `json:"items"`
83 }
84 if err := json.Unmarshal(listed.Result, &snapshot); err != nil {
85 t.Fatal(err)
86 }
87 if len(snapshot.Items) != 0 {
88 t.Fatalf("ACP left completed durable items queued: %s", listed.Result)
89 }
90 }
91
91 lines GO