@markjaquith/agency 1.11.1 → 2.0.0
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/README.md +302 -113
- package/cli.ts +123 -420
- package/index.ts +1 -1
- package/package.json +12 -17
- package/skills/agency/SKILL.md +258 -0
- package/src/cli.test.ts +290 -0
- package/src/commands/description.test.ts +103 -0
- package/src/commands/epic.test.ts +103 -0
- package/src/commands/epic.ts +101 -0
- package/src/commands/init.test.ts +42 -290
- package/src/commands/init.ts +15 -123
- package/src/commands/phase.ts +120 -0
- package/src/commands/pr.test.ts +19 -170
- package/src/commands/pr.ts +23 -179
- package/src/commands/repo.test.ts +80 -0
- package/src/commands/repo.ts +83 -0
- package/src/commands/status.test.ts +35 -477
- package/src/commands/status.ts +24 -213
- package/src/commands/task-phase.test.ts +206 -0
- package/src/commands/task.ts +83 -1285
- package/src/commands/validate.test.ts +91 -0
- package/src/commands/validate.ts +50 -0
- package/src/commands/work.test.ts +172 -419
- package/src/commands/work.ts +47 -166
- package/src/services/EpicService.test.ts +71 -0
- package/src/services/EpicService.ts +135 -0
- package/src/services/FileSystemService.ts +61 -92
- package/src/services/PhaseService.ts +323 -0
- package/src/services/PullRequestService.test.ts +386 -0
- package/src/services/PullRequestService.ts +143 -0
- package/src/services/RepositoryService.test.ts +106 -0
- package/src/services/RepositoryService.ts +159 -0
- package/src/services/TaskPhaseService.test.ts +217 -0
- package/src/services/TaskService.ts +192 -0
- package/src/services/WorkbaseService.test.ts +446 -0
- package/src/services/WorkbaseService.ts +493 -0
- package/src/services/WorktreeService.test.ts +599 -0
- package/src/services/WorktreeService.ts +357 -0
- package/src/test-utils.ts +41 -438
- package/src/types.ts +1 -215
- package/src/utils/command.ts +1 -5
- package/src/utils/effect.test.ts +24 -0
- package/src/utils/effect.ts +3 -273
- package/src/utils/process.test.ts +0 -15
- package/src/utils/process.ts +1 -60
- package/src/workbase/frontmatter.test.ts +50 -0
- package/src/workbase/frontmatter.ts +77 -0
- package/src/workbase/repository-reference.test.ts +25 -0
- package/src/workbase/repository-reference.ts +18 -0
- package/src/workbase/schemas.test.ts +146 -0
- package/src/workbase/schemas.ts +78 -0
- package/src/workbase/worktree-command.test.ts +61 -0
- package/src/workbase/worktree-command.ts +52 -0
- package/src/commands/base.test.ts +0 -198
- package/src/commands/base.ts +0 -198
- package/src/commands/clean.test.ts +0 -299
- package/src/commands/clean.ts +0 -274
- package/src/commands/completions.test.ts +0 -51
- package/src/commands/completions.ts +0 -308
- package/src/commands/emit.integration.test.ts +0 -549
- package/src/commands/emit.test.ts +0 -576
- package/src/commands/emit.ts +0 -700
- package/src/commands/emitted.test.ts +0 -226
- package/src/commands/emitted.ts +0 -57
- package/src/commands/loop.test.ts +0 -621
- package/src/commands/loop.ts +0 -406
- package/src/commands/merge.integration.test.ts +0 -195
- package/src/commands/merge.test.ts +0 -246
- package/src/commands/merge.ts +0 -244
- package/src/commands/pull.test.ts +0 -395
- package/src/commands/pull.ts +0 -205
- package/src/commands/push.test.ts +0 -641
- package/src/commands/push.ts +0 -482
- package/src/commands/rebase.test.ts +0 -521
- package/src/commands/rebase.ts +0 -232
- package/src/commands/save.test.ts +0 -247
- package/src/commands/save.ts +0 -162
- package/src/commands/source.test.ts +0 -195
- package/src/commands/source.ts +0 -72
- package/src/commands/switch.test.ts +0 -194
- package/src/commands/switch.ts +0 -84
- package/src/commands/task-branching.test.ts +0 -633
- package/src/commands/task-continue.test.ts +0 -311
- package/src/commands/task-edit.test.ts +0 -235
- package/src/commands/task-main.test.ts +0 -1066
- package/src/commands/task-squash.test.ts +0 -266
- package/src/commands/tasks.test.ts +0 -391
- package/src/commands/tasks.ts +0 -161
- package/src/commands/template-delete.test.ts +0 -178
- package/src/commands/template-delete.ts +0 -98
- package/src/commands/template-list.test.ts +0 -135
- package/src/commands/template-list.ts +0 -87
- package/src/commands/template-view.test.ts +0 -158
- package/src/commands/template-view.ts +0 -86
- package/src/commands/template.test.ts +0 -32
- package/src/commands/template.ts +0 -96
- package/src/commands/use.test.ts +0 -87
- package/src/commands/use.ts +0 -97
- package/src/constants.ts +0 -10
- package/src/errors.ts +0 -17
- package/src/schemas.ts +0 -33
- package/src/services/AgencyMetadataService.ts +0 -311
- package/src/services/ClaudeService.test.ts +0 -184
- package/src/services/ClaudeService.ts +0 -91
- package/src/services/ConfigService.ts +0 -115
- package/src/services/FilterRepoService.ts +0 -164
- package/src/services/FormatterService.test.ts +0 -432
- package/src/services/FormatterService.ts +0 -219
- package/src/services/GitService.ts +0 -1187
- package/src/services/MockFilterRepoService.ts +0 -140
- package/src/services/OpencodeService.ts +0 -263
- package/src/services/PromptService.ts +0 -183
- package/src/services/TemplateService.ts +0 -81
- package/src/types/native-exec.d.ts +0 -8
- package/src/utils/colors.ts +0 -178
- package/src/utils/git-path.ts +0 -6
- package/src/utils/glob.test.ts +0 -161
- package/src/utils/glob.ts +0 -78
- package/src/utils/paths.ts +0 -51
- package/src/utils/pr-branch.test.ts +0 -376
- package/src/utils/pr-branch.ts +0 -508
- package/src/utils/spinner.test.ts +0 -45
- package/src/utils/spinner.ts +0 -96
- package/src/utils/task-parser.test.ts +0 -172
- package/src/utils/task-parser.ts +0 -93
- package/templates/AGENCY.md +0 -22
- package/templates/AGENTS.md +0 -11
- package/templates/CLAUDE.md +0 -3
- package/templates/TASK.md +0 -5
- package/templates/opencode.json +0 -4
|
@@ -1,576 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
|
|
2
|
-
import { join } from "path"
|
|
3
|
-
import { symlink } from "fs/promises"
|
|
4
|
-
import { emit } from "../commands/emit"
|
|
5
|
-
import { task } from "../commands/task"
|
|
6
|
-
import { resolveGitInternalPath } from "../utils/git-path"
|
|
7
|
-
import {
|
|
8
|
-
createTempDir,
|
|
9
|
-
cleanupTempDir,
|
|
10
|
-
initGitRepo,
|
|
11
|
-
initAgency,
|
|
12
|
-
fileExists,
|
|
13
|
-
getGitOutput,
|
|
14
|
-
getCurrentBranch,
|
|
15
|
-
createCommit,
|
|
16
|
-
checkoutBranch,
|
|
17
|
-
createBranch,
|
|
18
|
-
addAndCommit,
|
|
19
|
-
setupRemote,
|
|
20
|
-
runTestEffect,
|
|
21
|
-
runTestEffectWithMockFilterRepo,
|
|
22
|
-
clearCapturedFilterRepoCalls,
|
|
23
|
-
getLastCapturedFilterRepoCall,
|
|
24
|
-
} from "../test-utils"
|
|
25
|
-
|
|
26
|
-
// Cache the git-filter-repo availability check (it doesn't change during test run)
|
|
27
|
-
let hasGitFilterRepoCache: boolean | null = null
|
|
28
|
-
async function checkGitFilterRepo(): Promise<boolean> {
|
|
29
|
-
if (hasGitFilterRepoCache === null) {
|
|
30
|
-
const proc = Bun.spawn(["which", "git-filter-repo"], {
|
|
31
|
-
stdout: "pipe",
|
|
32
|
-
stderr: "pipe",
|
|
33
|
-
})
|
|
34
|
-
await proc.exited
|
|
35
|
-
hasGitFilterRepoCache = proc.exitCode === 0
|
|
36
|
-
}
|
|
37
|
-
return hasGitFilterRepoCache
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe("emit command", () => {
|
|
41
|
-
let tempDir: string
|
|
42
|
-
let originalCwd: string
|
|
43
|
-
|
|
44
|
-
beforeEach(async () => {
|
|
45
|
-
tempDir = await createTempDir()
|
|
46
|
-
originalCwd = process.cwd()
|
|
47
|
-
process.chdir(tempDir)
|
|
48
|
-
|
|
49
|
-
// Set config path to non-existent file to use defaults
|
|
50
|
-
process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
|
|
51
|
-
// Set config dir to temp dir to avoid picking up user's config files
|
|
52
|
-
process.env.AGENCY_CONFIG_DIR = await createTempDir()
|
|
53
|
-
|
|
54
|
-
// Initialize git repo with main branch (already includes initial commit)
|
|
55
|
-
await initGitRepo(tempDir)
|
|
56
|
-
|
|
57
|
-
// Create a source branch (with agency-- prefix)
|
|
58
|
-
await createBranch(tempDir, "agency--test-feature")
|
|
59
|
-
|
|
60
|
-
// Initialize AGENTS.md and commit in one go
|
|
61
|
-
await initAgency(tempDir, "test")
|
|
62
|
-
|
|
63
|
-
await runTestEffect(task({ silent: true, fromCurrent: true }))
|
|
64
|
-
await addAndCommit(tempDir, "AGENTS.md", "Add AGENTS.md")
|
|
65
|
-
|
|
66
|
-
// Set up origin/main for git-filter-repo
|
|
67
|
-
await setupRemote(tempDir, "origin", tempDir)
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
afterEach(async () => {
|
|
71
|
-
process.chdir(originalCwd)
|
|
72
|
-
delete process.env.AGENCY_CONFIG_PATH
|
|
73
|
-
if (process.env.AGENCY_CONFIG_DIR) {
|
|
74
|
-
await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
|
|
75
|
-
delete process.env.AGENCY_CONFIG_DIR
|
|
76
|
-
}
|
|
77
|
-
await cleanupTempDir(tempDir)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe("basic functionality", () => {
|
|
81
|
-
test("fails when there are uncommitted changes", async () => {
|
|
82
|
-
// Create uncommitted changes
|
|
83
|
-
await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
|
|
84
|
-
|
|
85
|
-
expect(
|
|
86
|
-
runTestEffect(emit({ silent: true, skipFilter: true })),
|
|
87
|
-
).rejects.toThrow(/uncommitted changes/)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test("fails when there are staged but uncommitted changes", async () => {
|
|
91
|
-
// Create a staged but uncommitted change
|
|
92
|
-
await Bun.write(join(tempDir, "staged.txt"), "staged\n")
|
|
93
|
-
await Bun.spawn(["git", "add", "staged.txt"], {
|
|
94
|
-
cwd: tempDir,
|
|
95
|
-
stdout: "pipe",
|
|
96
|
-
stderr: "pipe",
|
|
97
|
-
}).exited
|
|
98
|
-
|
|
99
|
-
expect(
|
|
100
|
-
runTestEffect(emit({ silent: true, skipFilter: true })),
|
|
101
|
-
).rejects.toThrow(/uncommitted changes/)
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
test("throws error when git-filter-repo is not installed", async () => {
|
|
105
|
-
const hasGitFilterRepo = await checkGitFilterRepo()
|
|
106
|
-
if (hasGitFilterRepo) {
|
|
107
|
-
// Skip this test if git-filter-repo IS installed
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
await createBranch(tempDir, "agency--feature")
|
|
112
|
-
await createCommit(tempDir, "Feature commit")
|
|
113
|
-
|
|
114
|
-
expect(runTestEffect(emit({ silent: true }))).rejects.toThrow(
|
|
115
|
-
"git-filter-repo is not installed",
|
|
116
|
-
)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
test("creates emit branch with default name", async () => {
|
|
120
|
-
// Go back to main and create a fresh source branch (no inherited agency.json)
|
|
121
|
-
await checkoutBranch(tempDir, "main")
|
|
122
|
-
await createBranch(tempDir, "agency--feature")
|
|
123
|
-
// Create agency.json for this branch
|
|
124
|
-
await Bun.write(
|
|
125
|
-
join(tempDir, "agency.json"),
|
|
126
|
-
JSON.stringify({
|
|
127
|
-
version: 1,
|
|
128
|
-
injectedFiles: ["AGENTS.md"],
|
|
129
|
-
template: "test",
|
|
130
|
-
createdAt: new Date().toISOString(),
|
|
131
|
-
}),
|
|
132
|
-
)
|
|
133
|
-
await addAndCommit(tempDir, "agency.json", "Feature commit")
|
|
134
|
-
|
|
135
|
-
// Create emit branch (skip filter for speed)
|
|
136
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
137
|
-
|
|
138
|
-
// Check that emit branch exists (default pattern is %branch%, so emit is just "feature")
|
|
139
|
-
const branches = await getGitOutput(tempDir, [
|
|
140
|
-
"branch",
|
|
141
|
-
"--list",
|
|
142
|
-
"feature",
|
|
143
|
-
])
|
|
144
|
-
expect(branches.trim()).toContain("feature")
|
|
145
|
-
|
|
146
|
-
// Check we're still on the source branch
|
|
147
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
148
|
-
expect(currentBranch).toBe("agency--feature")
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test("sets emit branch upstream when remote branch exists", async () => {
|
|
152
|
-
await checkoutBranch(tempDir, "main")
|
|
153
|
-
await createBranch(tempDir, "agency--feature")
|
|
154
|
-
await Bun.write(
|
|
155
|
-
join(tempDir, "agency.json"),
|
|
156
|
-
JSON.stringify({
|
|
157
|
-
version: 1,
|
|
158
|
-
injectedFiles: ["AGENTS.md"],
|
|
159
|
-
template: "test",
|
|
160
|
-
createdAt: new Date().toISOString(),
|
|
161
|
-
}),
|
|
162
|
-
)
|
|
163
|
-
await addAndCommit(tempDir, "agency.json", "Feature commit")
|
|
164
|
-
|
|
165
|
-
await getGitOutput(tempDir, [
|
|
166
|
-
"update-ref",
|
|
167
|
-
"refs/remotes/origin/feature",
|
|
168
|
-
"HEAD",
|
|
169
|
-
])
|
|
170
|
-
|
|
171
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
172
|
-
|
|
173
|
-
const remote = await getGitOutput(tempDir, [
|
|
174
|
-
"config",
|
|
175
|
-
"--get",
|
|
176
|
-
"branch.feature.remote",
|
|
177
|
-
])
|
|
178
|
-
const merge = await getGitOutput(tempDir, [
|
|
179
|
-
"config",
|
|
180
|
-
"--get",
|
|
181
|
-
"branch.feature.merge",
|
|
182
|
-
])
|
|
183
|
-
expect(remote.trim()).toBe("origin")
|
|
184
|
-
expect(merge.trim()).toBe("refs/heads/feature")
|
|
185
|
-
|
|
186
|
-
await checkoutBranch(tempDir, "feature")
|
|
187
|
-
const pushRef = await getGitOutput(tempDir, [
|
|
188
|
-
"rev-parse",
|
|
189
|
-
"--abbrev-ref",
|
|
190
|
-
"@{push}",
|
|
191
|
-
])
|
|
192
|
-
expect(pushRef.trim()).toBe("origin/feature")
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
test("creates emit branch with custom name", async () => {
|
|
196
|
-
await createBranch(tempDir, "agency--feature")
|
|
197
|
-
await createCommit(tempDir, "Feature commit")
|
|
198
|
-
|
|
199
|
-
// Skip filter for speed - we're just testing branch creation
|
|
200
|
-
await runTestEffect(
|
|
201
|
-
emit({ emit: "custom-pr", silent: true, skipFilter: true }),
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
const branches = await getGitOutput(tempDir, [
|
|
205
|
-
"branch",
|
|
206
|
-
"--list",
|
|
207
|
-
"custom-pr",
|
|
208
|
-
])
|
|
209
|
-
expect(branches.trim()).toContain("custom-pr")
|
|
210
|
-
|
|
211
|
-
// Check we're still on the source branch
|
|
212
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
213
|
-
expect(currentBranch).toBe("agency--feature")
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
test("completes emit workflow successfully", async () => {
|
|
217
|
-
await createBranch(tempDir, "agency--feature")
|
|
218
|
-
await createCommit(tempDir, "Feature commit")
|
|
219
|
-
|
|
220
|
-
// Should complete without throwing (skip filter for speed)
|
|
221
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
222
|
-
|
|
223
|
-
// Should still be on source branch
|
|
224
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
225
|
-
expect(currentBranch).toBe("agency--feature")
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
test("preserves files on source branch after emit", async () => {
|
|
229
|
-
await createBranch(tempDir, "agency--feature")
|
|
230
|
-
await createCommit(tempDir, "Feature commit")
|
|
231
|
-
|
|
232
|
-
// Skip filter for speed - we're testing source branch preservation
|
|
233
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
234
|
-
|
|
235
|
-
// Should still be on source branch
|
|
236
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
237
|
-
expect(currentBranch).toBe("agency--feature")
|
|
238
|
-
|
|
239
|
-
// Check that test file still exists on source branch
|
|
240
|
-
expect(await fileExists(join(tempDir, "test.txt"))).toBe(true)
|
|
241
|
-
|
|
242
|
-
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
243
|
-
expect(files).toContain("test.txt")
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
test("original branch remains untouched", async () => {
|
|
247
|
-
await createBranch(tempDir, "agency--feature")
|
|
248
|
-
await createCommit(tempDir, "Feature commit")
|
|
249
|
-
|
|
250
|
-
// Create emit branch (skip filter for speed)
|
|
251
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
252
|
-
|
|
253
|
-
// Should still be on feature branch
|
|
254
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
255
|
-
expect(currentBranch).toBe("agency--feature")
|
|
256
|
-
|
|
257
|
-
// Check that AGENTS.md still exist on original branch
|
|
258
|
-
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
259
|
-
expect(files).toContain("AGENTS.md")
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
test("works correctly when run multiple times (recreates emit branch)", async () => {
|
|
263
|
-
await createBranch(tempDir, "agency--feature")
|
|
264
|
-
|
|
265
|
-
// Modify AGENTS.md on feature branch
|
|
266
|
-
await Bun.write(
|
|
267
|
-
join(tempDir, "AGENTS.md"),
|
|
268
|
-
"# Modified by feature branch\n",
|
|
269
|
-
)
|
|
270
|
-
await addAndCommit(tempDir, "AGENTS.md", "Modify AGENTS.md")
|
|
271
|
-
|
|
272
|
-
// Run emit command first time (skip filter for speed)
|
|
273
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
274
|
-
|
|
275
|
-
// Switch back to feature branch
|
|
276
|
-
await checkoutBranch(tempDir, "agency--feature")
|
|
277
|
-
|
|
278
|
-
// Make another commit
|
|
279
|
-
await createCommit(tempDir, "Another feature commit")
|
|
280
|
-
|
|
281
|
-
// Run emit command second time (skip filter for speed)
|
|
282
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
283
|
-
|
|
284
|
-
// Should complete successfully without interactive prompts and stay on source branch
|
|
285
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
286
|
-
expect(currentBranch).toBe("agency--feature")
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
test("accepts explicit base branch argument", async () => {
|
|
290
|
-
await createBranch(tempDir, "agency--feature")
|
|
291
|
-
await createCommit(tempDir, "Feature commit")
|
|
292
|
-
|
|
293
|
-
// Create emit branch with explicit base branch (skip filter for speed)
|
|
294
|
-
await runTestEffect(
|
|
295
|
-
emit({ baseBranch: "main", silent: true, skipFilter: true }),
|
|
296
|
-
)
|
|
297
|
-
|
|
298
|
-
// Should stay on source branch
|
|
299
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
300
|
-
expect(currentBranch).toBe("agency--feature")
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
test("throws error if provided base branch does not exist", async () => {
|
|
304
|
-
await createBranch(tempDir, "agency--feature")
|
|
305
|
-
await createCommit(tempDir, "Feature commit")
|
|
306
|
-
|
|
307
|
-
// This should fail even with skipFilter since base branch validation happens first
|
|
308
|
-
expect(
|
|
309
|
-
runTestEffect(
|
|
310
|
-
emit({ baseBranch: "nonexistent", silent: true, skipFilter: true }),
|
|
311
|
-
),
|
|
312
|
-
).rejects.toThrow("does not exist")
|
|
313
|
-
})
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
describe("error handling", () => {
|
|
317
|
-
test("throws error when not in a git repository", async () => {
|
|
318
|
-
const nonGitDir = await createTempDir()
|
|
319
|
-
process.chdir(nonGitDir)
|
|
320
|
-
|
|
321
|
-
expect(runTestEffect(emit({ silent: true }))).rejects.toThrow(
|
|
322
|
-
"Not in a git repository",
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
await cleanupTempDir(nonGitDir)
|
|
326
|
-
})
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
describe("silent mode", () => {
|
|
330
|
-
test("silent flag suppresses output", async () => {
|
|
331
|
-
await createBranch(tempDir, "agency--feature")
|
|
332
|
-
await createCommit(tempDir, "Feature commit")
|
|
333
|
-
|
|
334
|
-
const logs: string[] = []
|
|
335
|
-
const originalLog = console.log
|
|
336
|
-
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
337
|
-
|
|
338
|
-
// Skip filter for speed
|
|
339
|
-
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
340
|
-
|
|
341
|
-
console.log = originalLog
|
|
342
|
-
|
|
343
|
-
expect(logs.length).toBe(0)
|
|
344
|
-
})
|
|
345
|
-
})
|
|
346
|
-
|
|
347
|
-
describe("filter-repo command construction (with mock)", () => {
|
|
348
|
-
beforeEach(() => {
|
|
349
|
-
clearCapturedFilterRepoCalls()
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
test("throws when emitted CLAUDE.md references AGENCY.md", async () => {
|
|
353
|
-
await checkoutBranch(tempDir, "main")
|
|
354
|
-
await createBranch(tempDir, "agency--claude-reference-test")
|
|
355
|
-
|
|
356
|
-
await Bun.write(
|
|
357
|
-
join(tempDir, "agency.json"),
|
|
358
|
-
JSON.stringify({
|
|
359
|
-
version: 1,
|
|
360
|
-
injectedFiles: ["AGENTS.md"],
|
|
361
|
-
template: "test",
|
|
362
|
-
createdAt: new Date().toISOString(),
|
|
363
|
-
}),
|
|
364
|
-
)
|
|
365
|
-
await Bun.write(
|
|
366
|
-
join(tempDir, "CLAUDE.md"),
|
|
367
|
-
"# Instructions\n\n@AGENCY.md\n",
|
|
368
|
-
)
|
|
369
|
-
await addAndCommit(
|
|
370
|
-
tempDir,
|
|
371
|
-
["agency.json", "CLAUDE.md"],
|
|
372
|
-
"Add Claude agency reference",
|
|
373
|
-
)
|
|
374
|
-
|
|
375
|
-
await expect(
|
|
376
|
-
runTestEffectWithMockFilterRepo(emit({ silent: true })),
|
|
377
|
-
).rejects.toThrow(
|
|
378
|
-
"CLAUDE.md on emitted branch claude-reference-test still references @AGENCY.md",
|
|
379
|
-
)
|
|
380
|
-
})
|
|
381
|
-
|
|
382
|
-
test("constructs correct filter-repo arguments", async () => {
|
|
383
|
-
// Set up fresh branch with agency.json
|
|
384
|
-
await checkoutBranch(tempDir, "main")
|
|
385
|
-
await createBranch(tempDir, "agency--filter-test")
|
|
386
|
-
|
|
387
|
-
// Create agency.json with injected files
|
|
388
|
-
await Bun.write(
|
|
389
|
-
join(tempDir, "agency.json"),
|
|
390
|
-
JSON.stringify({
|
|
391
|
-
version: 1,
|
|
392
|
-
injectedFiles: ["AGENTS.md"],
|
|
393
|
-
template: "test",
|
|
394
|
-
createdAt: new Date().toISOString(),
|
|
395
|
-
}),
|
|
396
|
-
)
|
|
397
|
-
await addAndCommit(tempDir, "agency.json", "Add agency.json")
|
|
398
|
-
|
|
399
|
-
// Run emit with mock filter-repo (not skipFilter!)
|
|
400
|
-
await runTestEffectWithMockFilterRepo(emit({ silent: true }))
|
|
401
|
-
|
|
402
|
-
// Verify filter-repo was called with correct arguments
|
|
403
|
-
const lastCall = getLastCapturedFilterRepoCall()
|
|
404
|
-
expect(lastCall).toBeDefined()
|
|
405
|
-
|
|
406
|
-
// Should include paths for base files (TASK.md, AGENCY.md, agency.json)
|
|
407
|
-
// Note: CLAUDE.md is NOT in base files - it's only filtered if it was created
|
|
408
|
-
// by agency (in which case it would be in injectedFiles)
|
|
409
|
-
// and injected files (AGENTS.md)
|
|
410
|
-
expect(lastCall!.args).toContain("--path")
|
|
411
|
-
expect(lastCall!.args).toContain("TASK.md")
|
|
412
|
-
expect(lastCall!.args).toContain("AGENCY.md")
|
|
413
|
-
expect(lastCall!.args).toContain("agency.json")
|
|
414
|
-
expect(lastCall!.args).toContain("AGENTS.md")
|
|
415
|
-
|
|
416
|
-
// Should include invert-paths and force flags
|
|
417
|
-
expect(lastCall!.args).toContain("--invert-paths")
|
|
418
|
-
expect(lastCall!.args).toContain("--force")
|
|
419
|
-
|
|
420
|
-
// Should include refs for the commit range
|
|
421
|
-
expect(lastCall!.args).toContain("--refs")
|
|
422
|
-
|
|
423
|
-
// Should have GIT_CONFIG_GLOBAL env set
|
|
424
|
-
expect(lastCall!.env?.GIT_CONFIG_GLOBAL).toBe("")
|
|
425
|
-
})
|
|
426
|
-
|
|
427
|
-
test("streams git-filter-repo output in verbose mode", async () => {
|
|
428
|
-
await checkoutBranch(tempDir, "main")
|
|
429
|
-
await createBranch(tempDir, "agency--verbose-filter-test")
|
|
430
|
-
|
|
431
|
-
await Bun.write(
|
|
432
|
-
join(tempDir, "agency.json"),
|
|
433
|
-
JSON.stringify({
|
|
434
|
-
version: 1,
|
|
435
|
-
injectedFiles: ["AGENTS.md"],
|
|
436
|
-
template: "test",
|
|
437
|
-
createdAt: new Date().toISOString(),
|
|
438
|
-
}),
|
|
439
|
-
)
|
|
440
|
-
await addAndCommit(tempDir, "agency.json", "Add agency.json")
|
|
441
|
-
|
|
442
|
-
await runTestEffectWithMockFilterRepo(
|
|
443
|
-
emit({ silent: true, verbose: true }),
|
|
444
|
-
)
|
|
445
|
-
|
|
446
|
-
const lastCall = getLastCapturedFilterRepoCall()
|
|
447
|
-
expect(lastCall).toBeDefined()
|
|
448
|
-
expect(lastCall!.streamOutput).toBe(true)
|
|
449
|
-
expect(lastCall!.progressIntervalMs).toBe(5000)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
test("logs filter preflight diagnostics in verbose mode", async () => {
|
|
453
|
-
await checkoutBranch(tempDir, "main")
|
|
454
|
-
await createBranch(tempDir, "agency--filter-diagnostics-test")
|
|
455
|
-
|
|
456
|
-
await Bun.write(
|
|
457
|
-
join(tempDir, "agency.json"),
|
|
458
|
-
JSON.stringify({
|
|
459
|
-
version: 1,
|
|
460
|
-
injectedFiles: ["AGENTS.md"],
|
|
461
|
-
template: "test",
|
|
462
|
-
createdAt: new Date().toISOString(),
|
|
463
|
-
}),
|
|
464
|
-
)
|
|
465
|
-
await addAndCommit(tempDir, "agency.json", "Add agency.json")
|
|
466
|
-
|
|
467
|
-
const logs: string[] = []
|
|
468
|
-
const originalLog = console.log
|
|
469
|
-
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
470
|
-
|
|
471
|
-
try {
|
|
472
|
-
await runTestEffectWithMockFilterRepo(emit({ verbose: true }))
|
|
473
|
-
} finally {
|
|
474
|
-
console.log = originalLog
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
expect(logs.join("\n")).toContain("Commit range contains")
|
|
478
|
-
expect(logs.join("\n")).toContain("Filtered paths are touched by")
|
|
479
|
-
expect(logs.join("\n")).toContain("Filtered path diff contains")
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
test("includes symlink targets in files to filter", async () => {
|
|
483
|
-
// Set up fresh branch
|
|
484
|
-
await checkoutBranch(tempDir, "main")
|
|
485
|
-
await createBranch(tempDir, "agency--symlink-test")
|
|
486
|
-
|
|
487
|
-
// Create AGENTS.md as the real file
|
|
488
|
-
await Bun.write(join(tempDir, "AGENTS.md"), "# Real file\n")
|
|
489
|
-
|
|
490
|
-
// Create CLAUDE.md as a symlink to AGENTS.md
|
|
491
|
-
await symlink("AGENTS.md", join(tempDir, "CLAUDE.md"))
|
|
492
|
-
|
|
493
|
-
// Create agency.json with CLAUDE.md in injectedFiles (since it's no longer in baseFiles)
|
|
494
|
-
await Bun.write(
|
|
495
|
-
join(tempDir, "agency.json"),
|
|
496
|
-
JSON.stringify({
|
|
497
|
-
version: 1,
|
|
498
|
-
injectedFiles: ["CLAUDE.md"],
|
|
499
|
-
template: "test",
|
|
500
|
-
createdAt: new Date().toISOString(),
|
|
501
|
-
}),
|
|
502
|
-
)
|
|
503
|
-
await addAndCommit(
|
|
504
|
-
tempDir,
|
|
505
|
-
["AGENTS.md", "CLAUDE.md", "agency.json"],
|
|
506
|
-
"Add files with symlink",
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
// Run emit with mock filter-repo
|
|
510
|
-
await runTestEffectWithMockFilterRepo(emit({ silent: true }))
|
|
511
|
-
|
|
512
|
-
// Verify filter-repo was called with both CLAUDE.md AND AGENTS.md
|
|
513
|
-
const lastCall = getLastCapturedFilterRepoCall()
|
|
514
|
-
expect(lastCall).toBeDefined()
|
|
515
|
-
|
|
516
|
-
// Should include CLAUDE.md (the symlink)
|
|
517
|
-
expect(lastCall!.args).toContain("CLAUDE.md")
|
|
518
|
-
|
|
519
|
-
// Should also include AGENTS.md (the symlink target)
|
|
520
|
-
// This is the key assertion - the symlink target should be filtered too
|
|
521
|
-
expect(lastCall!.args).toContain("AGENTS.md")
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
test("expands injected dot-directory globs into filter paths", async () => {
|
|
525
|
-
await checkoutBranch(tempDir, "main")
|
|
526
|
-
await createBranch(tempDir, "agency--dotdir-test")
|
|
527
|
-
|
|
528
|
-
await Bun.spawn(["mkdir", "-p", join(tempDir, ".agents", "foo")], {
|
|
529
|
-
stdout: "pipe",
|
|
530
|
-
stderr: "pipe",
|
|
531
|
-
}).exited
|
|
532
|
-
|
|
533
|
-
await Bun.write(
|
|
534
|
-
join(tempDir, "agency.json"),
|
|
535
|
-
JSON.stringify({
|
|
536
|
-
version: 1,
|
|
537
|
-
injectedFiles: [".agents/**"],
|
|
538
|
-
template: "test",
|
|
539
|
-
createdAt: new Date().toISOString(),
|
|
540
|
-
}),
|
|
541
|
-
)
|
|
542
|
-
await Bun.write(join(tempDir, ".agents", "foo", "bar.whatever"), "test\n")
|
|
543
|
-
await addAndCommit(
|
|
544
|
-
tempDir,
|
|
545
|
-
["agency.json", ".agents/foo/bar.whatever"],
|
|
546
|
-
"Add template dot-directory file",
|
|
547
|
-
)
|
|
548
|
-
|
|
549
|
-
await runTestEffectWithMockFilterRepo(emit({ silent: true }))
|
|
550
|
-
|
|
551
|
-
const lastCall = getLastCapturedFilterRepoCall()
|
|
552
|
-
expect(lastCall).toBeDefined()
|
|
553
|
-
expect(lastCall!.args).toContain(".agents/foo/bar.whatever")
|
|
554
|
-
})
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
describe("git path resolution", () => {
|
|
558
|
-
test("resolves relative git internal paths from git root", () => {
|
|
559
|
-
expect(
|
|
560
|
-
resolveGitInternalPath(
|
|
561
|
-
"/repo/worktree",
|
|
562
|
-
"../.git/worktrees/worktree/filter-repo",
|
|
563
|
-
),
|
|
564
|
-
).toBe("/repo/.git/worktrees/worktree/filter-repo")
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
test("preserves absolute git internal paths", () => {
|
|
568
|
-
expect(
|
|
569
|
-
resolveGitInternalPath(
|
|
570
|
-
"/repo/worktree",
|
|
571
|
-
"/repo/.git/worktrees/worktree/filter-repo",
|
|
572
|
-
),
|
|
573
|
-
).toBe("/repo/.git/worktrees/worktree/filter-repo")
|
|
574
|
-
})
|
|
575
|
-
})
|
|
576
|
-
})
|