@markjaquith/agency 1.1.1 → 1.3.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/cli.ts +22 -0
- package/package.json +4 -2
- package/src/commands/clean.ts +4 -33
- package/src/commands/emit.integration.test.ts +277 -0
- package/src/commands/emit.test.ts +53 -208
- package/src/commands/emit.ts +5 -33
- package/src/commands/merge.integration.test.ts +195 -0
- package/src/commands/merge.test.ts +0 -119
- package/src/commands/merge.ts +3 -12
- package/src/commands/pr.ts +77 -0
- package/src/commands/push.ts +4 -15
- package/src/commands/rebase.ts +3 -11
- package/src/commands/status.ts +3 -7
- package/src/commands/tasks.ts +4 -21
- package/src/services/AgencyMetadataService.ts +3 -7
- package/src/services/FilterRepoService.ts +142 -0
- package/src/services/GitService.ts +196 -0
- package/src/services/MockFilterRepoService.ts +133 -0
- package/src/test-utils.ts +85 -2
- package/src/utils/pr-branch.ts +6 -14
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
createBranch,
|
|
16
16
|
addAndCommit,
|
|
17
17
|
setupRemote,
|
|
18
|
-
renameBranch,
|
|
19
18
|
runTestEffect,
|
|
19
|
+
runTestEffectWithMockFilterRepo,
|
|
20
|
+
clearCapturedFilterRepoCalls,
|
|
21
|
+
getLastCapturedFilterRepoCall,
|
|
20
22
|
} from "../test-utils"
|
|
21
23
|
|
|
22
24
|
// Cache the git-filter-repo availability check (it doesn't change during test run)
|
|
@@ -36,7 +38,6 @@ async function checkGitFilterRepo(): Promise<boolean> {
|
|
|
36
38
|
describe("emit command", () => {
|
|
37
39
|
let tempDir: string
|
|
38
40
|
let originalCwd: string
|
|
39
|
-
let hasGitFilterRepo: boolean
|
|
40
41
|
|
|
41
42
|
beforeEach(async () => {
|
|
42
43
|
tempDir = await createTempDir()
|
|
@@ -48,9 +49,6 @@ describe("emit command", () => {
|
|
|
48
49
|
// Set config dir to temp dir to avoid picking up user's config files
|
|
49
50
|
process.env.AGENCY_CONFIG_DIR = await createTempDir()
|
|
50
51
|
|
|
51
|
-
// Check if git-filter-repo is available (cached)
|
|
52
|
-
hasGitFilterRepo = await checkGitFilterRepo()
|
|
53
|
-
|
|
54
52
|
// Initialize git repo with main branch (already includes initial commit)
|
|
55
53
|
await initGitRepo(tempDir)
|
|
56
54
|
|
|
@@ -79,6 +77,7 @@ describe("emit command", () => {
|
|
|
79
77
|
|
|
80
78
|
describe("basic functionality", () => {
|
|
81
79
|
test("throws error when git-filter-repo is not installed", async () => {
|
|
80
|
+
const hasGitFilterRepo = await checkGitFilterRepo()
|
|
82
81
|
if (hasGitFilterRepo) {
|
|
83
82
|
// Skip this test if git-filter-repo IS installed
|
|
84
83
|
return
|
|
@@ -245,159 +244,48 @@ describe("emit command", () => {
|
|
|
245
244
|
})
|
|
246
245
|
})
|
|
247
246
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!hasGitFilterRepo) {
|
|
253
|
-
console.log("Skipping test: git-filter-repo not installed")
|
|
254
|
-
return
|
|
255
|
-
}
|
|
247
|
+
describe("error handling", () => {
|
|
248
|
+
test("throws error when not in a git repository", async () => {
|
|
249
|
+
const nonGitDir = await createTempDir()
|
|
250
|
+
process.chdir(nonGitDir)
|
|
256
251
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
await createBranch(tempDir, "agency/feature")
|
|
260
|
-
// Create agency.json with AGENTS.md as managed file
|
|
261
|
-
await Bun.write(
|
|
262
|
-
join(tempDir, "agency.json"),
|
|
263
|
-
JSON.stringify({
|
|
264
|
-
version: 1,
|
|
265
|
-
injectedFiles: ["AGENTS.md"],
|
|
266
|
-
template: "test",
|
|
267
|
-
createdAt: new Date().toISOString(),
|
|
268
|
-
}),
|
|
252
|
+
expect(runTestEffect(emit({ silent: true }))).rejects.toThrow(
|
|
253
|
+
"Not in a git repository",
|
|
269
254
|
)
|
|
270
|
-
await Bun.write(join(tempDir, "AGENTS.md"), "# Test AGENTS\n")
|
|
271
|
-
await addAndCommit(tempDir, "agency.json AGENTS.md", "Add agency files")
|
|
272
|
-
// Also create a test.txt file via createCommit
|
|
273
|
-
await createCommit(tempDir, "Feature commit")
|
|
274
|
-
|
|
275
|
-
// Create emit branch (this runs git-filter-repo)
|
|
276
|
-
await runTestEffect(emit({ silent: true }))
|
|
277
255
|
|
|
278
|
-
|
|
279
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
280
|
-
expect(currentBranch).toBe("agency/feature")
|
|
281
|
-
|
|
282
|
-
// Switch to emit branch to verify files
|
|
283
|
-
await checkoutBranch(tempDir, "feature")
|
|
284
|
-
|
|
285
|
-
// AGENTS.md should be filtered out
|
|
286
|
-
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
287
|
-
expect(files).not.toContain("AGENTS.md")
|
|
288
|
-
expect(files).not.toContain("AGENCY.md")
|
|
289
|
-
|
|
290
|
-
// But test.txt should still exist
|
|
291
|
-
expect(files).toContain("test.txt")
|
|
256
|
+
await cleanupTempDir(nonGitDir)
|
|
292
257
|
})
|
|
258
|
+
})
|
|
293
259
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// We're on test-feature which has AGENTS.md
|
|
301
|
-
// Add a feature-specific file to avoid conflicts
|
|
302
|
-
await Bun.write(join(tempDir, "feature.txt"), "feature content\n")
|
|
303
|
-
await addAndCommit(tempDir, "feature.txt", "Add feature file")
|
|
304
|
-
|
|
305
|
-
// Store merge-base before advancing main
|
|
306
|
-
const initialMergeBase = await getGitOutput(tempDir, [
|
|
307
|
-
"merge-base",
|
|
308
|
-
"agency/test-feature",
|
|
309
|
-
"main",
|
|
310
|
-
])
|
|
311
|
-
|
|
312
|
-
// Create initial emit branch
|
|
313
|
-
await runTestEffect(emit({ silent: true, baseBranch: "main" }))
|
|
314
|
-
|
|
315
|
-
// Should still be on test-feature branch
|
|
316
|
-
let currentBranch = await getCurrentBranch(tempDir)
|
|
317
|
-
expect(currentBranch).toBe("agency/test-feature")
|
|
318
|
-
|
|
319
|
-
// Switch to emit branch to verify AGENTS.md is filtered
|
|
320
|
-
await checkoutBranch(tempDir, "test-feature")
|
|
321
|
-
|
|
322
|
-
let files = await getGitOutput(tempDir, ["ls-files"])
|
|
323
|
-
expect(files).not.toContain("AGENTS.md")
|
|
324
|
-
expect(files).toContain("feature.txt")
|
|
325
|
-
|
|
326
|
-
// Switch back to source branch
|
|
327
|
-
await checkoutBranch(tempDir, "agency/test-feature")
|
|
328
|
-
|
|
329
|
-
// Simulate advancing main branch with a different file
|
|
330
|
-
await checkoutBranch(tempDir, "main")
|
|
331
|
-
await Bun.write(join(tempDir, "main-file.txt"), "main content\n")
|
|
332
|
-
await addAndCommit(tempDir, "main-file.txt", "Main branch advancement")
|
|
333
|
-
|
|
334
|
-
// Rebase test-feature onto new main
|
|
335
|
-
await checkoutBranch(tempDir, "agency/test-feature")
|
|
336
|
-
const rebaseProc = Bun.spawn(["git", "rebase", "main"], {
|
|
337
|
-
cwd: tempDir,
|
|
338
|
-
stdout: "pipe",
|
|
339
|
-
stderr: "pipe",
|
|
340
|
-
})
|
|
341
|
-
await rebaseProc.exited
|
|
342
|
-
if (rebaseProc.exitCode !== 0) {
|
|
343
|
-
const stderr = await new Response(rebaseProc.stderr).text()
|
|
344
|
-
throw new Error(`Rebase failed: ${stderr}`)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Verify merge-base has changed after rebase
|
|
348
|
-
const newMergeBase = await getGitOutput(tempDir, [
|
|
349
|
-
"merge-base",
|
|
350
|
-
"agency/test-feature",
|
|
351
|
-
"main",
|
|
352
|
-
])
|
|
353
|
-
expect(newMergeBase.trim()).not.toBe(initialMergeBase.trim())
|
|
354
|
-
|
|
355
|
-
// Recreate emit branch after rebase (this is where the bug would manifest)
|
|
356
|
-
await runTestEffect(emit({ silent: true, baseBranch: "main" }))
|
|
260
|
+
describe("silent mode", () => {
|
|
261
|
+
test("silent flag suppresses output", async () => {
|
|
262
|
+
await createBranch(tempDir, "agency/feature")
|
|
263
|
+
await createCommit(tempDir, "Feature commit")
|
|
357
264
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
265
|
+
const logs: string[] = []
|
|
266
|
+
const originalLog = console.log
|
|
267
|
+
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
361
268
|
|
|
362
|
-
//
|
|
363
|
-
await
|
|
269
|
+
// Skip filter for speed
|
|
270
|
+
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
364
271
|
|
|
365
|
-
|
|
366
|
-
files = await getGitOutput(tempDir, ["ls-files"])
|
|
367
|
-
expect(files).not.toContain("AGENTS.md")
|
|
368
|
-
expect(files).toContain("feature.txt")
|
|
369
|
-
expect(files).toContain("main-file.txt") // Should have main's file after rebase
|
|
272
|
+
console.log = originalLog
|
|
370
273
|
|
|
371
|
-
|
|
372
|
-
const logOutput = await getGitOutput(tempDir, [
|
|
373
|
-
"log",
|
|
374
|
-
"--oneline",
|
|
375
|
-
"main..test-feature",
|
|
376
|
-
])
|
|
377
|
-
expect(logOutput).toContain("Add feature file")
|
|
378
|
-
expect(logOutput).not.toContain("Add AGENTS.md")
|
|
274
|
+
expect(logs.length).toBe(0)
|
|
379
275
|
})
|
|
276
|
+
})
|
|
380
277
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
278
|
+
describe("filter-repo command construction (with mock)", () => {
|
|
279
|
+
beforeEach(() => {
|
|
280
|
+
clearCapturedFilterRepoCalls()
|
|
281
|
+
})
|
|
386
282
|
|
|
387
|
-
|
|
283
|
+
test("constructs correct filter-repo arguments", async () => {
|
|
284
|
+
// Set up fresh branch with agency.json
|
|
388
285
|
await checkoutBranch(tempDir, "main")
|
|
286
|
+
await createBranch(tempDir, "agency/filter-test")
|
|
389
287
|
|
|
390
|
-
// Create
|
|
391
|
-
await Bun.write(
|
|
392
|
-
join(tempDir, "CLAUDE.md"),
|
|
393
|
-
"# Original Claude Instructions\n\nSome content here.\n",
|
|
394
|
-
)
|
|
395
|
-
await addAndCommit(tempDir, "CLAUDE.md", "Add CLAUDE.md")
|
|
396
|
-
|
|
397
|
-
// Create a new feature branch
|
|
398
|
-
await createBranch(tempDir, "agency/claude-test")
|
|
399
|
-
|
|
400
|
-
// Initialize agency on this branch (this will modify CLAUDE.md)
|
|
288
|
+
// Create agency.json with injected files
|
|
401
289
|
await Bun.write(
|
|
402
290
|
join(tempDir, "agency.json"),
|
|
403
291
|
JSON.stringify({
|
|
@@ -407,76 +295,33 @@ describe("emit command", () => {
|
|
|
407
295
|
createdAt: new Date().toISOString(),
|
|
408
296
|
}),
|
|
409
297
|
)
|
|
410
|
-
await
|
|
411
|
-
|
|
412
|
-
// Simulate what agency task does - inject into CLAUDE.md
|
|
413
|
-
const originalClaude = await Bun.file(join(tempDir, "CLAUDE.md")).text()
|
|
414
|
-
const modifiedClaude = `${originalClaude}\n# Agency References\n@AGENTS.md\n@TASK.md\n`
|
|
415
|
-
await Bun.write(join(tempDir, "CLAUDE.md"), modifiedClaude)
|
|
416
|
-
|
|
417
|
-
await addAndCommit(
|
|
418
|
-
tempDir,
|
|
419
|
-
"agency.json AGENTS.md CLAUDE.md",
|
|
420
|
-
"Initialize agency files",
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
// Add a feature file
|
|
424
|
-
await createCommit(tempDir, "Feature commit")
|
|
425
|
-
|
|
426
|
-
// Create emit branch (this should filter CLAUDE.md)
|
|
427
|
-
await runTestEffect(emit({ silent: true, baseBranch: "main" }))
|
|
428
|
-
|
|
429
|
-
// Should still be on source branch
|
|
430
|
-
const currentBranch = await getCurrentBranch(tempDir)
|
|
431
|
-
expect(currentBranch).toBe("agency/claude-test")
|
|
432
|
-
|
|
433
|
-
// Switch to emit branch to verify CLAUDE.md is reverted to main's version
|
|
434
|
-
await checkoutBranch(tempDir, "claude-test")
|
|
435
|
-
|
|
436
|
-
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
437
|
-
expect(files).toContain("CLAUDE.md") // File should exist (from main)
|
|
438
|
-
expect(files).not.toContain("AGENTS.md") // Should be filtered
|
|
439
|
-
expect(files).not.toContain("TASK.md") // Should be filtered
|
|
440
|
-
expect(files).toContain("test.txt") // Feature file should exist
|
|
441
|
-
|
|
442
|
-
// Verify CLAUDE.md was reverted to original (no agency references)
|
|
443
|
-
const claudeContent = await Bun.file(join(tempDir, "CLAUDE.md")).text()
|
|
444
|
-
expect(claudeContent).toBe(
|
|
445
|
-
"# Original Claude Instructions\n\nSome content here.\n",
|
|
446
|
-
)
|
|
447
|
-
expect(claudeContent).not.toContain("@AGENTS.md")
|
|
448
|
-
expect(claudeContent).not.toContain("@TASK.md")
|
|
449
|
-
})
|
|
450
|
-
})
|
|
298
|
+
await addAndCommit(tempDir, "agency.json", "Add agency.json")
|
|
451
299
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const nonGitDir = await createTempDir()
|
|
455
|
-
process.chdir(nonGitDir)
|
|
456
|
-
|
|
457
|
-
expect(runTestEffect(emit({ silent: true }))).rejects.toThrow(
|
|
458
|
-
"Not in a git repository",
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
await cleanupTempDir(nonGitDir)
|
|
462
|
-
})
|
|
463
|
-
})
|
|
300
|
+
// Run emit with mock filter-repo (not skipFilter!)
|
|
301
|
+
await runTestEffectWithMockFilterRepo(emit({ silent: true }))
|
|
464
302
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
await createCommit(tempDir, "Feature commit")
|
|
303
|
+
// Verify filter-repo was called with correct arguments
|
|
304
|
+
const lastCall = getLastCapturedFilterRepoCall()
|
|
305
|
+
expect(lastCall).toBeDefined()
|
|
469
306
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
307
|
+
// Should include paths for base files (TASK.md, AGENCY.md, CLAUDE.md, agency.json)
|
|
308
|
+
// and injected files (AGENTS.md)
|
|
309
|
+
expect(lastCall!.args).toContain("--path")
|
|
310
|
+
expect(lastCall!.args).toContain("TASK.md")
|
|
311
|
+
expect(lastCall!.args).toContain("AGENCY.md")
|
|
312
|
+
expect(lastCall!.args).toContain("CLAUDE.md")
|
|
313
|
+
expect(lastCall!.args).toContain("agency.json")
|
|
314
|
+
expect(lastCall!.args).toContain("AGENTS.md")
|
|
473
315
|
|
|
474
|
-
//
|
|
475
|
-
|
|
316
|
+
// Should include invert-paths and force flags
|
|
317
|
+
expect(lastCall!.args).toContain("--invert-paths")
|
|
318
|
+
expect(lastCall!.args).toContain("--force")
|
|
476
319
|
|
|
477
|
-
|
|
320
|
+
// Should include refs for the commit range
|
|
321
|
+
expect(lastCall!.args).toContain("--refs")
|
|
478
322
|
|
|
479
|
-
|
|
323
|
+
// Should have GIT_CONFIG_GLOBAL env set
|
|
324
|
+
expect(lastCall!.env?.GIT_CONFIG_GLOBAL).toBe("")
|
|
480
325
|
})
|
|
481
326
|
})
|
|
482
327
|
})
|
package/src/commands/emit.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { BaseCommandOptions } from "../utils/command"
|
|
|
3
3
|
import { GitService } from "../services/GitService"
|
|
4
4
|
import { ConfigService } from "../services/ConfigService"
|
|
5
5
|
import { FileSystemService } from "../services/FileSystemService"
|
|
6
|
+
import { FilterRepoService } from "../services/FilterRepoService"
|
|
6
7
|
import {
|
|
7
8
|
makeEmitBranchName,
|
|
8
9
|
makeSourceBranchName,
|
|
@@ -49,9 +50,10 @@ const emitCore = (gitRoot: string, options: EmitOptions) =>
|
|
|
49
50
|
const git = yield* GitService
|
|
50
51
|
const configService = yield* ConfigService
|
|
51
52
|
const fs = yield* FileSystemService
|
|
53
|
+
const filterRepo = yield* FilterRepoService
|
|
52
54
|
|
|
53
55
|
// Check if git-filter-repo is installed
|
|
54
|
-
const hasFilterRepo = yield*
|
|
56
|
+
const hasFilterRepo = yield* filterRepo.isInstalled()
|
|
55
57
|
if (!hasFilterRepo) {
|
|
56
58
|
const isMac = process.platform === "darwin"
|
|
57
59
|
const installInstructions = isMac
|
|
@@ -197,8 +199,6 @@ const emitCore = (gitRoot: string, options: EmitOptions) =>
|
|
|
197
199
|
|
|
198
200
|
// Run git-filter-repo
|
|
199
201
|
const filterRepoArgs = [
|
|
200
|
-
"git",
|
|
201
|
-
"filter-repo",
|
|
202
202
|
...filesToFilter.flatMap((f) => ["--path", f]),
|
|
203
203
|
"--invert-paths",
|
|
204
204
|
"--force",
|
|
@@ -206,19 +206,12 @@ const emitCore = (gitRoot: string, options: EmitOptions) =>
|
|
|
206
206
|
`${forkPoint}..${emitBranchName}`,
|
|
207
207
|
]
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
yield* filterRepo.run(gitRoot, filterRepoArgs, {
|
|
210
210
|
env: { GIT_CONFIG_GLOBAL: "" },
|
|
211
|
-
captureOutput: true,
|
|
212
211
|
})
|
|
213
212
|
|
|
214
213
|
verboseLog("git-filter-repo completed")
|
|
215
214
|
|
|
216
|
-
if (result.exitCode !== 0) {
|
|
217
|
-
return yield* Effect.fail(
|
|
218
|
-
new Error(`git-filter-repo failed: ${result.stderr}`),
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
215
|
// Switch back to source branch (git-filter-repo may have checked out the emit branch)
|
|
223
216
|
yield* git.checkoutBranch(gitRoot, currentBranch)
|
|
224
217
|
})
|
|
@@ -326,26 +319,6 @@ const getRemoteTrackingBranch = (
|
|
|
326
319
|
return null
|
|
327
320
|
})
|
|
328
321
|
|
|
329
|
-
/**
|
|
330
|
-
* Check if one commit is an ancestor of another.
|
|
331
|
-
*/
|
|
332
|
-
const isAncestor = (
|
|
333
|
-
git: GitService,
|
|
334
|
-
gitRoot: string,
|
|
335
|
-
potentialAncestor: string,
|
|
336
|
-
commit: string,
|
|
337
|
-
) =>
|
|
338
|
-
git
|
|
339
|
-
.runGitCommand(
|
|
340
|
-
["git", "merge-base", "--is-ancestor", potentialAncestor, commit],
|
|
341
|
-
gitRoot,
|
|
342
|
-
{},
|
|
343
|
-
)
|
|
344
|
-
.pipe(
|
|
345
|
-
Effect.map((result) => result.exitCode === 0),
|
|
346
|
-
Effect.catchAll(() => Effect.succeed(false)),
|
|
347
|
-
)
|
|
348
|
-
|
|
349
322
|
/**
|
|
350
323
|
* Find the best fork point by checking both local and remote tracking branches.
|
|
351
324
|
*
|
|
@@ -399,8 +372,7 @@ const findBestForkPoint = (
|
|
|
399
372
|
|
|
400
373
|
// Strategy 3: Choose the more recent fork point
|
|
401
374
|
// If local fork point is an ancestor of remote, prefer remote (it's more recent)
|
|
402
|
-
const localIsAncestorOfRemote = yield* isAncestor(
|
|
403
|
-
git,
|
|
375
|
+
const localIsAncestorOfRemote = yield* git.isAncestor(
|
|
404
376
|
gitRoot,
|
|
405
377
|
localForkPoint,
|
|
406
378
|
remoteForkPoint,
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { merge } from "./merge"
|
|
4
|
+
import { emit } from "./emit"
|
|
5
|
+
import { task } from "./task"
|
|
6
|
+
import {
|
|
7
|
+
createTempDir,
|
|
8
|
+
cleanupTempDir,
|
|
9
|
+
initGitRepo,
|
|
10
|
+
initAgency,
|
|
11
|
+
getGitOutput,
|
|
12
|
+
getCurrentBranch,
|
|
13
|
+
createCommit,
|
|
14
|
+
branchExists,
|
|
15
|
+
checkoutBranch,
|
|
16
|
+
createBranch,
|
|
17
|
+
addAndCommit,
|
|
18
|
+
setupRemote,
|
|
19
|
+
deleteBranch,
|
|
20
|
+
runTestEffect,
|
|
21
|
+
} from "../test-utils"
|
|
22
|
+
|
|
23
|
+
// Cache the git-filter-repo availability check (it doesn't change during test run)
|
|
24
|
+
let hasGitFilterRepoCache: boolean | null = null
|
|
25
|
+
async function checkGitFilterRepo(): Promise<boolean> {
|
|
26
|
+
if (hasGitFilterRepoCache === null) {
|
|
27
|
+
const proc = Bun.spawn(["which", "git-filter-repo"], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
})
|
|
31
|
+
await proc.exited
|
|
32
|
+
hasGitFilterRepoCache = proc.exitCode === 0
|
|
33
|
+
}
|
|
34
|
+
return hasGitFilterRepoCache
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("merge command - integration tests (requires git-filter-repo)", () => {
|
|
38
|
+
let tempDir: string
|
|
39
|
+
let originalCwd: string
|
|
40
|
+
let hasGitFilterRepo: boolean
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
tempDir = await createTempDir()
|
|
44
|
+
originalCwd = process.cwd()
|
|
45
|
+
process.chdir(tempDir)
|
|
46
|
+
|
|
47
|
+
// Set config path to non-existent file to use defaults
|
|
48
|
+
process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
|
|
49
|
+
// Set config dir to temp dir to avoid picking up user's config files
|
|
50
|
+
process.env.AGENCY_CONFIG_DIR = await createTempDir()
|
|
51
|
+
|
|
52
|
+
// Check if git-filter-repo is available (cached)
|
|
53
|
+
hasGitFilterRepo = await checkGitFilterRepo()
|
|
54
|
+
|
|
55
|
+
// Initialize git repo with main branch (already includes initial commit)
|
|
56
|
+
await initGitRepo(tempDir)
|
|
57
|
+
|
|
58
|
+
// Set up origin for git-filter-repo
|
|
59
|
+
await setupRemote(tempDir, "origin", tempDir)
|
|
60
|
+
|
|
61
|
+
// Create a source branch (with agency/ prefix per new default config)
|
|
62
|
+
await createBranch(tempDir, "agency/feature")
|
|
63
|
+
|
|
64
|
+
// Initialize AGENTS.md on feature branch
|
|
65
|
+
await initAgency(tempDir, "test")
|
|
66
|
+
|
|
67
|
+
await runTestEffect(task({ silent: true, fromCurrent: true }))
|
|
68
|
+
|
|
69
|
+
// Ensure agency.json has baseBranch set (task should auto-detect it, but ensure it's there)
|
|
70
|
+
const agencyJsonPath = join(tempDir, "agency.json")
|
|
71
|
+
const agencyJson = await Bun.file(agencyJsonPath).json()
|
|
72
|
+
if (!agencyJson.baseBranch) {
|
|
73
|
+
agencyJson.baseBranch = "origin/main"
|
|
74
|
+
await Bun.write(
|
|
75
|
+
agencyJsonPath,
|
|
76
|
+
JSON.stringify(agencyJson, null, 2) + "\n",
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await addAndCommit(tempDir, "AGENTS.md agency.json", "Add AGENTS.md")
|
|
81
|
+
|
|
82
|
+
// Create another commit on feature branch
|
|
83
|
+
await createCommit(tempDir, "Feature work")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
process.chdir(originalCwd)
|
|
88
|
+
delete process.env.AGENCY_CONFIG_PATH
|
|
89
|
+
if (process.env.AGENCY_CONFIG_DIR) {
|
|
90
|
+
await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
|
|
91
|
+
delete process.env.AGENCY_CONFIG_DIR
|
|
92
|
+
}
|
|
93
|
+
await cleanupTempDir(tempDir)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe("merge from source branch", () => {
|
|
97
|
+
test("creates emit branch and merges when run from source branch", async () => {
|
|
98
|
+
if (!hasGitFilterRepo) {
|
|
99
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// We're on feature branch (source)
|
|
104
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
105
|
+
expect(currentBranch).toBe("agency/feature")
|
|
106
|
+
|
|
107
|
+
// Run merge - should create feature--PR and merge it to main
|
|
108
|
+
await runTestEffect(merge({ silent: true }))
|
|
109
|
+
|
|
110
|
+
// Should be on main branch after merge
|
|
111
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
112
|
+
expect(afterMergeBranch).toBe("main")
|
|
113
|
+
|
|
114
|
+
// emit branch should exist
|
|
115
|
+
const prExists = await branchExists(tempDir, "feature")
|
|
116
|
+
expect(prExists).toBe(true)
|
|
117
|
+
|
|
118
|
+
// Main should have the feature work but not AGENTS.md
|
|
119
|
+
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
120
|
+
expect(files).not.toContain("AGENTS.md")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("recreates emit branch if it already exists", async () => {
|
|
124
|
+
if (!hasGitFilterRepo) {
|
|
125
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create emit branch first
|
|
130
|
+
await runTestEffect(emit({ silent: true }))
|
|
131
|
+
|
|
132
|
+
// Go back to feature branch
|
|
133
|
+
await checkoutBranch(tempDir, "agency/feature")
|
|
134
|
+
|
|
135
|
+
// Make additional changes
|
|
136
|
+
await createCommit(tempDir, "More feature work")
|
|
137
|
+
|
|
138
|
+
// Run merge - should recreate emit branch with new changes
|
|
139
|
+
await runTestEffect(merge({ silent: true }))
|
|
140
|
+
|
|
141
|
+
// Should be on main after merge
|
|
142
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
143
|
+
expect(currentBranch).toBe("main")
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("merge from emit branch", () => {
|
|
148
|
+
test("merges emit branch directly when run from emit branch", async () => {
|
|
149
|
+
if (!hasGitFilterRepo) {
|
|
150
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create emit branch
|
|
155
|
+
await runTestEffect(emit({ silent: true }))
|
|
156
|
+
|
|
157
|
+
// emit() now stays on source branch, so we need to checkout to emit branch
|
|
158
|
+
await checkoutBranch(tempDir, "feature")
|
|
159
|
+
|
|
160
|
+
// We're on feature--PR now
|
|
161
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
162
|
+
expect(currentBranch).toBe("feature")
|
|
163
|
+
|
|
164
|
+
// Run merge - should merge feature--PR to main
|
|
165
|
+
await runTestEffect(merge({ silent: true }))
|
|
166
|
+
|
|
167
|
+
// Should be on main after merge
|
|
168
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
169
|
+
expect(afterMergeBranch).toBe("main")
|
|
170
|
+
|
|
171
|
+
// Main should have the feature work but not AGENTS.md
|
|
172
|
+
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
173
|
+
expect(files).not.toContain("AGENTS.md")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("throws error if emit branch has no corresponding source branch", async () => {
|
|
177
|
+
if (!hasGitFilterRepo) {
|
|
178
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create emit branch
|
|
183
|
+
await runTestEffect(emit({ silent: true }))
|
|
184
|
+
|
|
185
|
+
// pr() now stays on source branch, so checkout to emit branch
|
|
186
|
+
await checkoutBranch(tempDir, "feature")
|
|
187
|
+
|
|
188
|
+
// Delete the source branch
|
|
189
|
+
await deleteBranch(tempDir, "agency/feature", true)
|
|
190
|
+
|
|
191
|
+
// Try to merge - should fail (error message may vary since source branch is deleted)
|
|
192
|
+
await expect(runTestEffect(merge({ silent: true }))).rejects.toThrow()
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
})
|