@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.
@@ -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
- // Integration tests that actually run git-filter-repo to verify filtering behavior
249
- // These are slower but ensure the filtering logic works correctly
250
- describe("filtering integration (requires git-filter-repo)", () => {
251
- test("filters AGENTS.md from emit branch", async () => {
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
- // Go back to main and create a fresh source branch
258
- await checkoutBranch(tempDir, "main")
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
- // Should still be on feature branch
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
- test("handles emit branch recreation after source branch rebase", async () => {
295
- if (!hasGitFilterRepo) {
296
- console.log("Skipping test: git-filter-repo not installed")
297
- return
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
- // Should still be on test-feature branch
359
- currentBranch = await getCurrentBranch(tempDir)
360
- expect(currentBranch).toBe("agency/test-feature")
265
+ const logs: string[] = []
266
+ const originalLog = console.log
267
+ console.log = (...args: any[]) => logs.push(args.join(" "))
361
268
 
362
- // Switch to emit branch to verify files
363
- await checkoutBranch(tempDir, "test-feature")
269
+ // Skip filter for speed
270
+ await runTestEffect(emit({ silent: true, skipFilter: true }))
364
271
 
365
- // Verify AGENTS.md is still filtered and no extraneous changes
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
- // Verify that our feature commits exist but AGENTS.md commit is filtered
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
- test("filters pre-existing CLAUDE.md that gets edited by agency", async () => {
382
- if (!hasGitFilterRepo) {
383
- console.log("Skipping test: git-filter-repo not installed")
384
- return
385
- }
278
+ describe("filter-repo command construction (with mock)", () => {
279
+ beforeEach(() => {
280
+ clearCapturedFilterRepoCalls()
281
+ })
386
282
 
387
- // Start fresh on main branch
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 CLAUDE.md on main branch (simulating pre-existing file)
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 Bun.write(join(tempDir, "AGENTS.md"), "# Test AGENTS\n")
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
- describe("error handling", () => {
453
- test("throws error when not in a git repository", async () => {
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
- describe("silent mode", () => {
466
- test("silent flag suppresses output", async () => {
467
- await createBranch(tempDir, "agency/feature")
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
- const logs: string[] = []
471
- const originalLog = console.log
472
- console.log = (...args: any[]) => logs.push(args.join(" "))
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
- // Skip filter for speed
475
- await runTestEffect(emit({ silent: true, skipFilter: true }))
316
+ // Should include invert-paths and force flags
317
+ expect(lastCall!.args).toContain("--invert-paths")
318
+ expect(lastCall!.args).toContain("--force")
476
319
 
477
- console.log = originalLog
320
+ // Should include refs for the commit range
321
+ expect(lastCall!.args).toContain("--refs")
478
322
 
479
- expect(logs.length).toBe(0)
323
+ // Should have GIT_CONFIG_GLOBAL env set
324
+ expect(lastCall!.env?.GIT_CONFIG_GLOBAL).toBe("")
480
325
  })
481
326
  })
482
327
  })
@@ -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* git.checkCommandExists("git-filter-repo")
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
- const result = yield* git.runGitCommand(filterRepoArgs, gitRoot, {
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
+ })