返回 DeepSeek-Reasonix
task_background_queue_test.go
根目录 / internal / agent / task_background_queue_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "sync"
8 "testing"
9 "time"
10
11 "reasonix/internal/checkpoint"
12 "reasonix/internal/event"
13 "reasonix/internal/jobs"
14 "reasonix/internal/provider"
15 "reasonix/internal/tool"
16 )
17
18 // TestBackgroundTaskReturnsBeforeSlotFrees ensures run_in_background returns a
19 // job id even when the session concurrency pool is full, instead of blocking
20 // the parent tool call on Acquire.
21 func TestBackgroundTaskReturnsBeforeSlotFrees(t *testing.T) {
22 root := t.TempDir()
23 store := checkpoint.New("", root)
24 observer := checkpoint.NewMutationObserver(checkpoint.ObserverOptions{Store: store})
25 sched := NewSubagentScheduler(1, 1)
26 // Hold the only slot.
27 holdRelease, err := sched.Acquire(context.Background(), AcquireRequest{Writer: false})
28 if err != nil {
29 t.Fatal(err)
30 }
31
32 started := make(chan struct{})
33 prov := &blockingProvider{started: started}
34 jm := jobs.NewManager(event.Discard)
35 defer jm.Close()
36 ctx := jobs.WithManager(withCallContext(context.Background(), "bg", event.Discard, nil, false), jm)
37 ctx = jobs.WithSession(ctx, "sess-bg")
38 ctx = WithParentSession(ctx, "sess-bg")
39
40 task := NewTaskTool(prov, nil, tool.NewRegistry(), 20, 0, 0, 0, 0, 0, 0, 0.0, "", "sys", nil, 0, "", "", nil).
41 WithTranscripts(mustSubagentStore(t), root, "base", "high").
42 WithScheduler(sched).
43 WithMutationObserver(observer)
44
45 done := make(chan string, 1)
46 go func() {
47 out, err := task.Execute(ctx, json.RawMessage(`{"prompt":"work","run_in_background":true,"description":"queued"}`))
48 if err != nil {
49 done <- "err:" + err.Error()
50 return
51 }
52 done <- out
53 }()
54
55 var jobID string
56 select {
57 case out := <-done:
58 if !strings.Contains(out, "Started background task") {
59 t.Fatalf("want immediate job start, got %q", out)
60 }
61 if !strings.Contains(out, "queue") {
62 t.Fatalf("want queue note in background start message, got %q", out)
63 }
64 jobID = extractJobID(out)
65 if jobID == "" {
66 t.Fatalf("no background job id in output: %q", out)
67 }
68 case <-time.After(500 * time.Millisecond):
69 t.Fatal("background task blocked on concurrency slot instead of returning a job id")
70 }
71 if writers := observer.ActiveWriters(); len(writers) != 1 {
72 t.Fatalf("queued background writer = %+v, want one rewind exclusion", writers)
73 }
74
75 // Free the slot so the background job can finish (and not leak).
76 holdRelease()
77 select {
78 case <-started:
79 case <-time.After(2 * time.Second):
80 t.Fatal("background job never acquired slot / started provider")
81 }
82 result := jm.WaitForSession(context.Background(), "sess-bg", []string{jobID}, 5)
83 if len(result) != 1 || result[0].Status != jobs.Done {
84 t.Fatalf("background job result = %+v, want one completed job", result)
85 }
86 if writers := observer.ActiveWriters(); len(writers) != 0 {
87 t.Fatalf("completed background writer still registered: %+v", writers)
88 }
89 }
90
91 type blockingProvider struct {
92 started chan struct{}
93 once sync.Once
94 }
95
96 func (p *blockingProvider) Name() string { return "blocking" }
97
98 func (p *blockingProvider) Stream(_ context.Context, _ provider.Request) (<-chan provider.Chunk, error) {
99 p.once.Do(func() { close(p.started) })
100 ch := make(chan provider.Chunk, 1)
101 ch <- provider.Chunk{Type: provider.ChunkText, Text: "done"}
102 close(ch)
103 return ch, nil
104 }
105
105 lines GO