@markjaquith/agency 2.74.0 → 3.1.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.
Files changed (59) hide show
  1. package/README.md +29 -122
  2. package/cli-main.ts +10 -28
  3. package/cli.ts +1 -17
  4. package/package.json +1 -2
  5. package/schemas/agency-graph-v1.schema.json +1 -1
  6. package/src/cli-parser.test.ts +5 -19
  7. package/src/cli-parser.ts +12 -30
  8. package/src/cli.test.ts +6 -6
  9. package/src/commands/archive.test.ts +73 -1
  10. package/src/commands/archive.ts +18 -7
  11. package/src/commands/context.test.ts +0 -67
  12. package/src/commands/init.test.ts +1 -2
  13. package/src/commands/pr.test.ts +2 -87
  14. package/src/commands/pr.ts +2 -79
  15. package/src/commands/push.test.ts +1 -2
  16. package/src/graph-schema.ts +1 -1
  17. package/src/services/ArchiveBulkService.test.ts +1 -142
  18. package/src/services/ArchiveService.test.ts +1 -172
  19. package/src/services/ArchiveService.ts +59 -27
  20. package/src/services/ContextService.ts +3 -47
  21. package/src/services/DoctorService.ts +8 -24
  22. package/src/services/GraphService.test.ts +1 -0
  23. package/src/services/GraphService.ts +2 -5
  24. package/src/services/PhaseService.ts +70 -191
  25. package/src/services/PullRequestService.test.ts +1 -45
  26. package/src/services/PullRequestService.ts +1 -22
  27. package/src/services/PushService.test.ts +29 -295
  28. package/src/services/PushService.ts +10 -239
  29. package/src/services/RepositoryService.test.ts +1 -61
  30. package/src/services/RepositoryService.ts +2 -13
  31. package/src/services/ReviewService.test.ts +1 -84
  32. package/src/services/ReviewService.ts +9 -48
  33. package/src/services/SyncService.test.ts +0 -81
  34. package/src/services/SyncService.ts +12 -39
  35. package/src/services/TaskPhaseService.test.ts +0 -80
  36. package/src/services/TaskService.ts +1 -9
  37. package/src/services/VersionControlService.test.ts +4 -117
  38. package/src/services/VersionControlService.ts +3 -426
  39. package/src/services/WorkbaseService.test.ts +0 -20
  40. package/src/services/WorkbaseService.ts +1 -18
  41. package/src/services/WorktreeService.test.ts +1 -705
  42. package/src/services/WorktreeService.ts +395 -1604
  43. package/src/services/push-validation.ts +0 -7
  44. package/src/test-utils.ts +0 -4
  45. package/src/workbase/AGENTS.md +4 -5
  46. package/src/workbase/checkout-command.test.ts +1 -1
  47. package/src/workbase/checkout-command.ts +1 -1
  48. package/src/workbase/delivery-command.test.ts +4 -35
  49. package/src/workbase/delivery-command.ts +2 -15
  50. package/src/workbase/schemas.test.ts +0 -19
  51. package/src/workbase/schemas.ts +1 -2
  52. package/src/commands/vcs.test.ts +0 -75
  53. package/src/commands/vcs.ts +0 -72
  54. package/src/services/VcsMigrationService.test.ts +0 -348
  55. package/src/services/VcsMigrationService.ts +0 -857
  56. package/src/vcs-status-fast.ts +0 -310
  57. package/src/workbase/version-control.ts +0 -5
  58. package/src/workbase/workspace-command.test.ts +0 -70
  59. package/src/workbase/workspace-command.ts +0 -63
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
2
  import { Effect } from "effect"
3
- import { chmod, mkdir, realpath, rename, rm, stat } from "node:fs/promises"
3
+ import { mkdir, realpath, rename, rm } from "node:fs/promises"
4
4
  import { join, resolve } from "node:path"
5
5
  import {
6
6
  captureErrors,
@@ -38,32 +38,6 @@ const gitOutput = async (args: string[], cwd?: string) => {
38
38
  return stdout.trim()
39
39
  }
40
40
 
41
- const jj = async (args: string[], cwd?: string) => {
42
- const process = Bun.spawn(["jj", ...args], {
43
- cwd,
44
- stdout: "pipe",
45
- stderr: "pipe",
46
- })
47
- await process.exited
48
- if (process.exitCode !== 0)
49
- throw new Error(await new Response(process.stderr).text())
50
- }
51
-
52
- const jjOutput = async (args: string[], cwd?: string) => {
53
- const process = Bun.spawn(["jj", ...args], {
54
- cwd,
55
- stdout: "pipe",
56
- stderr: "pipe",
57
- })
58
- const [exitCode, stdout, stderr] = await Promise.all([
59
- process.exited,
60
- new Response(process.stdout).text(),
61
- new Response(process.stderr).text(),
62
- ])
63
- if (exitCode !== 0) throw new Error(stderr)
64
- return stdout.trim()
65
- }
66
-
67
41
  describe("WorktreeService", () => {
68
42
  let root: string
69
43
  let source: string
@@ -311,579 +285,6 @@ describe("WorktreeService", () => {
311
285
  )
312
286
  })
313
287
 
314
- test("materializes and removes a jj workspace for a jj workbase", async () => {
315
- if (!Bun.which("jj")) return
316
- const repository = join(root, "repos/agency")
317
- await rm(repository, { recursive: true, force: true })
318
- await git(["clone", source, repository])
319
- await jj(["git", "init", "--colocate", repository])
320
- await Bun.write(
321
- join(root, "agency.json"),
322
- JSON.stringify({
323
- version: 2,
324
- vcs: "jj",
325
- repositories: {
326
- agency: {
327
- remote: "https://example.com/agency.git",
328
- postCheckoutCommand: [
329
- "sh",
330
- "-c",
331
- 'printf "%s:%s" "$AGENCY_VCS" "$AGENCY_CHECKOUT_KIND" > post-checkout',
332
- ],
333
- },
334
- },
335
- }),
336
- )
337
- await runTestEffect(
338
- TaskService.pipe(
339
- Effect.flatMap((service) =>
340
- service.create(
341
- {
342
- id: "jj-example",
343
- ticketUrl: null,
344
- repo: "agency",
345
- branch: "task/jj-example",
346
- base: "main",
347
- },
348
- root,
349
- ),
350
- ),
351
- ),
352
- )
353
-
354
- const workspace = await runTestEffect(
355
- WorktreeService.pipe(
356
- Effect.flatMap((service) =>
357
- service.materialize("jj-example", undefined, root),
358
- ),
359
- ),
360
- )
361
- const jjMetadata = await stat(join(workspace.writablePath!, ".jj"))
362
- expect(jjMetadata.isFile() || jjMetadata.isDirectory()).toBe(true)
363
- expect(
364
- await Bun.file(join(workspace.writablePath!, "post-checkout")).text(),
365
- ).toBe("jj:writable")
366
- await jj(
367
- ["commit", "-m", "record post-checkout output"],
368
- workspace.writablePath!,
369
- )
370
- const inspection = await runTestEffect(
371
- WorktreeService.pipe(
372
- Effect.flatMap((service) =>
373
- service.inspect("jj-example", undefined, root),
374
- ),
375
- ),
376
- )
377
- expect(inspection.conflicts).toEqual([])
378
- expect(inspection.checkouts[0]).toMatchObject({
379
- exists: true,
380
- registered: true,
381
- actualBranch: "task/jj-example",
382
- })
383
-
384
- await runTestEffect(
385
- WorktreeService.pipe(
386
- Effect.flatMap((service) =>
387
- service.remove("jj-example", undefined, root),
388
- ),
389
- ),
390
- )
391
- expect(await Bun.file(workspace.writablePath!).exists()).toBe(false)
392
- })
393
-
394
- test("uses a custom jj workspace creator before the post-checkout hook", async () => {
395
- if (!Bun.which("jj")) return
396
- const repository = join(root, "repos/agency")
397
- await rm(repository, { recursive: true, force: true })
398
- await git(["clone", source, repository])
399
- await jj(["git", "init", "--colocate", repository])
400
- await Bun.write(
401
- join(root, "agency.json"),
402
- JSON.stringify({
403
- version: 2,
404
- vcs: "jj",
405
- workspaceCreateCommand: [
406
- "sh",
407
- "-c",
408
- 'jj -R "$1" workspace add --name "$3" -r "$4" "$2" && printf "%s\\n%s\\n" "$AGENCY_CHECKOUT_KIND" "$AGENCY_REQUESTED_REF" > "$2/creator-finished"',
409
- "workspace-creator",
410
- "{repo}",
411
- "{workspace}",
412
- "{name}",
413
- "{revision}",
414
- "{kind}",
415
- "{requestedRef}",
416
- ],
417
- repositories: {
418
- agency: {
419
- remote: "https://example.com/agency.git",
420
- postCheckoutCommand: [
421
- "sh",
422
- "-c",
423
- 'test -f creator-finished && printf "post-checkout" > hook-finished',
424
- ],
425
- },
426
- },
427
- }),
428
- )
429
- await runTestEffect(
430
- TaskService.pipe(
431
- Effect.flatMap((service) =>
432
- service.create(
433
- {
434
- id: "jj-custom",
435
- ticketUrl: null,
436
- repo: "agency",
437
- branch: "task/jj-custom",
438
- base: "main",
439
- },
440
- root,
441
- ),
442
- ),
443
- ),
444
- )
445
-
446
- const workspace = await runTestEffect(
447
- WorktreeService.pipe(
448
- Effect.flatMap((service) =>
449
- service.materialize("jj-custom", undefined, root),
450
- ),
451
- ),
452
- )
453
- expect(
454
- await Bun.file(join(workspace.writablePath!, "creator-finished")).text(),
455
- ).toBe("writable\ntask/jj-custom\n")
456
- expect(
457
- await Bun.file(join(workspace.writablePath!, "hook-finished")).text(),
458
- ).toBe("post-checkout")
459
- expect(workspace.operations[0]).toMatchObject({
460
- action: "create-workspace",
461
- command: expect.arrayContaining(["writable", "task/jj-custom"]),
462
- status: "completed",
463
- })
464
- })
465
-
466
- test("rolls back a jj workspace partially created by a custom command", async () => {
467
- if (!Bun.which("jj")) return
468
- const repository = join(root, "repos/agency")
469
- await rm(repository, { recursive: true, force: true })
470
- await git(["clone", source, repository])
471
- await jj(["git", "init", "--colocate", repository])
472
- await Bun.write(
473
- join(root, "agency.json"),
474
- JSON.stringify({
475
- version: 2,
476
- vcs: "jj",
477
- workspaceCreateCommand: [
478
- "sh",
479
- "-c",
480
- 'jj -R "$1" workspace add --name "$3" -r "$4" "$2" && echo adoption-failed >&2; exit 7',
481
- "workspace-creator",
482
- "{repo}",
483
- "{workspace}",
484
- "{name}",
485
- "{revision}",
486
- ],
487
- }),
488
- )
489
- await runTestEffect(
490
- TaskService.pipe(
491
- Effect.flatMap((service) =>
492
- service.create(
493
- {
494
- id: "jj-custom-failure",
495
- ticketUrl: null,
496
- repo: "agency",
497
- branch: "task/jj-custom-failure",
498
- base: "main",
499
- },
500
- root,
501
- ),
502
- ),
503
- ),
504
- )
505
- const workspacePath = join(root, "tasks/jj-custom-failure/code/agency")
506
-
507
- await expect(
508
- runTestEffect(
509
- WorktreeService.pipe(
510
- Effect.flatMap((service) =>
511
- service.materialize("jj-custom-failure", undefined, root),
512
- ),
513
- ),
514
- ),
515
- ).rejects.toThrow("adoption-failed")
516
- expect(await Bun.file(workspacePath).exists()).toBe(false)
517
- expect(
518
- await jjOutput(["workspace", "list", "-T", 'name ++ "\\n"'], repository),
519
- ).not.toContain("agency-jj-custom-failure-task-agency")
520
- })
521
-
522
- test("rejects and rolls back a custom jj workspace at the wrong revision", async () => {
523
- if (!Bun.which("jj")) return
524
- const repository = join(root, "repos/agency")
525
- await rm(repository, { recursive: true, force: true })
526
- await git(["clone", source, repository])
527
- await jj(["git", "init", "--colocate", repository])
528
- const hookMarker = join(root, "wrong-revision-hook")
529
- await Bun.write(
530
- join(root, "agency.json"),
531
- JSON.stringify({
532
- version: 2,
533
- vcs: "jj",
534
- workspaceCreateCommand: [
535
- "sh",
536
- "-c",
537
- 'jj -R "$1" workspace add --name "$3" -r "root()" "$2"',
538
- "workspace-creator",
539
- "{repo}",
540
- "{workspace}",
541
- "{name}",
542
- "{revision}",
543
- ],
544
- repositories: {
545
- agency: {
546
- remote: "https://example.com/agency.git",
547
- postCheckoutCommand: ["sh", "-c", 'touch "$1"', "hook", hookMarker],
548
- },
549
- },
550
- }),
551
- )
552
- await runTestEffect(
553
- TaskService.pipe(
554
- Effect.flatMap((service) =>
555
- service.create(
556
- {
557
- id: "jj-custom-wrong-revision",
558
- ticketUrl: null,
559
- repo: "agency",
560
- branch: "task/jj-custom-wrong-revision",
561
- base: "main",
562
- },
563
- root,
564
- ),
565
- ),
566
- ),
567
- )
568
- const workspacePath = join(
569
- root,
570
- "tasks/jj-custom-wrong-revision/code/agency",
571
- )
572
-
573
- await expect(
574
- runTestEffect(
575
- WorktreeService.pipe(
576
- Effect.flatMap((service) =>
577
- service.materialize("jj-custom-wrong-revision", undefined, root),
578
- ),
579
- ),
580
- ),
581
- ).rejects.toThrow("failed validation")
582
- expect(await Bun.file(workspacePath).exists()).toBe(false)
583
- expect(await Bun.file(hookMarker).exists()).toBe(false)
584
- })
585
-
586
- test("suspends and resumes the exact jj working-copy target", async () => {
587
- if (!Bun.which("jj")) return
588
- const repository = join(root, "repos/agency")
589
- await rm(repository, { recursive: true, force: true })
590
- await git(["clone", source, repository])
591
- await jj(["git", "init", "--colocate", repository])
592
- await Bun.write(
593
- join(root, "agency.json"),
594
- JSON.stringify({ version: 2, vcs: "jj" }),
595
- )
596
- await runTestEffect(
597
- TaskService.pipe(
598
- Effect.flatMap((service) =>
599
- service.create(
600
- {
601
- id: "jj-resume",
602
- ticketUrl: null,
603
- repo: "agency",
604
- branch: "task/jj-resume",
605
- base: "main",
606
- },
607
- root,
608
- ),
609
- ),
610
- ),
611
- )
612
- const workspace = await runTestEffect(
613
- WorktreeService.pipe(
614
- Effect.flatMap((service) =>
615
- service.materialize("jj-resume", undefined, root),
616
- ),
617
- ),
618
- )
619
- await jj(["new", "-m", "local-only parent"], workspace.writablePath!)
620
- await Bun.write(join(workspace.writablePath!, "local.txt"), "preserve me\n")
621
- await jj(
622
- ["describe", "-m", "local working target"],
623
- workspace.writablePath!,
624
- )
625
- const target = await jjOutput(
626
- ["log", "--no-graph", "-r", "@", "-T", 'commit_id ++ "\\t" ++ change_id'],
627
- workspace.writablePath!,
628
- )
629
- const [commitId, changeId] = target.split("\t")
630
- if (!commitId || !changeId)
631
- throw new Error(`Unexpected jj identity: ${target}`)
632
- await jj(["new", "-m", "temporary successor"], workspace.writablePath!)
633
- await jj(["edit", commitId], workspace.writablePath!)
634
-
635
- await runTestEffect(
636
- WorktreeService.pipe(
637
- Effect.flatMap((service) =>
638
- service.remove("jj-resume", undefined, root),
639
- ),
640
- ),
641
- )
642
- const resumePath = join(root, "tasks/jj-resume/.agency-jj-resume.json")
643
- const resume = await Bun.file(resumePath).json()
644
- expect(resume.checkouts[0]).toMatchObject({ commitId, changeId })
645
- expect(
646
- await jjOutput(
647
- [
648
- "log",
649
- "--no-graph",
650
- "-r",
651
- resume.checkouts[0].bookmark,
652
- "-T",
653
- "commit_id",
654
- ],
655
- repository,
656
- ),
657
- ).toBe(commitId)
658
- expect(
659
- await jjOutput(
660
- ["log", "--no-graph", "-r", "task/jj-resume", "-T", "commit_id"],
661
- repository,
662
- ).catch(() => null),
663
- ).toBeNull()
664
-
665
- await runTestEffect(
666
- WorktreeService.pipe(
667
- Effect.flatMap((service) =>
668
- service.materialize("jj-resume", undefined, root),
669
- ),
670
- ),
671
- )
672
- expect(
673
- await jjOutput(
674
- ["log", "--no-graph", "-r", "@", "-T", "commit_id"],
675
- workspace.writablePath!,
676
- ),
677
- ).toBe(commitId)
678
- expect(
679
- await Bun.file(join(workspace.writablePath!, "local.txt")).text(),
680
- ).toBe("preserve me\n")
681
- expect(await Bun.file(resumePath).exists()).toBe(false)
682
- expect(
683
- await jjOutput(
684
- [
685
- "log",
686
- "--no-graph",
687
- "-r",
688
- resume.checkouts[0].bookmark,
689
- "-T",
690
- "commit_id",
691
- ],
692
- repository,
693
- ).catch(() => null),
694
- ).toBeNull()
695
- })
696
-
697
- test("refuses stale jj resume identity and stale registrations", async () => {
698
- if (!Bun.which("jj")) return
699
- const repository = join(root, "repos/agency")
700
- await rm(repository, { recursive: true, force: true })
701
- await git(["clone", source, repository])
702
- await jj(["git", "init", "--colocate", repository])
703
- await Bun.write(
704
- join(root, "agency.json"),
705
- JSON.stringify({ version: 2, vcs: "jj" }),
706
- )
707
- for (const id of ["stale-resume", "stale-registration"]) {
708
- await runTestEffect(
709
- TaskService.pipe(
710
- Effect.flatMap((service) =>
711
- service.create(
712
- {
713
- id,
714
- ticketUrl: null,
715
- repo: "agency",
716
- branch: `task/${id}`,
717
- base: "main",
718
- },
719
- root,
720
- ),
721
- ),
722
- ),
723
- )
724
- }
725
- const suspended = await runTestEffect(
726
- WorktreeService.pipe(
727
- Effect.flatMap((service) =>
728
- service.materialize("stale-resume", undefined, root),
729
- ),
730
- ),
731
- )
732
- await runTestEffect(
733
- WorktreeService.pipe(
734
- Effect.flatMap((service) =>
735
- service.remove("stale-resume", undefined, root),
736
- ),
737
- ),
738
- )
739
- const resumePath = join(root, "tasks/stale-resume/.agency-jj-resume.json")
740
- const resume = await Bun.file(resumePath).json()
741
- await jj([
742
- "-R",
743
- repository,
744
- "bookmark",
745
- "delete",
746
- resume.checkouts[0].bookmark,
747
- ])
748
- await expect(
749
- runTestEffect(
750
- WorktreeService.pipe(
751
- Effect.flatMap((service) =>
752
- service.materialize("stale-resume", undefined, root),
753
- ),
754
- ),
755
- ),
756
- ).rejects.toThrow("recorded commit, change ID, path, or internal bookmark")
757
- expect(await Bun.file(resumePath).exists()).toBe(true)
758
- expect(await Bun.file(suspended.writablePath!).exists()).toBe(false)
759
-
760
- const stale = await runTestEffect(
761
- WorktreeService.pipe(
762
- Effect.flatMap((service) =>
763
- service.materialize("stale-registration", undefined, root),
764
- ),
765
- ),
766
- )
767
- await rm(join(stale.writablePath!, ".jj"), { recursive: true, force: true })
768
- await expect(
769
- runTestEffect(
770
- WorktreeService.pipe(
771
- Effect.flatMap((service) =>
772
- service.remove("stale-registration", undefined, root),
773
- ),
774
- ),
775
- ),
776
- ).rejects.toThrow("checkout cleanliness could not be verified")
777
- })
778
-
779
- test("retains jj resume state when workspace deletion and rollback fail", async () => {
780
- if (!Bun.which("jj")) return
781
- const repository = join(root, "repos/agency")
782
- await rm(repository, { recursive: true, force: true })
783
- await git(["clone", source, repository])
784
- await jj(["git", "init", "--colocate", repository])
785
- await Bun.write(
786
- join(root, "agency.json"),
787
- JSON.stringify({ version: 2, vcs: "jj" }),
788
- )
789
- await runTestEffect(
790
- TaskService.pipe(
791
- Effect.flatMap((service) =>
792
- service.create(
793
- {
794
- id: "jj-rollback",
795
- ticketUrl: null,
796
- repo: "agency",
797
- branch: "task/jj-rollback",
798
- base: "main",
799
- },
800
- root,
801
- ),
802
- ),
803
- ),
804
- )
805
- const workspace = await runTestEffect(
806
- WorktreeService.pipe(
807
- Effect.flatMap((service) =>
808
- service.materialize("jj-rollback", undefined, root),
809
- ),
810
- ),
811
- )
812
- const fakeBin = join(root, "fake-bin")
813
- await mkdir(fakeBin)
814
- const fakeRm = join(fakeBin, "rm")
815
- await Bun.write(
816
- fakeRm,
817
- `#!/bin/sh\nif [ "$1" = "-rf" ] && [ "$2" = ${JSON.stringify(workspace.writablePath!)} ]; then exit 1; fi\nexec ${JSON.stringify(Bun.which("rm")!)} "$@"\n`,
818
- )
819
- await chmod(fakeRm, 0o755)
820
- const originalPath = process.env.PATH
821
- process.env.PATH = `${fakeBin}:${originalPath}`
822
- try {
823
- await expect(
824
- runTestEffect(
825
- WorktreeService.pipe(
826
- Effect.flatMap((service) =>
827
- service.remove("jj-rollback", undefined, root),
828
- ),
829
- ),
830
- ),
831
- ).rejects.toThrow("requires manual recovery")
832
- } finally {
833
- process.env.PATH = originalPath
834
- }
835
- const resumePath = join(root, "tasks/jj-rollback/.agency-jj-resume.json")
836
- expect(await Bun.file(resumePath).exists()).toBe(true)
837
-
838
- await runTestEffect(
839
- WorktreeService.pipe(
840
- Effect.flatMap((service) =>
841
- service.materialize("jj-rollback", undefined, root),
842
- ),
843
- ),
844
- )
845
- expect((await stat(workspace.writablePath!)).isDirectory()).toBe(true)
846
- expect(await Bun.file(resumePath).exists()).toBe(false)
847
- })
848
-
849
- test("recommends a repository fetch when jj cannot resolve a base", async () => {
850
- if (!Bun.which("jj")) return
851
- const repository = join(root, "repos/agency")
852
- await rm(repository, { recursive: true, force: true })
853
- await git(["clone", source, repository])
854
- await jj(["git", "init", "--colocate", repository])
855
- await Bun.write(
856
- join(root, "agency.json"),
857
- JSON.stringify({ version: 2, vcs: "jj" }),
858
- )
859
- await runTestEffect(
860
- TaskService.pipe(
861
- Effect.flatMap((service) =>
862
- service.create(
863
- {
864
- id: "jj-missing",
865
- ticketUrl: null,
866
- repo: "agency",
867
- branch: "task/jj-missing",
868
- base: "absent-base",
869
- },
870
- root,
871
- ),
872
- ),
873
- ),
874
- )
875
-
876
- await expect(
877
- runTestEffect(
878
- WorktreeService.pipe(
879
- Effect.flatMap((service) =>
880
- service.materialize("jj-missing", undefined, root),
881
- ),
882
- ),
883
- ),
884
- ).rejects.toThrow("run 'agency repo fetch agency' and retry")
885
- })
886
-
887
288
  test("does not fetch the origin for an existing writable worktree", async () => {
888
289
  await runTestEffect(
889
290
  TaskService.pipe(
@@ -2424,111 +1825,6 @@ pr: null
2424
1825
  ).toBe(true)
2425
1826
  })
2426
1827
 
2427
- test("rebuilds clean drifted jj references without discarding changes", async () => {
2428
- if (!Bun.which("jj")) return
2429
- const first = new TextDecoder()
2430
- .decode(
2431
- Bun.spawnSync(["git", "-C", source, "rev-parse", "HEAD"], {
2432
- stdout: "pipe",
2433
- }).stdout,
2434
- )
2435
- .trim()
2436
- await Bun.write(join(source, "README.md"), "updated\n")
2437
- await git(["add", "README.md"], source)
2438
- await git(["-c", "commit.gpgsign=false", "commit", "-m", "update"], source)
2439
- const second = new TextDecoder()
2440
- .decode(
2441
- Bun.spawnSync(["git", "-C", source, "rev-parse", "HEAD"], {
2442
- stdout: "pipe",
2443
- }).stdout,
2444
- )
2445
- .trim()
2446
- const repository = join(root, "repos/agency")
2447
- await rm(repository, { recursive: true, force: true })
2448
- await jj(["git", "clone", "--no-colocate", source, repository])
2449
- await Bun.write(
2450
- join(root, "agency.json"),
2451
- JSON.stringify({ version: 2, vcs: "jj" }),
2452
- )
2453
- const createDriftedReview = async (id: string) => {
2454
- const task = await runTestEffect(
2455
- TaskService.pipe(
2456
- Effect.flatMap((service) =>
2457
- service.create(
2458
- {
2459
- id,
2460
- ticketUrl: null,
2461
- review: {
2462
- repo: "agency",
2463
- source: {
2464
- kind: "branch",
2465
- ref: "refs/heads/main",
2466
- },
2467
- commit: first,
2468
- refreshedAt: "2026-08-10T00:00:00.000Z",
2469
- },
2470
- },
2471
- root,
2472
- ),
2473
- ),
2474
- ),
2475
- )
2476
- const workspace = await runTestEffect(
2477
- WorktreeService.pipe(
2478
- Effect.flatMap((service) => service.materialize(id, undefined, root)),
2479
- ),
2480
- )
2481
- await Bun.write(task.path, task.content.replace(first, second))
2482
- return workspace
2483
- }
2484
-
2485
- const workspace = await createDriftedReview("drifted-review")
2486
-
2487
- await expect(
2488
- runTestEffect(
2489
- WorktreeService.pipe(
2490
- Effect.flatMap((service) =>
2491
- service.materialize("drifted-review", undefined, root),
2492
- ),
2493
- ),
2494
- ),
2495
- ).rejects.toThrow("does not match reference")
2496
- const inspection = await runTestEffect(
2497
- WorktreeService.pipe(
2498
- Effect.flatMap((service) =>
2499
- service.inspect("drifted-review", undefined, root),
2500
- ),
2501
- ),
2502
- )
2503
- expect(inspection.conflicts).toEqual([
2504
- expect.objectContaining({ kind: "reference-drift" }),
2505
- ])
2506
- await runTestEffect(
2507
- WorktreeService.pipe(
2508
- Effect.flatMap((service) =>
2509
- service.rebuild("drifted-review", undefined, root),
2510
- ),
2511
- ),
2512
- )
2513
- expect(
2514
- await Bun.file(join(workspace.reviewPath!, "README.md")).text(),
2515
- ).toBe("updated\n")
2516
-
2517
- const dirtyWorkspace = await createDriftedReview("dirty-drifted-review")
2518
- const dirtyPath = join(dirtyWorkspace.reviewPath!, "dirty.txt")
2519
- await Bun.write(dirtyPath, "preserve me\n")
2520
- await expect(
2521
- runTestEffect(
2522
- WorktreeService.pipe(
2523
- Effect.flatMap((service) =>
2524
- service.rebuild("dirty-drifted-review", undefined, root),
2525
- ),
2526
- ),
2527
- ),
2528
- ).rejects.toThrow("checkout has uncommitted changes")
2529
- expect(await Bun.file(dirtyPath).text()).toBe("preserve me\n")
2530
- })
2531
-
2532
1828
  test("restores removed worktrees when rebuild creation fails", async () => {
2533
1829
  await runTestEffect(
2534
1830
  TaskService.pipe(