@markjaquith/agency 3.2.13 → 3.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -2
- package/src/workbase/schemas.ts +12 -0
- package/fixtures/protocol/orchestration-recipes.json +0 -53
- package/pi-extensions/agency.test.ts +0 -117
- package/src/check-commits.test.ts +0 -58
- package/src/cli-parser.test.ts +0 -945
- package/src/cli.test.ts +0 -1889
- package/src/commands/act.test.ts +0 -418
- package/src/commands/archive.test.ts +0 -236
- package/src/commands/context.test.ts +0 -665
- package/src/commands/description.test.ts +0 -103
- package/src/commands/doctor.test.ts +0 -185
- package/src/commands/epic.test.ts +0 -196
- package/src/commands/init.test.ts +0 -117
- package/src/commands/integration.test.ts +0 -165
- package/src/commands/pr.test.ts +0 -248
- package/src/commands/push.test.ts +0 -56
- package/src/commands/read-only.test.ts +0 -161
- package/src/commands/repo.test.ts +0 -199
- package/src/commands/restore.test.ts +0 -106
- package/src/commands/status.test.ts +0 -113
- package/src/commands/sync.test.ts +0 -200
- package/src/commands/task-phase.test.ts +0 -410
- package/src/commands/task.test.ts +0 -281
- package/src/commands/validate.test.ts +0 -186
- package/src/commands/work.test.ts +0 -1375
- package/src/commands/workbase.test.ts +0 -170
- package/src/graph-schema.test.ts +0 -124
- package/src/protocol.test.ts +0 -163
- package/src/readiness.test.ts +0 -105
- package/src/services/ArchiveBulkService.test.ts +0 -384
- package/src/services/ArchiveService.test.ts +0 -1092
- package/src/services/EpicService.test.ts +0 -74
- package/src/services/FileSystemService.test.ts +0 -81
- package/src/services/GraphMutationService.test.ts +0 -486
- package/src/services/GraphService.test.ts +0 -494
- package/src/services/IntegrationService.test.ts +0 -1091
- package/src/services/LifecycleTransaction.test.ts +0 -145
- package/src/services/PullRequestService.test.ts +0 -716
- package/src/services/PushService.test.ts +0 -408
- package/src/services/ReadinessService.test.ts +0 -290
- package/src/services/RepositoryService.test.ts +0 -789
- package/src/services/ReviewService.test.ts +0 -408
- package/src/services/SyncService.test.ts +0 -1377
- package/src/services/TaskPhaseService.test.ts +0 -852
- package/src/services/VersionControlService.test.ts +0 -62
- package/src/services/WorkbaseService.test.ts +0 -910
- package/src/services/WorktreeLock.test.ts +0 -205
- package/src/services/WorktreePerformance.test.ts +0 -71
- package/src/services/WorktreeService.test.ts +0 -2292
- package/src/test-utils.ts +0 -110
- package/src/usage-log.test.ts +0 -188
- package/src/utils/chooser.test.ts +0 -192
- package/src/utils/effect.test.ts +0 -30
- package/src/utils/interactive.pty.test.ts +0 -128
- package/src/utils/interactive.test.tsx +0 -860
- package/src/utils/process.test.ts +0 -132
- package/src/utils/progress.test.ts +0 -37
- package/src/work-view.test.ts +0 -151
- package/src/workbase/agent-command.test.ts +0 -128
- package/src/workbase/checkout-command.test.ts +0 -62
- package/src/workbase/delivery-command.test.ts +0 -180
- package/src/workbase/dependency-graph.test.ts +0 -50
- package/src/workbase/execution-contract.test.ts +0 -161
- package/src/workbase/frontmatter.test.ts +0 -67
- package/src/workbase/repository-reference.test.ts +0 -25
- package/src/workbase/schemas.test.ts +0 -543
- package/src/workbase/work-target.test.ts +0 -108
- package/src/workbase/worktree-command.test.ts +0 -61
|
@@ -1,1375 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import { FileSystemService } from "../services/FileSystemService"
|
|
4
|
-
import { WorkbaseService } from "../services/WorkbaseService"
|
|
5
|
-
import { EpicService } from "../services/EpicService"
|
|
6
|
-
import { TaskService } from "../services/TaskService"
|
|
7
|
-
import { PhaseService } from "../services/PhaseService"
|
|
8
|
-
import { WorktreeService } from "../services/WorktreeService"
|
|
9
|
-
import { ReadinessService } from "../services/ReadinessService"
|
|
10
|
-
import { IntegrationService } from "../services/IntegrationService"
|
|
11
|
-
import { captureErrors, captureLogs } from "../test-utils"
|
|
12
|
-
import { work, workPrepare } from "./work"
|
|
13
|
-
import type { PickWorkTarget } from "../workbase/work-target"
|
|
14
|
-
import type { PickWorkbase } from "../workbase/workbase-choice"
|
|
15
|
-
import type { Progress } from "../utils/progress"
|
|
16
|
-
|
|
17
|
-
type ExecutionWorkspace = Effect.Effect.Success<
|
|
18
|
-
ReturnType<WorktreeService["materialize"]>
|
|
19
|
-
>
|
|
20
|
-
|
|
21
|
-
const singlePhaseWorkspace: ExecutionWorkspace = {
|
|
22
|
-
root: "/workbase",
|
|
23
|
-
taskPath: "/workbase/tasks/example/TASK.md",
|
|
24
|
-
phasePath: null,
|
|
25
|
-
codePath: "/workbase/tasks/example/code",
|
|
26
|
-
writablePath: "/workbase/tasks/example/code/agency",
|
|
27
|
-
reviewPath: null,
|
|
28
|
-
repo: "agency",
|
|
29
|
-
repos: [],
|
|
30
|
-
dryRun: false,
|
|
31
|
-
checkouts: [],
|
|
32
|
-
operations: [],
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const multiPhaseWorkspace: ExecutionWorkspace = {
|
|
36
|
-
root: "/workbase",
|
|
37
|
-
taskPath: "/workbase/tasks/example/TASK.md",
|
|
38
|
-
phasePath: "/workbase/tasks/example/phases/implementation/PHASE.md",
|
|
39
|
-
codePath: "/workbase/tasks/example/phases/implementation/code",
|
|
40
|
-
writablePath: "/workbase/tasks/example/phases/implementation/code/agency",
|
|
41
|
-
reviewPath: null,
|
|
42
|
-
repo: "agency",
|
|
43
|
-
repos: [],
|
|
44
|
-
dryRun: false,
|
|
45
|
-
checkouts: [],
|
|
46
|
-
operations: [],
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const taskDirectory = "/workbase/tasks/example"
|
|
50
|
-
const phaseDirectory = `${taskDirectory}/phases/implementation`
|
|
51
|
-
|
|
52
|
-
interface HarnessOptions {
|
|
53
|
-
readonly workspace?: ExecutionWorkspace
|
|
54
|
-
readonly materializeError?: Error
|
|
55
|
-
readonly available?: Readonly<Record<string, boolean>>
|
|
56
|
-
readonly chooserCommand?: readonly string[]
|
|
57
|
-
readonly agent?: "opencode2" | "opencode" | "pi" | "claude"
|
|
58
|
-
readonly agents?: Record<
|
|
59
|
-
string,
|
|
60
|
-
{
|
|
61
|
-
command: readonly [string, ...string[]]
|
|
62
|
-
autoCommand?: readonly [string, ...string[]]
|
|
63
|
-
resumeCommand?: readonly [string, ...string[]]
|
|
64
|
-
autoResumeCommand?: readonly [string, ...string[]]
|
|
65
|
-
environment?: Record<string, string>
|
|
66
|
-
}
|
|
67
|
-
>
|
|
68
|
-
readonly multiPhaseTasks?: readonly string[]
|
|
69
|
-
readonly epicRecords?: readonly any[]
|
|
70
|
-
readonly taskRecords?: readonly any[]
|
|
71
|
-
readonly phaseRecords?: readonly any[]
|
|
72
|
-
readonly outsideWorkbase?: boolean
|
|
73
|
-
readonly registeredWorkbases?: readonly string[]
|
|
74
|
-
readonly existingDirectories?: readonly string[]
|
|
75
|
-
readonly guardError?: Error
|
|
76
|
-
readonly launchError?: Error
|
|
77
|
-
readonly workTargetIds?: readonly string[]
|
|
78
|
-
readonly taskStatus?: "open" | "working" | "delegated" | "done" | "dropped"
|
|
79
|
-
readonly phaseStatus?: "open" | "working" | "delegated" | "done" | "dropped"
|
|
80
|
-
readonly taskStatuses?: Readonly<
|
|
81
|
-
Record<string, "open" | "working" | "delegated" | "done" | "dropped">
|
|
82
|
-
>
|
|
83
|
-
readonly phaseStatuses?: Readonly<
|
|
84
|
-
Record<string, "open" | "working" | "delegated" | "done" | "dropped">
|
|
85
|
-
>
|
|
86
|
-
readonly taskRepo?: string
|
|
87
|
-
readonly existingPaths?: readonly string[]
|
|
88
|
-
readonly validationIssues?: readonly { path: string; message: string }[]
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const createHarness = (options: HarnessOptions = {}) => {
|
|
92
|
-
const events: string[] = []
|
|
93
|
-
const probes: string[] = []
|
|
94
|
-
const statusUpdates: string[] = []
|
|
95
|
-
const shownTasks: string[] = []
|
|
96
|
-
const progressUpdates: string[] = []
|
|
97
|
-
let integrationSyncs = 0
|
|
98
|
-
const guards: Array<{ target: string; override?: boolean }> = []
|
|
99
|
-
const launches: Array<{
|
|
100
|
-
cli: string
|
|
101
|
-
args: readonly string[]
|
|
102
|
-
cwd: string
|
|
103
|
-
}> = []
|
|
104
|
-
const launchEnvironments: Array<Readonly<Record<string, string>>> = []
|
|
105
|
-
const launchProcessEnvironments: Array<Record<string, string | undefined>> =
|
|
106
|
-
[]
|
|
107
|
-
const materializeOptions: Array<
|
|
108
|
-
Parameters<WorktreeService["materialize"]>[3]
|
|
109
|
-
> = []
|
|
110
|
-
const taskStatuses = { ...options.taskStatuses }
|
|
111
|
-
const phaseStatuses = { ...options.phaseStatuses }
|
|
112
|
-
const worktrees = {
|
|
113
|
-
materialize: (
|
|
114
|
-
_taskId: string,
|
|
115
|
-
_phaseId?: string,
|
|
116
|
-
_root?: string,
|
|
117
|
-
commandOptions?: Parameters<WorktreeService["materialize"]>[3],
|
|
118
|
-
) => {
|
|
119
|
-
events.push("materialize")
|
|
120
|
-
materializeOptions.push(commandOptions)
|
|
121
|
-
return options.materializeError
|
|
122
|
-
? Effect.fail(options.materializeError)
|
|
123
|
-
: Effect.succeed(options.workspace ?? singlePhaseWorkspace)
|
|
124
|
-
},
|
|
125
|
-
}
|
|
126
|
-
const workbase = {
|
|
127
|
-
loadGlobalConfig: () => Effect.succeed({ agent: options.agent }),
|
|
128
|
-
discover: (path: string) =>
|
|
129
|
-
options.outsideWorkbase && path === "/outside"
|
|
130
|
-
? Effect.fail({
|
|
131
|
-
_tag: "WorkbaseNotFoundError" as const,
|
|
132
|
-
message: "No Agency workbase found from /outside",
|
|
133
|
-
})
|
|
134
|
-
: Effect.succeed("/workbase"),
|
|
135
|
-
listRegistered: () => Effect.succeed(options.registeredWorkbases ?? []),
|
|
136
|
-
getDefault: () => Effect.succeed(undefined),
|
|
137
|
-
loadConfig: () =>
|
|
138
|
-
Effect.succeed({
|
|
139
|
-
root: "/workbase",
|
|
140
|
-
config: {
|
|
141
|
-
version: 2 as const,
|
|
142
|
-
chooserCommand: options.chooserCommand,
|
|
143
|
-
agents: options.agents,
|
|
144
|
-
},
|
|
145
|
-
}),
|
|
146
|
-
repositoryAliases: () => Effect.succeed(["agency"]),
|
|
147
|
-
validate: () => {
|
|
148
|
-
const issues = options.validationIssues ?? []
|
|
149
|
-
return Effect.succeed({
|
|
150
|
-
root: "/workbase",
|
|
151
|
-
issues,
|
|
152
|
-
epicCount: 0,
|
|
153
|
-
taskCount: 1,
|
|
154
|
-
phaseCount: 0,
|
|
155
|
-
valid: issues.length === 0,
|
|
156
|
-
})
|
|
157
|
-
},
|
|
158
|
-
}
|
|
159
|
-
const epics = {
|
|
160
|
-
show: (id: string) =>
|
|
161
|
-
Effect.succeed({
|
|
162
|
-
id,
|
|
163
|
-
path: `/workbase/epics/${id}/EPIC.md`,
|
|
164
|
-
data: { tasks: [] },
|
|
165
|
-
}),
|
|
166
|
-
list: () => Effect.succeed(options.epicRecords ?? []),
|
|
167
|
-
}
|
|
168
|
-
const tasks = {
|
|
169
|
-
show: (id: string) => {
|
|
170
|
-
shownTasks.push(id)
|
|
171
|
-
return Effect.succeed({
|
|
172
|
-
id,
|
|
173
|
-
path: `/workbase/tasks/${id}/TASK.md`,
|
|
174
|
-
revision: "a".repeat(64),
|
|
175
|
-
data: options.multiPhaseTasks?.includes(id)
|
|
176
|
-
? { phases: [] }
|
|
177
|
-
: {
|
|
178
|
-
repo: options.taskRepo ?? "agency",
|
|
179
|
-
branch: `task/${id}`,
|
|
180
|
-
base: "main",
|
|
181
|
-
status: taskStatuses[id] ?? options.taskStatus ?? "open",
|
|
182
|
-
},
|
|
183
|
-
})
|
|
184
|
-
},
|
|
185
|
-
list: () => Effect.succeed(options.taskRecords ?? []),
|
|
186
|
-
setStatus: (id: string, status: string) => {
|
|
187
|
-
statusUpdates.push(`task:${id}:${status}`)
|
|
188
|
-
taskStatuses[id] = status as
|
|
189
|
-
| "open"
|
|
190
|
-
| "working"
|
|
191
|
-
| "delegated"
|
|
192
|
-
| "done"
|
|
193
|
-
| "dropped"
|
|
194
|
-
return Effect.void
|
|
195
|
-
},
|
|
196
|
-
}
|
|
197
|
-
const phases = {
|
|
198
|
-
show: (taskId: string, id: string) =>
|
|
199
|
-
Effect.succeed({
|
|
200
|
-
taskId,
|
|
201
|
-
id,
|
|
202
|
-
path: `/workbase/tasks/${taskId}/phases/${id}/PHASE.md`,
|
|
203
|
-
revision: "b".repeat(64),
|
|
204
|
-
data: {
|
|
205
|
-
repo: "agency",
|
|
206
|
-
branch: `task/${id}`,
|
|
207
|
-
base: "main",
|
|
208
|
-
status:
|
|
209
|
-
phaseStatuses[`${taskId}/${id}`] ?? options.phaseStatus ?? "open",
|
|
210
|
-
},
|
|
211
|
-
}),
|
|
212
|
-
list: () => Effect.succeed(options.phaseRecords ?? []),
|
|
213
|
-
setStatus: (taskId: string, id: string, status: string) => {
|
|
214
|
-
statusUpdates.push(`phase:${taskId}:${id}:${status}`)
|
|
215
|
-
phaseStatuses[`${taskId}/${id}`] = status as
|
|
216
|
-
| "open"
|
|
217
|
-
| "working"
|
|
218
|
-
| "delegated"
|
|
219
|
-
| "done"
|
|
220
|
-
| "dropped"
|
|
221
|
-
return Effect.void
|
|
222
|
-
},
|
|
223
|
-
}
|
|
224
|
-
const readiness = {
|
|
225
|
-
getWorkTargetIds: () =>
|
|
226
|
-
Effect.succeed(
|
|
227
|
-
new Set(
|
|
228
|
-
options.workTargetIds ?? [
|
|
229
|
-
...(options.epicRecords ?? []).map(
|
|
230
|
-
(record: any) => `epic:${record.id}`,
|
|
231
|
-
),
|
|
232
|
-
...(options.taskRecords ?? []).map((record: any) =>
|
|
233
|
-
"phases" in record.data
|
|
234
|
-
? `task:${record.id}`
|
|
235
|
-
: `execution-unit:task/${record.id}`,
|
|
236
|
-
),
|
|
237
|
-
...(options.phaseRecords ?? []).map(
|
|
238
|
-
(record: any) =>
|
|
239
|
-
`execution-unit:phase/${record.taskId}/${record.id}`,
|
|
240
|
-
),
|
|
241
|
-
],
|
|
242
|
-
),
|
|
243
|
-
),
|
|
244
|
-
guardWorkTarget: (target: string, _root: string, override?: boolean) => {
|
|
245
|
-
if (options.guardError || override) events.push("guard")
|
|
246
|
-
guards.push({ target, override })
|
|
247
|
-
return options.guardError && !override
|
|
248
|
-
? Effect.fail(options.guardError)
|
|
249
|
-
: Effect.void
|
|
250
|
-
},
|
|
251
|
-
}
|
|
252
|
-
const integrations = {
|
|
253
|
-
sync: () => {
|
|
254
|
-
integrationSyncs += 1
|
|
255
|
-
return Effect.succeed({
|
|
256
|
-
root: "/workbase",
|
|
257
|
-
files: [
|
|
258
|
-
{
|
|
259
|
-
name: "opencode",
|
|
260
|
-
state: "managed",
|
|
261
|
-
},
|
|
262
|
-
],
|
|
263
|
-
})
|
|
264
|
-
},
|
|
265
|
-
syncRoot: () => {
|
|
266
|
-
integrationSyncs += 1
|
|
267
|
-
return Effect.succeed({
|
|
268
|
-
root: "/workbase",
|
|
269
|
-
files: [
|
|
270
|
-
{
|
|
271
|
-
name: "opencode",
|
|
272
|
-
state: "managed",
|
|
273
|
-
},
|
|
274
|
-
],
|
|
275
|
-
})
|
|
276
|
-
},
|
|
277
|
-
}
|
|
278
|
-
const fs = {
|
|
279
|
-
isDirectory: (path: string) =>
|
|
280
|
-
Effect.succeed(
|
|
281
|
-
path === "/workbase/epics" || path === "/workbase/tasks"
|
|
282
|
-
? false
|
|
283
|
-
: (options.existingDirectories?.includes(path) ?? true),
|
|
284
|
-
),
|
|
285
|
-
readFile: (path: string) =>
|
|
286
|
-
Effect.succeed(path.endsWith("agency.json") ? '{"version":2}\n' : ""),
|
|
287
|
-
readDirectory: () => Effect.succeed([]),
|
|
288
|
-
exists: (path: string) =>
|
|
289
|
-
Effect.succeed(options.existingPaths?.includes(path) ?? false),
|
|
290
|
-
realPath: (path: string) => Effect.succeed(path),
|
|
291
|
-
runCommand: (args: readonly string[]) => {
|
|
292
|
-
const cli = args[1]!
|
|
293
|
-
events.push(`probe:${cli}`)
|
|
294
|
-
probes.push(cli)
|
|
295
|
-
return Effect.succeed({
|
|
296
|
-
exitCode: options.available?.[cli] === false ? 1 : 0,
|
|
297
|
-
stdout: "",
|
|
298
|
-
stderr: "",
|
|
299
|
-
})
|
|
300
|
-
},
|
|
301
|
-
}
|
|
302
|
-
const launch = (
|
|
303
|
-
cli: string,
|
|
304
|
-
args: readonly string[],
|
|
305
|
-
cwd: string,
|
|
306
|
-
environment: Readonly<Record<string, string>>,
|
|
307
|
-
) => {
|
|
308
|
-
events.push(`launch:${cli}`)
|
|
309
|
-
launches.push({ cli, args, cwd })
|
|
310
|
-
launchEnvironments.push(environment)
|
|
311
|
-
launchProcessEnvironments.push({
|
|
312
|
-
AGENCY_SESSION_ID: process.env.AGENCY_SESSION_ID,
|
|
313
|
-
AGENCY_TARGET: process.env.AGENCY_TARGET,
|
|
314
|
-
AGENCY_PROMPT: process.env.AGENCY_PROMPT,
|
|
315
|
-
})
|
|
316
|
-
if (options.launchError) throw options.launchError
|
|
317
|
-
}
|
|
318
|
-
const defaultPick: PickWorkTarget = () => Effect.succeed(null)
|
|
319
|
-
const defaultPickWorkbase: PickWorkbase = () => Effect.succeed(null)
|
|
320
|
-
const progress: Progress = {
|
|
321
|
-
start: (message) => progressUpdates.push(`start:${message}`),
|
|
322
|
-
succeed: (message) => progressUpdates.push(`succeed:${message}`),
|
|
323
|
-
fail: (message) => progressUpdates.push(`fail:${message}`),
|
|
324
|
-
}
|
|
325
|
-
const run = (
|
|
326
|
-
commandOptions: Parameters<typeof work>[0],
|
|
327
|
-
pick: PickWorkTarget = defaultPick,
|
|
328
|
-
pickBase: PickWorkbase = defaultPickWorkbase,
|
|
329
|
-
preserveSession = false,
|
|
330
|
-
) => {
|
|
331
|
-
const agencySessionId = process.env.AGENCY_SESSION_ID
|
|
332
|
-
if (!preserveSession) delete process.env.AGENCY_SESSION_ID
|
|
333
|
-
return Effect.runPromise(
|
|
334
|
-
work(commandOptions, launch, pick, progress, pickBase).pipe(
|
|
335
|
-
Effect.provideService(WorktreeService, worktrees as never),
|
|
336
|
-
Effect.provideService(FileSystemService, fs as never),
|
|
337
|
-
Effect.provideService(WorkbaseService, workbase as never),
|
|
338
|
-
Effect.provideService(EpicService, epics as never),
|
|
339
|
-
Effect.provideService(TaskService, tasks as never),
|
|
340
|
-
Effect.provideService(PhaseService, phases as never),
|
|
341
|
-
Effect.provideService(ReadinessService, readiness as never),
|
|
342
|
-
Effect.provideService(IntegrationService, integrations as never),
|
|
343
|
-
) as Effect.Effect<void, unknown, never>,
|
|
344
|
-
).finally(() => {
|
|
345
|
-
if (agencySessionId !== undefined) {
|
|
346
|
-
process.env.AGENCY_SESSION_ID = agencySessionId
|
|
347
|
-
}
|
|
348
|
-
})
|
|
349
|
-
}
|
|
350
|
-
const runPrepare = (commandOptions: Parameters<typeof workPrepare>[0]) =>
|
|
351
|
-
Effect.runPromise(
|
|
352
|
-
workPrepare(commandOptions).pipe(
|
|
353
|
-
Effect.provideService(WorktreeService, worktrees as never),
|
|
354
|
-
Effect.provideService(FileSystemService, fs as never),
|
|
355
|
-
Effect.provideService(WorkbaseService, workbase as never),
|
|
356
|
-
Effect.provideService(TaskService, tasks as never),
|
|
357
|
-
Effect.provideService(PhaseService, phases as never),
|
|
358
|
-
Effect.provideService(ReadinessService, readiness as never),
|
|
359
|
-
) as Effect.Effect<void, unknown, never>,
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
return {
|
|
363
|
-
events,
|
|
364
|
-
probes,
|
|
365
|
-
launches,
|
|
366
|
-
launchEnvironments,
|
|
367
|
-
launchProcessEnvironments,
|
|
368
|
-
materializeOptions,
|
|
369
|
-
statusUpdates,
|
|
370
|
-
taskStatuses,
|
|
371
|
-
phaseStatuses,
|
|
372
|
-
shownTasks,
|
|
373
|
-
progressUpdates,
|
|
374
|
-
guards,
|
|
375
|
-
get integrationSyncs() {
|
|
376
|
-
return integrationSyncs
|
|
377
|
-
},
|
|
378
|
-
run,
|
|
379
|
-
runPrepare,
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
describe("work command", () => {
|
|
384
|
-
test("reconciles managed integration files before preparing work", async () => {
|
|
385
|
-
const harness = createHarness()
|
|
386
|
-
|
|
387
|
-
await harness.run({ taskId: "example", opencode: true })
|
|
388
|
-
|
|
389
|
-
expect(harness.integrationSyncs).toBe(1)
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
test("guards execution targets before materialization and honors --force", async () => {
|
|
393
|
-
const blocked = createHarness({ guardError: new Error("blocked") })
|
|
394
|
-
await expect(
|
|
395
|
-
blocked.run({ taskId: "example", opencode: true }),
|
|
396
|
-
).rejects.toThrow("blocked")
|
|
397
|
-
expect(blocked.events).toEqual(["guard"])
|
|
398
|
-
expect(blocked.guards).toEqual([
|
|
399
|
-
{ target: "execution-unit:task/example", override: undefined },
|
|
400
|
-
])
|
|
401
|
-
|
|
402
|
-
const forced = createHarness({ guardError: new Error("blocked") })
|
|
403
|
-
await forced.run({ taskId: "example", opencode: true, force: true })
|
|
404
|
-
expect(forced.events).toEqual([
|
|
405
|
-
"guard",
|
|
406
|
-
"materialize",
|
|
407
|
-
"probe:opencode",
|
|
408
|
-
"launch:opencode",
|
|
409
|
-
])
|
|
410
|
-
expect(forced.guards[0]).toEqual({
|
|
411
|
-
target: "execution-unit:task/example",
|
|
412
|
-
override: true,
|
|
413
|
-
})
|
|
414
|
-
expect(forced.materializeOptions[0]?.validationAlreadyPerformed).toBe(false)
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
test("reuses successful readiness validation during materialization", async () => {
|
|
418
|
-
const harness = createHarness()
|
|
419
|
-
|
|
420
|
-
await harness.run({ taskId: "example", opencode: true })
|
|
421
|
-
|
|
422
|
-
expect(harness.materializeOptions[0]?.validationAlreadyPerformed).toBe(true)
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
test("reopens forced terminal tasks through open before launching as working", async () => {
|
|
426
|
-
for (const previousStatus of ["done", "dropped"] as const) {
|
|
427
|
-
const harness = createHarness({
|
|
428
|
-
taskStatuses: { example: previousStatus },
|
|
429
|
-
})
|
|
430
|
-
const output = await captureLogs(() =>
|
|
431
|
-
harness.run({ taskId: "example", opencode: true, force: true }),
|
|
432
|
-
)
|
|
433
|
-
|
|
434
|
-
expect(harness.statusUpdates).toEqual([
|
|
435
|
-
"task:example:open",
|
|
436
|
-
"task:example:working",
|
|
437
|
-
])
|
|
438
|
-
expect(harness.taskStatuses.example).toBe("working")
|
|
439
|
-
expect(output).toEqual([
|
|
440
|
-
`Reopened task/example from ${previousStatus} as working`,
|
|
441
|
-
])
|
|
442
|
-
}
|
|
443
|
-
})
|
|
444
|
-
|
|
445
|
-
test("reopens forced terminal phases through open before launching as working", async () => {
|
|
446
|
-
for (const previousStatus of ["done", "dropped"] as const) {
|
|
447
|
-
const harness = createHarness({
|
|
448
|
-
workspace: multiPhaseWorkspace,
|
|
449
|
-
multiPhaseTasks: ["example"],
|
|
450
|
-
phaseStatuses: { "example/implementation": previousStatus },
|
|
451
|
-
})
|
|
452
|
-
const output = await captureLogs(() =>
|
|
453
|
-
harness.run({
|
|
454
|
-
taskId: "example",
|
|
455
|
-
phaseId: "implementation",
|
|
456
|
-
opencode: true,
|
|
457
|
-
force: true,
|
|
458
|
-
}),
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
expect(harness.statusUpdates).toEqual([
|
|
462
|
-
"phase:example:implementation:open",
|
|
463
|
-
"phase:example:implementation:working",
|
|
464
|
-
])
|
|
465
|
-
expect(harness.phaseStatuses["example/implementation"]).toBe("working")
|
|
466
|
-
expect(output).toEqual([
|
|
467
|
-
`Reopened phase/example/implementation from ${previousStatus} as working`,
|
|
468
|
-
])
|
|
469
|
-
}
|
|
470
|
-
})
|
|
471
|
-
|
|
472
|
-
test("reports a forced reopen as one machine result", async () => {
|
|
473
|
-
const harness = createHarness({ taskStatuses: { example: "done" } })
|
|
474
|
-
const output = await captureLogs(() =>
|
|
475
|
-
harness.run({
|
|
476
|
-
taskId: "example",
|
|
477
|
-
opencode: true,
|
|
478
|
-
force: true,
|
|
479
|
-
json: true,
|
|
480
|
-
}),
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
expect(JSON.parse(output.join("\n"))).toEqual({
|
|
484
|
-
target: "task/example",
|
|
485
|
-
reopened: true,
|
|
486
|
-
previousStatus: "done",
|
|
487
|
-
status: "working",
|
|
488
|
-
})
|
|
489
|
-
})
|
|
490
|
-
|
|
491
|
-
test("offers only launchable targets to the interactive chooser", async () => {
|
|
492
|
-
const harness = createHarness({
|
|
493
|
-
taskRecords: [
|
|
494
|
-
{
|
|
495
|
-
id: "ready",
|
|
496
|
-
path: "/workbase/tasks/ready/TASK.md",
|
|
497
|
-
data: { status: "open" },
|
|
498
|
-
},
|
|
499
|
-
{
|
|
500
|
-
id: "blocked",
|
|
501
|
-
path: "/workbase/tasks/blocked/TASK.md",
|
|
502
|
-
data: { status: "open" },
|
|
503
|
-
},
|
|
504
|
-
],
|
|
505
|
-
workTargetIds: ["execution-unit:task/ready"],
|
|
506
|
-
})
|
|
507
|
-
let labels: readonly string[] = []
|
|
508
|
-
const pick: PickWorkTarget = (choices) => {
|
|
509
|
-
labels = choices.map((choice) => choice.plainLabel)
|
|
510
|
-
return Effect.succeed(null)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await harness.run({ cwd: "/workbase" }, pick)
|
|
514
|
-
expect(labels).toEqual(["[open] task ready"])
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
test("prepares without launching or changing lifecycle status", async () => {
|
|
518
|
-
const harness = createHarness({ existingDirectories: [] })
|
|
519
|
-
|
|
520
|
-
const [output] = await captureLogs(() =>
|
|
521
|
-
harness.runPrepare({
|
|
522
|
-
cwd: "/workbase",
|
|
523
|
-
directory: "example",
|
|
524
|
-
json: true,
|
|
525
|
-
dryRun: true,
|
|
526
|
-
}),
|
|
527
|
-
)
|
|
528
|
-
|
|
529
|
-
expect(harness.events).toEqual(["materialize"])
|
|
530
|
-
expect(harness.launches).toEqual([])
|
|
531
|
-
expect(harness.statusUpdates).toEqual([])
|
|
532
|
-
expect(harness.materializeOptions[0]).toMatchObject({
|
|
533
|
-
json: true,
|
|
534
|
-
dryRun: true,
|
|
535
|
-
validationAlreadyPerformed: true,
|
|
536
|
-
})
|
|
537
|
-
const result = JSON.parse(output!)
|
|
538
|
-
expect(result.validationEvidence.status).toBe("refreshed")
|
|
539
|
-
expect(result.validationEvidence.reasons).toEqual(["not-supplied"])
|
|
540
|
-
expect(result.execution).toMatchObject({
|
|
541
|
-
capability: "agency-execution-v1",
|
|
542
|
-
mode: "preview",
|
|
543
|
-
executionIdentity: { target: "execution-unit:task/example" },
|
|
544
|
-
workspace: { state: "planned" },
|
|
545
|
-
commands: {
|
|
546
|
-
work: { argv: ["agency", "work", ".", "--auto"] },
|
|
547
|
-
},
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
const [reusedOutput] = await captureLogs(() =>
|
|
551
|
-
harness.runPrepare({
|
|
552
|
-
cwd: "/workbase",
|
|
553
|
-
directory: "example",
|
|
554
|
-
json: true,
|
|
555
|
-
dryRun: true,
|
|
556
|
-
evidence: JSON.stringify(result.validationEvidence.evidence),
|
|
557
|
-
}),
|
|
558
|
-
)
|
|
559
|
-
expect(JSON.parse(reusedOutput!).validationEvidence).toEqual(
|
|
560
|
-
expect.objectContaining({ status: "reused", reasons: [] }),
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
const conflicting = createHarness({
|
|
564
|
-
existingDirectories: [],
|
|
565
|
-
taskRepo: "other",
|
|
566
|
-
})
|
|
567
|
-
await expect(
|
|
568
|
-
conflicting.runPrepare({
|
|
569
|
-
cwd: "/workbase",
|
|
570
|
-
directory: "example",
|
|
571
|
-
dryRun: true,
|
|
572
|
-
evidence: JSON.stringify(result.validationEvidence.evidence),
|
|
573
|
-
}),
|
|
574
|
-
).rejects.toThrow("Recalled repository conflicts")
|
|
575
|
-
})
|
|
576
|
-
|
|
577
|
-
test("resolves task and phase document paths", async () => {
|
|
578
|
-
const taskDocument = "/workbase/tasks/example/TASK.md"
|
|
579
|
-
const taskHarness = createHarness({
|
|
580
|
-
existingDirectories: [],
|
|
581
|
-
existingPaths: [taskDocument],
|
|
582
|
-
})
|
|
583
|
-
|
|
584
|
-
await taskHarness.runPrepare({
|
|
585
|
-
directory: taskDocument,
|
|
586
|
-
dryRun: true,
|
|
587
|
-
silent: true,
|
|
588
|
-
})
|
|
589
|
-
expect(taskHarness.guards[0]?.target).toBe("execution-unit:task/example")
|
|
590
|
-
expect(taskHarness.shownTasks).toEqual(["example", "example"])
|
|
591
|
-
|
|
592
|
-
const phaseDocument =
|
|
593
|
-
"/workbase/tasks/example/phases/implementation/PHASE.md"
|
|
594
|
-
const phaseHarness = createHarness({
|
|
595
|
-
existingDirectories: [],
|
|
596
|
-
existingPaths: [phaseDocument],
|
|
597
|
-
multiPhaseTasks: ["example"],
|
|
598
|
-
workspace: multiPhaseWorkspace,
|
|
599
|
-
})
|
|
600
|
-
|
|
601
|
-
await phaseHarness.runPrepare({
|
|
602
|
-
directory: phaseDocument,
|
|
603
|
-
dryRun: true,
|
|
604
|
-
silent: true,
|
|
605
|
-
})
|
|
606
|
-
expect(phaseHarness.guards[0]?.target).toBe(
|
|
607
|
-
"execution-unit:phase/example/implementation",
|
|
608
|
-
)
|
|
609
|
-
expect(phaseHarness.materializeOptions).toHaveLength(1)
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
test("returns structured validation issues before readiness or materialization", async () => {
|
|
613
|
-
const harness = createHarness({
|
|
614
|
-
existingDirectories: [],
|
|
615
|
-
validationIssues: [
|
|
616
|
-
{
|
|
617
|
-
path: "tasks/example/TASK.md",
|
|
618
|
-
message: "Repository alias 'missing' is not configured",
|
|
619
|
-
},
|
|
620
|
-
],
|
|
621
|
-
})
|
|
622
|
-
|
|
623
|
-
await expect(
|
|
624
|
-
harness.runPrepare({
|
|
625
|
-
cwd: "/workbase",
|
|
626
|
-
directory: "example",
|
|
627
|
-
dryRun: true,
|
|
628
|
-
}),
|
|
629
|
-
).rejects.toThrow(
|
|
630
|
-
"Workbase validation failed with 1 issue:\n- tasks/example/TASK.md: Repository alias 'missing' is not configured",
|
|
631
|
-
)
|
|
632
|
-
expect(harness.guards).toEqual([])
|
|
633
|
-
expect(harness.events).toEqual([])
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
test("force prepares a phase blocked by an active dependency without launching or changing status", async () => {
|
|
637
|
-
const blocked = createHarness({
|
|
638
|
-
workspace: multiPhaseWorkspace,
|
|
639
|
-
multiPhaseTasks: ["example"],
|
|
640
|
-
guardError: new Error("Phase dependency is working"),
|
|
641
|
-
})
|
|
642
|
-
await expect(
|
|
643
|
-
blocked.runPrepare({
|
|
644
|
-
cwd: "/workbase",
|
|
645
|
-
taskId: "example",
|
|
646
|
-
phaseId: "implementation",
|
|
647
|
-
}),
|
|
648
|
-
).rejects.toThrow("Phase dependency is working")
|
|
649
|
-
expect(blocked.events).toEqual(["guard"])
|
|
650
|
-
|
|
651
|
-
const forced = createHarness({
|
|
652
|
-
workspace: multiPhaseWorkspace,
|
|
653
|
-
multiPhaseTasks: ["example"],
|
|
654
|
-
guardError: new Error("Phase dependency is working"),
|
|
655
|
-
})
|
|
656
|
-
await forced.runPrepare({
|
|
657
|
-
cwd: "/workbase",
|
|
658
|
-
taskId: "example",
|
|
659
|
-
phaseId: "implementation",
|
|
660
|
-
force: true,
|
|
661
|
-
silent: true,
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
expect(forced.guards).toEqual([
|
|
665
|
-
{
|
|
666
|
-
target: "execution-unit:phase/example/implementation",
|
|
667
|
-
override: true,
|
|
668
|
-
},
|
|
669
|
-
])
|
|
670
|
-
expect(forced.events).toEqual(["guard", "materialize"])
|
|
671
|
-
expect(forced.materializeOptions[0]).toMatchObject({
|
|
672
|
-
force: true,
|
|
673
|
-
validationAlreadyPerformed: true,
|
|
674
|
-
})
|
|
675
|
-
expect(forced.launches).toEqual([])
|
|
676
|
-
expect(forced.statusUpdates).toEqual([])
|
|
677
|
-
})
|
|
678
|
-
|
|
679
|
-
test("launches an epic agent from an epic directory", async () => {
|
|
680
|
-
const harness = createHarness()
|
|
681
|
-
|
|
682
|
-
await harness.run({
|
|
683
|
-
cwd: "/workbase/epics/delivery",
|
|
684
|
-
directory: ".",
|
|
685
|
-
opencode: true,
|
|
686
|
-
auto: true,
|
|
687
|
-
})
|
|
688
|
-
|
|
689
|
-
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
690
|
-
expect(harness.launches[0]).toEqual({
|
|
691
|
-
cli: "opencode",
|
|
692
|
-
args: [
|
|
693
|
-
"opencode",
|
|
694
|
-
"--prompt",
|
|
695
|
-
"Agency worker launch target: epic:delivery. Work on the epic. Read /workbase/epics/delivery/EPIC.md.",
|
|
696
|
-
],
|
|
697
|
-
cwd: "/workbase/epics/delivery",
|
|
698
|
-
})
|
|
699
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
700
|
-
expect(
|
|
701
|
-
harness.launchEnvironments[0]?.OPENCODE_CONFIG_CONTENT,
|
|
702
|
-
).toBeUndefined()
|
|
703
|
-
})
|
|
704
|
-
|
|
705
|
-
test("resolves an existing positional path before treating it as a task ID", async () => {
|
|
706
|
-
const harness = createHarness({
|
|
707
|
-
existingDirectories: ["/workbase/tasks/delivery"],
|
|
708
|
-
})
|
|
709
|
-
|
|
710
|
-
await harness.run({
|
|
711
|
-
cwd: "/workbase/tasks/delivery",
|
|
712
|
-
directory: ".",
|
|
713
|
-
opencode: true,
|
|
714
|
-
})
|
|
715
|
-
|
|
716
|
-
expect(harness.shownTasks).toEqual(["delivery", "delivery"])
|
|
717
|
-
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
718
|
-
})
|
|
719
|
-
|
|
720
|
-
test("treats a positional value as a task ID when it is not a directory", async () => {
|
|
721
|
-
const harness = createHarness({ existingDirectories: [] })
|
|
722
|
-
|
|
723
|
-
await harness.run({
|
|
724
|
-
cwd: "/workbase",
|
|
725
|
-
directory: "delivery",
|
|
726
|
-
opencode: true,
|
|
727
|
-
})
|
|
728
|
-
|
|
729
|
-
expect(harness.shownTasks).toEqual(["delivery", "delivery"])
|
|
730
|
-
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
731
|
-
})
|
|
732
|
-
|
|
733
|
-
test("launches a multi-phase task agent without materializing", async () => {
|
|
734
|
-
const harness = createHarness({ multiPhaseTasks: ["delivery"] })
|
|
735
|
-
|
|
736
|
-
await harness.run({
|
|
737
|
-
cwd: "/workbase/tasks/delivery",
|
|
738
|
-
directory: ".",
|
|
739
|
-
opencode: true,
|
|
740
|
-
auto: true,
|
|
741
|
-
})
|
|
742
|
-
|
|
743
|
-
expect(harness.events).toEqual(["probe:opencode", "launch:opencode"])
|
|
744
|
-
expect(harness.launches[0]).toEqual({
|
|
745
|
-
cli: "opencode",
|
|
746
|
-
args: [
|
|
747
|
-
"opencode",
|
|
748
|
-
"--prompt",
|
|
749
|
-
"Agency worker launch target: task:delivery. Work on the task. Read /workbase/tasks/delivery/TASK.md.",
|
|
750
|
-
],
|
|
751
|
-
cwd: "/workbase/tasks/delivery",
|
|
752
|
-
})
|
|
753
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
754
|
-
})
|
|
755
|
-
|
|
756
|
-
test("infers a phase from a nested checkout directory", async () => {
|
|
757
|
-
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
758
|
-
|
|
759
|
-
await harness.run({
|
|
760
|
-
cwd: "/workbase/tasks/example/phases/implementation/code/agency/src",
|
|
761
|
-
directory: ".",
|
|
762
|
-
opencode: true,
|
|
763
|
-
auto: true,
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
expect(harness.events).toEqual([
|
|
767
|
-
"materialize",
|
|
768
|
-
"probe:opencode",
|
|
769
|
-
"launch:opencode",
|
|
770
|
-
])
|
|
771
|
-
expect(harness.launches[0]).toEqual({
|
|
772
|
-
cli: "opencode",
|
|
773
|
-
args: [
|
|
774
|
-
"opencode",
|
|
775
|
-
"--prompt",
|
|
776
|
-
"Agency worker launch target: execution-unit:phase/example/implementation. Start the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
777
|
-
],
|
|
778
|
-
cwd: phaseDirectory,
|
|
779
|
-
})
|
|
780
|
-
expect(harness.statusUpdates).toEqual([
|
|
781
|
-
"phase:example:implementation:working",
|
|
782
|
-
])
|
|
783
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
784
|
-
})
|
|
785
|
-
|
|
786
|
-
test("infers a single-phase task from a nested checkout directory", async () => {
|
|
787
|
-
const harness = createHarness()
|
|
788
|
-
|
|
789
|
-
await harness.run({
|
|
790
|
-
cwd: "/workbase/tasks/example/code/agency/src",
|
|
791
|
-
directory: ".",
|
|
792
|
-
opencode: true,
|
|
793
|
-
})
|
|
794
|
-
|
|
795
|
-
expect(harness.events[0]).toBe("materialize")
|
|
796
|
-
expect(harness.launches[0]?.cwd).toBe(taskDirectory)
|
|
797
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
798
|
-
})
|
|
799
|
-
|
|
800
|
-
test("selects a target when no directory is provided", async () => {
|
|
801
|
-
const phase = {
|
|
802
|
-
taskId: "delivery",
|
|
803
|
-
id: "build",
|
|
804
|
-
path: "/workbase/tasks/delivery/phases/build/PHASE.md",
|
|
805
|
-
data: {},
|
|
806
|
-
}
|
|
807
|
-
const harness = createHarness({
|
|
808
|
-
workspace: {
|
|
809
|
-
...multiPhaseWorkspace,
|
|
810
|
-
taskPath: "/workbase/tasks/delivery/TASK.md",
|
|
811
|
-
phasePath: "/workbase/tasks/delivery/phases/build/PHASE.md",
|
|
812
|
-
},
|
|
813
|
-
taskRecords: [
|
|
814
|
-
{
|
|
815
|
-
id: "delivery",
|
|
816
|
-
path: "/workbase/tasks/delivery/TASK.md",
|
|
817
|
-
data: { phases: [{ id: "build" }] },
|
|
818
|
-
},
|
|
819
|
-
],
|
|
820
|
-
phaseRecords: [phase],
|
|
821
|
-
})
|
|
822
|
-
const pick: PickWorkTarget = (choices) =>
|
|
823
|
-
Effect.succeed(
|
|
824
|
-
choices.find((choice) => choice.label.includes("build"))!.target,
|
|
825
|
-
)
|
|
826
|
-
|
|
827
|
-
await harness.run({ cwd: "/workbase/tasks/example", opencode: true }, pick)
|
|
828
|
-
|
|
829
|
-
expect(harness.events).toEqual([
|
|
830
|
-
"materialize",
|
|
831
|
-
"probe:opencode",
|
|
832
|
-
"launch:opencode",
|
|
833
|
-
])
|
|
834
|
-
expect(harness.launches[0]?.cwd).toBe(
|
|
835
|
-
"/workbase/tasks/delivery/phases/build",
|
|
836
|
-
)
|
|
837
|
-
})
|
|
838
|
-
|
|
839
|
-
test("requires an explicit target when input is disabled", async () => {
|
|
840
|
-
const harness = createHarness()
|
|
841
|
-
|
|
842
|
-
await expect(
|
|
843
|
-
harness.run({
|
|
844
|
-
cwd: "/workbase",
|
|
845
|
-
opencode: true,
|
|
846
|
-
inputAllowed: false,
|
|
847
|
-
}),
|
|
848
|
-
).rejects.toThrow("provide a directory, task ID, or --epic")
|
|
849
|
-
expect(harness.events).toEqual([])
|
|
850
|
-
})
|
|
851
|
-
|
|
852
|
-
test("runs an explicit target when input is disabled", async () => {
|
|
853
|
-
const harness = createHarness({ existingDirectories: [] })
|
|
854
|
-
|
|
855
|
-
await harness.run({
|
|
856
|
-
cwd: "/workbase",
|
|
857
|
-
directory: "example",
|
|
858
|
-
opencode: true,
|
|
859
|
-
inputAllowed: false,
|
|
860
|
-
})
|
|
861
|
-
|
|
862
|
-
expect(harness.events).toEqual([
|
|
863
|
-
"materialize",
|
|
864
|
-
"probe:opencode",
|
|
865
|
-
"launch:opencode",
|
|
866
|
-
])
|
|
867
|
-
})
|
|
868
|
-
|
|
869
|
-
test("selects a registered workbase when local discovery fails", async () => {
|
|
870
|
-
const harness = createHarness({
|
|
871
|
-
outsideWorkbase: true,
|
|
872
|
-
registeredWorkbases: ["/first", "/workbase"],
|
|
873
|
-
})
|
|
874
|
-
const selections: string[][] = []
|
|
875
|
-
const pickBase: PickWorkbase = (workbases) => {
|
|
876
|
-
selections.push([...workbases])
|
|
877
|
-
return Effect.succeed("/workbase")
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
await harness.run(
|
|
881
|
-
{ cwd: "/outside", taskId: "example", opencode: true },
|
|
882
|
-
undefined,
|
|
883
|
-
pickBase,
|
|
884
|
-
)
|
|
885
|
-
|
|
886
|
-
expect(selections).toEqual([["/first", "/workbase"]])
|
|
887
|
-
expect(harness.events).toEqual([
|
|
888
|
-
"materialize",
|
|
889
|
-
"probe:opencode",
|
|
890
|
-
"launch:opencode",
|
|
891
|
-
])
|
|
892
|
-
})
|
|
893
|
-
|
|
894
|
-
test("does not select a registered workbase when input is disabled", async () => {
|
|
895
|
-
const harness = createHarness({
|
|
896
|
-
outsideWorkbase: true,
|
|
897
|
-
registeredWorkbases: ["/workbase"],
|
|
898
|
-
existingDirectories: [],
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
await expect(
|
|
902
|
-
harness.run({
|
|
903
|
-
cwd: "/outside",
|
|
904
|
-
directory: "example",
|
|
905
|
-
opencode: true,
|
|
906
|
-
inputAllowed: false,
|
|
907
|
-
}),
|
|
908
|
-
).rejects.toThrow("provide an explicit path or run Agency from a workbase")
|
|
909
|
-
expect(harness.events).toEqual([])
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
test("explains how to register a workbase when none are known", async () => {
|
|
913
|
-
const harness = createHarness({ outsideWorkbase: true })
|
|
914
|
-
|
|
915
|
-
await expect(harness.run({ cwd: "/outside" })).rejects.toThrow(
|
|
916
|
-
"agency workbase add <path>",
|
|
917
|
-
)
|
|
918
|
-
expect(harness.events).toEqual([])
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
test("passes the configured chooser command to the shared picker", async () => {
|
|
922
|
-
const harness = createHarness({
|
|
923
|
-
chooserCommand: ["gum", "filter"],
|
|
924
|
-
epicRecords: [
|
|
925
|
-
{
|
|
926
|
-
id: "delivery",
|
|
927
|
-
path: "/workbase/epics/delivery/EPIC.md",
|
|
928
|
-
data: { tasks: [] },
|
|
929
|
-
},
|
|
930
|
-
],
|
|
931
|
-
})
|
|
932
|
-
let command: readonly string[] | undefined
|
|
933
|
-
const pick: PickWorkTarget = (_choices, chooserCommand) => {
|
|
934
|
-
command = chooserCommand
|
|
935
|
-
return Effect.succeed(null)
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
await harness.run({ cwd: "/workbase" }, pick)
|
|
939
|
-
|
|
940
|
-
expect(command).toEqual(["gum", "filter"])
|
|
941
|
-
expect(harness.launches).toEqual([])
|
|
942
|
-
})
|
|
943
|
-
|
|
944
|
-
test("rejects conflicting agent flags before materialization", async () => {
|
|
945
|
-
const harness = createHarness()
|
|
946
|
-
|
|
947
|
-
await expect(
|
|
948
|
-
harness.run({ taskId: "example", opencode: true, claude: true }),
|
|
949
|
-
).rejects.toThrow("Cannot combine --agent, --opencode, and --claude")
|
|
950
|
-
expect(harness.events).toEqual([])
|
|
951
|
-
})
|
|
952
|
-
|
|
953
|
-
test("rejects combining explicit epic and task targets", async () => {
|
|
954
|
-
const harness = createHarness()
|
|
955
|
-
|
|
956
|
-
await expect(
|
|
957
|
-
harness.run({ epicId: "delivery", taskId: "example" }),
|
|
958
|
-
).rejects.toThrow("Cannot combine --epic")
|
|
959
|
-
expect(harness.events).toEqual([])
|
|
960
|
-
})
|
|
961
|
-
|
|
962
|
-
test("launches OpenCode in the task directory with explicit context", async () => {
|
|
963
|
-
const harness = createHarness()
|
|
964
|
-
|
|
965
|
-
await harness.run({ taskId: "example", opencode: true })
|
|
966
|
-
|
|
967
|
-
expect(harness.events).toEqual([
|
|
968
|
-
"materialize",
|
|
969
|
-
"probe:opencode",
|
|
970
|
-
"launch:opencode",
|
|
971
|
-
])
|
|
972
|
-
expect(harness.launches).toEqual([
|
|
973
|
-
{
|
|
974
|
-
cli: "opencode",
|
|
975
|
-
args: ["opencode"],
|
|
976
|
-
cwd: taskDirectory,
|
|
977
|
-
},
|
|
978
|
-
])
|
|
979
|
-
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe("")
|
|
980
|
-
expect(harness.statusUpdates).toEqual(["task:example:working"])
|
|
981
|
-
expect(harness.launchEnvironments[0]?.OPENCODE_CONFIG).toBeUndefined()
|
|
982
|
-
expect(harness.progressUpdates).toEqual([
|
|
983
|
-
"start:Preparing workspace...",
|
|
984
|
-
"succeed:Workspace ready",
|
|
985
|
-
])
|
|
986
|
-
})
|
|
987
|
-
|
|
988
|
-
test("launches OpenCode in the phase directory with explicit context", async () => {
|
|
989
|
-
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
990
|
-
|
|
991
|
-
await harness.run({
|
|
992
|
-
taskId: "example",
|
|
993
|
-
phaseId: "implementation",
|
|
994
|
-
opencode: true,
|
|
995
|
-
})
|
|
996
|
-
|
|
997
|
-
expect(harness.launches[0]?.cwd).toBe(phaseDirectory)
|
|
998
|
-
})
|
|
999
|
-
|
|
1000
|
-
test("sends the generated prompt only with --auto", async () => {
|
|
1001
|
-
const harness = createHarness()
|
|
1002
|
-
|
|
1003
|
-
await harness.run({ taskId: "example", opencode: true, auto: true })
|
|
1004
|
-
|
|
1005
|
-
expect(harness.launches[0]?.args).toEqual([
|
|
1006
|
-
"opencode",
|
|
1007
|
-
"--prompt",
|
|
1008
|
-
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
1009
|
-
])
|
|
1010
|
-
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe(
|
|
1011
|
-
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
1012
|
-
)
|
|
1013
|
-
expect(harness.launchProcessEnvironments[0]).toEqual({
|
|
1014
|
-
AGENCY_SESSION_ID: harness.launchEnvironments[0]?.AGENCY_SESSION_ID,
|
|
1015
|
-
AGENCY_TARGET: "execution-unit:task/example",
|
|
1016
|
-
AGENCY_PROMPT:
|
|
1017
|
-
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
1018
|
-
})
|
|
1019
|
-
})
|
|
1020
|
-
|
|
1021
|
-
test("continues existing work with the resume command", async () => {
|
|
1022
|
-
const harness = createHarness({ taskStatus: "working" })
|
|
1023
|
-
|
|
1024
|
-
await harness.run({ taskId: "example", opencode: true, auto: true })
|
|
1025
|
-
|
|
1026
|
-
expect(harness.launches[0]?.args).toEqual([
|
|
1027
|
-
"opencode",
|
|
1028
|
-
"--continue",
|
|
1029
|
-
"--prompt",
|
|
1030
|
-
"Agency worker launch target: execution-unit:task/example. Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
1031
|
-
])
|
|
1032
|
-
expect(harness.launchEnvironments[0]?.AGENCY_PROMPT).toBe(
|
|
1033
|
-
"Agency worker launch target: execution-unit:task/example. Continue the task. Read /workbase/tasks/example/TASK.md.",
|
|
1034
|
-
)
|
|
1035
|
-
})
|
|
1036
|
-
|
|
1037
|
-
test("resumes OpenCode deterministically when a session identity exists", async () => {
|
|
1038
|
-
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
1039
|
-
process.env.AGENCY_SESSION_ID = "existing-session"
|
|
1040
|
-
try {
|
|
1041
|
-
await harness.run(
|
|
1042
|
-
{
|
|
1043
|
-
taskId: "example",
|
|
1044
|
-
phaseId: "implementation",
|
|
1045
|
-
opencode: true,
|
|
1046
|
-
auto: true,
|
|
1047
|
-
},
|
|
1048
|
-
undefined,
|
|
1049
|
-
undefined,
|
|
1050
|
-
true,
|
|
1051
|
-
)
|
|
1052
|
-
} finally {
|
|
1053
|
-
delete process.env.AGENCY_SESSION_ID
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
expect(harness.launches[0]).toEqual({
|
|
1057
|
-
cli: "opencode",
|
|
1058
|
-
args: [
|
|
1059
|
-
"opencode",
|
|
1060
|
-
"--continue",
|
|
1061
|
-
"--prompt",
|
|
1062
|
-
"Agency worker launch target: execution-unit:phase/example/implementation. Continue the task. Read /workbase/tasks/example/TASK.md and /workbase/tasks/example/phases/implementation/PHASE.md.",
|
|
1063
|
-
],
|
|
1064
|
-
cwd: phaseDirectory,
|
|
1065
|
-
})
|
|
1066
|
-
})
|
|
1067
|
-
|
|
1068
|
-
test("expands a named agent with shared context", async () => {
|
|
1069
|
-
const harness = createHarness({
|
|
1070
|
-
available: { codex: true },
|
|
1071
|
-
agents: {
|
|
1072
|
-
custom: {
|
|
1073
|
-
command: ["codex"],
|
|
1074
|
-
autoCommand: ["codex", "--task", "{task}", "{prompt}"],
|
|
1075
|
-
environment: {
|
|
1076
|
-
CUSTOM_TARGET: "{target}",
|
|
1077
|
-
AGENCY_TARGET: "cannot-override",
|
|
1078
|
-
},
|
|
1079
|
-
},
|
|
1080
|
-
},
|
|
1081
|
-
})
|
|
1082
|
-
|
|
1083
|
-
await harness.run({ taskId: "example", agent: "custom", auto: true })
|
|
1084
|
-
|
|
1085
|
-
expect(harness.probes).toEqual(["codex"])
|
|
1086
|
-
expect(harness.launches[0]).toEqual({
|
|
1087
|
-
cli: "codex",
|
|
1088
|
-
args: [
|
|
1089
|
-
"codex",
|
|
1090
|
-
"--task",
|
|
1091
|
-
"example",
|
|
1092
|
-
"Agency worker launch target: execution-unit:task/example. Start the task. Read /workbase/tasks/example/TASK.md.",
|
|
1093
|
-
],
|
|
1094
|
-
cwd: taskDirectory,
|
|
1095
|
-
})
|
|
1096
|
-
expect(harness.launchEnvironments[0]).toMatchObject({
|
|
1097
|
-
AGENCY_AGENT: "custom",
|
|
1098
|
-
AGENCY_WORKBASE: "/workbase",
|
|
1099
|
-
AGENCY_TARGET: "execution-unit:task/example",
|
|
1100
|
-
AGENCY_TASK_ID: "example",
|
|
1101
|
-
AGENCY_PHASE_ID: "",
|
|
1102
|
-
CUSTOM_TARGET: "execution-unit:task/example",
|
|
1103
|
-
})
|
|
1104
|
-
})
|
|
1105
|
-
|
|
1106
|
-
test("prints the exact command contract without launching and omits secrets", async () => {
|
|
1107
|
-
const harness = createHarness({
|
|
1108
|
-
available: { agent: true },
|
|
1109
|
-
agents: {
|
|
1110
|
-
custom: {
|
|
1111
|
-
command: ["agent"],
|
|
1112
|
-
autoCommand: ["agent", "{prompt}"],
|
|
1113
|
-
environment: {
|
|
1114
|
-
VISIBLE: "{task}",
|
|
1115
|
-
API_TOKEN: "do-not-print",
|
|
1116
|
-
},
|
|
1117
|
-
},
|
|
1118
|
-
},
|
|
1119
|
-
})
|
|
1120
|
-
|
|
1121
|
-
const output = await captureLogs(() =>
|
|
1122
|
-
harness.run({
|
|
1123
|
-
taskId: "example",
|
|
1124
|
-
agent: "custom",
|
|
1125
|
-
printCommand: true,
|
|
1126
|
-
}),
|
|
1127
|
-
)
|
|
1128
|
-
const printed = JSON.parse(output.join("\n"))
|
|
1129
|
-
|
|
1130
|
-
expect(harness.launches).toEqual([])
|
|
1131
|
-
expect(harness.statusUpdates).toEqual([])
|
|
1132
|
-
expect(printed.cwd).toBe(taskDirectory)
|
|
1133
|
-
expect(printed.argv).toEqual(["agent"])
|
|
1134
|
-
expect(printed.environment.AGENCY_PROMPT).toBe("")
|
|
1135
|
-
expect(printed.environment.VISIBLE).toBe("example")
|
|
1136
|
-
expect(printed.environment.API_TOKEN).toBeUndefined()
|
|
1137
|
-
})
|
|
1138
|
-
|
|
1139
|
-
test("prints the phase directory in the command contract", async () => {
|
|
1140
|
-
const harness = createHarness({ workspace: multiPhaseWorkspace })
|
|
1141
|
-
const output = await captureLogs(() =>
|
|
1142
|
-
harness.run({
|
|
1143
|
-
taskId: "example",
|
|
1144
|
-
phaseId: "implementation",
|
|
1145
|
-
opencode: true,
|
|
1146
|
-
printCommand: true,
|
|
1147
|
-
}),
|
|
1148
|
-
)
|
|
1149
|
-
|
|
1150
|
-
expect(JSON.parse(output.join("\n")).cwd).toBe(phaseDirectory)
|
|
1151
|
-
})
|
|
1152
|
-
|
|
1153
|
-
test("does not reopen a forced terminal target in print-only mode", async () => {
|
|
1154
|
-
const harness = createHarness({ taskStatuses: { example: "done" } })
|
|
1155
|
-
|
|
1156
|
-
await captureLogs(() =>
|
|
1157
|
-
harness.run({
|
|
1158
|
-
taskId: "example",
|
|
1159
|
-
opencode: true,
|
|
1160
|
-
force: true,
|
|
1161
|
-
printCommand: true,
|
|
1162
|
-
}),
|
|
1163
|
-
)
|
|
1164
|
-
|
|
1165
|
-
expect(harness.statusUpdates).toEqual([])
|
|
1166
|
-
expect(harness.taskStatuses.example).toBe("done")
|
|
1167
|
-
})
|
|
1168
|
-
|
|
1169
|
-
test("leaves managed OpenCode access to the project plugin", async () => {
|
|
1170
|
-
const harness = createHarness()
|
|
1171
|
-
const output = await captureLogs(() =>
|
|
1172
|
-
harness.run({ taskId: "example", opencode: true, printCommand: true }),
|
|
1173
|
-
)
|
|
1174
|
-
const printed = JSON.parse(output.join("\n"))
|
|
1175
|
-
|
|
1176
|
-
expect(printed.environment.OPENCODE_CONFIG).toBeUndefined()
|
|
1177
|
-
expect(printed.environment.OPENCODE_CONFIG_CONTENT).toBeUndefined()
|
|
1178
|
-
})
|
|
1179
|
-
|
|
1180
|
-
test("provides the writable checkout to plugins without changing the project", async () => {
|
|
1181
|
-
const harness = createHarness()
|
|
1182
|
-
await harness.run({ taskId: "example", opencode: true })
|
|
1183
|
-
|
|
1184
|
-
expect(harness.launches[0]).toEqual({
|
|
1185
|
-
cli: "opencode",
|
|
1186
|
-
args: ["opencode"],
|
|
1187
|
-
cwd: taskDirectory,
|
|
1188
|
-
})
|
|
1189
|
-
expect(harness.launchEnvironments[0]?.AGENCY_WRITABLE_CHECKOUT).toBe(
|
|
1190
|
-
"/workbase/tasks/example/code/agency",
|
|
1191
|
-
)
|
|
1192
|
-
expect(
|
|
1193
|
-
harness.launchEnvironments[0]?.OPENCODE_CONFIG_CONTENT,
|
|
1194
|
-
).toBeUndefined()
|
|
1195
|
-
})
|
|
1196
|
-
|
|
1197
|
-
test("automatically prefers opencode2", async () => {
|
|
1198
|
-
const harness = createHarness()
|
|
1199
|
-
|
|
1200
|
-
await harness.run({ taskId: "example" })
|
|
1201
|
-
|
|
1202
|
-
expect(harness.probes).toEqual(["opencode2"])
|
|
1203
|
-
expect(harness.launches[0]).toEqual({
|
|
1204
|
-
cli: "opencode2",
|
|
1205
|
-
args: ["opencode2"],
|
|
1206
|
-
cwd: taskDirectory,
|
|
1207
|
-
})
|
|
1208
|
-
})
|
|
1209
|
-
|
|
1210
|
-
test("automatically falls back from opencode2 to opencode", async () => {
|
|
1211
|
-
const harness = createHarness({ available: { opencode2: false } })
|
|
1212
|
-
|
|
1213
|
-
await harness.run({ taskId: "example" })
|
|
1214
|
-
|
|
1215
|
-
expect(harness.probes).toEqual(["opencode2", "opencode"])
|
|
1216
|
-
expect(harness.launches[0]).toEqual({
|
|
1217
|
-
cli: "opencode",
|
|
1218
|
-
args: ["opencode"],
|
|
1219
|
-
cwd: taskDirectory,
|
|
1220
|
-
})
|
|
1221
|
-
})
|
|
1222
|
-
|
|
1223
|
-
test("automatically falls back from OpenCode to Claude", async () => {
|
|
1224
|
-
const harness = createHarness({
|
|
1225
|
-
available: { opencode2: false, opencode: false, pi: false },
|
|
1226
|
-
})
|
|
1227
|
-
|
|
1228
|
-
await harness.run({ taskId: "example" })
|
|
1229
|
-
|
|
1230
|
-
expect(harness.probes).toEqual(["opencode2", "opencode", "pi", "claude"])
|
|
1231
|
-
expect(harness.launches[0]).toEqual({
|
|
1232
|
-
cli: "claude",
|
|
1233
|
-
args: ["claude"],
|
|
1234
|
-
cwd: taskDirectory,
|
|
1235
|
-
})
|
|
1236
|
-
})
|
|
1237
|
-
|
|
1238
|
-
test("automatically falls back from OpenCode to Pi", async () => {
|
|
1239
|
-
const harness = createHarness({
|
|
1240
|
-
available: { opencode2: false, opencode: false },
|
|
1241
|
-
})
|
|
1242
|
-
|
|
1243
|
-
await harness.run({ taskId: "example" })
|
|
1244
|
-
|
|
1245
|
-
expect(harness.probes).toEqual(["opencode2", "opencode", "pi"])
|
|
1246
|
-
expect(harness.launches[0]).toMatchObject({ cli: "pi", args: ["pi"] })
|
|
1247
|
-
})
|
|
1248
|
-
|
|
1249
|
-
test("uses the global agent before automatic detection", async () => {
|
|
1250
|
-
const harness = createHarness({ agent: "pi" })
|
|
1251
|
-
|
|
1252
|
-
await harness.run({ taskId: "example" })
|
|
1253
|
-
|
|
1254
|
-
expect(harness.probes).toEqual(["pi"])
|
|
1255
|
-
expect(harness.launches[0]).toMatchObject({ cli: "pi", args: ["pi"] })
|
|
1256
|
-
})
|
|
1257
|
-
|
|
1258
|
-
test("lets an invocation agent override the global agent", async () => {
|
|
1259
|
-
const harness = createHarness({ agent: "pi" })
|
|
1260
|
-
|
|
1261
|
-
await harness.run({ taskId: "example", agent: "claude" })
|
|
1262
|
-
|
|
1263
|
-
expect(harness.probes).toEqual(["claude"])
|
|
1264
|
-
expect(harness.launches[0]).toMatchObject({
|
|
1265
|
-
cli: "claude",
|
|
1266
|
-
args: ["claude"],
|
|
1267
|
-
})
|
|
1268
|
-
})
|
|
1269
|
-
|
|
1270
|
-
test("does not fall back when OpenCode is explicitly required", async () => {
|
|
1271
|
-
const harness = createHarness({ available: { opencode: false } })
|
|
1272
|
-
|
|
1273
|
-
await expect(
|
|
1274
|
-
harness.run({ taskId: "example", opencode: true }),
|
|
1275
|
-
).rejects.toThrow("opencode CLI tool not found")
|
|
1276
|
-
expect(harness.probes).toEqual(["opencode"])
|
|
1277
|
-
expect(harness.launches).toEqual([])
|
|
1278
|
-
expect(harness.statusUpdates).toEqual([])
|
|
1279
|
-
})
|
|
1280
|
-
|
|
1281
|
-
test("launches explicitly requested Claude", async () => {
|
|
1282
|
-
const harness = createHarness()
|
|
1283
|
-
|
|
1284
|
-
await harness.run({ taskId: "example", claude: true })
|
|
1285
|
-
|
|
1286
|
-
expect(harness.probes).toEqual(["claude"])
|
|
1287
|
-
expect(harness.launches[0]).toEqual({
|
|
1288
|
-
cli: "claude",
|
|
1289
|
-
args: ["claude"],
|
|
1290
|
-
cwd: taskDirectory,
|
|
1291
|
-
})
|
|
1292
|
-
})
|
|
1293
|
-
|
|
1294
|
-
test("fails when neither agent tool is available", async () => {
|
|
1295
|
-
const harness = createHarness({
|
|
1296
|
-
available: {
|
|
1297
|
-
opencode2: false,
|
|
1298
|
-
opencode: false,
|
|
1299
|
-
pi: false,
|
|
1300
|
-
claude: false,
|
|
1301
|
-
},
|
|
1302
|
-
})
|
|
1303
|
-
|
|
1304
|
-
await expect(harness.run({ taskId: "example" })).rejects.toThrow(
|
|
1305
|
-
"claude CLI tool not found",
|
|
1306
|
-
)
|
|
1307
|
-
expect(harness.probes).toEqual(["opencode2", "opencode", "pi", "claude"])
|
|
1308
|
-
expect(harness.launches).toEqual([])
|
|
1309
|
-
})
|
|
1310
|
-
|
|
1311
|
-
test("does not probe or launch when materialization fails", async () => {
|
|
1312
|
-
const harness = createHarness({
|
|
1313
|
-
materializeError: new Error("materialization failed"),
|
|
1314
|
-
})
|
|
1315
|
-
|
|
1316
|
-
await expect(harness.run({ taskId: "example" })).rejects.toThrow(
|
|
1317
|
-
"materialization failed",
|
|
1318
|
-
)
|
|
1319
|
-
expect(harness.events).toEqual(["materialize"])
|
|
1320
|
-
expect(harness.progressUpdates).toEqual([
|
|
1321
|
-
"start:Preparing workspace...",
|
|
1322
|
-
"fail:Workspace preparation failed",
|
|
1323
|
-
])
|
|
1324
|
-
})
|
|
1325
|
-
|
|
1326
|
-
test("does not reopen a terminal target when preparation fails", async () => {
|
|
1327
|
-
const harness = createHarness({
|
|
1328
|
-
taskStatuses: { example: "dropped" },
|
|
1329
|
-
materializeError: new Error("materialization failed"),
|
|
1330
|
-
})
|
|
1331
|
-
|
|
1332
|
-
await expect(
|
|
1333
|
-
harness.run({ taskId: "example", force: true }),
|
|
1334
|
-
).rejects.toThrow("materialization failed")
|
|
1335
|
-
expect(harness.statusUpdates).toEqual([])
|
|
1336
|
-
expect(harness.taskStatuses.example).toBe("dropped")
|
|
1337
|
-
})
|
|
1338
|
-
|
|
1339
|
-
test("retains working when launch fails after a forced reopen", async () => {
|
|
1340
|
-
const harness = createHarness({
|
|
1341
|
-
taskStatuses: { example: "done" },
|
|
1342
|
-
launchError: new Error("launch failed"),
|
|
1343
|
-
})
|
|
1344
|
-
|
|
1345
|
-
await expect(
|
|
1346
|
-
harness.run({ taskId: "example", force: true, silent: true }),
|
|
1347
|
-
).rejects.toThrow("launch failed")
|
|
1348
|
-
expect(harness.statusUpdates).toEqual([
|
|
1349
|
-
"task:example:open",
|
|
1350
|
-
"task:example:working",
|
|
1351
|
-
])
|
|
1352
|
-
expect(harness.taskStatuses.example).toBe("working")
|
|
1353
|
-
})
|
|
1354
|
-
|
|
1355
|
-
test("respects silent and verbose logging options", async () => {
|
|
1356
|
-
const verboseHarness = createHarness()
|
|
1357
|
-
const verboseLogs = await captureErrors(() =>
|
|
1358
|
-
verboseHarness.run({ taskId: "example", verbose: true }),
|
|
1359
|
-
)
|
|
1360
|
-
expect(verboseLogs).toEqual([
|
|
1361
|
-
"Launching command: opencode2 (cwd: /workbase/tasks/example)",
|
|
1362
|
-
])
|
|
1363
|
-
expect(verboseHarness.materializeOptions[0]?.verbose).toBe(true)
|
|
1364
|
-
|
|
1365
|
-
const silentHarness = createHarness()
|
|
1366
|
-
const silentLogs = await captureErrors(() =>
|
|
1367
|
-
silentHarness.run({
|
|
1368
|
-
taskId: "example",
|
|
1369
|
-
verbose: true,
|
|
1370
|
-
silent: true,
|
|
1371
|
-
}),
|
|
1372
|
-
)
|
|
1373
|
-
expect(silentLogs).toEqual([])
|
|
1374
|
-
})
|
|
1375
|
-
})
|