@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,132 +0,0 @@
|
|
|
1
|
-
import { describe, expect, spyOn, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises"
|
|
3
|
-
import { tmpdir } from "node:os"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
import { Effect, Fiber } from "effect"
|
|
6
|
-
import { spawnProcess } from "./process"
|
|
7
|
-
|
|
8
|
-
const waitFor = async <A>(attempt: () => Promise<A>): Promise<A> => {
|
|
9
|
-
const deadline = Date.now() + 2_000
|
|
10
|
-
while (true) {
|
|
11
|
-
try {
|
|
12
|
-
return await attempt()
|
|
13
|
-
} catch (error) {
|
|
14
|
-
if (Date.now() >= deadline) throw error
|
|
15
|
-
await Bun.sleep(10)
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const isProcessRunning = (pid: number): boolean => {
|
|
21
|
-
try {
|
|
22
|
-
process.kill(pid, 0)
|
|
23
|
-
return true
|
|
24
|
-
} catch (error) {
|
|
25
|
-
if (
|
|
26
|
-
typeof error === "object" &&
|
|
27
|
-
error !== null &&
|
|
28
|
-
"code" in error &&
|
|
29
|
-
error.code === "ESRCH"
|
|
30
|
-
) {
|
|
31
|
-
return false
|
|
32
|
-
}
|
|
33
|
-
throw error
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
describe("spawnProcess", () => {
|
|
38
|
-
test("forwards and captures output in tee mode", async () => {
|
|
39
|
-
const forwardedStdout: Uint8Array[] = []
|
|
40
|
-
const forwardedStderr: Uint8Array[] = []
|
|
41
|
-
const stdout = spyOn(process.stdout, "write").mockImplementation(((
|
|
42
|
-
chunk: Uint8Array,
|
|
43
|
-
) => {
|
|
44
|
-
forwardedStdout.push(chunk)
|
|
45
|
-
return true
|
|
46
|
-
}) as never)
|
|
47
|
-
const stderr = spyOn(process.stderr, "write").mockImplementation(((
|
|
48
|
-
chunk: Uint8Array,
|
|
49
|
-
) => {
|
|
50
|
-
forwardedStderr.push(chunk)
|
|
51
|
-
return true
|
|
52
|
-
}) as never)
|
|
53
|
-
|
|
54
|
-
try {
|
|
55
|
-
const result = await Effect.runPromise(
|
|
56
|
-
spawnProcess(
|
|
57
|
-
["sh", "-c", "printf 'standard output'; printf 'standard error' >&2"],
|
|
58
|
-
{ stdout: "tee", stderr: "tee" },
|
|
59
|
-
),
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
expect(result).toEqual({
|
|
63
|
-
stdout: "standard output",
|
|
64
|
-
stderr: "standard error",
|
|
65
|
-
exitCode: 0,
|
|
66
|
-
})
|
|
67
|
-
expect(Buffer.concat(forwardedStdout).toString()).toBe("standard output")
|
|
68
|
-
expect(Buffer.concat(forwardedStderr).toString()).toBe("standard error")
|
|
69
|
-
} finally {
|
|
70
|
-
stdout.mockRestore()
|
|
71
|
-
stderr.mockRestore()
|
|
72
|
-
}
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
test("captures large stdout and stderr without hanging", async () => {
|
|
76
|
-
const line = "x".repeat(4096)
|
|
77
|
-
const lineCount = 256
|
|
78
|
-
const script = [
|
|
79
|
-
`const line = ${JSON.stringify(line)}`,
|
|
80
|
-
`const indexes = Array.from({ length: ${lineCount} }, (_, i) => i)`,
|
|
81
|
-
"const stdout = indexes.map((i) => `out:${i}:${line}`).join(`\n`)",
|
|
82
|
-
"const stderr = indexes.map((i) => `err:${i}:${line}`).join(`\n`)",
|
|
83
|
-
"const write = (stream, output) => new Promise((resolve, reject) => {",
|
|
84
|
-
"stream.write(output, (error) => error ? reject(error) : resolve())",
|
|
85
|
-
"})",
|
|
86
|
-
"await Promise.all([write(process.stdout, stdout), write(process.stderr, stderr)])",
|
|
87
|
-
].join("\n")
|
|
88
|
-
|
|
89
|
-
const result = await Effect.runPromise(
|
|
90
|
-
spawnProcess([process.execPath, "-e", script]),
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
expect(result.exitCode).toBe(0)
|
|
94
|
-
expect(result.stdout).toContain("out:0:")
|
|
95
|
-
expect(result.stdout).toContain(`out:${lineCount - 1}:`)
|
|
96
|
-
expect(result.stderr).toContain("err:0:")
|
|
97
|
-
expect(result.stderr).toContain(`err:${lineCount - 1}:`)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test("terminates timed-out process groups", async () => {
|
|
101
|
-
const startedAt = performance.now()
|
|
102
|
-
await expect(
|
|
103
|
-
Effect.runPromise(
|
|
104
|
-
spawnProcess(["sh", "-c", "sleep 30 & wait"], { timeoutMs: 25 }),
|
|
105
|
-
),
|
|
106
|
-
).rejects.toThrow("Process timed out")
|
|
107
|
-
expect(performance.now() - startedAt).toBeLessThan(1_000)
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
test("terminates the subprocess when interrupted", async () => {
|
|
111
|
-
const directory = await mkdtemp(join(tmpdir(), "agency-process-"))
|
|
112
|
-
const pidPath = join(directory, "pid")
|
|
113
|
-
const script = [
|
|
114
|
-
`process.on("SIGTERM", () => {})`,
|
|
115
|
-
`await Bun.write(${JSON.stringify(pidPath)}, String(process.pid))`,
|
|
116
|
-
`setInterval(() => process.stdout.write("running\\n"), 10)`,
|
|
117
|
-
].join("\n")
|
|
118
|
-
const fiber = Effect.runFork(spawnProcess([process.execPath, "-e", script]))
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const pid = Number(await waitFor(() => readFile(pidPath, "utf8")))
|
|
122
|
-
expect(isProcessRunning(pid)).toBe(true)
|
|
123
|
-
|
|
124
|
-
await Effect.runPromise(Fiber.interrupt(fiber))
|
|
125
|
-
|
|
126
|
-
expect(isProcessRunning(pid)).toBe(false)
|
|
127
|
-
} finally {
|
|
128
|
-
await Effect.runPromise(Fiber.interrupt(fiber))
|
|
129
|
-
await rm(directory, { recursive: true, force: true })
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
})
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import { createProgress } from "./progress"
|
|
3
|
-
|
|
4
|
-
describe("progress", () => {
|
|
5
|
-
test("replaces an active TTY line with its completed state", () => {
|
|
6
|
-
const output: string[] = []
|
|
7
|
-
const progress = createProgress(
|
|
8
|
-
{},
|
|
9
|
-
{ isTTY: true, write: (text) => output.push(text) },
|
|
10
|
-
)
|
|
11
|
-
|
|
12
|
-
progress.start("Preparing workspace...")
|
|
13
|
-
progress.succeed("Workspace ready")
|
|
14
|
-
|
|
15
|
-
expect(output).toEqual([
|
|
16
|
-
"\r\x1b[2K\x1b[2m○\x1b[0m Preparing workspace...",
|
|
17
|
-
"\r\x1b[2K\x1b[32m✓\x1b[0m Workspace ready\n",
|
|
18
|
-
])
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test("stays quiet for silent or non-TTY output", () => {
|
|
22
|
-
const output: string[] = []
|
|
23
|
-
for (const [silent, isTTY] of [
|
|
24
|
-
[true, true],
|
|
25
|
-
[false, false],
|
|
26
|
-
] as const) {
|
|
27
|
-
const progress = createProgress(
|
|
28
|
-
{ silent },
|
|
29
|
-
{ isTTY, write: (text) => output.push(text) },
|
|
30
|
-
)
|
|
31
|
-
progress.start("Preparing workspace...")
|
|
32
|
-
progress.fail("Workspace preparation failed")
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
expect(output).toEqual([])
|
|
36
|
-
})
|
|
37
|
-
})
|
package/src/work-view.test.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { Effect } from "effect"
|
|
3
|
-
import { mkdir } from "node:fs/promises"
|
|
4
|
-
import { dirname, join } from "node:path"
|
|
5
|
-
import { cleanupTempDir, createTempDir, runTestEffect } from "./test-utils"
|
|
6
|
-
import { getWorkViews } from "./work-view"
|
|
7
|
-
|
|
8
|
-
const write = async (root: string, path: string, content: string) => {
|
|
9
|
-
const fullPath = join(root, path)
|
|
10
|
-
await mkdir(dirname(fullPath), { recursive: true })
|
|
11
|
-
await Bun.write(fullPath, content)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("work views", () => {
|
|
15
|
-
let root: string
|
|
16
|
-
|
|
17
|
-
beforeEach(async () => {
|
|
18
|
-
root = await createTempDir()
|
|
19
|
-
await write(root, "agency.json", '{"version":2}\n')
|
|
20
|
-
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
21
|
-
await write(
|
|
22
|
-
root,
|
|
23
|
-
"epics/delivery/EPIC.md",
|
|
24
|
-
`---
|
|
25
|
-
ticketUrl: https://example.com/delivery
|
|
26
|
-
repos:
|
|
27
|
-
- repo: agency
|
|
28
|
-
ref: main
|
|
29
|
-
tasks:
|
|
30
|
-
- id: zeta
|
|
31
|
-
- id: alpha
|
|
32
|
-
---
|
|
33
|
-
`,
|
|
34
|
-
)
|
|
35
|
-
await write(
|
|
36
|
-
root,
|
|
37
|
-
"tasks/zeta/TASK.md",
|
|
38
|
-
`---
|
|
39
|
-
ticketUrl: null
|
|
40
|
-
epic: delivery
|
|
41
|
-
phases:
|
|
42
|
-
- id: verify
|
|
43
|
-
dependsOn: [implement]
|
|
44
|
-
- id: implement
|
|
45
|
-
---
|
|
46
|
-
`,
|
|
47
|
-
)
|
|
48
|
-
await write(
|
|
49
|
-
root,
|
|
50
|
-
"tasks/zeta/phases/verify/PHASE.md",
|
|
51
|
-
`---
|
|
52
|
-
repo: agency
|
|
53
|
-
branch: feat/verify
|
|
54
|
-
base: main
|
|
55
|
-
pr: null
|
|
56
|
-
status: open
|
|
57
|
-
---
|
|
58
|
-
`,
|
|
59
|
-
)
|
|
60
|
-
await write(
|
|
61
|
-
root,
|
|
62
|
-
"tasks/zeta/phases/implement/PHASE.md",
|
|
63
|
-
`---
|
|
64
|
-
repo: agency
|
|
65
|
-
branch: feat/implement
|
|
66
|
-
base: main
|
|
67
|
-
pr: https://github.com/example/agency/pull/1
|
|
68
|
-
status: dropped
|
|
69
|
-
---
|
|
70
|
-
`,
|
|
71
|
-
)
|
|
72
|
-
await mkdir(join(root, "tasks/zeta/phases/implement/code/agency"), {
|
|
73
|
-
recursive: true,
|
|
74
|
-
})
|
|
75
|
-
await write(
|
|
76
|
-
root,
|
|
77
|
-
"tasks/alpha/TASK.md",
|
|
78
|
-
`---
|
|
79
|
-
ticketUrl: null
|
|
80
|
-
epic: delivery
|
|
81
|
-
repo: agency
|
|
82
|
-
branch: feat/alpha
|
|
83
|
-
base: main
|
|
84
|
-
pr: null
|
|
85
|
-
status: open
|
|
86
|
-
---
|
|
87
|
-
`,
|
|
88
|
-
)
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
afterEach(async () => cleanupTempDir(root))
|
|
92
|
-
|
|
93
|
-
const views = (options: Record<string, unknown> = {}) =>
|
|
94
|
-
runTestEffect(Effect.suspend(() => getWorkViews({ cwd: root, ...options })))
|
|
95
|
-
|
|
96
|
-
test("uses declared graph order and exposes operational state", async () => {
|
|
97
|
-
const result = await views()
|
|
98
|
-
|
|
99
|
-
expect(result.taskRows.map((row) => row.id)).toEqual(["zeta", "alpha"])
|
|
100
|
-
expect(result.phaseRows.map((row) => row.id)).toEqual([
|
|
101
|
-
"verify",
|
|
102
|
-
"implement",
|
|
103
|
-
])
|
|
104
|
-
expect(result.executionRows.map((row) => row.key)).toEqual([
|
|
105
|
-
"zeta/verify",
|
|
106
|
-
"zeta/implement",
|
|
107
|
-
"alpha",
|
|
108
|
-
])
|
|
109
|
-
expect(result.phaseRows[0]).toMatchObject({
|
|
110
|
-
parent: "zeta",
|
|
111
|
-
status: "open",
|
|
112
|
-
readiness: "blocked",
|
|
113
|
-
repositories: "agency",
|
|
114
|
-
branch: "feat/verify",
|
|
115
|
-
pr: "absent",
|
|
116
|
-
worktree: "absent",
|
|
117
|
-
})
|
|
118
|
-
expect(result.phaseRows[1]).toMatchObject({
|
|
119
|
-
readiness: "terminal",
|
|
120
|
-
pr: "present",
|
|
121
|
-
worktree: "materialized",
|
|
122
|
-
})
|
|
123
|
-
expect(result.taskRows[0]).toMatchObject({
|
|
124
|
-
parent: "delivery",
|
|
125
|
-
branch: "multiple",
|
|
126
|
-
pr: "1/2 present",
|
|
127
|
-
worktree: "1/2 materialized",
|
|
128
|
-
})
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
test("composes lifecycle, repository, readiness, and PR filters", async () => {
|
|
132
|
-
expect(
|
|
133
|
-
(await views({ statuses: ["dropped"] })).executionRows.map(
|
|
134
|
-
(row) => row.key,
|
|
135
|
-
),
|
|
136
|
-
).toEqual(["zeta/implement"])
|
|
137
|
-
expect(
|
|
138
|
-
(await views({ repositories: ["agency"], blocked: true })).phaseRows.map(
|
|
139
|
-
(row) => row.id,
|
|
140
|
-
),
|
|
141
|
-
).toEqual(["verify"])
|
|
142
|
-
expect(
|
|
143
|
-
(await views({ ready: true, pr: false })).executionRows.map(
|
|
144
|
-
(row) => row.key,
|
|
145
|
-
),
|
|
146
|
-
).toEqual(["alpha"])
|
|
147
|
-
expect((await views({ pr: true })).phaseRows.map((row) => row.id)).toEqual([
|
|
148
|
-
"implement",
|
|
149
|
-
])
|
|
150
|
-
})
|
|
151
|
-
})
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
3
|
-
printableEnvironment,
|
|
4
|
-
resolveAgentCommand,
|
|
5
|
-
agentEnvironment,
|
|
6
|
-
validateAgents,
|
|
7
|
-
} from "./agent-command"
|
|
8
|
-
|
|
9
|
-
const variables = {
|
|
10
|
-
prompt: "Read the task.",
|
|
11
|
-
workbase: "/workbase",
|
|
12
|
-
target: "execution-unit:phase/task/build",
|
|
13
|
-
task: "task",
|
|
14
|
-
phase: "build",
|
|
15
|
-
sessionId: "session-1",
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe("agent commands", () => {
|
|
19
|
-
test("uses promptless interactive commands for built-in presets", () => {
|
|
20
|
-
expect(
|
|
21
|
-
resolveAgentCommand("opencode2", undefined, variables, false).argv,
|
|
22
|
-
).toEqual(["opencode2"])
|
|
23
|
-
expect(
|
|
24
|
-
resolveAgentCommand("opencode2", undefined, variables, true).argv,
|
|
25
|
-
).toEqual(["opencode2", "--continue"])
|
|
26
|
-
expect(
|
|
27
|
-
resolveAgentCommand("opencode", undefined, variables, false).argv,
|
|
28
|
-
).toEqual(["opencode"])
|
|
29
|
-
expect(
|
|
30
|
-
resolveAgentCommand("opencode", undefined, variables, true).argv,
|
|
31
|
-
).toEqual(["opencode", "--continue"])
|
|
32
|
-
expect(resolveAgentCommand("pi", undefined, variables, false).argv).toEqual(
|
|
33
|
-
["pi"],
|
|
34
|
-
)
|
|
35
|
-
expect(resolveAgentCommand("pi", undefined, variables, true).argv).toEqual([
|
|
36
|
-
"pi",
|
|
37
|
-
"--continue",
|
|
38
|
-
])
|
|
39
|
-
expect(
|
|
40
|
-
resolveAgentCommand("claude", undefined, variables, true).argv,
|
|
41
|
-
).toEqual(["claude", "--continue"])
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test("uses autonomous commands when a prompt is requested", () => {
|
|
45
|
-
expect(
|
|
46
|
-
resolveAgentCommand("opencode2", undefined, variables, false, true).argv,
|
|
47
|
-
).toEqual(["opencode2", "--prompt", "Read the task."])
|
|
48
|
-
expect(
|
|
49
|
-
resolveAgentCommand("opencode2", undefined, variables, true, true).argv,
|
|
50
|
-
).toEqual(["opencode2", "--continue", "--prompt", "Read the task."])
|
|
51
|
-
expect(
|
|
52
|
-
resolveAgentCommand("opencode", undefined, variables, false, true).argv,
|
|
53
|
-
).toEqual(["opencode", "--prompt", "Read the task."])
|
|
54
|
-
expect(
|
|
55
|
-
resolveAgentCommand("opencode", undefined, variables, true, true).argv,
|
|
56
|
-
).toEqual(["opencode", "--continue", "--prompt", "Read the task."])
|
|
57
|
-
expect(
|
|
58
|
-
resolveAgentCommand("pi", undefined, variables, false, true).argv,
|
|
59
|
-
).toEqual(["pi", "Read the task."])
|
|
60
|
-
expect(
|
|
61
|
-
resolveAgentCommand("claude", undefined, variables, true, true).argv,
|
|
62
|
-
).toEqual(["claude", "--continue", "Read the task."])
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
test("expands configured argv and environment without a shell", () => {
|
|
66
|
-
const resolved = resolveAgentCommand(
|
|
67
|
-
"custom",
|
|
68
|
-
{
|
|
69
|
-
custom: {
|
|
70
|
-
command: ["agent"],
|
|
71
|
-
autoCommand: ["agent", "--target={target}", "{prompt}"],
|
|
72
|
-
environment: { CUSTOM_SESSION: "{sessionId}" },
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
variables,
|
|
76
|
-
false,
|
|
77
|
-
true,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
expect(resolved).toEqual({
|
|
81
|
-
argv: [
|
|
82
|
-
"agent",
|
|
83
|
-
"--target=execution-unit:phase/task/build",
|
|
84
|
-
"Read the task.",
|
|
85
|
-
],
|
|
86
|
-
environment: { CUSTOM_SESSION: "session-1" },
|
|
87
|
-
})
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test("rejects --auto for configured agents without an auto command", () => {
|
|
91
|
-
expect(() =>
|
|
92
|
-
resolveAgentCommand(
|
|
93
|
-
"custom",
|
|
94
|
-
{ custom: { command: ["agent"] } },
|
|
95
|
-
variables,
|
|
96
|
-
false,
|
|
97
|
-
true,
|
|
98
|
-
),
|
|
99
|
-
).toThrow("Agent 'custom' does not support --auto")
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
test("rejects unknown placeholders", () => {
|
|
103
|
-
expect(() =>
|
|
104
|
-
validateAgents({ custom: { command: ["agent", "{unknown}"] } }),
|
|
105
|
-
).toThrow("Unknown agent 'custom' placeholder: {unknown}")
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test("provides normalized Agency environment and filters secret values", () => {
|
|
109
|
-
const environment = {
|
|
110
|
-
...agentEnvironment("custom", variables),
|
|
111
|
-
VISIBLE: "yes",
|
|
112
|
-
ACCESS_TOKEN: "secret",
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
expect(environment).toMatchObject({
|
|
116
|
-
AGENCY_AGENT: "custom",
|
|
117
|
-
AGENCY_INVOCATION_SOURCE: "agent",
|
|
118
|
-
AGENCY_SESSION_ID: "session-1",
|
|
119
|
-
AGENCY_WORKBASE: "/workbase",
|
|
120
|
-
AGENCY_TARGET: "execution-unit:phase/task/build",
|
|
121
|
-
AGENCY_TASK_ID: "task",
|
|
122
|
-
AGENCY_PHASE_ID: "build",
|
|
123
|
-
AGENCY_PROMPT: "Read the task.",
|
|
124
|
-
})
|
|
125
|
-
expect(printableEnvironment(environment).VISIBLE).toBe("yes")
|
|
126
|
-
expect(printableEnvironment(environment).ACCESS_TOKEN).toBeUndefined()
|
|
127
|
-
})
|
|
128
|
-
})
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
3
|
-
expandPostCheckoutCommand,
|
|
4
|
-
postCheckoutCommandEnvironment,
|
|
5
|
-
} from "./checkout-command"
|
|
6
|
-
|
|
7
|
-
const variables = {
|
|
8
|
-
repoAlias: "app",
|
|
9
|
-
repositoryPath: "/work/repos/app",
|
|
10
|
-
checkoutPath: "/work/tasks/example/code/app",
|
|
11
|
-
checkoutKind: "reference" as const,
|
|
12
|
-
requestedRef: "main",
|
|
13
|
-
base: "",
|
|
14
|
-
vcs: "git" as const,
|
|
15
|
-
workbaseRoot: "/work",
|
|
16
|
-
taskId: "example",
|
|
17
|
-
phaseId: "",
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
describe("post-checkout command templates", () => {
|
|
21
|
-
test("expands all context placeholders without shell interpolation", () => {
|
|
22
|
-
expect(
|
|
23
|
-
expandPostCheckoutCommand(
|
|
24
|
-
[
|
|
25
|
-
"tool",
|
|
26
|
-
"{repoAlias}",
|
|
27
|
-
"{repositoryPath}",
|
|
28
|
-
"{checkoutPath}",
|
|
29
|
-
"{checkoutKind}",
|
|
30
|
-
"{requestedRef}",
|
|
31
|
-
"{base}",
|
|
32
|
-
"{vcs}",
|
|
33
|
-
"{workbaseRoot}",
|
|
34
|
-
"{taskId}",
|
|
35
|
-
"{phaseId}",
|
|
36
|
-
],
|
|
37
|
-
variables,
|
|
38
|
-
),
|
|
39
|
-
).toEqual(["tool", ...Object.values(variables)])
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test("rejects unknown placeholders", () => {
|
|
43
|
-
expect(() =>
|
|
44
|
-
expandPostCheckoutCommand(["tool", "{unknown}"], variables),
|
|
45
|
-
).toThrow("{unknown}")
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
test("provides matching environment variables with empty optional values", () => {
|
|
49
|
-
expect(postCheckoutCommandEnvironment(variables)).toEqual({
|
|
50
|
-
AGENCY_REPO_ALIAS: variables.repoAlias,
|
|
51
|
-
AGENCY_REPOSITORY_PATH: variables.repositoryPath,
|
|
52
|
-
AGENCY_CHECKOUT_PATH: variables.checkoutPath,
|
|
53
|
-
AGENCY_CHECKOUT_KIND: variables.checkoutKind,
|
|
54
|
-
AGENCY_REQUESTED_REF: variables.requestedRef,
|
|
55
|
-
AGENCY_BASE: "",
|
|
56
|
-
AGENCY_VCS: variables.vcs,
|
|
57
|
-
AGENCY_WORKBASE_ROOT: variables.workbaseRoot,
|
|
58
|
-
AGENCY_TASK_ID: variables.taskId,
|
|
59
|
-
AGENCY_PHASE_ID: "",
|
|
60
|
-
})
|
|
61
|
-
})
|
|
62
|
-
})
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test"
|
|
2
|
-
import {
|
|
3
|
-
parseGitHubPullRequest,
|
|
4
|
-
parseGitHubPullRequestList,
|
|
5
|
-
parsePullRequestRecord,
|
|
6
|
-
resolveDeliveryCommand,
|
|
7
|
-
resolveGitHubCreateCommand,
|
|
8
|
-
validateDelivery,
|
|
9
|
-
} from "./delivery-command"
|
|
10
|
-
|
|
11
|
-
const delivery = {
|
|
12
|
-
provider: "forge",
|
|
13
|
-
remote: "upstream",
|
|
14
|
-
createCommand: ["forge", "create", "{repository}", "{branch}"],
|
|
15
|
-
queryCommand: ["forge", "query", "{identifier}"],
|
|
16
|
-
environment: { FORGE_BASE: "{base}" },
|
|
17
|
-
} as const
|
|
18
|
-
|
|
19
|
-
const variables = {
|
|
20
|
-
repository: "example/agency",
|
|
21
|
-
branch: "feat/example",
|
|
22
|
-
base: "main",
|
|
23
|
-
draft: "false",
|
|
24
|
-
url: "",
|
|
25
|
-
identifier: "",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("delivery commands", () => {
|
|
29
|
-
test("expands argv and environment without shell evaluation", () => {
|
|
30
|
-
expect(resolveDeliveryCommand(delivery, "create", variables)).toEqual({
|
|
31
|
-
argv: ["forge", "create", "example/agency", "feat/example"],
|
|
32
|
-
environment: { FORGE_BASE: "main" },
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
test("builds the default GitHub create command", () => {
|
|
37
|
-
const input = {
|
|
38
|
-
repository: "example/agency",
|
|
39
|
-
base: "main",
|
|
40
|
-
draft: false,
|
|
41
|
-
head: "feat/example",
|
|
42
|
-
} as const
|
|
43
|
-
expect(resolveGitHubCreateCommand(input)).toEqual({
|
|
44
|
-
argv: [
|
|
45
|
-
"gh",
|
|
46
|
-
"pr",
|
|
47
|
-
"create",
|
|
48
|
-
"--fill",
|
|
49
|
-
"--repo",
|
|
50
|
-
"example/agency",
|
|
51
|
-
"--base",
|
|
52
|
-
"main",
|
|
53
|
-
"--head",
|
|
54
|
-
"feat/example",
|
|
55
|
-
],
|
|
56
|
-
environment: {},
|
|
57
|
-
})
|
|
58
|
-
expect(
|
|
59
|
-
resolveGitHubCreateCommand({
|
|
60
|
-
...input,
|
|
61
|
-
title: "Requested",
|
|
62
|
-
head: "feat/example",
|
|
63
|
-
labels: ["ai-assisted", "platform"],
|
|
64
|
-
}),
|
|
65
|
-
).toEqual({
|
|
66
|
-
argv: [
|
|
67
|
-
"gh",
|
|
68
|
-
"pr",
|
|
69
|
-
"create",
|
|
70
|
-
"--fill",
|
|
71
|
-
"--title",
|
|
72
|
-
"Requested",
|
|
73
|
-
"--repo",
|
|
74
|
-
"example/agency",
|
|
75
|
-
"--base",
|
|
76
|
-
"main",
|
|
77
|
-
"--head",
|
|
78
|
-
"feat/example",
|
|
79
|
-
"--label",
|
|
80
|
-
"ai-assisted",
|
|
81
|
-
"--label",
|
|
82
|
-
"platform",
|
|
83
|
-
],
|
|
84
|
-
environment: {},
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test("rejects unknown placeholders", () => {
|
|
89
|
-
expect(() =>
|
|
90
|
-
validateDelivery({ ...delivery, queryCommand: ["forge", "{unknown}"] }),
|
|
91
|
-
).toThrow("Unknown delivery provider 'forge' placeholder")
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
test("requires complete and consistent normalized records", () => {
|
|
95
|
-
const record = {
|
|
96
|
-
provider: "forge",
|
|
97
|
-
repository: "example/agency",
|
|
98
|
-
identifier: "17",
|
|
99
|
-
url: "https://forge.example/example/agency/pulls/17",
|
|
100
|
-
state: "merged",
|
|
101
|
-
draft: false,
|
|
102
|
-
merged: true,
|
|
103
|
-
} as const
|
|
104
|
-
expect(parsePullRequestRecord(JSON.stringify(record))).toEqual(record)
|
|
105
|
-
expect(() =>
|
|
106
|
-
parsePullRequestRecord(JSON.stringify({ ...record, merged: false })),
|
|
107
|
-
).toThrow("inconsistent merge state")
|
|
108
|
-
expect(
|
|
109
|
-
parsePullRequestRecord(JSON.stringify({ ...record, mergeable: null })),
|
|
110
|
-
).toEqual({ ...record, mergeable: null })
|
|
111
|
-
expect(() =>
|
|
112
|
-
parsePullRequestRecord(JSON.stringify({ ...record, mergeable: "yes" })),
|
|
113
|
-
).toThrow("valid pull request record")
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test("normalizes GitHub state and mergeability", () => {
|
|
117
|
-
const base = {
|
|
118
|
-
number: 17,
|
|
119
|
-
url: "https://github.com/example/agency/pull/17",
|
|
120
|
-
title: "Ship",
|
|
121
|
-
isDraft: false,
|
|
122
|
-
headRefName: "feat/example",
|
|
123
|
-
baseRefName: "main",
|
|
124
|
-
headRepository: { nameWithOwner: "fork/agency" },
|
|
125
|
-
mergedAt: null,
|
|
126
|
-
mergeCommit: null,
|
|
127
|
-
mergeable: "UNKNOWN",
|
|
128
|
-
}
|
|
129
|
-
expect(
|
|
130
|
-
parseGitHubPullRequest(
|
|
131
|
-
JSON.stringify({
|
|
132
|
-
...base,
|
|
133
|
-
state: "OPEN",
|
|
134
|
-
baseRepository: { nameWithOwner: "example/agency" },
|
|
135
|
-
mergeable: "MERGEABLE",
|
|
136
|
-
}),
|
|
137
|
-
),
|
|
138
|
-
).toMatchObject({
|
|
139
|
-
state: "open",
|
|
140
|
-
merged: false,
|
|
141
|
-
headRepository: "fork/agency",
|
|
142
|
-
headBranch: "feat/example",
|
|
143
|
-
baseRepository: "example/agency",
|
|
144
|
-
baseBranch: "main",
|
|
145
|
-
mergeable: true,
|
|
146
|
-
})
|
|
147
|
-
expect(
|
|
148
|
-
parseGitHubPullRequest(
|
|
149
|
-
JSON.stringify({
|
|
150
|
-
...base,
|
|
151
|
-
state: "OPEN",
|
|
152
|
-
mergeable: "CONFLICTING",
|
|
153
|
-
}),
|
|
154
|
-
),
|
|
155
|
-
).toMatchObject({ state: "open", merged: false, mergeable: false })
|
|
156
|
-
expect(
|
|
157
|
-
parseGitHubPullRequest(
|
|
158
|
-
JSON.stringify({
|
|
159
|
-
...base,
|
|
160
|
-
state: "CLOSED",
|
|
161
|
-
mergedAt: "2026-07-21T00:00:00Z",
|
|
162
|
-
mergeCommit: { oid: "abc" },
|
|
163
|
-
mergeable: "UNKNOWN",
|
|
164
|
-
}),
|
|
165
|
-
),
|
|
166
|
-
).toMatchObject({ state: "merged", merged: true, mergeable: null })
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test("rejects malformed GitHub detail and list responses", () => {
|
|
170
|
-
expect(() => parseGitHubPullRequest("not-json")).toThrow(
|
|
171
|
-
"GitHub CLI did not return valid JSON for pull request",
|
|
172
|
-
)
|
|
173
|
-
expect(() => parseGitHubPullRequest("{}")).toThrow(
|
|
174
|
-
"GitHub CLI did not return a valid pull request",
|
|
175
|
-
)
|
|
176
|
-
expect(() => parseGitHubPullRequestList("{}")).toThrow(
|
|
177
|
-
"GitHub CLI did not return a valid pull request list",
|
|
178
|
-
)
|
|
179
|
-
})
|
|
180
|
-
})
|