返回 DeepSeek-Reasonix
jobs_recorder_test.go
根目录 / internal / jobs / jobs_recorder_test.go
1 package jobs
2
3 import (
4 "context"
5 "io"
6 "sync"
7 "testing"
8 "time"
9
10 "reasonix/internal/event"
11 )
12
13 // recordingRecorder captures lifecycle calls for assertions.
14 type recordingRecorder struct {
15 mu sync.Mutex
16 starts []string
17 dones []string
18 status []Status
19 }
20
21 func (r *recordingRecorder) RecordStart(id, kind, label string) {
22 r.mu.Lock()
23 defer r.mu.Unlock()
24 r.starts = append(r.starts, id+"|"+kind+"|"+label)
25 }
26
27 func (r *recordingRecorder) RecordDone(id string, st Status, err error) {
28 r.mu.Lock()
29 defer r.mu.Unlock()
30 r.dones = append(r.dones, id)
31 r.status = append(r.status, st)
32 }
33
34 func (r *recordingRecorder) snapshot() (starts, dones []string, status []Status) {
35 r.mu.Lock()
36 defer r.mu.Unlock()
37 return append([]string(nil), r.starts...), append([]string(nil), r.dones...), append([]Status(nil), r.status...)
38 }
39
40 func TestTaskRecorderHook_StartAndDone(t *testing.T) {
41 rec := &recordingRecorder{}
42 m := NewManager(event.Discard, WithTaskRecorder(rec))
43 defer m.Close()
44
45 j := m.Start("task", "demo", func(ctx context.Context, out io.Writer) (string, error) {
46 return "answer", nil
47 })
48 if res := m.Wait(context.Background(), []string{j.ID}, 5); len(res) != 1 || res[0].Status != Done {
49 t.Fatalf("wait = %+v", res)
50 }
51
52 starts, dones, status := rec.snapshot()
53 wantStart := j.ID + "|task|demo"
54 if len(starts) != 1 || starts[0] != wantStart {
55 t.Fatalf("starts = %v, want [%s]", starts, wantStart)
56 }
57 if len(dones) != 1 || dones[0] != j.ID || len(status) != 1 || status[0] != Done {
58 t.Fatalf("dones = %v status = %v, want [%s] [done]", dones, status, j.ID)
59 }
60 }
61
62 func TestTaskRecorderHook_Failed(t *testing.T) {
63 rec := &recordingRecorder{}
64 m := NewManager(event.Discard, WithTaskRecorder(rec))
65 defer m.Close()
66
67 j := m.Start("bash", "", func(ctx context.Context, out io.Writer) (string, error) {
68 return "", context.DeadlineExceeded
69 })
70 if res := m.Wait(context.Background(), []string{j.ID}, 5); len(res) != 1 || res[0].Status != Failed {
71 t.Fatalf("wait = %+v", res)
72 }
73
74 _, dones, status := rec.snapshot()
75 if len(dones) != 1 || status[0] != Failed {
76 t.Fatalf("dones = %v status = %v, want [%s] [failed]", dones, status, j.ID)
77 }
78 }
79
80 func TestTaskRecorderHook_Killed(t *testing.T) {
81 rec := &recordingRecorder{}
82 m := NewManager(event.Discard, WithTaskRecorder(rec))
83 defer m.Close()
84
85 block := make(chan struct{})
86 j := m.Start("task", "", func(ctx context.Context, out io.Writer) (string, error) {
87 <-block // hang until killed
88 return "", nil
89 })
90 time.Sleep(50 * time.Millisecond)
91 m.Kill(j.ID)
92 close(block)
93 m.Wait(context.Background(), []string{j.ID}, 5)
94
95 _, dones, status := rec.snapshot()
96 if len(dones) != 1 || status[0] != Killed {
97 t.Fatalf("dones = %v status = %v, want [%s] [killed]", dones, status, j.ID)
98 }
99 }
100
101 func TestTaskRecorderHook_SetAfterConstruction(t *testing.T) {
102 rec := &recordingRecorder{}
103 m := NewManager(event.Discard)
104 m.SetTaskRecorder(rec)
105 defer m.Close()
106
107 j := m.Start("bash", "echo", func(ctx context.Context, out io.Writer) (string, error) {
108 return "", nil
109 })
110 m.Wait(context.Background(), []string{j.ID}, 5)
111
112 starts, _, _ := rec.snapshot()
113 if len(starts) != 1 {
114 t.Fatalf("starts = %v, want 1 call", starts)
115 }
116 }
117
118 func TestTaskRecorderHook_NilRecorderIsNoop(t *testing.T) {
119 m := NewManager(event.Discard, WithTaskRecorder(nil))
120 defer m.Close()
121
122 j := m.Start("bash", "echo", func(ctx context.Context, out io.Writer) (string, error) {
123 return "", nil
124 })
125 if res := m.Wait(context.Background(), []string{j.ID}, 5); len(res) != 1 {
126 t.Fatalf("wait = %+v", res)
127 }
128 }
129
129 lines GO