@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.
- package/README.md +29 -122
- package/cli-main.ts +10 -28
- package/cli.ts +1 -17
- package/package.json +1 -2
- package/schemas/agency-graph-v1.schema.json +1 -1
- package/src/cli-parser.test.ts +5 -19
- package/src/cli-parser.ts +12 -30
- package/src/cli.test.ts +6 -6
- package/src/commands/archive.test.ts +73 -1
- package/src/commands/archive.ts +18 -7
- package/src/commands/context.test.ts +0 -67
- package/src/commands/init.test.ts +1 -2
- package/src/commands/pr.test.ts +2 -87
- package/src/commands/pr.ts +2 -79
- package/src/commands/push.test.ts +1 -2
- package/src/graph-schema.ts +1 -1
- package/src/services/ArchiveBulkService.test.ts +1 -142
- package/src/services/ArchiveService.test.ts +1 -172
- package/src/services/ArchiveService.ts +59 -27
- package/src/services/ContextService.ts +3 -47
- package/src/services/DoctorService.ts +8 -24
- package/src/services/GraphService.test.ts +1 -0
- package/src/services/GraphService.ts +2 -5
- package/src/services/PhaseService.ts +70 -191
- package/src/services/PullRequestService.test.ts +1 -45
- package/src/services/PullRequestService.ts +1 -22
- package/src/services/PushService.test.ts +29 -295
- package/src/services/PushService.ts +10 -239
- package/src/services/RepositoryService.test.ts +1 -61
- package/src/services/RepositoryService.ts +2 -13
- package/src/services/ReviewService.test.ts +1 -84
- package/src/services/ReviewService.ts +9 -48
- package/src/services/SyncService.test.ts +0 -81
- package/src/services/SyncService.ts +12 -39
- package/src/services/TaskPhaseService.test.ts +0 -80
- package/src/services/TaskService.ts +1 -9
- package/src/services/VersionControlService.test.ts +4 -117
- package/src/services/VersionControlService.ts +3 -426
- package/src/services/WorkbaseService.test.ts +0 -20
- package/src/services/WorkbaseService.ts +1 -18
- package/src/services/WorktreeService.test.ts +1 -705
- package/src/services/WorktreeService.ts +395 -1604
- package/src/services/push-validation.ts +0 -7
- package/src/test-utils.ts +0 -4
- package/src/workbase/AGENTS.md +4 -5
- package/src/workbase/checkout-command.test.ts +1 -1
- package/src/workbase/checkout-command.ts +1 -1
- package/src/workbase/delivery-command.test.ts +4 -35
- package/src/workbase/delivery-command.ts +2 -15
- package/src/workbase/schemas.test.ts +0 -19
- package/src/workbase/schemas.ts +1 -2
- package/src/commands/vcs.test.ts +0 -75
- package/src/commands/vcs.ts +0 -72
- package/src/services/VcsMigrationService.test.ts +0 -348
- package/src/services/VcsMigrationService.ts +0 -857
- package/src/vcs-status-fast.ts +0 -310
- package/src/workbase/version-control.ts +0 -5
- package/src/workbase/workspace-command.test.ts +0 -70
- 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 {
|
|
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(
|