@markjaquith/agency 0.5.1 → 0.6.1

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.
@@ -0,0 +1,521 @@
1
+ import { test, expect, describe, beforeEach, afterEach } from "bun:test"
2
+ import { join } from "path"
3
+ import { rebase } from "./rebase"
4
+ import {
5
+ createTempDir,
6
+ cleanupTempDir,
7
+ initGitRepo,
8
+ getCurrentBranch,
9
+ createCommit,
10
+ checkoutBranch,
11
+ runTestEffect,
12
+ } from "../test-utils"
13
+
14
+ async function createBranch(cwd: string, branchName: string): Promise<void> {
15
+ await Bun.spawn(["git", "checkout", "-b", branchName], {
16
+ cwd,
17
+ stdout: "pipe",
18
+ stderr: "pipe",
19
+ }).exited
20
+ }
21
+
22
+ async function setupAgencyJson(
23
+ gitRoot: string,
24
+ baseBranch?: string,
25
+ emitBranch?: string,
26
+ ): Promise<void> {
27
+ const agencyJson = {
28
+ version: 1,
29
+ injectedFiles: ["AGENTS.md", "TASK.md", "opencode.json"],
30
+ template: "test",
31
+ createdAt: new Date().toISOString(),
32
+ ...(baseBranch ? { baseBranch } : {}),
33
+ ...(emitBranch ? { emitBranch } : {}),
34
+ }
35
+ await Bun.write(
36
+ join(gitRoot, "agency.json"),
37
+ JSON.stringify(agencyJson, null, 2) + "\n",
38
+ )
39
+ await Bun.spawn(["git", "add", "agency.json"], {
40
+ cwd: gitRoot,
41
+ stdout: "pipe",
42
+ stderr: "pipe",
43
+ }).exited
44
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Add agency.json"], {
45
+ cwd: gitRoot,
46
+ stdout: "pipe",
47
+ stderr: "pipe",
48
+ }).exited
49
+ }
50
+
51
+ async function getCommitCount(cwd: string, branch: string): Promise<number> {
52
+ const proc = Bun.spawn(["git", "rev-list", "--count", branch], {
53
+ cwd,
54
+ stdout: "pipe",
55
+ stderr: "pipe",
56
+ })
57
+ await proc.exited
58
+ const output = await new Response(proc.stdout).text()
59
+ return parseInt(output.trim(), 10)
60
+ }
61
+
62
+ async function fileExists(gitRoot: string, filename: string): Promise<boolean> {
63
+ const file = Bun.file(join(gitRoot, filename))
64
+ return await file.exists()
65
+ }
66
+
67
+ describe("rebase command", () => {
68
+ let tempDir: string
69
+ let originalCwd: string
70
+
71
+ beforeEach(async () => {
72
+ tempDir = await createTempDir()
73
+ originalCwd = process.cwd()
74
+ process.chdir(tempDir)
75
+
76
+ // Set config path to non-existent file to use defaults
77
+ process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
78
+
79
+ // Initialize git repo with initial commit on main
80
+ await initGitRepo(tempDir)
81
+ await Bun.write(join(tempDir, "initial.txt"), "initial content\n")
82
+ await Bun.spawn(["git", "add", "initial.txt"], {
83
+ cwd: tempDir,
84
+ stdout: "pipe",
85
+ stderr: "pipe",
86
+ }).exited
87
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Initial commit"], {
88
+ cwd: tempDir,
89
+ stdout: "pipe",
90
+ stderr: "pipe",
91
+ }).exited
92
+ })
93
+
94
+ afterEach(async () => {
95
+ process.chdir(originalCwd)
96
+ await cleanupTempDir(tempDir)
97
+ delete process.env.AGENCY_CONFIG_PATH
98
+ })
99
+
100
+ test("fails when not on an agency source branch", async () => {
101
+ // Should fail on main branch without agency.json
102
+ expect(
103
+ runTestEffect(
104
+ rebase({
105
+ silent: true,
106
+ verbose: false,
107
+ }),
108
+ ),
109
+ ).rejects.toThrow(/does not have agency\.json/)
110
+ })
111
+
112
+ test("fails when agency.json is invalid", async () => {
113
+ // Create invalid agency.json
114
+ await Bun.write(join(tempDir, "agency.json"), "invalid json")
115
+ await Bun.spawn(["git", "add", "agency.json"], {
116
+ cwd: tempDir,
117
+ stdout: "pipe",
118
+ stderr: "pipe",
119
+ }).exited
120
+ await createCommit(tempDir, "Add invalid agency.json")
121
+
122
+ expect(
123
+ runTestEffect(
124
+ rebase({
125
+ silent: true,
126
+ verbose: false,
127
+ }),
128
+ ),
129
+ ).rejects.toThrow(/does not have agency\.json/)
130
+ })
131
+
132
+ test("fails when there are uncommitted changes", async () => {
133
+ // Create feature branch with agency.json
134
+ await createBranch(tempDir, "agency/feature")
135
+ await setupAgencyJson(tempDir, "main")
136
+
137
+ // Create uncommitted changes
138
+ await Bun.write(join(tempDir, "uncommitted.txt"), "uncommitted\n")
139
+
140
+ expect(
141
+ runTestEffect(
142
+ rebase({
143
+ silent: true,
144
+ verbose: false,
145
+ }),
146
+ ),
147
+ ).rejects.toThrow(/uncommitted changes/)
148
+ })
149
+
150
+ test("rebases source branch onto main successfully", async () => {
151
+ // Create a feature branch
152
+ await createBranch(tempDir, "agency/feature")
153
+ await setupAgencyJson(tempDir, "main", "feature")
154
+ await Bun.write(join(tempDir, "feature1.txt"), "feature 1\n")
155
+ await Bun.spawn(["git", "add", "feature1.txt"], {
156
+ cwd: tempDir,
157
+ stdout: "pipe",
158
+ stderr: "pipe",
159
+ }).exited
160
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Add feature 1"], {
161
+ cwd: tempDir,
162
+ stdout: "pipe",
163
+ stderr: "pipe",
164
+ }).exited
165
+
166
+ const commitCountBefore = await getCommitCount(tempDir, "agency/feature")
167
+
168
+ // Add commits to main
169
+ await checkoutBranch(tempDir, "main")
170
+ await Bun.write(join(tempDir, "main1.txt"), "main 1\n")
171
+ await Bun.spawn(["git", "add", "main1.txt"], {
172
+ cwd: tempDir,
173
+ stdout: "pipe",
174
+ stderr: "pipe",
175
+ }).exited
176
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Main commit 1"], {
177
+ cwd: tempDir,
178
+ stdout: "pipe",
179
+ stderr: "pipe",
180
+ }).exited
181
+ await Bun.write(join(tempDir, "main2.txt"), "main 2\n")
182
+ await Bun.spawn(["git", "add", "main2.txt"], {
183
+ cwd: tempDir,
184
+ stdout: "pipe",
185
+ stderr: "pipe",
186
+ }).exited
187
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Main commit 2"], {
188
+ cwd: tempDir,
189
+ stdout: "pipe",
190
+ stderr: "pipe",
191
+ }).exited
192
+
193
+ // Switch back to feature and rebase
194
+ await checkoutBranch(tempDir, "agency/feature")
195
+
196
+ await runTestEffect(
197
+ rebase({
198
+ silent: true,
199
+ verbose: false,
200
+ baseBranch: "main",
201
+ }),
202
+ )
203
+
204
+ // Verify we're still on the feature branch
205
+ const currentBranch = await getCurrentBranch(tempDir)
206
+ expect(currentBranch).toBe("agency/feature")
207
+
208
+ // Verify commit count increased (original commits + new commits from main)
209
+ const commitCountAfter = await getCommitCount(tempDir, "agency/feature")
210
+ expect(commitCountAfter).toBe(commitCountBefore + 2)
211
+
212
+ // Verify all files exist
213
+ expect(await fileExists(tempDir, "initial.txt")).toBe(true)
214
+ expect(await fileExists(tempDir, "main1.txt")).toBe(true)
215
+ expect(await fileExists(tempDir, "main2.txt")).toBe(true)
216
+ expect(await fileExists(tempDir, "feature1.txt")).toBe(true)
217
+ expect(await fileExists(tempDir, "agency.json")).toBe(true)
218
+ })
219
+
220
+ test("uses base branch from agency.json by default", async () => {
221
+ // Create a dev branch
222
+ await checkoutBranch(tempDir, "main")
223
+ await createBranch(tempDir, "develop")
224
+ await Bun.write(join(tempDir, "dev1.txt"), "dev 1\n")
225
+ await Bun.spawn(["git", "add", "dev1.txt"], {
226
+ cwd: tempDir,
227
+ stdout: "pipe",
228
+ stderr: "pipe",
229
+ }).exited
230
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Dev commit 1"], {
231
+ cwd: tempDir,
232
+ stdout: "pipe",
233
+ stderr: "pipe",
234
+ }).exited
235
+
236
+ // Create feature branch based on develop
237
+ await createBranch(tempDir, "agency/feature")
238
+ await setupAgencyJson(tempDir, "develop", "feature")
239
+ await Bun.write(join(tempDir, "feature1.txt"), "feature 1\n")
240
+ await Bun.spawn(["git", "add", "feature1.txt"], {
241
+ cwd: tempDir,
242
+ stdout: "pipe",
243
+ stderr: "pipe",
244
+ }).exited
245
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Add feature 1"], {
246
+ cwd: tempDir,
247
+ stdout: "pipe",
248
+ stderr: "pipe",
249
+ }).exited
250
+
251
+ // Add more commits to develop
252
+ await checkoutBranch(tempDir, "develop")
253
+ await Bun.write(join(tempDir, "dev2.txt"), "dev 2\n")
254
+ await Bun.spawn(["git", "add", "dev2.txt"], {
255
+ cwd: tempDir,
256
+ stdout: "pipe",
257
+ stderr: "pipe",
258
+ }).exited
259
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Dev commit 2"], {
260
+ cwd: tempDir,
261
+ stdout: "pipe",
262
+ stderr: "pipe",
263
+ }).exited
264
+
265
+ // Switch back to feature and rebase (should use develop from agency.json)
266
+ await checkoutBranch(tempDir, "agency/feature")
267
+
268
+ await runTestEffect(
269
+ rebase({
270
+ silent: true,
271
+ verbose: false,
272
+ }),
273
+ )
274
+
275
+ // Verify dev2.txt exists (came from develop)
276
+ expect(await fileExists(tempDir, "dev2.txt")).toBe(true)
277
+ })
278
+
279
+ test("preserves agency files during rebase", async () => {
280
+ // Create feature branch with agency files
281
+ await createBranch(tempDir, "agency/feature")
282
+ await setupAgencyJson(tempDir, "main", "feature")
283
+
284
+ // Add TASK.md and AGENTS.md
285
+ await Bun.write(join(tempDir, "TASK.md"), "# Task\nTest task\n")
286
+ await Bun.write(join(tempDir, "AGENTS.md"), "# Agents\nTest agents\n")
287
+ await Bun.spawn(["git", "add", "TASK.md", "AGENTS.md"], {
288
+ cwd: tempDir,
289
+ stdout: "pipe",
290
+ stderr: "pipe",
291
+ }).exited
292
+ await createCommit(tempDir, "Add agency files")
293
+
294
+ // Add commit to main
295
+ await checkoutBranch(tempDir, "main")
296
+ await Bun.write(join(tempDir, "main1.txt"), "main 1\n")
297
+ await Bun.spawn(["git", "add", "main1.txt"], {
298
+ cwd: tempDir,
299
+ stdout: "pipe",
300
+ stderr: "pipe",
301
+ }).exited
302
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Main commit"], {
303
+ cwd: tempDir,
304
+ stdout: "pipe",
305
+ stderr: "pipe",
306
+ }).exited
307
+
308
+ // Rebase feature branch
309
+ await checkoutBranch(tempDir, "agency/feature")
310
+
311
+ await runTestEffect(
312
+ rebase({
313
+ silent: true,
314
+ verbose: false,
315
+ baseBranch: "main",
316
+ }),
317
+ )
318
+
319
+ // Verify agency files still exist
320
+ expect(await fileExists(tempDir, "agency.json")).toBe(true)
321
+ expect(await fileExists(tempDir, "TASK.md")).toBe(true)
322
+ expect(await fileExists(tempDir, "AGENTS.md")).toBe(true)
323
+
324
+ // Verify content is preserved
325
+ const taskContent = await Bun.file(join(tempDir, "TASK.md")).text()
326
+ expect(taskContent).toContain("Test task")
327
+ })
328
+
329
+ test("can specify explicit base branch", async () => {
330
+ // Create custom base branch
331
+ await createBranch(tempDir, "custom-base")
332
+ await Bun.write(join(tempDir, "custom.txt"), "custom\n")
333
+ await Bun.spawn(["git", "add", "custom.txt"], {
334
+ cwd: tempDir,
335
+ stdout: "pipe",
336
+ stderr: "pipe",
337
+ }).exited
338
+ await Bun.spawn(
339
+ ["git", "commit", "--no-verify", "-m", "Custom base commit"],
340
+ {
341
+ cwd: tempDir,
342
+ stdout: "pipe",
343
+ stderr: "pipe",
344
+ },
345
+ ).exited
346
+
347
+ // Create feature branch from main
348
+ await checkoutBranch(tempDir, "main")
349
+ await createBranch(tempDir, "agency/feature")
350
+ await setupAgencyJson(tempDir, "main", "feature")
351
+
352
+ // Rebase onto custom-base instead of main
353
+ await runTestEffect(
354
+ rebase({
355
+ silent: true,
356
+ verbose: false,
357
+ baseBranch: "custom-base",
358
+ }),
359
+ )
360
+
361
+ // Verify custom.txt exists
362
+ expect(await fileExists(tempDir, "custom.txt")).toBe(true)
363
+ })
364
+
365
+ test("updates emit branch with --emit flag", async () => {
366
+ // Create feature branch with agency.json
367
+ await createBranch(tempDir, "agency/feature")
368
+ await setupAgencyJson(tempDir, "main", "feature")
369
+
370
+ // Add commit to main
371
+ await checkoutBranch(tempDir, "main")
372
+ await Bun.write(join(tempDir, "main1.txt"), "main 1\n")
373
+ await Bun.spawn(["git", "add", "main1.txt"], {
374
+ cwd: tempDir,
375
+ stdout: "pipe",
376
+ stderr: "pipe",
377
+ }).exited
378
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Main commit"], {
379
+ cwd: tempDir,
380
+ stdout: "pipe",
381
+ stderr: "pipe",
382
+ }).exited
383
+
384
+ // Rebase feature branch with new emit branch name
385
+ await checkoutBranch(tempDir, "agency/feature")
386
+
387
+ await runTestEffect(
388
+ rebase({
389
+ silent: true,
390
+ verbose: false,
391
+ baseBranch: "main",
392
+ emit: "new-emit-branch",
393
+ }),
394
+ )
395
+
396
+ // Verify agency.json has updated emit branch
397
+ const agencyJsonFile = Bun.file(join(tempDir, "agency.json"))
398
+ const agencyJson = await agencyJsonFile.json()
399
+ expect(agencyJson.emitBranch).toBe("new-emit-branch")
400
+
401
+ // Verify we're still on the feature branch
402
+ const currentBranch = await getCurrentBranch(tempDir)
403
+ expect(currentBranch).toBe("agency/feature")
404
+
405
+ // Verify there's a commit for the emit branch update
406
+ const proc = Bun.spawn(["git", "log", "--oneline", "-1"], {
407
+ cwd: tempDir,
408
+ stdout: "pipe",
409
+ stderr: "pipe",
410
+ })
411
+ await proc.exited
412
+ const output = await new Response(proc.stdout).text()
413
+ expect(output).toContain(
414
+ "chore: agency rebase (main) agency/feature → new-emit-branch",
415
+ )
416
+ })
417
+
418
+ test("preserves other metadata when updating emit branch", async () => {
419
+ // Create feature branch with agency.json containing standard fields
420
+ await createBranch(tempDir, "agency/feature")
421
+ const agencyJson = {
422
+ version: 1,
423
+ injectedFiles: ["AGENTS.md", "TASK.md", "opencode.json"],
424
+ template: "test",
425
+ createdAt: "2024-01-01T00:00:00.000Z",
426
+ baseBranch: "main",
427
+ emitBranch: "feature",
428
+ }
429
+ await Bun.write(
430
+ join(tempDir, "agency.json"),
431
+ JSON.stringify(agencyJson, null, 2) + "\n",
432
+ )
433
+ await Bun.spawn(["git", "add", "agency.json"], {
434
+ cwd: tempDir,
435
+ stdout: "pipe",
436
+ stderr: "pipe",
437
+ }).exited
438
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Add agency.json"], {
439
+ cwd: tempDir,
440
+ stdout: "pipe",
441
+ stderr: "pipe",
442
+ }).exited
443
+
444
+ // Add commit to main
445
+ await checkoutBranch(tempDir, "main")
446
+ await Bun.write(join(tempDir, "main1.txt"), "main 1\n")
447
+ await Bun.spawn(["git", "add", "main1.txt"], {
448
+ cwd: tempDir,
449
+ stdout: "pipe",
450
+ stderr: "pipe",
451
+ }).exited
452
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Main commit"], {
453
+ cwd: tempDir,
454
+ stdout: "pipe",
455
+ stderr: "pipe",
456
+ }).exited
457
+
458
+ // Rebase with new emit branch
459
+ await checkoutBranch(tempDir, "agency/feature")
460
+
461
+ await runTestEffect(
462
+ rebase({
463
+ silent: true,
464
+ verbose: false,
465
+ baseBranch: "main",
466
+ emit: "updated-emit",
467
+ }),
468
+ )
469
+
470
+ // Verify all standard metadata fields are preserved
471
+ const updatedJsonFile = Bun.file(join(tempDir, "agency.json"))
472
+ const updatedJson = await updatedJsonFile.json()
473
+ expect(updatedJson.version).toBe(1)
474
+ expect(updatedJson.injectedFiles).toEqual([
475
+ "AGENTS.md",
476
+ "TASK.md",
477
+ "opencode.json",
478
+ ])
479
+ expect(updatedJson.template).toBe("test")
480
+ expect(updatedJson.createdAt).toBe("2024-01-01T00:00:00.000Z")
481
+ expect(updatedJson.baseBranch).toBe("main")
482
+ expect(updatedJson.emitBranch).toBe("updated-emit")
483
+ })
484
+
485
+ test("supports deprecated --branch flag for backward compatibility", async () => {
486
+ // Create feature branch with agency.json
487
+ await createBranch(tempDir, "agency/feature")
488
+ await setupAgencyJson(tempDir, "main", "feature")
489
+
490
+ // Add commit to main
491
+ await checkoutBranch(tempDir, "main")
492
+ await Bun.write(join(tempDir, "main1.txt"), "main 1\n")
493
+ await Bun.spawn(["git", "add", "main1.txt"], {
494
+ cwd: tempDir,
495
+ stdout: "pipe",
496
+ stderr: "pipe",
497
+ }).exited
498
+ await Bun.spawn(["git", "commit", "--no-verify", "-m", "Main commit"], {
499
+ cwd: tempDir,
500
+ stdout: "pipe",
501
+ stderr: "pipe",
502
+ }).exited
503
+
504
+ // Rebase feature branch with --branch (deprecated)
505
+ await checkoutBranch(tempDir, "agency/feature")
506
+
507
+ await runTestEffect(
508
+ rebase({
509
+ silent: true,
510
+ verbose: false,
511
+ baseBranch: "main",
512
+ branch: "branch-flag-emit",
513
+ }),
514
+ )
515
+
516
+ // Verify agency.json has updated emit branch
517
+ const agencyJsonFile = Bun.file(join(tempDir, "agency.json"))
518
+ const agencyJson = await agencyJsonFile.json()
519
+ expect(agencyJson.emitBranch).toBe("branch-flag-emit")
520
+ })
521
+ })