返回 DeepSeek-Reasonix
task_machine_test.go
根目录 / internal / cli / task_machine_test.go
1 package cli
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "io"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12 "time"
13
14 "reasonix/internal/agent"
15 "reasonix/internal/config"
16 "reasonix/internal/event"
17 "reasonix/internal/jobs"
18 )
19
20 func TestTaskMachineListUsesContentFreePersistedMetadata(t *testing.T) {
21 identityKey := installMachineTestIdentity(t)
22 dir := t.TempDir()
23 saveMachineTestSession(t, dir, "session", time.Date(2026, 7, 23, 13, 0, 0, 0, time.UTC))
24 path := filepath.Join(dir, "session.jsonl")
25 manager := jobs.NewManager(event.Discard)
26 manager.SetActiveSessionPath("session", path)
27 job := manager.StartForSession("session", "task", "PRIVATE TASK LABEL", func(context.Context, io.Writer) (string, error) {
28 return "PRIVATE TASK OUTPUT", nil
29 })
30 manager.WaitForSession(context.Background(), "session", []string{job.ID}, 1)
31 manager.Close()
32
33 var out bytes.Buffer
34 if code := runTaskCommand([]string{"list", "--json", "--dir", dir}, &out); code != 0 {
35 t.Fatalf("task list exit code = %d, output = %s", code, out.String())
36 }
37 var response machineTaskList
38 if err := json.Unmarshal(out.Bytes(), &response); err != nil {
39 t.Fatalf("decode task list: %v", err)
40 }
41 if len(response.Tasks) != 1 || response.Tasks[0].ID != job.ID || response.Tasks[0].Status != "done" {
42 t.Fatalf("tasks = %+v", response.Tasks)
43 }
44 if response.Tasks[0].Kind != "background" || response.Tasks[0].SessionID != machineSessionIDWithKey("session", identityKey) {
45 t.Fatalf("task projection = %+v", response.Tasks[0])
46 }
47 if !response.Tasks[0].ArtifactComplete {
48 t.Fatalf("persisted task artifact should be complete: %+v", response.Tasks[0])
49 }
50 if strings.Contains(out.String(), "PRIVATE") || strings.Contains(out.String(), dir) {
51 t.Fatalf("task output leaked private data: %s", out.String())
52 }
53
54 if err := os.Remove(filepath.Join(jobs.ArtifactDir(path), job.ID+".log")); err != nil {
55 t.Fatal(err)
56 }
57 out.Reset()
58 if code := runTaskCommand([]string{"list", "--json", "--dir", dir}, &out); code != 0 {
59 t.Fatalf("task list after artifact removal exit code = %d, output = %s", code, out.String())
60 }
61 response = machineTaskList{}
62 if err := json.Unmarshal(out.Bytes(), &response); err != nil {
63 t.Fatalf("decode task list after artifact removal: %v", err)
64 }
65 if len(response.Tasks) != 1 || response.Tasks[0].ArtifactComplete {
66 t.Fatalf("task projection after artifact removal = %+v", response.Tasks)
67 }
68 }
69
70 func TestTaskMachineProjectsSubagentLifecycleAndArtifactCompleteness(t *testing.T) {
71 identityKey := installMachineTestIdentity(t)
72 dir := t.TempDir()
73 saveMachineTestSession(t, dir, "session", time.Now())
74 subDir := filepath.Join(dir, "subagents")
75 if err := os.MkdirAll(subDir, 0o700); err != nil {
76 t.Fatal(err)
77 }
78 now := time.Now().UTC()
79 metas := []agent.SubagentMeta{
80 {Ref: "sa_running", CreatedAt: now, UpdatedAt: now, Status: agent.SubagentRunning, Kind: "task", ParentSession: "session"},
81 {Ref: "sa_complete", CreatedAt: now.Add(-time.Minute), UpdatedAt: now, Status: agent.SubagentCompleted, Kind: "task", ParentSession: "session"},
82 {Ref: "sa_missing", CreatedAt: now.Add(-2 * time.Minute), UpdatedAt: now, Status: agent.SubagentCompleted, Kind: "task", ParentSession: "session"},
83 }
84 for _, meta := range metas {
85 data, err := json.Marshal(meta)
86 if err != nil {
87 t.Fatal(err)
88 }
89 if err := os.WriteFile(filepath.Join(subDir, meta.Ref+".meta.json"), data, 0o600); err != nil {
90 t.Fatal(err)
91 }
92 }
93 if err := os.WriteFile(filepath.Join(subDir, "sa_complete.jsonl"), []byte("persisted transcript\n"), 0o600); err != nil {
94 t.Fatal(err)
95 }
96
97 tasks, err := machineTasks(dir, machineSessionIDWithKey("session", identityKey), identityKey)
98 if err != nil {
99 t.Fatal(err)
100 }
101 byID := make(map[string]machineTask, len(tasks))
102 for _, task := range tasks {
103 byID[task.ID] = task
104 }
105 if got := byID["sa_running"]; got.Status != string(agent.SubagentInterrupted) || got.FinishedAt != "" || got.ArtifactComplete {
106 t.Fatalf("stale running projection = %+v", got)
107 }
108 if got := byID["sa_complete"]; got.Status != string(agent.SubagentCompleted) || got.FinishedAt == "" || !got.ArtifactComplete {
109 t.Fatalf("completed projection = %+v", got)
110 }
111 if got := byID["sa_missing"]; got.FinishedAt == "" || got.ArtifactComplete {
112 t.Fatalf("missing artifact projection = %+v", got)
113 }
114
115 lease, err := agent.TryAcquireSessionLease(filepath.Join(dir, "session.jsonl"))
116 if err != nil {
117 t.Fatal(err)
118 }
119 defer lease.Release()
120 tasks, err = machineTasks(dir, machineSessionIDWithKey("session", identityKey), identityKey)
121 if err != nil {
122 t.Fatal(err)
123 }
124 for _, task := range tasks {
125 if task.ID == "sa_running" && (task.Status != string(agent.SubagentRunning) || task.FinishedAt != "" || task.ArtifactComplete) {
126 t.Fatalf("live running projection = %+v", task)
127 }
128 }
129 }
130
131 func TestTaskMachineProjectRootUsesProjectStore(t *testing.T) {
132 identityKey := installMachineTestIdentity(t)
133 projectRoot := t.TempDir()
134 sessionDir := config.ProjectSessionDir(projectRoot)
135 saveMachineTestSession(t, sessionDir, "session", time.Date(2026, 7, 23, 13, 30, 0, 0, time.UTC))
136 path := filepath.Join(sessionDir, "session.jsonl")
137 manager := jobs.NewManager(event.Discard)
138 manager.SetActiveSessionPath("session", path)
139 job := manager.StartForSession("session", "task", "PRIVATE TASK LABEL", func(context.Context, io.Writer) (string, error) {
140 return "PRIVATE TASK OUTPUT", nil
141 })
142 manager.WaitForSession(context.Background(), "session", []string{job.ID}, 1)
143 manager.Close()
144
145 var out bytes.Buffer
146 if code := runTaskCommand([]string{"list", "--json", "--project-root", projectRoot}, &out); code != 0 {
147 t.Fatalf("task list exit code = %d, output = %s", code, out.String())
148 }
149 var response machineTaskList
150 if err := json.Unmarshal(out.Bytes(), &response); err != nil {
151 t.Fatalf("decode task list: %v", err)
152 }
153 if len(response.Tasks) != 1 || response.Tasks[0].ID != job.ID || response.Tasks[0].SessionID != machineSessionIDWithKey("session", identityKey) {
154 t.Fatalf("tasks = %+v, want project task", response.Tasks)
155 }
156 }
157
158 func TestTaskMachineShowRequiresNonZeroForMissingTask(t *testing.T) {
159 installMachineTestIdentity(t)
160 dir := t.TempDir()
161 var out bytes.Buffer
162 if code := runTaskCommand([]string{"show", "--json", "missing", "--dir", dir}, &out); code != 1 {
163 t.Fatalf("exit code = %d, output = %s", code, out.String())
164 }
165 var response machineErrorResponse
166 if err := json.Unmarshal(out.Bytes(), &response); err != nil {
167 t.Fatalf("decode error: %v", err)
168 }
169 if response.Error.Code != "task_not_found" {
170 t.Fatalf("response = %+v", response)
171 }
172 }
173
174 func TestTaskMachineEmptyListUsesAnArray(t *testing.T) {
175 installMachineTestIdentity(t)
176 var out bytes.Buffer
177 if code := runTaskCommand([]string{"list", "--json", "--dir", t.TempDir()}, &out); code != 0 {
178 t.Fatalf("task list exit code = %d, output = %s", code, out.String())
179 }
180 var response machineTaskList
181 if err := json.Unmarshal(out.Bytes(), &response); err != nil {
182 t.Fatal(err)
183 }
184 if response.Tasks == nil {
185 t.Fatalf("tasks must be [] in empty response: %s", out.String())
186 }
187 }
188
189 func TestTaskMachineRejectsConflictingSessionSources(t *testing.T) {
190 installMachineTestIdentity(t)
191 dir := t.TempDir()
192 var out bytes.Buffer
193 if code := runTaskCommand([]string{"list", "--json", "--dir", dir, "--project-root", dir}, &out); code != 2 {
194 t.Fatalf("exit code = %d, output = %s", code, out.String())
195 }
196 var response machineErrorResponse
197 if err := json.Unmarshal(out.Bytes(), &response); err != nil {
198 t.Fatal(err)
199 }
200 if response.Error.Code != "invalid_argument" {
201 t.Fatalf("response = %+v", response)
202 }
203 }
204
204 lines GO