@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,910 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import { mkdir, realpath, rm, symlink } from "node:fs/promises"
|
|
4
|
-
import { dirname, join } from "node:path"
|
|
5
|
-
import { cleanupTempDir, createTempDir, runTestEffect } from "../test-utils"
|
|
6
|
-
import { FileSystemService } from "./FileSystemService"
|
|
7
|
-
import { WorkbaseService } from "./WorkbaseService"
|
|
8
|
-
|
|
9
|
-
const write = async (root: string, path: string, content: string) => {
|
|
10
|
-
const fullPath = join(root, path)
|
|
11
|
-
await mkdir(dirname(fullPath), { recursive: true })
|
|
12
|
-
await Bun.write(fullPath, content)
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("WorkbaseService", () => {
|
|
16
|
-
let root: string
|
|
17
|
-
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
root = await createTempDir()
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
afterEach(async () => {
|
|
23
|
-
await cleanupTempDir(root)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test("discovers a workbase from a nested directory", async () => {
|
|
27
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
28
|
-
await mkdir(join(root, "nested/repository/src"), { recursive: true })
|
|
29
|
-
|
|
30
|
-
const discovered = await runTestEffect(
|
|
31
|
-
WorkbaseService.pipe(
|
|
32
|
-
Effect.flatMap((service) =>
|
|
33
|
-
service.discover(join(root, "nested/repository/src")),
|
|
34
|
-
),
|
|
35
|
-
),
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
expect(discovered).toBe(root)
|
|
39
|
-
expect(await Bun.file(join(root, "AGENTS.md")).exists()).toBe(false)
|
|
40
|
-
expect(
|
|
41
|
-
await Bun.file(join(root, ".opencode/opencode.jsonc")).exists(),
|
|
42
|
-
).toBe(false)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test("loads and validates the global agent", async () => {
|
|
46
|
-
const configDirectory = join(root, "config")
|
|
47
|
-
await write(
|
|
48
|
-
configDirectory,
|
|
49
|
-
"agency/agency.json",
|
|
50
|
-
JSON.stringify({ agent: "pi" }),
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
const config = await runTestEffect(
|
|
54
|
-
WorkbaseService.pipe(
|
|
55
|
-
Effect.flatMap((service) => service.loadGlobalConfig(configDirectory)),
|
|
56
|
-
),
|
|
57
|
-
)
|
|
58
|
-
expect(config).toEqual({ agent: "pi" })
|
|
59
|
-
|
|
60
|
-
await write(
|
|
61
|
-
configDirectory,
|
|
62
|
-
"agency/agency.json",
|
|
63
|
-
JSON.stringify({ agent: "codex" }),
|
|
64
|
-
)
|
|
65
|
-
await expect(
|
|
66
|
-
runTestEffect(
|
|
67
|
-
WorkbaseService.pipe(
|
|
68
|
-
Effect.flatMap((service) =>
|
|
69
|
-
service.loadGlobalConfig(configDirectory),
|
|
70
|
-
),
|
|
71
|
-
),
|
|
72
|
-
),
|
|
73
|
-
).rejects.toThrow("Invalid global Agency configuration")
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test("treats declared but missing repositories as valid aliases", async () => {
|
|
77
|
-
await write(
|
|
78
|
-
root,
|
|
79
|
-
"agency.json",
|
|
80
|
-
JSON.stringify({
|
|
81
|
-
version: 2,
|
|
82
|
-
repositories: {
|
|
83
|
-
agency: { remote: "https://example.com/agency.git" },
|
|
84
|
-
},
|
|
85
|
-
}),
|
|
86
|
-
)
|
|
87
|
-
await write(
|
|
88
|
-
root,
|
|
89
|
-
"tasks/portable/TASK.md",
|
|
90
|
-
`---
|
|
91
|
-
ticketUrl: null
|
|
92
|
-
repo: agency
|
|
93
|
-
branch: task/portable
|
|
94
|
-
base: main
|
|
95
|
-
pr: null
|
|
96
|
-
---
|
|
97
|
-
`,
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
const report = await runTestEffect(
|
|
101
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
expect(report.valid).toBe(true)
|
|
105
|
-
expect(report.issues).toEqual([])
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("reads configuration and documents once during validation", async () => {
|
|
109
|
-
await write(
|
|
110
|
-
root,
|
|
111
|
-
"agency.json",
|
|
112
|
-
JSON.stringify({
|
|
113
|
-
version: 2,
|
|
114
|
-
repositories: {
|
|
115
|
-
agency: { remote: "https://example.com/agency.git" },
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
)
|
|
119
|
-
await write(
|
|
120
|
-
root,
|
|
121
|
-
"tasks/example/TASK.md",
|
|
122
|
-
`---
|
|
123
|
-
ticketUrl: null
|
|
124
|
-
repo: agency
|
|
125
|
-
branch: task/example
|
|
126
|
-
base: main
|
|
127
|
-
pr: null
|
|
128
|
-
---
|
|
129
|
-
`,
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
const fs = await Effect.runPromise(
|
|
133
|
-
FileSystemService.pipe(Effect.provide(FileSystemService.Default)),
|
|
134
|
-
)
|
|
135
|
-
const reads: string[] = []
|
|
136
|
-
const exists: string[] = []
|
|
137
|
-
const trackedFs = {
|
|
138
|
-
...fs,
|
|
139
|
-
readFile: (path: string) => {
|
|
140
|
-
reads.push(path)
|
|
141
|
-
return fs.readFile(path)
|
|
142
|
-
},
|
|
143
|
-
exists: (path: string) => {
|
|
144
|
-
exists.push(path)
|
|
145
|
-
return fs.exists(path)
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const report = await Effect.runPromise(
|
|
150
|
-
WorkbaseService.pipe(
|
|
151
|
-
Effect.flatMap((service) => service.validate(root)),
|
|
152
|
-
Effect.provide(WorkbaseService.Default),
|
|
153
|
-
Effect.provideService(FileSystemService, trackedFs),
|
|
154
|
-
) as Effect.Effect<unknown, unknown, never>,
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
expect(report).toMatchObject({ valid: true, taskCount: 1 })
|
|
158
|
-
expect(
|
|
159
|
-
reads.filter((path) => path === join(root, "agency.json")),
|
|
160
|
-
).toHaveLength(2)
|
|
161
|
-
expect(
|
|
162
|
-
reads.filter((path) => path === join(root, "tasks/example/TASK.md")),
|
|
163
|
-
).toHaveLength(1)
|
|
164
|
-
expect(exists).not.toContain(join(root, "tasks/example/TASK.md"))
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
test("propagates document read failures instead of reporting them as missing", async () => {
|
|
168
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
169
|
-
await mkdir(join(root, "tasks/example/TASK.md"), { recursive: true })
|
|
170
|
-
|
|
171
|
-
await expect(
|
|
172
|
-
runTestEffect(
|
|
173
|
-
WorkbaseService.pipe(
|
|
174
|
-
Effect.flatMap((service) => service.validate(root)),
|
|
175
|
-
),
|
|
176
|
-
),
|
|
177
|
-
).rejects.toThrow(
|
|
178
|
-
`Failed to read file: ${join(root, "tasks/example/TASK.md")}`,
|
|
179
|
-
)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
test("validates non-PR completion invariants without rejecting legacy done work", async () => {
|
|
183
|
-
await write(
|
|
184
|
-
root,
|
|
185
|
-
"agency.json",
|
|
186
|
-
JSON.stringify({
|
|
187
|
-
version: 2,
|
|
188
|
-
repositories: {
|
|
189
|
-
agency: { remote: "https://example.com/agency.git" },
|
|
190
|
-
},
|
|
191
|
-
}),
|
|
192
|
-
)
|
|
193
|
-
await write(
|
|
194
|
-
root,
|
|
195
|
-
"tasks/invalid/TASK.md",
|
|
196
|
-
`---
|
|
197
|
-
ticketUrl: null
|
|
198
|
-
repo: agency
|
|
199
|
-
branch: task/invalid
|
|
200
|
-
base: main
|
|
201
|
-
pr: https://github.com/example/agency/pull/1
|
|
202
|
-
status: working
|
|
203
|
-
completion:
|
|
204
|
-
mode: non-pr
|
|
205
|
-
completedAt: 2026-07-23T18:00:00.000Z
|
|
206
|
-
summary: Investigation completed.
|
|
207
|
-
---
|
|
208
|
-
`,
|
|
209
|
-
)
|
|
210
|
-
await write(
|
|
211
|
-
root,
|
|
212
|
-
"tasks/legacy/TASK.md",
|
|
213
|
-
`---
|
|
214
|
-
ticketUrl: null
|
|
215
|
-
repo: agency
|
|
216
|
-
branch: task/legacy
|
|
217
|
-
base: main
|
|
218
|
-
pr: null
|
|
219
|
-
status: done
|
|
220
|
-
---
|
|
221
|
-
`,
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
const report = await runTestEffect(
|
|
225
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
|
|
226
|
-
)
|
|
227
|
-
expect(report.issues).toContainEqual({
|
|
228
|
-
path: "tasks/invalid/TASK.md",
|
|
229
|
-
message: "Non-PR completion requires status 'done'",
|
|
230
|
-
})
|
|
231
|
-
expect(report.issues).toContainEqual({
|
|
232
|
-
path: "tasks/invalid/TASK.md",
|
|
233
|
-
message: "Non-PR completion cannot have a recorded pull request",
|
|
234
|
-
})
|
|
235
|
-
expect(
|
|
236
|
-
report.issues.some((issue) => issue.path === "tasks/legacy/TASK.md"),
|
|
237
|
-
).toBe(false)
|
|
238
|
-
})
|
|
239
|
-
|
|
240
|
-
test("registers canonical workbase paths without duplicates", async () => {
|
|
241
|
-
const workbaseRoot = join(root, "workbase")
|
|
242
|
-
const nested = join(workbaseRoot, "nested")
|
|
243
|
-
const configDirectory = join(root, "config")
|
|
244
|
-
await write(workbaseRoot, "agency.json", '{"version":2}\n')
|
|
245
|
-
await mkdir(nested, { recursive: true })
|
|
246
|
-
|
|
247
|
-
const first = await runTestEffect(
|
|
248
|
-
WorkbaseService.pipe(
|
|
249
|
-
Effect.flatMap((service) => service.register(nested, configDirectory)),
|
|
250
|
-
),
|
|
251
|
-
)
|
|
252
|
-
await runTestEffect(
|
|
253
|
-
WorkbaseService.pipe(
|
|
254
|
-
Effect.flatMap((service) =>
|
|
255
|
-
service.register(workbaseRoot, configDirectory),
|
|
256
|
-
),
|
|
257
|
-
),
|
|
258
|
-
)
|
|
259
|
-
const registered = await runTestEffect(
|
|
260
|
-
WorkbaseService.pipe(
|
|
261
|
-
Effect.flatMap((service) => service.listRegistered(configDirectory)),
|
|
262
|
-
),
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
expect(first.path).toBe(await realpath(workbaseRoot))
|
|
266
|
-
expect(registered).toEqual([first.path])
|
|
267
|
-
expect(
|
|
268
|
-
await Bun.file(join(configDirectory, "agency/workbases.json")).json(),
|
|
269
|
-
).toEqual({ version: 2, workbases: [first] })
|
|
270
|
-
})
|
|
271
|
-
|
|
272
|
-
test("resolves names and stable IDs and manages the default", async () => {
|
|
273
|
-
const workbaseRoot = join(root, "workbase")
|
|
274
|
-
const configDirectory = join(root, "config")
|
|
275
|
-
await write(workbaseRoot, "agency.json", '{"version":2}\n')
|
|
276
|
-
|
|
277
|
-
const registered = await runTestEffect(
|
|
278
|
-
WorkbaseService.pipe(
|
|
279
|
-
Effect.flatMap((service) =>
|
|
280
|
-
service.register(workbaseRoot, configDirectory, "primary"),
|
|
281
|
-
),
|
|
282
|
-
),
|
|
283
|
-
)
|
|
284
|
-
const result = await runTestEffect(
|
|
285
|
-
WorkbaseService.pipe(
|
|
286
|
-
Effect.flatMap((service) =>
|
|
287
|
-
Effect.gen(function* () {
|
|
288
|
-
yield* service.setDefault("primary", configDirectory)
|
|
289
|
-
return {
|
|
290
|
-
byName: yield* service.resolveRegistered(
|
|
291
|
-
"primary",
|
|
292
|
-
configDirectory,
|
|
293
|
-
),
|
|
294
|
-
byId: yield* service.resolveRegistered(
|
|
295
|
-
registered.id,
|
|
296
|
-
configDirectory,
|
|
297
|
-
),
|
|
298
|
-
defaultWorkbase: yield* service.getDefault(configDirectory),
|
|
299
|
-
}
|
|
300
|
-
}),
|
|
301
|
-
),
|
|
302
|
-
),
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
expect(result.byName).toBe(registered.path)
|
|
306
|
-
expect(result.byId).toBe(registered.path)
|
|
307
|
-
expect(result.defaultWorkbase).toEqual(registered)
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
test("shows, names, clears names, and defaults by path", async () => {
|
|
311
|
-
const workbaseRoot = join(root, "workbase")
|
|
312
|
-
const configDirectory = join(root, "config")
|
|
313
|
-
await write(workbaseRoot, "agency.json", '{"version":2}\n')
|
|
314
|
-
const registered = await runTestEffect(
|
|
315
|
-
WorkbaseService.pipe(
|
|
316
|
-
Effect.flatMap((service) =>
|
|
317
|
-
service.register(workbaseRoot, configDirectory),
|
|
318
|
-
),
|
|
319
|
-
),
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
const result = await runTestEffect(
|
|
323
|
-
WorkbaseService.pipe(
|
|
324
|
-
Effect.flatMap((service) =>
|
|
325
|
-
Effect.gen(function* () {
|
|
326
|
-
const named = yield* service.nameRegistered(
|
|
327
|
-
workbaseRoot,
|
|
328
|
-
"primary",
|
|
329
|
-
configDirectory,
|
|
330
|
-
)
|
|
331
|
-
const shown = yield* service.showRegistered(
|
|
332
|
-
"primary",
|
|
333
|
-
configDirectory,
|
|
334
|
-
)
|
|
335
|
-
const defaultWorkbase = yield* service.setDefault(
|
|
336
|
-
workbaseRoot,
|
|
337
|
-
configDirectory,
|
|
338
|
-
)
|
|
339
|
-
const unnamed = yield* service.nameRegistered(
|
|
340
|
-
registered.id,
|
|
341
|
-
null,
|
|
342
|
-
configDirectory,
|
|
343
|
-
)
|
|
344
|
-
return { named, shown, defaultWorkbase, unnamed }
|
|
345
|
-
}),
|
|
346
|
-
),
|
|
347
|
-
),
|
|
348
|
-
)
|
|
349
|
-
|
|
350
|
-
expect(result.named.name).toBe("primary")
|
|
351
|
-
expect(result.shown).toEqual(result.named)
|
|
352
|
-
expect(result.defaultWorkbase?.id).toBe(registered.id)
|
|
353
|
-
expect(result.unnamed).toEqual({ id: registered.id, path: registered.path })
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
test("rejects names that collide with stable IDs", async () => {
|
|
357
|
-
const firstRoot = join(root, "first")
|
|
358
|
-
const secondRoot = join(root, "second")
|
|
359
|
-
const configDirectory = join(root, "config")
|
|
360
|
-
await write(firstRoot, "agency.json", '{"version":2}\n')
|
|
361
|
-
await write(secondRoot, "agency.json", '{"version":2}\n')
|
|
362
|
-
const first = await runTestEffect(
|
|
363
|
-
WorkbaseService.pipe(
|
|
364
|
-
Effect.flatMap((service) =>
|
|
365
|
-
service.register(firstRoot, configDirectory),
|
|
366
|
-
),
|
|
367
|
-
),
|
|
368
|
-
)
|
|
369
|
-
|
|
370
|
-
await expect(
|
|
371
|
-
runTestEffect(
|
|
372
|
-
WorkbaseService.pipe(
|
|
373
|
-
Effect.flatMap((service) =>
|
|
374
|
-
service.register(secondRoot, configDirectory, first.id),
|
|
375
|
-
),
|
|
376
|
-
),
|
|
377
|
-
),
|
|
378
|
-
).rejects.toThrow("already registered")
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
test("rejects invalid workbase names before writing the registry", async () => {
|
|
382
|
-
const workbaseRoot = join(root, "workbase")
|
|
383
|
-
const configDirectory = join(root, "config")
|
|
384
|
-
await write(workbaseRoot, "agency.json", '{"version":2}\n')
|
|
385
|
-
|
|
386
|
-
for (const name of ["bad/name", ""]) {
|
|
387
|
-
await expect(
|
|
388
|
-
runTestEffect(
|
|
389
|
-
WorkbaseService.pipe(
|
|
390
|
-
Effect.flatMap((service) =>
|
|
391
|
-
service.register(workbaseRoot, configDirectory, name),
|
|
392
|
-
),
|
|
393
|
-
),
|
|
394
|
-
),
|
|
395
|
-
).rejects.toThrow("Invalid workbase name")
|
|
396
|
-
}
|
|
397
|
-
expect(
|
|
398
|
-
await Bun.file(join(configDirectory, "agency/workbases.json")).exists(),
|
|
399
|
-
).toBe(false)
|
|
400
|
-
})
|
|
401
|
-
|
|
402
|
-
test("removes a registration by an equivalent symlink path", async () => {
|
|
403
|
-
const workbaseRoot = join(root, "workbase")
|
|
404
|
-
const linkedRoot = join(root, "linked")
|
|
405
|
-
const configDirectory = join(root, "config")
|
|
406
|
-
await write(workbaseRoot, "agency.json", '{"version":2}\n')
|
|
407
|
-
await symlink(workbaseRoot, linkedRoot)
|
|
408
|
-
await runTestEffect(
|
|
409
|
-
WorkbaseService.pipe(
|
|
410
|
-
Effect.flatMap((service) =>
|
|
411
|
-
service.register(workbaseRoot, configDirectory),
|
|
412
|
-
),
|
|
413
|
-
),
|
|
414
|
-
)
|
|
415
|
-
|
|
416
|
-
const removed = await runTestEffect(
|
|
417
|
-
WorkbaseService.pipe(
|
|
418
|
-
Effect.flatMap((service) =>
|
|
419
|
-
service.removeRegistered(linkedRoot, configDirectory),
|
|
420
|
-
),
|
|
421
|
-
),
|
|
422
|
-
)
|
|
423
|
-
expect(removed.path).toBe(await realpath(workbaseRoot))
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
test("migrates legacy registrations and prunes stale paths", async () => {
|
|
427
|
-
const workbaseRoot = join(root, "workbase")
|
|
428
|
-
const staleRoot = join(root, "stale")
|
|
429
|
-
const configDirectory = join(root, "config")
|
|
430
|
-
const registryPath = join(configDirectory, "agency/workbases.json")
|
|
431
|
-
await write(workbaseRoot, "agency.json", '{"version":2}\n')
|
|
432
|
-
await write(staleRoot, "agency.json", '{"version":2}\n')
|
|
433
|
-
await write(
|
|
434
|
-
configDirectory,
|
|
435
|
-
"agency/workbases.json",
|
|
436
|
-
JSON.stringify({ version: 1, workbases: [workbaseRoot, staleRoot] }),
|
|
437
|
-
)
|
|
438
|
-
await rm(staleRoot, { recursive: true })
|
|
439
|
-
|
|
440
|
-
const result = await runTestEffect(
|
|
441
|
-
WorkbaseService.pipe(
|
|
442
|
-
Effect.flatMap((service) =>
|
|
443
|
-
Effect.gen(function* () {
|
|
444
|
-
const before = yield* service.listRegistrations(configDirectory)
|
|
445
|
-
const removed = yield* service.pruneRegistered(configDirectory)
|
|
446
|
-
return { before, removed }
|
|
447
|
-
}),
|
|
448
|
-
),
|
|
449
|
-
),
|
|
450
|
-
)
|
|
451
|
-
|
|
452
|
-
expect(result.before.workbases).toHaveLength(2)
|
|
453
|
-
expect(result.before.workbases[0]?.id).toStartWith("wb-")
|
|
454
|
-
expect(result.removed.map((entry) => entry.path)).toEqual([staleRoot])
|
|
455
|
-
expect(await Bun.file(registryPath).json()).toEqual({
|
|
456
|
-
version: 2,
|
|
457
|
-
workbases: [result.before.workbases[0]],
|
|
458
|
-
})
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
test("rejects an invalid worktree command template", async () => {
|
|
462
|
-
await write(
|
|
463
|
-
root,
|
|
464
|
-
"agency.json",
|
|
465
|
-
JSON.stringify({
|
|
466
|
-
version: 2,
|
|
467
|
-
worktreeCreateCommand: ["tool", "{repo}"],
|
|
468
|
-
}),
|
|
469
|
-
)
|
|
470
|
-
|
|
471
|
-
await expect(
|
|
472
|
-
runTestEffect(
|
|
473
|
-
WorkbaseService.pipe(
|
|
474
|
-
Effect.flatMap((service) => service.discover(root)),
|
|
475
|
-
),
|
|
476
|
-
),
|
|
477
|
-
).rejects.toThrow("{worktree}")
|
|
478
|
-
})
|
|
479
|
-
|
|
480
|
-
test("rejects an unknown post-checkout command placeholder", async () => {
|
|
481
|
-
await write(
|
|
482
|
-
root,
|
|
483
|
-
"agency.json",
|
|
484
|
-
JSON.stringify({
|
|
485
|
-
version: 2,
|
|
486
|
-
repositories: {
|
|
487
|
-
agency: {
|
|
488
|
-
remote: "https://example.com/agency.git",
|
|
489
|
-
postCheckoutCommand: ["tool", "{unknown}"],
|
|
490
|
-
},
|
|
491
|
-
},
|
|
492
|
-
}),
|
|
493
|
-
)
|
|
494
|
-
|
|
495
|
-
await expect(
|
|
496
|
-
runTestEffect(
|
|
497
|
-
WorkbaseService.pipe(
|
|
498
|
-
Effect.flatMap((service) => service.discover(root)),
|
|
499
|
-
),
|
|
500
|
-
),
|
|
501
|
-
).rejects.toThrow("Repository 'agency'")
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
test("rejects an unknown agent command placeholder", async () => {
|
|
505
|
-
await write(
|
|
506
|
-
root,
|
|
507
|
-
"agency.json",
|
|
508
|
-
JSON.stringify({
|
|
509
|
-
version: 2,
|
|
510
|
-
agents: { custom: { command: ["agent", "{unknown}"] } },
|
|
511
|
-
}),
|
|
512
|
-
)
|
|
513
|
-
|
|
514
|
-
await expect(
|
|
515
|
-
runTestEffect(
|
|
516
|
-
WorkbaseService.pipe(
|
|
517
|
-
Effect.flatMap((service) => service.discover(root)),
|
|
518
|
-
),
|
|
519
|
-
),
|
|
520
|
-
).rejects.toThrow("{unknown}")
|
|
521
|
-
})
|
|
522
|
-
|
|
523
|
-
test("validates a workbase with an epic and multi-phase task", async () => {
|
|
524
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
525
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
526
|
-
await mkdir(join(root, "repos/effect"), { recursive: true })
|
|
527
|
-
await write(
|
|
528
|
-
root,
|
|
529
|
-
"epics/example/EPIC.md",
|
|
530
|
-
`---
|
|
531
|
-
ticketUrl: https://example.com/epics/example
|
|
532
|
-
repos:
|
|
533
|
-
- repo: agency
|
|
534
|
-
ref: main
|
|
535
|
-
tasks:
|
|
536
|
-
- id: example-task
|
|
537
|
-
---
|
|
538
|
-
|
|
539
|
-
# Example
|
|
540
|
-
`,
|
|
541
|
-
)
|
|
542
|
-
await write(
|
|
543
|
-
root,
|
|
544
|
-
"tasks/example-task/TASK.md",
|
|
545
|
-
`---
|
|
546
|
-
ticketUrl: https://example.com/tasks/example
|
|
547
|
-
epic: example
|
|
548
|
-
phases:
|
|
549
|
-
- id: implementation
|
|
550
|
-
---
|
|
551
|
-
|
|
552
|
-
# Example task
|
|
553
|
-
`,
|
|
554
|
-
)
|
|
555
|
-
await write(
|
|
556
|
-
root,
|
|
557
|
-
"tasks/example-task/phases/implementation/PHASE.md",
|
|
558
|
-
`---
|
|
559
|
-
repo: agency
|
|
560
|
-
repos:
|
|
561
|
-
- repo: effect
|
|
562
|
-
ref: main
|
|
563
|
-
branch: task/example
|
|
564
|
-
base: main
|
|
565
|
-
pr: null
|
|
566
|
-
---
|
|
567
|
-
|
|
568
|
-
# Implementation
|
|
569
|
-
`,
|
|
570
|
-
)
|
|
571
|
-
|
|
572
|
-
const report = await runTestEffect(
|
|
573
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
|
|
574
|
-
)
|
|
575
|
-
|
|
576
|
-
expect(report.valid).toBe(true)
|
|
577
|
-
expect(report.issues).toEqual([])
|
|
578
|
-
expect(report.epicCount).toBe(1)
|
|
579
|
-
expect(report.taskCount).toBe(1)
|
|
580
|
-
expect(report.phaseCount).toBe(1)
|
|
581
|
-
})
|
|
582
|
-
|
|
583
|
-
test("reports schema, alias, backlink, and dependency errors", async () => {
|
|
584
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
585
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
586
|
-
await write(
|
|
587
|
-
root,
|
|
588
|
-
"epics/example/EPIC.md",
|
|
589
|
-
`---
|
|
590
|
-
ticketUrl: https://example.com/epics/example
|
|
591
|
-
repos:
|
|
592
|
-
- repo: missing
|
|
593
|
-
ref: main
|
|
594
|
-
tasks:
|
|
595
|
-
- id: example-task
|
|
596
|
-
dependsOn:
|
|
597
|
-
- absent-task
|
|
598
|
-
---
|
|
599
|
-
|
|
600
|
-
# Example
|
|
601
|
-
`,
|
|
602
|
-
)
|
|
603
|
-
await write(
|
|
604
|
-
root,
|
|
605
|
-
"tasks/example-task/TASK.md",
|
|
606
|
-
`---
|
|
607
|
-
ticketUrl: https://example.com/tasks/example
|
|
608
|
-
repo: agency
|
|
609
|
-
repos:
|
|
610
|
-
- repo: agency
|
|
611
|
-
ref: main
|
|
612
|
-
branch: task/example
|
|
613
|
-
base: main
|
|
614
|
-
pr: null
|
|
615
|
-
---
|
|
616
|
-
|
|
617
|
-
# Example task
|
|
618
|
-
`,
|
|
619
|
-
)
|
|
620
|
-
await write(
|
|
621
|
-
root,
|
|
622
|
-
"tasks/bad-schema/TASK.md",
|
|
623
|
-
`---
|
|
624
|
-
ticketUrl: https://example.com/tasks/bad-schema
|
|
625
|
-
repo: agency
|
|
626
|
-
branch: task/bad-schema
|
|
627
|
-
base: main
|
|
628
|
-
pr: not-a-url
|
|
629
|
-
---
|
|
630
|
-
|
|
631
|
-
# Bad schema
|
|
632
|
-
`,
|
|
633
|
-
)
|
|
634
|
-
|
|
635
|
-
const report = await runTestEffect(
|
|
636
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
|
|
637
|
-
)
|
|
638
|
-
|
|
639
|
-
expect(report.valid).toBe(false)
|
|
640
|
-
expect(report.issues.map((issue) => issue.message).join("\n")).toContain(
|
|
641
|
-
"Unknown repository alias 'missing'",
|
|
642
|
-
)
|
|
643
|
-
expect(report.issues.map((issue) => issue.message).join("\n")).toContain(
|
|
644
|
-
"Unknown task dependency 'absent-task'",
|
|
645
|
-
)
|
|
646
|
-
expect(report.issues.map((issue) => issue.message).join("\n")).toContain(
|
|
647
|
-
"Task must reference parent epic 'example'",
|
|
648
|
-
)
|
|
649
|
-
expect(report.issues.map((issue) => issue.message).join("\n")).toContain(
|
|
650
|
-
"Repository 'agency' cannot also be a reference",
|
|
651
|
-
)
|
|
652
|
-
expect(
|
|
653
|
-
report.issues.some(
|
|
654
|
-
(issue) => issue.path.endsWith("bad-schema/TASK.md") && issue.message,
|
|
655
|
-
),
|
|
656
|
-
).toBe(true)
|
|
657
|
-
expect(report.issues.some((issue) => issue.path.endsWith("TASK.md"))).toBe(
|
|
658
|
-
true,
|
|
659
|
-
)
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
test("reports duplicate writable branch ownership", async () => {
|
|
663
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
664
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
665
|
-
for (const id of ["first", "second"]) {
|
|
666
|
-
await write(
|
|
667
|
-
root,
|
|
668
|
-
`tasks/${id}/TASK.md`,
|
|
669
|
-
`---
|
|
670
|
-
ticketUrl: https://example.com/tasks/${id}
|
|
671
|
-
repo: agency
|
|
672
|
-
branch: task/shared
|
|
673
|
-
base: main
|
|
674
|
-
pr: null
|
|
675
|
-
---
|
|
676
|
-
`,
|
|
677
|
-
)
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
const report = await runTestEffect(
|
|
681
|
-
WorkbaseService.pipe(Effect.flatMap((service) => service.validate(root))),
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
expect(report.valid).toBe(false)
|
|
685
|
-
expect(report.issues).toContainEqual({
|
|
686
|
-
path: "tasks/second/TASK.md",
|
|
687
|
-
message:
|
|
688
|
-
"Writable branch 'task/shared' for repository 'agency' is also owned by tasks/first/TASK.md",
|
|
689
|
-
})
|
|
690
|
-
})
|
|
691
|
-
|
|
692
|
-
test("reports high-risk structural validation failures", async () => {
|
|
693
|
-
const fixtures: readonly {
|
|
694
|
-
name: string
|
|
695
|
-
files: Readonly<Record<string, string>>
|
|
696
|
-
directories?: readonly string[]
|
|
697
|
-
expected: readonly string[]
|
|
698
|
-
}[] = [
|
|
699
|
-
{
|
|
700
|
-
name: "missing-documents",
|
|
701
|
-
files: {},
|
|
702
|
-
directories: [
|
|
703
|
-
"epics/missing-epic",
|
|
704
|
-
"tasks/missing-task",
|
|
705
|
-
"tasks/missing-task/phases/missing-phase",
|
|
706
|
-
],
|
|
707
|
-
expected: [
|
|
708
|
-
"epics/missing-epic/EPIC.md: Required document is missing",
|
|
709
|
-
"tasks/missing-task/TASK.md: Required document is missing",
|
|
710
|
-
"tasks/missing-task/phases/missing-phase/PHASE.md: Required document is missing",
|
|
711
|
-
],
|
|
712
|
-
},
|
|
713
|
-
{
|
|
714
|
-
name: "duplicates",
|
|
715
|
-
files: {
|
|
716
|
-
"epics/duplicate/EPIC.md": `---
|
|
717
|
-
ticketUrl: https://example.com/epics/duplicate
|
|
718
|
-
repos:
|
|
719
|
-
- repo: agency
|
|
720
|
-
ref: main
|
|
721
|
-
- repo: agency
|
|
722
|
-
ref: release
|
|
723
|
-
tasks:
|
|
724
|
-
- id: child
|
|
725
|
-
- id: child
|
|
726
|
-
---
|
|
727
|
-
`,
|
|
728
|
-
"tasks/child/TASK.md": `---
|
|
729
|
-
ticketUrl: https://example.com/tasks/child
|
|
730
|
-
epic: duplicate
|
|
731
|
-
phases:
|
|
732
|
-
- id: implementation
|
|
733
|
-
- id: implementation
|
|
734
|
-
---
|
|
735
|
-
`,
|
|
736
|
-
"tasks/child/phases/implementation/PHASE.md": `---
|
|
737
|
-
repo: agency
|
|
738
|
-
branch: task/duplicate
|
|
739
|
-
base: main
|
|
740
|
-
pr: null
|
|
741
|
-
---
|
|
742
|
-
`,
|
|
743
|
-
},
|
|
744
|
-
expected: [
|
|
745
|
-
"Repository references must be unique",
|
|
746
|
-
"Epic task IDs must be unique",
|
|
747
|
-
"Task phase IDs must be unique",
|
|
748
|
-
],
|
|
749
|
-
},
|
|
750
|
-
{
|
|
751
|
-
name: "backlinks",
|
|
752
|
-
files: {
|
|
753
|
-
"epics/parent/EPIC.md": `---
|
|
754
|
-
ticketUrl: https://example.com/epics/parent
|
|
755
|
-
repos:
|
|
756
|
-
- repo: agency
|
|
757
|
-
ref: main
|
|
758
|
-
tasks:
|
|
759
|
-
- id: missing-backlink
|
|
760
|
-
---
|
|
761
|
-
`,
|
|
762
|
-
"tasks/missing-backlink/TASK.md": `---
|
|
763
|
-
ticketUrl: https://example.com/tasks/missing-backlink
|
|
764
|
-
repo: agency
|
|
765
|
-
branch: task/missing-backlink
|
|
766
|
-
base: main
|
|
767
|
-
pr: null
|
|
768
|
-
---
|
|
769
|
-
`,
|
|
770
|
-
"tasks/unlisted/TASK.md": `---
|
|
771
|
-
ticketUrl: https://example.com/tasks/unlisted
|
|
772
|
-
epic: parent
|
|
773
|
-
repo: agency
|
|
774
|
-
branch: task/unlisted
|
|
775
|
-
base: main
|
|
776
|
-
pr: null
|
|
777
|
-
---
|
|
778
|
-
`,
|
|
779
|
-
},
|
|
780
|
-
expected: [
|
|
781
|
-
"Task must reference parent epic 'parent'",
|
|
782
|
-
"Epic does not list child task 'unlisted'",
|
|
783
|
-
],
|
|
784
|
-
},
|
|
785
|
-
{
|
|
786
|
-
name: "dependency-cycles",
|
|
787
|
-
files: {
|
|
788
|
-
"epics/cycles/EPIC.md": `---
|
|
789
|
-
ticketUrl: https://example.com/epics/cycles
|
|
790
|
-
repos:
|
|
791
|
-
- repo: agency
|
|
792
|
-
ref: main
|
|
793
|
-
tasks:
|
|
794
|
-
- id: first
|
|
795
|
-
dependsOn: [second]
|
|
796
|
-
- id: second
|
|
797
|
-
dependsOn: [first]
|
|
798
|
-
---
|
|
799
|
-
`,
|
|
800
|
-
"tasks/first/TASK.md": `---
|
|
801
|
-
ticketUrl: https://example.com/tasks/first
|
|
802
|
-
epic: cycles
|
|
803
|
-
phases:
|
|
804
|
-
- id: contract
|
|
805
|
-
dependsOn: [delivery]
|
|
806
|
-
- id: delivery
|
|
807
|
-
dependsOn: [contract]
|
|
808
|
-
---
|
|
809
|
-
`,
|
|
810
|
-
"tasks/first/phases/contract/PHASE.md": `---
|
|
811
|
-
repo: agency
|
|
812
|
-
branch: task/contract
|
|
813
|
-
base: main
|
|
814
|
-
pr: null
|
|
815
|
-
---
|
|
816
|
-
`,
|
|
817
|
-
"tasks/first/phases/delivery/PHASE.md": `---
|
|
818
|
-
repo: agency
|
|
819
|
-
branch: task/delivery
|
|
820
|
-
base: main
|
|
821
|
-
pr: null
|
|
822
|
-
---
|
|
823
|
-
`,
|
|
824
|
-
"tasks/second/TASK.md": `---
|
|
825
|
-
ticketUrl: https://example.com/tasks/second
|
|
826
|
-
epic: cycles
|
|
827
|
-
repo: agency
|
|
828
|
-
branch: task/second
|
|
829
|
-
base: main
|
|
830
|
-
pr: null
|
|
831
|
-
---
|
|
832
|
-
`,
|
|
833
|
-
},
|
|
834
|
-
expected: [
|
|
835
|
-
"Task dependency cycle includes 'first'",
|
|
836
|
-
"Phase dependency cycle includes 'contract'",
|
|
837
|
-
],
|
|
838
|
-
},
|
|
839
|
-
{
|
|
840
|
-
name: "phase-layout",
|
|
841
|
-
files: {
|
|
842
|
-
"tasks/multi/TASK.md": `---
|
|
843
|
-
ticketUrl: https://example.com/tasks/multi
|
|
844
|
-
phases:
|
|
845
|
-
- id: listed
|
|
846
|
-
---
|
|
847
|
-
`,
|
|
848
|
-
"tasks/multi/phases/listed/PHASE.md": `---
|
|
849
|
-
repo: agency
|
|
850
|
-
branch: task/listed
|
|
851
|
-
base: main
|
|
852
|
-
pr: null
|
|
853
|
-
---
|
|
854
|
-
`,
|
|
855
|
-
"tasks/multi/phases/unlisted/PHASE.md": `---
|
|
856
|
-
repo: agency
|
|
857
|
-
branch: task/unlisted
|
|
858
|
-
base: main
|
|
859
|
-
pr: null
|
|
860
|
-
---
|
|
861
|
-
`,
|
|
862
|
-
"tasks/single/TASK.md": `---
|
|
863
|
-
ticketUrl: https://example.com/tasks/single
|
|
864
|
-
repo: agency
|
|
865
|
-
branch: task/single
|
|
866
|
-
base: main
|
|
867
|
-
pr: null
|
|
868
|
-
---
|
|
869
|
-
`,
|
|
870
|
-
"tasks/single/phases/unexpected/PHASE.md": `---
|
|
871
|
-
repo: agency
|
|
872
|
-
branch: task/unexpected
|
|
873
|
-
base: main
|
|
874
|
-
pr: null
|
|
875
|
-
---
|
|
876
|
-
`,
|
|
877
|
-
},
|
|
878
|
-
expected: [
|
|
879
|
-
"Unlisted phase 'unlisted'",
|
|
880
|
-
"Single-phase task cannot contain phase directories",
|
|
881
|
-
],
|
|
882
|
-
},
|
|
883
|
-
]
|
|
884
|
-
|
|
885
|
-
for (const fixture of fixtures) {
|
|
886
|
-
const fixtureRoot = join(root, fixture.name)
|
|
887
|
-
await write(fixtureRoot, "agency.json", '{"version":2}\n')
|
|
888
|
-
await mkdir(join(fixtureRoot, "repos/agency"), { recursive: true })
|
|
889
|
-
for (const directory of fixture.directories ?? []) {
|
|
890
|
-
await mkdir(join(fixtureRoot, directory), { recursive: true })
|
|
891
|
-
}
|
|
892
|
-
for (const [path, content] of Object.entries(fixture.files)) {
|
|
893
|
-
await write(fixtureRoot, path, content)
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const report = await runTestEffect(
|
|
897
|
-
WorkbaseService.pipe(
|
|
898
|
-
Effect.flatMap((service) => service.validate(fixtureRoot)),
|
|
899
|
-
),
|
|
900
|
-
)
|
|
901
|
-
const messages = report.issues
|
|
902
|
-
.map((issue) => `${issue.path}: ${issue.message}`)
|
|
903
|
-
.join("\n")
|
|
904
|
-
expect(report.valid, fixture.name).toBe(false)
|
|
905
|
-
for (const expected of fixture.expected) {
|
|
906
|
-
expect(messages, fixture.name).toContain(expected)
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
})
|
|
910
|
-
})
|