@markjaquith/agency 3.2.14 → 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 -711
- 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
package/src/cli.test.ts
DELETED
|
@@ -1,1889 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
afterAll,
|
|
3
|
-
afterEach,
|
|
4
|
-
describe,
|
|
5
|
-
expect,
|
|
6
|
-
setDefaultTimeout,
|
|
7
|
-
test,
|
|
8
|
-
} from "bun:test"
|
|
9
|
-
import { access, chmod, mkdir, realpath, stat, symlink } from "node:fs/promises"
|
|
10
|
-
import { join, sep } from "node:path"
|
|
11
|
-
import errorFixture from "../fixtures/protocol/error.json"
|
|
12
|
-
import successFixture from "../fixtures/protocol/success.json"
|
|
13
|
-
import { cleanupTempDir, createTempDir } from "./test-utils"
|
|
14
|
-
|
|
15
|
-
const projectRoot = join(import.meta.dir, "..")
|
|
16
|
-
const cliPath = join(projectRoot, "cli.ts")
|
|
17
|
-
const isolatedConfigHome = await createTempDir()
|
|
18
|
-
|
|
19
|
-
setDefaultTimeout(15_000)
|
|
20
|
-
|
|
21
|
-
afterAll(() => cleanupTempDir(isolatedConfigHome))
|
|
22
|
-
|
|
23
|
-
interface CliResult {
|
|
24
|
-
exitCode: number
|
|
25
|
-
stdout: string
|
|
26
|
-
stderr: string
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function runCli(
|
|
30
|
-
args: string[],
|
|
31
|
-
cwd = projectRoot,
|
|
32
|
-
env?: Record<string, string>,
|
|
33
|
-
): Promise<CliResult> {
|
|
34
|
-
const subprocess = Bun.spawn([process.execPath, cliPath, ...args], {
|
|
35
|
-
cwd,
|
|
36
|
-
env: {
|
|
37
|
-
...process.env,
|
|
38
|
-
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
39
|
-
XDG_STATE_HOME: isolatedConfigHome,
|
|
40
|
-
AGENCY_NO_USAGE_LOG: "1",
|
|
41
|
-
...env,
|
|
42
|
-
},
|
|
43
|
-
stdout: "pipe",
|
|
44
|
-
stderr: "pipe",
|
|
45
|
-
})
|
|
46
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
47
|
-
subprocess.exited,
|
|
48
|
-
new Response(subprocess.stdout).text(),
|
|
49
|
-
new Response(subprocess.stderr).text(),
|
|
50
|
-
])
|
|
51
|
-
return { exitCode, stdout, stderr }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function parseJson(result: CliResult) {
|
|
55
|
-
expect(result.exitCode, JSON.stringify(result)).toBe(0)
|
|
56
|
-
expect(result.stderr).toBe("")
|
|
57
|
-
const envelope = JSON.parse(result.stdout)
|
|
58
|
-
expect(envelope).toMatchObject({ version: 1, ok: true })
|
|
59
|
-
return envelope.result
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function runGit(args: string[]) {
|
|
63
|
-
const subprocess = Bun.spawn(["git", ...args], {
|
|
64
|
-
stdout: "pipe",
|
|
65
|
-
stderr: "pipe",
|
|
66
|
-
})
|
|
67
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
68
|
-
subprocess.exited,
|
|
69
|
-
new Response(subprocess.stdout).text(),
|
|
70
|
-
new Response(subprocess.stderr).text(),
|
|
71
|
-
])
|
|
72
|
-
if (exitCode !== 0) throw new Error(stderr)
|
|
73
|
-
return stdout
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function startGitDaemon(basePath: string) {
|
|
77
|
-
const port = 20000 + Math.floor(Math.random() * 20000)
|
|
78
|
-
const process = Bun.spawn(
|
|
79
|
-
[
|
|
80
|
-
"git",
|
|
81
|
-
"daemon",
|
|
82
|
-
"--reuseaddr",
|
|
83
|
-
"--export-all",
|
|
84
|
-
`--base-path=${basePath}`,
|
|
85
|
-
"--listen=127.0.0.1",
|
|
86
|
-
`--port=${port}`,
|
|
87
|
-
basePath,
|
|
88
|
-
],
|
|
89
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
90
|
-
)
|
|
91
|
-
const remote = `git://127.0.0.1:${port}/source.git`
|
|
92
|
-
for (let attempt = 0; attempt < 40; attempt++) {
|
|
93
|
-
const probe = Bun.spawn(["git", "ls-remote", remote], {
|
|
94
|
-
stdout: "ignore",
|
|
95
|
-
stderr: "ignore",
|
|
96
|
-
})
|
|
97
|
-
if ((await probe.exited) === 0) return { process, remote }
|
|
98
|
-
await Bun.sleep(25)
|
|
99
|
-
}
|
|
100
|
-
process.kill()
|
|
101
|
-
throw new Error("Git daemon did not start")
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
describe("CLI", () => {
|
|
105
|
-
const tempDirs: string[] = []
|
|
106
|
-
const daemons: Bun.Subprocess[] = []
|
|
107
|
-
|
|
108
|
-
afterEach(async () => {
|
|
109
|
-
await Promise.all(
|
|
110
|
-
daemons.splice(0).map(async (daemon) => {
|
|
111
|
-
daemon.kill()
|
|
112
|
-
await daemon.exited
|
|
113
|
-
}),
|
|
114
|
-
)
|
|
115
|
-
await Promise.all(tempDirs.splice(0).map(cleanupTempDir))
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
test("reports version and routes top-level help with intentional statuses", async () => {
|
|
119
|
-
const version = await runCli(["--version"])
|
|
120
|
-
expect(version).toEqual({
|
|
121
|
-
exitCode: 0,
|
|
122
|
-
stdout: "v0.0.0-development\n",
|
|
123
|
-
stderr: "",
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
const noArgs = await runCli([])
|
|
127
|
-
expect(noArgs.exitCode).toBe(1)
|
|
128
|
-
expect(noArgs.stdout).toContain("Usage: agency <command> [options]")
|
|
129
|
-
expect(noArgs.stderr).toBe("")
|
|
130
|
-
|
|
131
|
-
const help = await runCli(["--help"])
|
|
132
|
-
expect(help.exitCode).toBe(0)
|
|
133
|
-
expect(help.stdout).toContain("Usage: agency <command> [options]")
|
|
134
|
-
expect(help.stderr).toBe("")
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
test("keeps the published protocol fixtures synchronized with CLI output", async () => {
|
|
138
|
-
const parent = await createTempDir()
|
|
139
|
-
tempDirs.push(parent)
|
|
140
|
-
const root = join(parent, "workbase")
|
|
141
|
-
const success = await runCli(["init", root, "--json"], parent)
|
|
142
|
-
expect(success.exitCode).toBe(0)
|
|
143
|
-
expect(success.stderr).toBe("")
|
|
144
|
-
expect(success.stdout.endsWith("\n")).toBe(true)
|
|
145
|
-
const successEnvelope = JSON.parse(success.stdout)
|
|
146
|
-
expect(successEnvelope.result.root).toBe(root)
|
|
147
|
-
expect({
|
|
148
|
-
...successEnvelope,
|
|
149
|
-
result: { ...successEnvelope.result, root: "/work/agency" },
|
|
150
|
-
}).toEqual(successFixture)
|
|
151
|
-
|
|
152
|
-
const failure = await runCli(["unknown", "--json"])
|
|
153
|
-
expect(failure.exitCode).toBe(1)
|
|
154
|
-
expect(failure.stderr).toBe("")
|
|
155
|
-
expect(failure.stdout.endsWith("\n")).toBe(true)
|
|
156
|
-
expect(JSON.parse(failure.stdout)).toEqual(errorFixture)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test("reports unknown commands and preserves tagged error messages", async () => {
|
|
160
|
-
const unknown = await runCli(["unknown"])
|
|
161
|
-
expect(unknown.exitCode).toBe(1)
|
|
162
|
-
expect(unknown.stdout).toBe("")
|
|
163
|
-
expect(unknown.stderr).toContain("Unknown command 'unknown'")
|
|
164
|
-
expect(unknown.stderr).toContain("Usage: agency <command> [options]")
|
|
165
|
-
|
|
166
|
-
const cwd = await createTempDir()
|
|
167
|
-
tempDirs.push(cwd)
|
|
168
|
-
const taggedError = await runCli(["repo", "list"], cwd)
|
|
169
|
-
expect(taggedError.exitCode).toBe(1)
|
|
170
|
-
expect(taggedError.stdout).toBe("")
|
|
171
|
-
expect(taggedError.stderr).toContain("ⓘ No Agency workbase found from")
|
|
172
|
-
expect(taggedError.stderr).not.toContain("An error has occurred")
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
test("records successful and failed invocations without argument values", async () => {
|
|
176
|
-
const state = await createTempDir()
|
|
177
|
-
tempDirs.push(state)
|
|
178
|
-
const env = {
|
|
179
|
-
XDG_STATE_HOME: state,
|
|
180
|
-
AGENCY_SESSION_ID: "cli-session",
|
|
181
|
-
AGENCY_NO_USAGE_LOG: "0",
|
|
182
|
-
AGENCY_INVOCATION_SOURCE: "automation",
|
|
183
|
-
AGENCY_USAGE_TEST: "1",
|
|
184
|
-
}
|
|
185
|
-
expect((await runCli(["--version"], projectRoot, env)).exitCode).toBe(0)
|
|
186
|
-
expect(
|
|
187
|
-
(
|
|
188
|
-
await runCli(
|
|
189
|
-
[
|
|
190
|
-
"task",
|
|
191
|
-
"create",
|
|
192
|
-
"private-customer-id",
|
|
193
|
-
"--repo",
|
|
194
|
-
"private-repository",
|
|
195
|
-
"--description",
|
|
196
|
-
"private free-form input",
|
|
197
|
-
"--cwd",
|
|
198
|
-
"/private/customer/path",
|
|
199
|
-
],
|
|
200
|
-
projectRoot,
|
|
201
|
-
env,
|
|
202
|
-
)
|
|
203
|
-
).exitCode,
|
|
204
|
-
).toBe(1)
|
|
205
|
-
expect(
|
|
206
|
-
(await runCli(["unknown", "--private-flag=value"], projectRoot, env))
|
|
207
|
-
.exitCode,
|
|
208
|
-
).toBe(1)
|
|
209
|
-
|
|
210
|
-
const exported = await runCli(["usage", "export"], projectRoot, env)
|
|
211
|
-
expect(exported).toMatchObject({ exitCode: 0, stderr: "" })
|
|
212
|
-
const events = exported.stdout
|
|
213
|
-
.trim()
|
|
214
|
-
.split("\n")
|
|
215
|
-
.map((line) => JSON.parse(line))
|
|
216
|
-
expect(events).toEqual([
|
|
217
|
-
expect.objectContaining({
|
|
218
|
-
journeyId: expect.stringMatching(/^sha256:[a-f0-9]{64}$/),
|
|
219
|
-
journeySequence: 1,
|
|
220
|
-
invocationSource: "automation",
|
|
221
|
-
isTest: true,
|
|
222
|
-
commandPath: "version",
|
|
223
|
-
flagNames: ["version"],
|
|
224
|
-
outcome: "success",
|
|
225
|
-
outcomeCode: "SUCCESS",
|
|
226
|
-
}),
|
|
227
|
-
expect.objectContaining({
|
|
228
|
-
journeySequence: 2,
|
|
229
|
-
commandPath: "task/create",
|
|
230
|
-
flagNames: ["cwd", "description", "repo"],
|
|
231
|
-
outcome: "failure",
|
|
232
|
-
outcomeCode: "WORKBASE_NOT_FOUND",
|
|
233
|
-
}),
|
|
234
|
-
expect.objectContaining({
|
|
235
|
-
journeySequence: 3,
|
|
236
|
-
commandPath: "invalid",
|
|
237
|
-
flagNames: [],
|
|
238
|
-
outcome: "failure",
|
|
239
|
-
outcomeCode: "CLI_USAGE",
|
|
240
|
-
}),
|
|
241
|
-
])
|
|
242
|
-
for (const value of [
|
|
243
|
-
"cli-session",
|
|
244
|
-
"private-customer-id",
|
|
245
|
-
"private-repository",
|
|
246
|
-
"private free-form input",
|
|
247
|
-
"/private/customer/path",
|
|
248
|
-
"private-flag",
|
|
249
|
-
"value",
|
|
250
|
-
]) {
|
|
251
|
-
expect(exported.stdout).not.toContain(value)
|
|
252
|
-
}
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
test("records status-based non-PR completion", async () => {
|
|
256
|
-
const root = await createTempDir()
|
|
257
|
-
tempDirs.push(root)
|
|
258
|
-
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
259
|
-
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
260
|
-
parseJson(
|
|
261
|
-
await runCli(
|
|
262
|
-
["task", "create", "non-pr", "--repo", "agency", "--json"],
|
|
263
|
-
root,
|
|
264
|
-
),
|
|
265
|
-
)
|
|
266
|
-
parseJson(
|
|
267
|
-
await runCli(
|
|
268
|
-
[
|
|
269
|
-
"task",
|
|
270
|
-
"status",
|
|
271
|
-
"non-pr",
|
|
272
|
-
"done",
|
|
273
|
-
"--no-pull-request",
|
|
274
|
-
"--summary",
|
|
275
|
-
"Investigation completed without changes.",
|
|
276
|
-
"--evidence-url",
|
|
277
|
-
"https://example.com/result",
|
|
278
|
-
"--json",
|
|
279
|
-
],
|
|
280
|
-
root,
|
|
281
|
-
),
|
|
282
|
-
)
|
|
283
|
-
expect(
|
|
284
|
-
parseJson(await runCli(["task", "show", "non-pr", "--json"], root)).data,
|
|
285
|
-
).toMatchObject({
|
|
286
|
-
status: "done",
|
|
287
|
-
completion: {
|
|
288
|
-
mode: "non-pr",
|
|
289
|
-
summary: "Investigation completed without changes.",
|
|
290
|
-
evidenceUrl: "https://example.com/result",
|
|
291
|
-
},
|
|
292
|
-
})
|
|
293
|
-
const prUpdate = await runCli(
|
|
294
|
-
[
|
|
295
|
-
"task",
|
|
296
|
-
"update",
|
|
297
|
-
"non-pr",
|
|
298
|
-
"--pr-url",
|
|
299
|
-
"https://github.com/example/agency/pull/1",
|
|
300
|
-
],
|
|
301
|
-
root,
|
|
302
|
-
)
|
|
303
|
-
expect(prUpdate.exitCode).toBe(1)
|
|
304
|
-
expect(prUpdate.stderr).toContain("Reopen non-PR completed work")
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
test("routes graph mutations through structured output", async () => {
|
|
308
|
-
const root = await createTempDir()
|
|
309
|
-
tempDirs.push(root)
|
|
310
|
-
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
311
|
-
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
312
|
-
parseJson(
|
|
313
|
-
await runCli(
|
|
314
|
-
[
|
|
315
|
-
"epic",
|
|
316
|
-
"create",
|
|
317
|
-
"delivery",
|
|
318
|
-
"--ticket-url",
|
|
319
|
-
"https://example.com/delivery",
|
|
320
|
-
"--repo",
|
|
321
|
-
"agency:main",
|
|
322
|
-
"--json",
|
|
323
|
-
],
|
|
324
|
-
root,
|
|
325
|
-
),
|
|
326
|
-
)
|
|
327
|
-
for (const id of ["first", "second"]) {
|
|
328
|
-
parseJson(
|
|
329
|
-
await runCli(
|
|
330
|
-
[
|
|
331
|
-
"task",
|
|
332
|
-
"create",
|
|
333
|
-
id,
|
|
334
|
-
"--repo",
|
|
335
|
-
"agency",
|
|
336
|
-
"--epic",
|
|
337
|
-
"delivery",
|
|
338
|
-
"--json",
|
|
339
|
-
],
|
|
340
|
-
root,
|
|
341
|
-
),
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
const observed = parseJson(
|
|
345
|
-
await runCli(["task", "show", "second", "--json"], root),
|
|
346
|
-
)
|
|
347
|
-
const taskPath = join(root, "tasks/second/TASK.md")
|
|
348
|
-
await Bun.write(
|
|
349
|
-
taskPath,
|
|
350
|
-
`${await Bun.file(taskPath).text()}\nConcurrent edit.\n`,
|
|
351
|
-
)
|
|
352
|
-
const conflict = await runCli(
|
|
353
|
-
[
|
|
354
|
-
"task",
|
|
355
|
-
"update",
|
|
356
|
-
"second",
|
|
357
|
-
"--description",
|
|
358
|
-
"Stale update",
|
|
359
|
-
"--if-revision",
|
|
360
|
-
observed.revision,
|
|
361
|
-
"--json",
|
|
362
|
-
],
|
|
363
|
-
root,
|
|
364
|
-
)
|
|
365
|
-
expect(conflict.exitCode).toBe(1)
|
|
366
|
-
expect(JSON.parse(conflict.stdout)).toMatchObject({
|
|
367
|
-
ok: false,
|
|
368
|
-
error: {
|
|
369
|
-
code: "REVISION_CONFLICT",
|
|
370
|
-
retryable: true,
|
|
371
|
-
fields: {
|
|
372
|
-
path: "tasks/second/TASK.md",
|
|
373
|
-
expectedRevision: observed.revision,
|
|
374
|
-
currentRevision: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
375
|
-
},
|
|
376
|
-
},
|
|
377
|
-
})
|
|
378
|
-
expect(await Bun.file(taskPath).text()).toContain("Concurrent edit.")
|
|
379
|
-
|
|
380
|
-
const updated = parseJson(
|
|
381
|
-
await runCli(
|
|
382
|
-
["task", "update", "second", "--description", "Revised", "--json"],
|
|
383
|
-
root,
|
|
384
|
-
),
|
|
385
|
-
)
|
|
386
|
-
expect(updated).toMatchObject({
|
|
387
|
-
operation: "task.update",
|
|
388
|
-
entity: { kind: "task", id: "second" },
|
|
389
|
-
validation: { valid: true, scope: ["tasks/second/TASK.md"] },
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
const dependency = parseJson(
|
|
393
|
-
await runCli(
|
|
394
|
-
["task", "dependency", "add", "second", "first", "--json"],
|
|
395
|
-
root,
|
|
396
|
-
),
|
|
397
|
-
)
|
|
398
|
-
expect(dependency).toMatchObject({
|
|
399
|
-
operation: "task.dependency.add",
|
|
400
|
-
changedPaths: ["epics/delivery/EPIC.md"],
|
|
401
|
-
})
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
test("emits one versioned error envelope for usage and command failures", async () => {
|
|
405
|
-
const usage = await runCli(["unknown", "--json"])
|
|
406
|
-
expect(usage.exitCode).toBe(1)
|
|
407
|
-
expect(usage.stderr).toBe("")
|
|
408
|
-
expect(usage.stdout.trim().split("\n")).toHaveLength(1)
|
|
409
|
-
expect(JSON.parse(usage.stdout)).toEqual({
|
|
410
|
-
version: 1,
|
|
411
|
-
ok: false,
|
|
412
|
-
error: {
|
|
413
|
-
code: "CLI_USAGE",
|
|
414
|
-
message:
|
|
415
|
-
"Unknown command 'unknown'.\n\nUsage: agency <command> [options]",
|
|
416
|
-
fields: {
|
|
417
|
-
detail: "Unknown command 'unknown'.",
|
|
418
|
-
usage: "agency <command> [options]",
|
|
419
|
-
},
|
|
420
|
-
retryable: false,
|
|
421
|
-
remediation:
|
|
422
|
-
"Correct the arguments using the usage value in error.fields.",
|
|
423
|
-
},
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
const cwd = await createTempDir()
|
|
427
|
-
tempDirs.push(cwd)
|
|
428
|
-
const commandFailure = await runCli(
|
|
429
|
-
["repo", "list", "--json", "--silent"],
|
|
430
|
-
cwd,
|
|
431
|
-
)
|
|
432
|
-
expect(commandFailure.exitCode).toBe(1)
|
|
433
|
-
expect(commandFailure.stderr).toBe("")
|
|
434
|
-
expect(JSON.parse(commandFailure.stdout)).toMatchObject({
|
|
435
|
-
version: 1,
|
|
436
|
-
ok: false,
|
|
437
|
-
error: {
|
|
438
|
-
code: "WORKBASE_NOT_FOUND",
|
|
439
|
-
retryable: false,
|
|
440
|
-
},
|
|
441
|
-
})
|
|
442
|
-
})
|
|
443
|
-
|
|
444
|
-
test("preserves the JSON error contract when archiving an already archived task", async () => {
|
|
445
|
-
const root = await createTempDir()
|
|
446
|
-
tempDirs.push(root)
|
|
447
|
-
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
448
|
-
expect(
|
|
449
|
-
Bun.spawnSync(["git", "init", "--bare", join(root, "repos/agency")])
|
|
450
|
-
.exitCode,
|
|
451
|
-
).toBe(0)
|
|
452
|
-
await mkdir(join(root, "tasks/example"), { recursive: true })
|
|
453
|
-
await Bun.write(
|
|
454
|
-
join(root, "tasks/example/TASK.md"),
|
|
455
|
-
`---
|
|
456
|
-
ticketUrl: null
|
|
457
|
-
repo: agency
|
|
458
|
-
branch: task/example
|
|
459
|
-
base: main
|
|
460
|
-
pr: null
|
|
461
|
-
status: dropped
|
|
462
|
-
---
|
|
463
|
-
|
|
464
|
-
# Example
|
|
465
|
-
`,
|
|
466
|
-
)
|
|
467
|
-
|
|
468
|
-
expect(
|
|
469
|
-
(await runCli(["archive", "."], join(root, "tasks/example"))).exitCode,
|
|
470
|
-
).toBe(0)
|
|
471
|
-
const result = await runCli(["archive", "task", "example", "--json"], root)
|
|
472
|
-
|
|
473
|
-
expect(result.exitCode).toBe(1)
|
|
474
|
-
expect(result.stderr).toBe("")
|
|
475
|
-
expect(JSON.parse(result.stdout)).toEqual({
|
|
476
|
-
version: 1,
|
|
477
|
-
ok: false,
|
|
478
|
-
error: {
|
|
479
|
-
code: "TASK_ERROR",
|
|
480
|
-
message: "Task 'example' is already archived",
|
|
481
|
-
fields: {},
|
|
482
|
-
retryable: false,
|
|
483
|
-
},
|
|
484
|
-
})
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
test("rejects malformed input before running a command", async () => {
|
|
488
|
-
const parent = await createTempDir()
|
|
489
|
-
tempDirs.push(parent)
|
|
490
|
-
const root = join(parent, "workbase")
|
|
491
|
-
const result = await runCli(["init", root, "extra"])
|
|
492
|
-
|
|
493
|
-
expect(result.exitCode).toBe(1)
|
|
494
|
-
expect(result.stderr).toContain("Usage: agency init [path] [--json]")
|
|
495
|
-
await expect(access(root)).rejects.toThrow()
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
test("resolves relative init paths from explicit cwd", async () => {
|
|
499
|
-
const parent = await createTempDir()
|
|
500
|
-
tempDirs.push(parent)
|
|
501
|
-
const result = parseJson(
|
|
502
|
-
await runCli(["init", "child", "--cwd", parent, "--json"], projectRoot),
|
|
503
|
-
)
|
|
504
|
-
expect(result.root).toBe(join(parent, "child"))
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
test("refuses guided input without a TTY or with --no-input", async () => {
|
|
508
|
-
for (const args of [
|
|
509
|
-
["task", "new"],
|
|
510
|
-
["task", "new", "example", "--no-input"],
|
|
511
|
-
]) {
|
|
512
|
-
const result = await runCli(args)
|
|
513
|
-
expect(result.exitCode).toBe(1)
|
|
514
|
-
expect(result.stderr).toContain("task new requires interactive input")
|
|
515
|
-
expect(result.stderr).toContain("agency task create")
|
|
516
|
-
}
|
|
517
|
-
const actResult = await runCli(["act", "--no-input"])
|
|
518
|
-
expect(actResult.exitCode).toBe(1)
|
|
519
|
-
expect(actResult.stderr).toContain("agency act requires interactive input")
|
|
520
|
-
})
|
|
521
|
-
|
|
522
|
-
test("routes command help and global options on either side of commands", async () => {
|
|
523
|
-
const commands = [
|
|
524
|
-
["act", "Usage: agency act"],
|
|
525
|
-
["init", "Usage: agency init"],
|
|
526
|
-
["workbase", "Usage: agency workbase"],
|
|
527
|
-
["integration", "Usage: agency integration"],
|
|
528
|
-
["repo", "Usage: agency repo"],
|
|
529
|
-
["epic", "Usage: agency epic"],
|
|
530
|
-
["task", "Usage: agency task"],
|
|
531
|
-
["phase", "Usage: agency phase"],
|
|
532
|
-
["archive", "Usage: agency archive"],
|
|
533
|
-
["restore", "Usage: agency restore"],
|
|
534
|
-
["work", "Usage: agency work"],
|
|
535
|
-
["push", "Usage: agency push"],
|
|
536
|
-
["pr", "Work with GitHub pull requests."],
|
|
537
|
-
["status", "Usage: agency status"],
|
|
538
|
-
["validate", "Usage: agency validate"],
|
|
539
|
-
["context", "Usage: agency context"],
|
|
540
|
-
["graph", "Usage: agency graph"],
|
|
541
|
-
["next", "Usage: agency next"],
|
|
542
|
-
] as const
|
|
543
|
-
for (let offset = 0; offset < commands.length; offset += 4) {
|
|
544
|
-
const results = await Promise.all(
|
|
545
|
-
commands.slice(offset, offset + 4).map(async ([command, usage]) => ({
|
|
546
|
-
usage,
|
|
547
|
-
result: await runCli([command, "--help"]),
|
|
548
|
-
})),
|
|
549
|
-
)
|
|
550
|
-
for (const { result, usage } of results) {
|
|
551
|
-
expect(result.exitCode).toBe(0)
|
|
552
|
-
expect(result.stdout).toContain(usage)
|
|
553
|
-
expect(result.stderr).toBe("")
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const helpBefore = await runCli(["--help", "task"])
|
|
558
|
-
expect(helpBefore.exitCode).toBe(0)
|
|
559
|
-
expect(helpBefore.stdout).toContain("Usage: agency task <subcommand>")
|
|
560
|
-
expect(helpBefore.stderr).toBe("")
|
|
561
|
-
|
|
562
|
-
const root = await createTempDir()
|
|
563
|
-
tempDirs.push(root)
|
|
564
|
-
const before = await runCli(["--silent", "init", root])
|
|
565
|
-
expect(before).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
566
|
-
|
|
567
|
-
const after = await runCli(["status", "--silent"], root)
|
|
568
|
-
expect(after).toEqual({ exitCode: 0, stdout: "", stderr: "" })
|
|
569
|
-
}, 30_000)
|
|
570
|
-
|
|
571
|
-
test("passes pr arguments and exit status through to gh", async () => {
|
|
572
|
-
const root = await createTempDir()
|
|
573
|
-
tempDirs.push(root)
|
|
574
|
-
const bin = join(root, "bin")
|
|
575
|
-
const capture = join(root, "capture")
|
|
576
|
-
await mkdir(bin)
|
|
577
|
-
await Bun.write(
|
|
578
|
-
join(bin, "gh"),
|
|
579
|
-
`#!/bin/sh
|
|
580
|
-
printf '%s\n' "$PWD" "$@" > "$GH_CAPTURE"
|
|
581
|
-
exit 23
|
|
582
|
-
`,
|
|
583
|
-
)
|
|
584
|
-
await chmod(join(bin, "gh"), 0o755)
|
|
585
|
-
const args = ["pr", "create", "--title", "two words", "--", "literal"]
|
|
586
|
-
|
|
587
|
-
const result = await runCli(args, root, {
|
|
588
|
-
PATH: `${bin}:${process.env.PATH ?? ""}`,
|
|
589
|
-
GH_CAPTURE: capture,
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
expect(result).toEqual({ exitCode: 23, stdout: "", stderr: "" })
|
|
593
|
-
expect((await Bun.file(capture).text()).trim().split("\n")).toEqual([
|
|
594
|
-
await realpath(root),
|
|
595
|
-
...args,
|
|
596
|
-
])
|
|
597
|
-
})
|
|
598
|
-
|
|
599
|
-
test("lists ready work and exposes excluded blockers through one result", async () => {
|
|
600
|
-
const root = await createTempDir()
|
|
601
|
-
tempDirs.push(root)
|
|
602
|
-
await Bun.write(join(root, "agency.json"), '{"version":2}\n')
|
|
603
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
604
|
-
for (const id of ["ready", "finished"]) {
|
|
605
|
-
parseJson(
|
|
606
|
-
await runCli(
|
|
607
|
-
["task", "create", id, "--repo", "agency", "--json"],
|
|
608
|
-
root,
|
|
609
|
-
),
|
|
610
|
-
)
|
|
611
|
-
}
|
|
612
|
-
parseJson(
|
|
613
|
-
await runCli(["task", "status", "finished", "dropped", "--json"], root),
|
|
614
|
-
)
|
|
615
|
-
|
|
616
|
-
const human = await runCli(["next"], root)
|
|
617
|
-
expect(human).toMatchObject({ exitCode: 0, stderr: "" })
|
|
618
|
-
expect(human.stdout).toContain("1. task/ready")
|
|
619
|
-
expect(human.stdout).not.toContain("task/finished")
|
|
620
|
-
|
|
621
|
-
const result = parseJson(await runCli(["next", "--select", "--json"], root))
|
|
622
|
-
expect(result.selected).toMatchObject({ key: "task/ready", rank: 1 })
|
|
623
|
-
expect(result.ready.map((item: any) => item.key)).toEqual(["task/ready"])
|
|
624
|
-
expect(result.excluded).toMatchObject([
|
|
625
|
-
{
|
|
626
|
-
key: "task/finished",
|
|
627
|
-
status: "dropped",
|
|
628
|
-
terminal: true,
|
|
629
|
-
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
630
|
-
},
|
|
631
|
-
])
|
|
632
|
-
|
|
633
|
-
const blockedPr = await runCli(["pr", "create", "finished", "--json"], root)
|
|
634
|
-
expect(blockedPr.exitCode).toBe(1)
|
|
635
|
-
expect(blockedPr.stderr).toBe("")
|
|
636
|
-
expect(JSON.parse(blockedPr.stdout)).toMatchObject({
|
|
637
|
-
ok: false,
|
|
638
|
-
error: {
|
|
639
|
-
code: "EXECUTION_BLOCKED",
|
|
640
|
-
fields: {
|
|
641
|
-
status: "dropped",
|
|
642
|
-
blockers: [{ kind: "status", reason: "Task status is dropped" }],
|
|
643
|
-
},
|
|
644
|
-
},
|
|
645
|
-
})
|
|
646
|
-
})
|
|
647
|
-
|
|
648
|
-
test("reports and synchronizes managed integration files", async () => {
|
|
649
|
-
const root = await createTempDir()
|
|
650
|
-
tempDirs.push(root)
|
|
651
|
-
expect((await runCli(["init", root])).exitCode).toBe(0)
|
|
652
|
-
|
|
653
|
-
const before = parseJson(
|
|
654
|
-
await runCli(["integration", "status", "--json"], root),
|
|
655
|
-
)
|
|
656
|
-
expect(before.files).toMatchObject([
|
|
657
|
-
{ name: "agents", state: "managed" },
|
|
658
|
-
{ name: "opencode", state: "managed" },
|
|
659
|
-
{ name: "opencode-plugin", state: "managed" },
|
|
660
|
-
{ name: "opencode-tui", state: "managed" },
|
|
661
|
-
{ name: "opencode-tui-plugin", state: "managed" },
|
|
662
|
-
])
|
|
663
|
-
|
|
664
|
-
const synced = parseJson(
|
|
665
|
-
await runCli(["integration", "sync", "--json"], root),
|
|
666
|
-
)
|
|
667
|
-
expect(synced.files).toMatchObject([
|
|
668
|
-
{ name: "agents", state: "managed", changed: false },
|
|
669
|
-
{ name: "opencode", state: "managed", changed: false },
|
|
670
|
-
{ name: "opencode-plugin", state: "managed", changed: false },
|
|
671
|
-
{ name: "opencode-tui", state: "managed", changed: false },
|
|
672
|
-
{ name: "opencode-tui-plugin", state: "managed", changed: false },
|
|
673
|
-
])
|
|
674
|
-
})
|
|
675
|
-
|
|
676
|
-
test("registers and lists workbases", async () => {
|
|
677
|
-
const parent = await createTempDir()
|
|
678
|
-
tempDirs.push(parent)
|
|
679
|
-
const root = join(parent, "workbase")
|
|
680
|
-
const env = { XDG_CONFIG_HOME: join(parent, "config") }
|
|
681
|
-
|
|
682
|
-
expect(
|
|
683
|
-
parseJson(await runCli(["init", root, "--json"], parent, env)),
|
|
684
|
-
).toEqual({
|
|
685
|
-
root,
|
|
686
|
-
})
|
|
687
|
-
const registration = parseJson(
|
|
688
|
-
await runCli(
|
|
689
|
-
[
|
|
690
|
-
"workbase",
|
|
691
|
-
"add",
|
|
692
|
-
"workbase",
|
|
693
|
-
"--cwd",
|
|
694
|
-
parent,
|
|
695
|
-
"--name",
|
|
696
|
-
"primary",
|
|
697
|
-
"--json",
|
|
698
|
-
],
|
|
699
|
-
projectRoot,
|
|
700
|
-
env,
|
|
701
|
-
),
|
|
702
|
-
)
|
|
703
|
-
expect(registration).toMatchObject({
|
|
704
|
-
name: "primary",
|
|
705
|
-
path: await realpath(root),
|
|
706
|
-
})
|
|
707
|
-
expect(
|
|
708
|
-
parseJson(await runCli(["workbase", "list", "--json"], parent, env)),
|
|
709
|
-
).toEqual({ workbases: [registration] })
|
|
710
|
-
|
|
711
|
-
await mkdir(join(root, "repos", "agency"), { recursive: true })
|
|
712
|
-
parseJson(
|
|
713
|
-
await runCli(
|
|
714
|
-
[
|
|
715
|
-
"task",
|
|
716
|
-
"create",
|
|
717
|
-
"explicit",
|
|
718
|
-
"--repo",
|
|
719
|
-
"agency",
|
|
720
|
-
"--workbase",
|
|
721
|
-
"primary",
|
|
722
|
-
"--no-input",
|
|
723
|
-
"--json",
|
|
724
|
-
],
|
|
725
|
-
parent,
|
|
726
|
-
env,
|
|
727
|
-
),
|
|
728
|
-
)
|
|
729
|
-
const shown = parseJson(
|
|
730
|
-
await runCli(
|
|
731
|
-
[
|
|
732
|
-
"task",
|
|
733
|
-
"show",
|
|
734
|
-
"--task",
|
|
735
|
-
"explicit",
|
|
736
|
-
"--workbase",
|
|
737
|
-
registration.id,
|
|
738
|
-
"--no-input",
|
|
739
|
-
"--json",
|
|
740
|
-
],
|
|
741
|
-
parent,
|
|
742
|
-
env,
|
|
743
|
-
),
|
|
744
|
-
)
|
|
745
|
-
expect(shown.id).toBe("explicit")
|
|
746
|
-
const inferred = parseJson(
|
|
747
|
-
await runCli(
|
|
748
|
-
["context", "--cwd", join(root, "tasks", "explicit"), "--json"],
|
|
749
|
-
projectRoot,
|
|
750
|
-
env,
|
|
751
|
-
),
|
|
752
|
-
)
|
|
753
|
-
expect(inferred.target).toMatchObject({
|
|
754
|
-
kind: "task",
|
|
755
|
-
taskId: "explicit",
|
|
756
|
-
})
|
|
757
|
-
const nextByCwd = parseJson(
|
|
758
|
-
await runCli(
|
|
759
|
-
["next", "--cwd", root, "--select", "--json"],
|
|
760
|
-
projectRoot,
|
|
761
|
-
env,
|
|
762
|
-
),
|
|
763
|
-
)
|
|
764
|
-
expect(nextByCwd.selected.key).toBe("task/explicit")
|
|
765
|
-
const nextByRegistration = parseJson(
|
|
766
|
-
await runCli(
|
|
767
|
-
["next", "--workbase", "primary", "--select", "--json"],
|
|
768
|
-
projectRoot,
|
|
769
|
-
env,
|
|
770
|
-
),
|
|
771
|
-
)
|
|
772
|
-
expect(nextByRegistration.selected.key).toBe("task/explicit")
|
|
773
|
-
|
|
774
|
-
parseJson(
|
|
775
|
-
await runCli(["workbase", "default", "primary", "--json"], parent, env),
|
|
776
|
-
)
|
|
777
|
-
expect(
|
|
778
|
-
parseJson(await runCli(["task", "list", "--json"], parent, env)),
|
|
779
|
-
).toHaveLength(1)
|
|
780
|
-
const explicitOutside = await runCli(
|
|
781
|
-
["task", "list", "--cwd", parent, "--json"],
|
|
782
|
-
projectRoot,
|
|
783
|
-
env,
|
|
784
|
-
)
|
|
785
|
-
expect(explicitOutside.exitCode).toBe(1)
|
|
786
|
-
expect(explicitOutside.stderr).toBe("")
|
|
787
|
-
expect(JSON.parse(explicitOutside.stdout).error.message).toContain(
|
|
788
|
-
"No Agency workbase found from",
|
|
789
|
-
)
|
|
790
|
-
}, 20_000)
|
|
791
|
-
|
|
792
|
-
test("exports equivalent JSON and JSONL graph contracts", async () => {
|
|
793
|
-
const root = await createTempDir()
|
|
794
|
-
tempDirs.push(root)
|
|
795
|
-
expect((await runCli(["init", root])).exitCode).toBe(0)
|
|
796
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
797
|
-
await mkdir(join(root, "tasks/example"), { recursive: true })
|
|
798
|
-
await Bun.write(
|
|
799
|
-
join(root, "tasks/example/TASK.md"),
|
|
800
|
-
`---
|
|
801
|
-
ticketUrl: null
|
|
802
|
-
repo: agency
|
|
803
|
-
branch: feat/example
|
|
804
|
-
base: main
|
|
805
|
-
pr: null
|
|
806
|
-
status: open
|
|
807
|
-
---
|
|
808
|
-
|
|
809
|
-
# Example
|
|
810
|
-
`,
|
|
811
|
-
)
|
|
812
|
-
|
|
813
|
-
const json = parseJson(
|
|
814
|
-
await runCli(
|
|
815
|
-
["graph", "--json", "--include", "bodies", "--kind", "task"],
|
|
816
|
-
root,
|
|
817
|
-
),
|
|
818
|
-
)
|
|
819
|
-
expect(json).toMatchObject({
|
|
820
|
-
version: 1,
|
|
821
|
-
includes: ["bodies"],
|
|
822
|
-
nodes: [
|
|
823
|
-
{ id: "task:example", body: expect.stringContaining("# Example") },
|
|
824
|
-
],
|
|
825
|
-
edges: [],
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
const streamed = await runCli(
|
|
829
|
-
["graph", "--jsonl", "--include", "bodies", "--kind", "task"],
|
|
830
|
-
root,
|
|
831
|
-
)
|
|
832
|
-
expect(streamed.exitCode).toBe(0)
|
|
833
|
-
expect(streamed.stderr).toBe("")
|
|
834
|
-
const records = streamed.stdout
|
|
835
|
-
.trim()
|
|
836
|
-
.split("\n")
|
|
837
|
-
.map((line) => JSON.parse(line))
|
|
838
|
-
expect(records.map((record) => record.type)).toEqual([
|
|
839
|
-
"meta",
|
|
840
|
-
"node",
|
|
841
|
-
"end",
|
|
842
|
-
])
|
|
843
|
-
const reconstructed = {
|
|
844
|
-
...records[0].graph,
|
|
845
|
-
nodes: records
|
|
846
|
-
.filter((record) => record.type === "node")
|
|
847
|
-
.map((record) => record.node),
|
|
848
|
-
edges: records
|
|
849
|
-
.filter((record) => record.type === "edge")
|
|
850
|
-
.map((record) => record.edge),
|
|
851
|
-
}
|
|
852
|
-
expect(reconstructed).toEqual(json)
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
test("lets JSON override silent and disables interactive task input", async () => {
|
|
856
|
-
const root = await createTempDir()
|
|
857
|
-
tempDirs.push(root)
|
|
858
|
-
const result = await runCli(["init", root, "--json", "--silent"])
|
|
859
|
-
expect(parseJson(result)).toEqual({ root })
|
|
860
|
-
|
|
861
|
-
const interactive = await runCli(["task", "new", "--json"])
|
|
862
|
-
expect(interactive.exitCode).toBe(1)
|
|
863
|
-
expect(interactive.stderr).toBe("")
|
|
864
|
-
expect(JSON.parse(interactive.stdout)).toMatchObject({
|
|
865
|
-
version: 1,
|
|
866
|
-
ok: false,
|
|
867
|
-
error: { code: "COMMAND_FAILED", retryable: false },
|
|
868
|
-
})
|
|
869
|
-
})
|
|
870
|
-
|
|
871
|
-
test("materializes a linked repository through the CLI", async () => {
|
|
872
|
-
const parent = await createTempDir()
|
|
873
|
-
tempDirs.push(parent)
|
|
874
|
-
const root = join(parent, "workbase")
|
|
875
|
-
const source = join(parent, "source")
|
|
876
|
-
await runGit(["init", "--initial-branch=main", source])
|
|
877
|
-
await Bun.write(join(source, "README.md"), "materialize\n")
|
|
878
|
-
for (const args of [
|
|
879
|
-
["config", "user.email", "test@example.com"],
|
|
880
|
-
["config", "user.name", "Test"],
|
|
881
|
-
["add", "README.md"],
|
|
882
|
-
["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
|
|
883
|
-
[
|
|
884
|
-
"remote",
|
|
885
|
-
"add",
|
|
886
|
-
"origin",
|
|
887
|
-
"https://example.com/agency-tests/source.git",
|
|
888
|
-
],
|
|
889
|
-
]) {
|
|
890
|
-
await runGit(["-C", source, ...args])
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
parseJson(await runCli(["init", root, "--json"], parent))
|
|
894
|
-
const configPath = join(root, "agency.json")
|
|
895
|
-
await Bun.write(
|
|
896
|
-
configPath,
|
|
897
|
-
JSON.stringify({ ...(await Bun.file(configPath).json()), vcs: "git" }),
|
|
898
|
-
)
|
|
899
|
-
parseJson(await runCli(["repo", "link", "agency", source, "--json"], root))
|
|
900
|
-
const materialized = parseJson(
|
|
901
|
-
await runCli(["repo", "materialize", "agency", "--json"], root),
|
|
902
|
-
)
|
|
903
|
-
|
|
904
|
-
expect(materialized).toMatchObject({
|
|
905
|
-
alias: "agency",
|
|
906
|
-
kind: "bare",
|
|
907
|
-
target: null,
|
|
908
|
-
states: ["declared", "materialized"],
|
|
909
|
-
})
|
|
910
|
-
expect(await Bun.file(join(root, "repos/agency/HEAD")).exists()).toBe(true)
|
|
911
|
-
})
|
|
912
|
-
|
|
913
|
-
test("prepares a workspace and reports a non-mutating dry-run", async () => {
|
|
914
|
-
const parent = await createTempDir()
|
|
915
|
-
tempDirs.push(parent)
|
|
916
|
-
const root = join(parent, "workbase")
|
|
917
|
-
const source = join(parent, "source")
|
|
918
|
-
expect(
|
|
919
|
-
Bun.spawnSync(["git", "init", "--initial-branch=main", source]).exitCode,
|
|
920
|
-
).toBe(0)
|
|
921
|
-
await Bun.write(join(source, "README.md"), "example\n")
|
|
922
|
-
for (const args of [
|
|
923
|
-
["config", "user.email", "test@example.com"],
|
|
924
|
-
["config", "user.name", "Test"],
|
|
925
|
-
["add", "README.md"],
|
|
926
|
-
["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
|
|
927
|
-
["remote", "add", "origin", source],
|
|
928
|
-
]) {
|
|
929
|
-
expect(Bun.spawnSync(["git", "-C", source, ...args]).exitCode).toBe(0)
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
parseJson(await runCli(["init", root, "--json"], parent))
|
|
933
|
-
await Bun.write(
|
|
934
|
-
join(root, "agency.json"),
|
|
935
|
-
JSON.stringify({
|
|
936
|
-
version: 2,
|
|
937
|
-
repositories: {
|
|
938
|
-
agency: {
|
|
939
|
-
remote: "https://example.com/agency-tests/source.git",
|
|
940
|
-
},
|
|
941
|
-
},
|
|
942
|
-
}),
|
|
943
|
-
)
|
|
944
|
-
parseJson(await runCli(["repo", "link", "agency", source, "--json"], root))
|
|
945
|
-
parseJson(
|
|
946
|
-
await runCli(
|
|
947
|
-
[
|
|
948
|
-
"task",
|
|
949
|
-
"create",
|
|
950
|
-
"example",
|
|
951
|
-
"--repo",
|
|
952
|
-
"agency",
|
|
953
|
-
"--branch",
|
|
954
|
-
"feat/example",
|
|
955
|
-
"--base",
|
|
956
|
-
"main",
|
|
957
|
-
"--json",
|
|
958
|
-
],
|
|
959
|
-
root,
|
|
960
|
-
),
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
const planned = parseJson(
|
|
964
|
-
await runCli(["work", "prepare", "example", "--dry-run", "--json"], root),
|
|
965
|
-
)
|
|
966
|
-
const workbaseRoot = await realpath(root)
|
|
967
|
-
expect(planned).toMatchObject({
|
|
968
|
-
dryRun: true,
|
|
969
|
-
taskPath: join(workbaseRoot, "tasks/example/TASK.md"),
|
|
970
|
-
phasePath: null,
|
|
971
|
-
checkouts: [
|
|
972
|
-
{
|
|
973
|
-
repo: "agency",
|
|
974
|
-
kind: "writable",
|
|
975
|
-
action: "created",
|
|
976
|
-
resolvedCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
|
|
977
|
-
},
|
|
978
|
-
],
|
|
979
|
-
})
|
|
980
|
-
await expect(access(join(root, "tasks/example/code"))).rejects.toThrow()
|
|
981
|
-
|
|
982
|
-
const prepared = parseJson(
|
|
983
|
-
await runCli(["work", "prepare", "example", "--json"], root),
|
|
984
|
-
)
|
|
985
|
-
expect(prepared).toMatchObject({
|
|
986
|
-
dryRun: false,
|
|
987
|
-
checkouts: [{ action: "created", kind: "writable" }],
|
|
988
|
-
})
|
|
989
|
-
const task = parseJson(
|
|
990
|
-
await runCli(["task", "show", "example", "--json"], root),
|
|
991
|
-
)
|
|
992
|
-
expect(task.data.status).toBe("open")
|
|
993
|
-
|
|
994
|
-
const listed = parseJson(await runCli(["worktree", "list", "--json"], root))
|
|
995
|
-
expect(listed).toEqual([
|
|
996
|
-
expect.objectContaining({
|
|
997
|
-
owner: expect.objectContaining({ kind: "task", taskId: "example" }),
|
|
998
|
-
checkouts: [
|
|
999
|
-
expect.objectContaining({
|
|
1000
|
-
repo: "agency",
|
|
1001
|
-
kind: "writable",
|
|
1002
|
-
registered: true,
|
|
1003
|
-
actualBranch: "feat/example",
|
|
1004
|
-
actualCommit: expect.stringMatching(/^[0-9a-f]{40}$/),
|
|
1005
|
-
dirty: false,
|
|
1006
|
-
}),
|
|
1007
|
-
],
|
|
1008
|
-
}),
|
|
1009
|
-
])
|
|
1010
|
-
const preparedAgain = parseJson(
|
|
1011
|
-
await runCli(
|
|
1012
|
-
["work", "prepare", join(root, "tasks/example/TASK.md"), "--json"],
|
|
1013
|
-
parent,
|
|
1014
|
-
),
|
|
1015
|
-
)
|
|
1016
|
-
expect(preparedAgain.checkouts).toEqual([
|
|
1017
|
-
expect.objectContaining({ action: "reused", kind: "writable" }),
|
|
1018
|
-
])
|
|
1019
|
-
const rebuild = parseJson(
|
|
1020
|
-
await runCli(
|
|
1021
|
-
["worktree", "rebuild", "example", "--dry-run", "--json"],
|
|
1022
|
-
root,
|
|
1023
|
-
),
|
|
1024
|
-
)
|
|
1025
|
-
expect(rebuild).toMatchObject({
|
|
1026
|
-
operation: "rebuild",
|
|
1027
|
-
dryRun: true,
|
|
1028
|
-
actions: [
|
|
1029
|
-
`remove ${join(workbaseRoot, "tasks/example/code/agency")}`,
|
|
1030
|
-
`create ${join(workbaseRoot, "tasks/example/code/agency")}`,
|
|
1031
|
-
],
|
|
1032
|
-
})
|
|
1033
|
-
expect(
|
|
1034
|
-
await Bun.file(join(root, "tasks/example/code/agency/README.md")).text(),
|
|
1035
|
-
).toBe("example\n")
|
|
1036
|
-
})
|
|
1037
|
-
|
|
1038
|
-
test.skipIf(
|
|
1039
|
-
Bun.which("opencode") === null || process.env.AGENCY_TEST_OPENCODE !== "1",
|
|
1040
|
-
)(
|
|
1041
|
-
"provides effective whole-workbase OpenCode access from every launch topology",
|
|
1042
|
-
async () => {
|
|
1043
|
-
const parent = await createTempDir()
|
|
1044
|
-
tempDirs.push(parent)
|
|
1045
|
-
const root = join(parent, "workbase")
|
|
1046
|
-
const source = join(parent, "source")
|
|
1047
|
-
expect(
|
|
1048
|
-
Bun.spawnSync(["git", "init", "--initial-branch=main", source])
|
|
1049
|
-
.exitCode,
|
|
1050
|
-
).toBe(0)
|
|
1051
|
-
await Bun.write(join(source, "README.md"), "example\n")
|
|
1052
|
-
await mkdir(join(source, ".claude/skills/repository-skill"), {
|
|
1053
|
-
recursive: true,
|
|
1054
|
-
})
|
|
1055
|
-
await Bun.write(
|
|
1056
|
-
join(source, ".claude/skills/repository-skill/SKILL.md"),
|
|
1057
|
-
"---\nname: repository-skill\ndescription: Repository discovery test.\n---\n\nRepository skill content.\n",
|
|
1058
|
-
)
|
|
1059
|
-
for (const args of [
|
|
1060
|
-
["config", "user.email", "test@example.com"],
|
|
1061
|
-
["config", "user.name", "Test"],
|
|
1062
|
-
["add", "."],
|
|
1063
|
-
["-c", "commit.gpgsign=false", "commit", "-m", "initial"],
|
|
1064
|
-
]) {
|
|
1065
|
-
expect(Bun.spawnSync(["git", "-C", source, ...args]).exitCode).toBe(0)
|
|
1066
|
-
}
|
|
1067
|
-
await runGit(["clone", "--bare", source, join(parent, "source.git")])
|
|
1068
|
-
const daemon = await startGitDaemon(parent)
|
|
1069
|
-
daemons.push(daemon.process)
|
|
1070
|
-
await runGit(["-C", source, "remote", "add", "origin", daemon.remote])
|
|
1071
|
-
|
|
1072
|
-
parseJson(await runCli(["init", root, "--json"], parent))
|
|
1073
|
-
expect(
|
|
1074
|
-
Bun.spawnSync(["git", "init", "--initial-branch=main", root]).exitCode,
|
|
1075
|
-
).toBe(0)
|
|
1076
|
-
const agencyConfigPath = join(root, "agency.json")
|
|
1077
|
-
const agencyConfig = JSON.parse(await Bun.file(agencyConfigPath).text())
|
|
1078
|
-
await Bun.write(
|
|
1079
|
-
agencyConfigPath,
|
|
1080
|
-
`${JSON.stringify(
|
|
1081
|
-
{
|
|
1082
|
-
...agencyConfig,
|
|
1083
|
-
agents: { noop: { command: ["true"] } },
|
|
1084
|
-
},
|
|
1085
|
-
null,
|
|
1086
|
-
2,
|
|
1087
|
-
)}\n`,
|
|
1088
|
-
)
|
|
1089
|
-
parseJson(
|
|
1090
|
-
await runCli(["repo", "link", "agency", source, "--json"], root),
|
|
1091
|
-
)
|
|
1092
|
-
parseJson(
|
|
1093
|
-
await runCli(
|
|
1094
|
-
[
|
|
1095
|
-
"epic",
|
|
1096
|
-
"create",
|
|
1097
|
-
"delivery",
|
|
1098
|
-
"--ticket-url",
|
|
1099
|
-
"https://example.com/delivery",
|
|
1100
|
-
"--repo",
|
|
1101
|
-
"agency:main",
|
|
1102
|
-
"--json",
|
|
1103
|
-
],
|
|
1104
|
-
root,
|
|
1105
|
-
),
|
|
1106
|
-
)
|
|
1107
|
-
for (const [id, branch] of [
|
|
1108
|
-
["example", "feat/example"],
|
|
1109
|
-
["sibling", "feat/sibling"],
|
|
1110
|
-
] as const) {
|
|
1111
|
-
parseJson(
|
|
1112
|
-
await runCli(
|
|
1113
|
-
[
|
|
1114
|
-
"task",
|
|
1115
|
-
"create",
|
|
1116
|
-
id,
|
|
1117
|
-
"--repo",
|
|
1118
|
-
"agency",
|
|
1119
|
-
"--branch",
|
|
1120
|
-
branch,
|
|
1121
|
-
"--base",
|
|
1122
|
-
"main",
|
|
1123
|
-
"--epic",
|
|
1124
|
-
"delivery",
|
|
1125
|
-
"--json",
|
|
1126
|
-
],
|
|
1127
|
-
root,
|
|
1128
|
-
),
|
|
1129
|
-
)
|
|
1130
|
-
}
|
|
1131
|
-
parseJson(
|
|
1132
|
-
await runCli(
|
|
1133
|
-
["task", "create", "pipeline", "--multi-phase", "--json"],
|
|
1134
|
-
root,
|
|
1135
|
-
),
|
|
1136
|
-
)
|
|
1137
|
-
parseJson(
|
|
1138
|
-
await runCli(
|
|
1139
|
-
[
|
|
1140
|
-
"phase",
|
|
1141
|
-
"create",
|
|
1142
|
-
"pipeline",
|
|
1143
|
-
"build",
|
|
1144
|
-
"--repo",
|
|
1145
|
-
"agency",
|
|
1146
|
-
"--branch",
|
|
1147
|
-
"feat/pipeline-build",
|
|
1148
|
-
"--base",
|
|
1149
|
-
"main",
|
|
1150
|
-
"--json",
|
|
1151
|
-
],
|
|
1152
|
-
root,
|
|
1153
|
-
),
|
|
1154
|
-
)
|
|
1155
|
-
|
|
1156
|
-
parseJson(await runCli(["work", "prepare", "example", "--json"], root))
|
|
1157
|
-
parseJson(
|
|
1158
|
-
await runCli(
|
|
1159
|
-
[
|
|
1160
|
-
"work",
|
|
1161
|
-
"prepare",
|
|
1162
|
-
"--task",
|
|
1163
|
-
"pipeline",
|
|
1164
|
-
"--phase",
|
|
1165
|
-
"build",
|
|
1166
|
-
"--json",
|
|
1167
|
-
],
|
|
1168
|
-
root,
|
|
1169
|
-
),
|
|
1170
|
-
)
|
|
1171
|
-
const synced = parseJson(
|
|
1172
|
-
await runCli(["integration", "sync", "--json"], root),
|
|
1173
|
-
)
|
|
1174
|
-
expect(
|
|
1175
|
-
synced.files.every((file: { changed: boolean }) => !file.changed),
|
|
1176
|
-
).toBe(true)
|
|
1177
|
-
|
|
1178
|
-
const workbaseRoot = await realpath(root)
|
|
1179
|
-
const config = await Bun.file(
|
|
1180
|
-
join(workbaseRoot, ".opencode/opencode.jsonc"),
|
|
1181
|
-
).text()
|
|
1182
|
-
expect(config).not.toContain(workbaseRoot)
|
|
1183
|
-
const documents = [
|
|
1184
|
-
join(workbaseRoot, "tasks/example/TASK.md"),
|
|
1185
|
-
join(workbaseRoot, "epics/delivery/EPIC.md"),
|
|
1186
|
-
join(workbaseRoot, "tasks/sibling/TASK.md"),
|
|
1187
|
-
]
|
|
1188
|
-
for (const document of documents) {
|
|
1189
|
-
expect(await Bun.file(document).exists()).toBe(true)
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
const launches = [
|
|
1193
|
-
{
|
|
1194
|
-
args: ["--task", "example"],
|
|
1195
|
-
cwd: join(workbaseRoot, "tasks/example"),
|
|
1196
|
-
checkoutPath: join(workbaseRoot, "tasks/example/code/agency"),
|
|
1197
|
-
skillPath: join(
|
|
1198
|
-
workbaseRoot,
|
|
1199
|
-
"tasks/example/code/agency/.claude/skills",
|
|
1200
|
-
),
|
|
1201
|
-
},
|
|
1202
|
-
{
|
|
1203
|
-
args: ["--task", "pipeline", "--phase", "build"],
|
|
1204
|
-
cwd: join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
1205
|
-
checkoutPath: join(
|
|
1206
|
-
workbaseRoot,
|
|
1207
|
-
"tasks/pipeline/phases/build/code/agency",
|
|
1208
|
-
),
|
|
1209
|
-
skillPath: join(
|
|
1210
|
-
workbaseRoot,
|
|
1211
|
-
"tasks/pipeline/phases/build/code/agency/.claude/skills",
|
|
1212
|
-
),
|
|
1213
|
-
},
|
|
1214
|
-
{
|
|
1215
|
-
args: ["--epic", "delivery"],
|
|
1216
|
-
cwd: join(workbaseRoot, "epics/delivery"),
|
|
1217
|
-
checkoutPath: undefined,
|
|
1218
|
-
skillPath: undefined,
|
|
1219
|
-
},
|
|
1220
|
-
{
|
|
1221
|
-
args: ["--task", "pipeline"],
|
|
1222
|
-
cwd: join(workbaseRoot, "tasks/pipeline"),
|
|
1223
|
-
checkoutPath: undefined,
|
|
1224
|
-
skillPath: undefined,
|
|
1225
|
-
},
|
|
1226
|
-
]
|
|
1227
|
-
for (const launch of launches) {
|
|
1228
|
-
const printed = await runCli(
|
|
1229
|
-
["work", ...launch.args, "--opencode", "--print-command", "--force"],
|
|
1230
|
-
root,
|
|
1231
|
-
)
|
|
1232
|
-
expect(printed.exitCode).toBe(0)
|
|
1233
|
-
expect(printed.stderr).toBe("")
|
|
1234
|
-
const contract = JSON.parse(printed.stdout)
|
|
1235
|
-
expect(contract.cwd).toBe(launch.cwd)
|
|
1236
|
-
expect(contract.argv).toEqual(["opencode"])
|
|
1237
|
-
expect(contract.environment.OPENCODE_CONFIG).toBeUndefined()
|
|
1238
|
-
expect(contract.environment.AGENCY_WRITABLE_CHECKOUT).toBe(
|
|
1239
|
-
launch.checkoutPath,
|
|
1240
|
-
)
|
|
1241
|
-
expect(contract.environment.OPENCODE_CONFIG_CONTENT).toBeUndefined()
|
|
1242
|
-
if (launch === launches[1]) {
|
|
1243
|
-
const context = parseJson(
|
|
1244
|
-
await runCli(["context", ".", "--json"], contract.cwd),
|
|
1245
|
-
)
|
|
1246
|
-
expect(context.target).toMatchObject({
|
|
1247
|
-
kind: "phase",
|
|
1248
|
-
taskId: "pipeline",
|
|
1249
|
-
phaseId: "build",
|
|
1250
|
-
})
|
|
1251
|
-
}
|
|
1252
|
-
const environment = {
|
|
1253
|
-
...process.env,
|
|
1254
|
-
...contract.environment,
|
|
1255
|
-
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
1256
|
-
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
1257
|
-
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
1258
|
-
OPENCODE_DISABLE_MODELS_FETCH: "1",
|
|
1259
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
1260
|
-
model: "opencode/big-pickle",
|
|
1261
|
-
}),
|
|
1262
|
-
}
|
|
1263
|
-
const probe = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
|
|
1264
|
-
cwd: contract.cwd,
|
|
1265
|
-
env: environment,
|
|
1266
|
-
})
|
|
1267
|
-
expect(probe.exitCode, probe.stderr.toString()).toBe(0)
|
|
1268
|
-
const agent = JSON.parse(probe.stdout.toString())
|
|
1269
|
-
expect(agent.permission).toEqual(
|
|
1270
|
-
expect.arrayContaining([
|
|
1271
|
-
expect.objectContaining({
|
|
1272
|
-
permission: "external_directory",
|
|
1273
|
-
pattern: join(workbaseRoot, "*"),
|
|
1274
|
-
action: "allow",
|
|
1275
|
-
}),
|
|
1276
|
-
]),
|
|
1277
|
-
)
|
|
1278
|
-
const configProbe = Bun.spawnSync(["opencode", "debug", "config"], {
|
|
1279
|
-
cwd: contract.cwd,
|
|
1280
|
-
env: environment,
|
|
1281
|
-
})
|
|
1282
|
-
expect(configProbe.exitCode).toBe(0)
|
|
1283
|
-
const effectiveConfig = JSON.parse(configProbe.stdout.toString())
|
|
1284
|
-
expect(effectiveConfig.permission.external_directory).toMatchObject({
|
|
1285
|
-
[join(workbaseRoot, "*")]: "allow",
|
|
1286
|
-
})
|
|
1287
|
-
expect(effectiveConfig.plugin_origins).toEqual(
|
|
1288
|
-
expect.arrayContaining([
|
|
1289
|
-
expect.objectContaining({
|
|
1290
|
-
spec: expect.stringContaining("agency-repository-skills.ts"),
|
|
1291
|
-
}),
|
|
1292
|
-
]),
|
|
1293
|
-
)
|
|
1294
|
-
if (launch.skillPath) {
|
|
1295
|
-
expect(effectiveConfig.skills.paths).toContain(
|
|
1296
|
-
`${launch.skillPath}${sep}.`,
|
|
1297
|
-
)
|
|
1298
|
-
const skillProbe = Bun.spawnSync(
|
|
1299
|
-
[
|
|
1300
|
-
"opencode",
|
|
1301
|
-
"debug",
|
|
1302
|
-
"agent",
|
|
1303
|
-
"build",
|
|
1304
|
-
"--tool",
|
|
1305
|
-
"skill",
|
|
1306
|
-
"--params",
|
|
1307
|
-
JSON.stringify({ name: "repository-skill" }),
|
|
1308
|
-
],
|
|
1309
|
-
{ cwd: contract.cwd, env: environment },
|
|
1310
|
-
)
|
|
1311
|
-
expect(skillProbe.exitCode).toBe(0)
|
|
1312
|
-
expect(
|
|
1313
|
-
JSON.parse(skillProbe.stdout.toString()).result.output,
|
|
1314
|
-
).toContain("Repository skill content.")
|
|
1315
|
-
} else {
|
|
1316
|
-
expect(
|
|
1317
|
-
(effectiveConfig.skills?.paths ?? []).filter((path: string) =>
|
|
1318
|
-
path.endsWith(`${sep}.`),
|
|
1319
|
-
),
|
|
1320
|
-
).toEqual([])
|
|
1321
|
-
}
|
|
1322
|
-
if (launch === launches[0]) {
|
|
1323
|
-
expect(effectiveConfig.instructions).toContain(".agency/AGENTS.md")
|
|
1324
|
-
expect(effectiveConfig.agent.agency).toBeUndefined()
|
|
1325
|
-
expect(effectiveConfig.references).toEqual({
|
|
1326
|
-
workbase: {
|
|
1327
|
-
path: "..",
|
|
1328
|
-
description:
|
|
1329
|
-
"Complete Agency workbase context; write authority still comes only from agency context",
|
|
1330
|
-
},
|
|
1331
|
-
})
|
|
1332
|
-
for (const document of documents) {
|
|
1333
|
-
const read = Bun.spawnSync(
|
|
1334
|
-
[
|
|
1335
|
-
"opencode",
|
|
1336
|
-
"debug",
|
|
1337
|
-
"agent",
|
|
1338
|
-
"build",
|
|
1339
|
-
"--tool",
|
|
1340
|
-
"read",
|
|
1341
|
-
"--params",
|
|
1342
|
-
JSON.stringify({ filePath: document }),
|
|
1343
|
-
],
|
|
1344
|
-
{ cwd: contract.cwd, env: environment },
|
|
1345
|
-
)
|
|
1346
|
-
expect(read.exitCode).toBe(0)
|
|
1347
|
-
const result = JSON.parse(read.stdout.toString())
|
|
1348
|
-
expect(result.result.output).toContain(`<path>${document}</path>`)
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
const agencyBin = join(parent, "bin")
|
|
1354
|
-
await mkdir(agencyBin)
|
|
1355
|
-
await symlink(cliPath, join(agencyBin, "agency"))
|
|
1356
|
-
const directEnvironment: Record<string, string | undefined> = {
|
|
1357
|
-
...process.env,
|
|
1358
|
-
PATH: `${agencyBin}:${process.env.PATH ?? ""}`,
|
|
1359
|
-
XDG_CONFIG_HOME: isolatedConfigHome,
|
|
1360
|
-
OPENCODE_DISABLE_AUTOUPDATE: "1",
|
|
1361
|
-
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
|
|
1362
|
-
OPENCODE_DISABLE_MODELS_FETCH: "1",
|
|
1363
|
-
}
|
|
1364
|
-
delete directEnvironment.OPENCODE_CONFIG
|
|
1365
|
-
delete directEnvironment.OPENCODE_CONFIG_CONTENT
|
|
1366
|
-
delete directEnvironment.AGENCY_WORKBASE
|
|
1367
|
-
delete directEnvironment.AGENCY_TASK_ID
|
|
1368
|
-
delete directEnvironment.AGENCY_PHASE_ID
|
|
1369
|
-
delete directEnvironment.AGENCY_WRITABLE_CHECKOUT
|
|
1370
|
-
const directDirectories = [
|
|
1371
|
-
join(workbaseRoot, "tasks/example"),
|
|
1372
|
-
join(workbaseRoot, "tasks/pipeline"),
|
|
1373
|
-
join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
1374
|
-
join(workbaseRoot, "epics/delivery"),
|
|
1375
|
-
]
|
|
1376
|
-
for (const cwd of directDirectories) {
|
|
1377
|
-
const probe = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
|
|
1378
|
-
cwd,
|
|
1379
|
-
env: directEnvironment,
|
|
1380
|
-
})
|
|
1381
|
-
expect(probe.exitCode).toBe(0)
|
|
1382
|
-
const agent = JSON.parse(probe.stdout.toString())
|
|
1383
|
-
expect(agent.permission).toEqual(
|
|
1384
|
-
expect.arrayContaining([
|
|
1385
|
-
expect.objectContaining({
|
|
1386
|
-
permission: "external_directory",
|
|
1387
|
-
pattern: join(workbaseRoot, "*"),
|
|
1388
|
-
action: "allow",
|
|
1389
|
-
}),
|
|
1390
|
-
]),
|
|
1391
|
-
)
|
|
1392
|
-
const configProbe = Bun.spawnSync(["opencode", "debug", "config"], {
|
|
1393
|
-
cwd,
|
|
1394
|
-
env: directEnvironment,
|
|
1395
|
-
})
|
|
1396
|
-
expect(configProbe.exitCode).toBe(0)
|
|
1397
|
-
const effectiveConfig = JSON.parse(configProbe.stdout.toString())
|
|
1398
|
-
expect(effectiveConfig.permission.external_directory).toMatchObject({
|
|
1399
|
-
[join(workbaseRoot, "*")]: "allow",
|
|
1400
|
-
})
|
|
1401
|
-
expect(effectiveConfig.plugin_origins).toEqual(
|
|
1402
|
-
expect.arrayContaining([
|
|
1403
|
-
expect.objectContaining({
|
|
1404
|
-
spec: expect.stringContaining("agency-repository-skills.ts"),
|
|
1405
|
-
}),
|
|
1406
|
-
]),
|
|
1407
|
-
)
|
|
1408
|
-
for (const document of documents) {
|
|
1409
|
-
const read = Bun.spawnSync(
|
|
1410
|
-
[
|
|
1411
|
-
"opencode",
|
|
1412
|
-
"debug",
|
|
1413
|
-
"agent",
|
|
1414
|
-
"build",
|
|
1415
|
-
"--tool",
|
|
1416
|
-
"read",
|
|
1417
|
-
"--params",
|
|
1418
|
-
JSON.stringify({ filePath: document }),
|
|
1419
|
-
],
|
|
1420
|
-
{ cwd, env: directEnvironment },
|
|
1421
|
-
)
|
|
1422
|
-
expect(read.exitCode).toBe(0)
|
|
1423
|
-
expect(JSON.parse(read.stdout.toString()).result.output).toContain(
|
|
1424
|
-
`<path>${document}</path>`,
|
|
1425
|
-
)
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
const denied = Bun.spawnSync(["opencode", "debug", "agent", "build"], {
|
|
1429
|
-
cwd: directDirectories[0],
|
|
1430
|
-
env: {
|
|
1431
|
-
...directEnvironment,
|
|
1432
|
-
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
1433
|
-
model: "opencode/big-pickle",
|
|
1434
|
-
permission: { external_directory: { "*": "deny" } },
|
|
1435
|
-
}),
|
|
1436
|
-
},
|
|
1437
|
-
})
|
|
1438
|
-
expect(denied.exitCode).toBe(0)
|
|
1439
|
-
const deniedRules = JSON.parse(denied.stdout.toString()).permission
|
|
1440
|
-
const managedRule = deniedRules.findIndex(
|
|
1441
|
-
(rule: any) =>
|
|
1442
|
-
rule.permission === "external_directory" &&
|
|
1443
|
-
rule.pattern === join(workbaseRoot, "*") &&
|
|
1444
|
-
rule.action === "allow",
|
|
1445
|
-
)
|
|
1446
|
-
const userRule = deniedRules.findLastIndex(
|
|
1447
|
-
(rule: any) =>
|
|
1448
|
-
rule.permission === "external_directory" &&
|
|
1449
|
-
rule.pattern === "*" &&
|
|
1450
|
-
rule.action === "deny",
|
|
1451
|
-
)
|
|
1452
|
-
expect(managedRule).toBeGreaterThanOrEqual(0)
|
|
1453
|
-
expect(userRule).toBeGreaterThan(managedRule)
|
|
1454
|
-
for (const cwd of [
|
|
1455
|
-
join(workbaseRoot, "tasks/example"),
|
|
1456
|
-
join(workbaseRoot, "tasks/pipeline/phases/build"),
|
|
1457
|
-
]) {
|
|
1458
|
-
const manualSkill = Bun.spawnSync(
|
|
1459
|
-
[
|
|
1460
|
-
"opencode",
|
|
1461
|
-
"debug",
|
|
1462
|
-
"agent",
|
|
1463
|
-
"build",
|
|
1464
|
-
"--tool",
|
|
1465
|
-
"skill",
|
|
1466
|
-
"--params",
|
|
1467
|
-
JSON.stringify({ name: "repository-skill" }),
|
|
1468
|
-
],
|
|
1469
|
-
{ cwd, env: directEnvironment },
|
|
1470
|
-
)
|
|
1471
|
-
expect(manualSkill.exitCode, manualSkill.stderr.toString()).toBe(0)
|
|
1472
|
-
expect(
|
|
1473
|
-
JSON.parse(manualSkill.stdout.toString()).result.output,
|
|
1474
|
-
).toContain("Repository skill content.")
|
|
1475
|
-
}
|
|
1476
|
-
|
|
1477
|
-
parseJson(
|
|
1478
|
-
await runCli(["task", "status", "example", "dropped", "--json"], root),
|
|
1479
|
-
)
|
|
1480
|
-
const blocked = await runCli(
|
|
1481
|
-
["work", "--task", "example", "--agent", "noop"],
|
|
1482
|
-
root,
|
|
1483
|
-
)
|
|
1484
|
-
expect(blocked.exitCode).toBe(1)
|
|
1485
|
-
expect(blocked.stderr).toContain("Task status is dropped")
|
|
1486
|
-
expect(
|
|
1487
|
-
parseJson(await runCli(["task", "show", "example", "--json"], root))
|
|
1488
|
-
.data.status,
|
|
1489
|
-
).toBe("dropped")
|
|
1490
|
-
|
|
1491
|
-
const resumedTask = await runCli(
|
|
1492
|
-
["work", "--task", "example", "--agent", "noop", "--force"],
|
|
1493
|
-
root,
|
|
1494
|
-
)
|
|
1495
|
-
expect(resumedTask).toMatchObject({ exitCode: 0, stderr: "" })
|
|
1496
|
-
expect(resumedTask.stdout).toContain(
|
|
1497
|
-
"Reopened task/example from dropped as working",
|
|
1498
|
-
)
|
|
1499
|
-
expect(
|
|
1500
|
-
parseJson(await runCli(["task", "show", "example", "--json"], root))
|
|
1501
|
-
.data.status,
|
|
1502
|
-
).toBe("working")
|
|
1503
|
-
|
|
1504
|
-
parseJson(
|
|
1505
|
-
await runCli(
|
|
1506
|
-
["phase", "status", "pipeline", "build", "dropped", "--json"],
|
|
1507
|
-
root,
|
|
1508
|
-
),
|
|
1509
|
-
)
|
|
1510
|
-
const resumedPhase = await runCli(
|
|
1511
|
-
[
|
|
1512
|
-
"work",
|
|
1513
|
-
"--task",
|
|
1514
|
-
"pipeline",
|
|
1515
|
-
"--phase",
|
|
1516
|
-
"build",
|
|
1517
|
-
"--agent",
|
|
1518
|
-
"noop",
|
|
1519
|
-
"--force",
|
|
1520
|
-
],
|
|
1521
|
-
root,
|
|
1522
|
-
)
|
|
1523
|
-
expect(resumedPhase).toMatchObject({ exitCode: 0, stderr: "" })
|
|
1524
|
-
expect(resumedPhase.stdout).toContain(
|
|
1525
|
-
"Reopened phase/pipeline/build from dropped as working",
|
|
1526
|
-
)
|
|
1527
|
-
const graph = parseJson(await runCli(["graph", "--json"], root))
|
|
1528
|
-
expect(
|
|
1529
|
-
graph.nodes.find((node: any) => node.id === "task:pipeline").status,
|
|
1530
|
-
).toBe("working")
|
|
1531
|
-
},
|
|
1532
|
-
360_000,
|
|
1533
|
-
)
|
|
1534
|
-
|
|
1535
|
-
test("envelopes help and version output in machine mode", async () => {
|
|
1536
|
-
const help = await runCli(["status", "--help", "--json"])
|
|
1537
|
-
expect(parseJson(help)).toContain("Usage: agency status")
|
|
1538
|
-
|
|
1539
|
-
const version = await runCli(["status", "--version", "--json"])
|
|
1540
|
-
expect(parseJson(version)).toEqual({ version: "0.0.0-development" })
|
|
1541
|
-
|
|
1542
|
-
const jsonlHelp = await runCli(["graph", "--help", "--jsonl"])
|
|
1543
|
-
expect(parseJson(jsonlHelp)).toContain("Usage: agency graph")
|
|
1544
|
-
|
|
1545
|
-
const invalidJsonl = await runCli([
|
|
1546
|
-
"graph",
|
|
1547
|
-
"--jsonl",
|
|
1548
|
-
"--include",
|
|
1549
|
-
"secrets",
|
|
1550
|
-
])
|
|
1551
|
-
expect(invalidJsonl.exitCode).toBe(1)
|
|
1552
|
-
expect(invalidJsonl.stderr).toBe("")
|
|
1553
|
-
expect(invalidJsonl.stdout.trim().split("\n")).toHaveLength(1)
|
|
1554
|
-
expect(JSON.parse(invalidJsonl.stdout)).toMatchObject({
|
|
1555
|
-
version: 1,
|
|
1556
|
-
ok: false,
|
|
1557
|
-
error: { code: "CLI_USAGE" },
|
|
1558
|
-
})
|
|
1559
|
-
})
|
|
1560
|
-
|
|
1561
|
-
test("runs a multi-phase domain workflow through subprocesses", async () => {
|
|
1562
|
-
const parent = await createTempDir()
|
|
1563
|
-
tempDirs.push(parent)
|
|
1564
|
-
const root = join(parent, "workbase")
|
|
1565
|
-
const source = join(parent, "source")
|
|
1566
|
-
|
|
1567
|
-
const git = Bun.spawn(["git", "init", "--initial-branch=main", source], {
|
|
1568
|
-
stdout: "ignore",
|
|
1569
|
-
stderr: "pipe",
|
|
1570
|
-
})
|
|
1571
|
-
expect(await git.exited).toBe(0)
|
|
1572
|
-
await runGit([
|
|
1573
|
-
"-C",
|
|
1574
|
-
source,
|
|
1575
|
-
"remote",
|
|
1576
|
-
"add",
|
|
1577
|
-
"origin",
|
|
1578
|
-
"https://example.com/agency-tests/source.git",
|
|
1579
|
-
])
|
|
1580
|
-
|
|
1581
|
-
expect(parseJson(await runCli(["init", root, "--json"]))).toEqual({
|
|
1582
|
-
root,
|
|
1583
|
-
})
|
|
1584
|
-
const workbaseRoot = await realpath(root)
|
|
1585
|
-
expect(
|
|
1586
|
-
parseJson(
|
|
1587
|
-
await runCli(["repo", "link", "agency", source, "--json"], root),
|
|
1588
|
-
),
|
|
1589
|
-
).toEqual({
|
|
1590
|
-
alias: "agency",
|
|
1591
|
-
path: join(workbaseRoot, "repos/agency"),
|
|
1592
|
-
})
|
|
1593
|
-
for (const alias of ["effect", "tooling"]) {
|
|
1594
|
-
parseJson(await runCli(["repo", "link", alias, source, "--json"], root))
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
const epic = parseJson(
|
|
1598
|
-
await runCli(
|
|
1599
|
-
[
|
|
1600
|
-
"epic",
|
|
1601
|
-
"create",
|
|
1602
|
-
"delivery",
|
|
1603
|
-
"--ticket-url",
|
|
1604
|
-
"https://example.com/epics/delivery",
|
|
1605
|
-
"--repo",
|
|
1606
|
-
"agency:main",
|
|
1607
|
-
"--json",
|
|
1608
|
-
],
|
|
1609
|
-
root,
|
|
1610
|
-
),
|
|
1611
|
-
)
|
|
1612
|
-
expect(epic).toMatchObject({
|
|
1613
|
-
id: "delivery",
|
|
1614
|
-
data: { repos: [{ repo: "agency", ref: "main" }], tasks: [] },
|
|
1615
|
-
})
|
|
1616
|
-
|
|
1617
|
-
const task = parseJson(
|
|
1618
|
-
await runCli(
|
|
1619
|
-
[
|
|
1620
|
-
"task",
|
|
1621
|
-
"create",
|
|
1622
|
-
"ship",
|
|
1623
|
-
"--ticket-url",
|
|
1624
|
-
"https://example.com/tasks/ship",
|
|
1625
|
-
"--epic",
|
|
1626
|
-
"delivery",
|
|
1627
|
-
"--multi-phase",
|
|
1628
|
-
"--json",
|
|
1629
|
-
],
|
|
1630
|
-
root,
|
|
1631
|
-
),
|
|
1632
|
-
)
|
|
1633
|
-
expect(task).toMatchObject({
|
|
1634
|
-
id: "ship",
|
|
1635
|
-
data: { epic: "delivery", phases: [] },
|
|
1636
|
-
})
|
|
1637
|
-
|
|
1638
|
-
for (const [id, branch] of [
|
|
1639
|
-
["prepare", "task/ship-prepare"],
|
|
1640
|
-
["implement", "task/ship-implement"],
|
|
1641
|
-
] as const) {
|
|
1642
|
-
const phase = parseJson(
|
|
1643
|
-
await runCli(
|
|
1644
|
-
[
|
|
1645
|
-
"phase",
|
|
1646
|
-
"create",
|
|
1647
|
-
"ship",
|
|
1648
|
-
id,
|
|
1649
|
-
"--repo",
|
|
1650
|
-
"agency",
|
|
1651
|
-
"--branch",
|
|
1652
|
-
branch,
|
|
1653
|
-
"--base",
|
|
1654
|
-
"main",
|
|
1655
|
-
"--json",
|
|
1656
|
-
],
|
|
1657
|
-
root,
|
|
1658
|
-
),
|
|
1659
|
-
)
|
|
1660
|
-
expect(phase).toMatchObject({ taskId: "ship", id })
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
const release = parseJson(
|
|
1664
|
-
await runCli(
|
|
1665
|
-
[
|
|
1666
|
-
"phase",
|
|
1667
|
-
"create",
|
|
1668
|
-
"ship",
|
|
1669
|
-
"release",
|
|
1670
|
-
"--repo",
|
|
1671
|
-
"agency",
|
|
1672
|
-
"--branch",
|
|
1673
|
-
"task/ship-release",
|
|
1674
|
-
"--base",
|
|
1675
|
-
"main",
|
|
1676
|
-
"--depends-on",
|
|
1677
|
-
"prepare",
|
|
1678
|
-
"--depends-on",
|
|
1679
|
-
"implement",
|
|
1680
|
-
"--reference",
|
|
1681
|
-
"effect:main",
|
|
1682
|
-
"--reference",
|
|
1683
|
-
"tooling:main",
|
|
1684
|
-
"--json",
|
|
1685
|
-
],
|
|
1686
|
-
root,
|
|
1687
|
-
),
|
|
1688
|
-
)
|
|
1689
|
-
expect(release.data).toMatchObject({
|
|
1690
|
-
branch: "task/ship-release",
|
|
1691
|
-
base: "main",
|
|
1692
|
-
repos: [
|
|
1693
|
-
{ repo: "effect", ref: "main" },
|
|
1694
|
-
{ repo: "tooling", ref: "main" },
|
|
1695
|
-
],
|
|
1696
|
-
})
|
|
1697
|
-
|
|
1698
|
-
const epicList = parseJson(await runCli(["epic", "list", "--json"], root))
|
|
1699
|
-
expect(epicList).toHaveLength(1)
|
|
1700
|
-
expect(epicList[0].data.tasks).toEqual([{ id: "ship" }])
|
|
1701
|
-
|
|
1702
|
-
const taskShow = parseJson(
|
|
1703
|
-
await runCli(["task", "show", "ship", "--json"], root),
|
|
1704
|
-
)
|
|
1705
|
-
expect(taskShow.data.phases).toEqual([
|
|
1706
|
-
{ id: "prepare" },
|
|
1707
|
-
{ id: "implement" },
|
|
1708
|
-
{ id: "release", dependsOn: ["prepare", "implement"] },
|
|
1709
|
-
])
|
|
1710
|
-
const plainTaskShow = await runCli(["task", "show", "ship"], root)
|
|
1711
|
-
expect(plainTaskShow.exitCode).toBe(0)
|
|
1712
|
-
expect(plainTaskShow.stdout).toContain(
|
|
1713
|
-
"ticketUrl: https://example.com/tasks/ship",
|
|
1714
|
-
)
|
|
1715
|
-
expect(plainTaskShow.stderr).toBe("")
|
|
1716
|
-
|
|
1717
|
-
const phaseList = parseJson(
|
|
1718
|
-
await runCli(["phase", "list", "ship", "--json"], root),
|
|
1719
|
-
)
|
|
1720
|
-
expect(phaseList.map((phase: { id: string }) => phase.id)).toEqual([
|
|
1721
|
-
"prepare",
|
|
1722
|
-
"implement",
|
|
1723
|
-
"release",
|
|
1724
|
-
])
|
|
1725
|
-
const phaseTable = await runCli(
|
|
1726
|
-
["phase", "list", "ship", "--repository", "agency", "--no-pr"],
|
|
1727
|
-
root,
|
|
1728
|
-
)
|
|
1729
|
-
expect(phaseTable.stdout).toMatch(
|
|
1730
|
-
/PHASE\s+PARENT\s+STATUS\s+READINESS\s+REPOSITORIES\s+BRANCH/,
|
|
1731
|
-
)
|
|
1732
|
-
expect(phaseTable.stdout.indexOf("prepare")).toBeLessThan(
|
|
1733
|
-
phaseTable.stdout.indexOf("implement"),
|
|
1734
|
-
)
|
|
1735
|
-
|
|
1736
|
-
const phaseShow = parseJson(
|
|
1737
|
-
await runCli(["phase", "show", "ship", "release", "--json"], root),
|
|
1738
|
-
)
|
|
1739
|
-
expect(phaseShow).toMatchObject({ taskId: "ship", id: "release" })
|
|
1740
|
-
|
|
1741
|
-
const status = parseJson(await runCli(["status", "--json"], root))
|
|
1742
|
-
expect(status).toMatchObject({
|
|
1743
|
-
root: workbaseRoot,
|
|
1744
|
-
epicCount: 1,
|
|
1745
|
-
taskCount: 1,
|
|
1746
|
-
phaseCount: 3,
|
|
1747
|
-
valid: true,
|
|
1748
|
-
issues: [],
|
|
1749
|
-
})
|
|
1750
|
-
const doctor = parseJson(await runCli(["doctor", "--json"], root))
|
|
1751
|
-
expect(doctor).toMatchObject({
|
|
1752
|
-
version: 1,
|
|
1753
|
-
root: workbaseRoot,
|
|
1754
|
-
checks: expect.arrayContaining([
|
|
1755
|
-
expect.objectContaining({ id: "tool.git", status: "pass" }),
|
|
1756
|
-
expect.objectContaining({ id: "workbase.validation", status: "pass" }),
|
|
1757
|
-
]),
|
|
1758
|
-
})
|
|
1759
|
-
|
|
1760
|
-
const validation = parseJson(
|
|
1761
|
-
await runCli(["validate", root, "--json"], parent),
|
|
1762
|
-
)
|
|
1763
|
-
expect(validation).toEqual({
|
|
1764
|
-
root,
|
|
1765
|
-
issues: [],
|
|
1766
|
-
epicCount: 1,
|
|
1767
|
-
taskCount: 1,
|
|
1768
|
-
phaseCount: 3,
|
|
1769
|
-
valid: true,
|
|
1770
|
-
})
|
|
1771
|
-
}, 30_000)
|
|
1772
|
-
|
|
1773
|
-
test("restores portable repositories in a fresh workbase clone", async () => {
|
|
1774
|
-
const parent = await createTempDir()
|
|
1775
|
-
tempDirs.push(parent)
|
|
1776
|
-
const sourceWorktree = join(parent, "source-worktree")
|
|
1777
|
-
const source = join(parent, "source.git")
|
|
1778
|
-
const root = join(parent, "workbase")
|
|
1779
|
-
const restored = join(parent, "restored")
|
|
1780
|
-
|
|
1781
|
-
await runGit(["init", "--initial-branch=main", sourceWorktree])
|
|
1782
|
-
await Bun.write(join(sourceWorktree, "README.md"), "portable\n")
|
|
1783
|
-
await runGit([
|
|
1784
|
-
"-C",
|
|
1785
|
-
sourceWorktree,
|
|
1786
|
-
"config",
|
|
1787
|
-
"user.email",
|
|
1788
|
-
"test@example.com",
|
|
1789
|
-
])
|
|
1790
|
-
await runGit(["-C", sourceWorktree, "config", "user.name", "Test"])
|
|
1791
|
-
await runGit(["-C", sourceWorktree, "add", "README.md"])
|
|
1792
|
-
await runGit([
|
|
1793
|
-
"-C",
|
|
1794
|
-
sourceWorktree,
|
|
1795
|
-
"-c",
|
|
1796
|
-
"commit.gpgsign=false",
|
|
1797
|
-
"commit",
|
|
1798
|
-
"-m",
|
|
1799
|
-
"initial",
|
|
1800
|
-
])
|
|
1801
|
-
await runGit(["clone", "--bare", sourceWorktree, source])
|
|
1802
|
-
const daemon = await startGitDaemon(parent)
|
|
1803
|
-
|
|
1804
|
-
try {
|
|
1805
|
-
parseJson(await runCli(["init", root, "--json"], parent))
|
|
1806
|
-
parseJson(
|
|
1807
|
-
await runCli(["repo", "add", "agency", daemon.remote, "--json"], root),
|
|
1808
|
-
)
|
|
1809
|
-
parseJson(
|
|
1810
|
-
await runCli(
|
|
1811
|
-
[
|
|
1812
|
-
"task",
|
|
1813
|
-
"create",
|
|
1814
|
-
"portable",
|
|
1815
|
-
"--repo",
|
|
1816
|
-
"agency",
|
|
1817
|
-
"--branch",
|
|
1818
|
-
"feat/portable",
|
|
1819
|
-
"--base",
|
|
1820
|
-
"main",
|
|
1821
|
-
"--json",
|
|
1822
|
-
],
|
|
1823
|
-
root,
|
|
1824
|
-
),
|
|
1825
|
-
)
|
|
1826
|
-
|
|
1827
|
-
await runGit(["init", "--initial-branch=main", root])
|
|
1828
|
-
await runGit(["-C", root, "config", "user.email", "test@example.com"])
|
|
1829
|
-
await runGit(["-C", root, "config", "user.name", "Test"])
|
|
1830
|
-
await runGit(["-C", root, "add", "."])
|
|
1831
|
-
await runGit([
|
|
1832
|
-
"-C",
|
|
1833
|
-
root,
|
|
1834
|
-
"-c",
|
|
1835
|
-
"commit.gpgsign=false",
|
|
1836
|
-
"commit",
|
|
1837
|
-
"-m",
|
|
1838
|
-
"portable workbase",
|
|
1839
|
-
])
|
|
1840
|
-
const tracked = await runGit(["-C", root, "ls-files"])
|
|
1841
|
-
expect(tracked).toContain("agency.json")
|
|
1842
|
-
expect(tracked).not.toContain("repos/agency")
|
|
1843
|
-
|
|
1844
|
-
await runGit(["clone", root, restored])
|
|
1845
|
-
const planned = parseJson(
|
|
1846
|
-
await runCli(["repo", "setup", "--dry-run", "--json"], restored),
|
|
1847
|
-
)
|
|
1848
|
-
expect(planned.actions).toEqual([
|
|
1849
|
-
expect.objectContaining({
|
|
1850
|
-
alias: "agency",
|
|
1851
|
-
kind: "materialize",
|
|
1852
|
-
status: "planned",
|
|
1853
|
-
}),
|
|
1854
|
-
])
|
|
1855
|
-
expect(await Bun.file(join(restored, "repos/agency/HEAD")).exists()).toBe(
|
|
1856
|
-
false,
|
|
1857
|
-
)
|
|
1858
|
-
|
|
1859
|
-
const applied = parseJson(
|
|
1860
|
-
await runCli(["repo", "setup", "--apply", "--json"], restored),
|
|
1861
|
-
)
|
|
1862
|
-
expect(applied.actions[0]).toMatchObject({
|
|
1863
|
-
alias: "agency",
|
|
1864
|
-
status: "applied",
|
|
1865
|
-
})
|
|
1866
|
-
expect((await stat(join(restored, "repos/agency/HEAD"))).isFile()).toBe(
|
|
1867
|
-
true,
|
|
1868
|
-
)
|
|
1869
|
-
|
|
1870
|
-
const prepared = parseJson(
|
|
1871
|
-
await runCli(["work", "prepare", "portable", "--json"], restored),
|
|
1872
|
-
)
|
|
1873
|
-
expect(prepared.checkouts).toEqual([
|
|
1874
|
-
expect.objectContaining({
|
|
1875
|
-
repo: "agency",
|
|
1876
|
-
action: "created",
|
|
1877
|
-
}),
|
|
1878
|
-
])
|
|
1879
|
-
expect(
|
|
1880
|
-
await Bun.file(
|
|
1881
|
-
join(restored, "tasks/portable/code/agency/README.md"),
|
|
1882
|
-
).text(),
|
|
1883
|
-
).toBe("portable\n")
|
|
1884
|
-
} finally {
|
|
1885
|
-
daemon.process.kill()
|
|
1886
|
-
await daemon.process.exited
|
|
1887
|
-
}
|
|
1888
|
-
}, 30_000)
|
|
1889
|
-
})
|