@indigoai-us/hq-cloud 6.15.31 → 6.15.33

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 (131) hide show
  1. package/dist/bandwidth.d.ts +128 -0
  2. package/dist/bandwidth.d.ts.map +1 -0
  3. package/dist/bandwidth.js +254 -0
  4. package/dist/bandwidth.js.map +1 -0
  5. package/dist/bandwidth.test.d.ts +2 -0
  6. package/dist/bandwidth.test.d.ts.map +1 -0
  7. package/dist/bandwidth.test.js +243 -0
  8. package/dist/bandwidth.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-company.js +13 -24
  11. package/dist/bin/sync-runner-company.js.map +1 -1
  12. package/dist/bin/sync-runner-company.test.js +71 -0
  13. package/dist/bin/sync-runner-company.test.js.map +1 -1
  14. package/dist/bin/sync-runner-watch-loop.d.ts +10 -0
  15. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  16. package/dist/bin/sync-runner-watch-loop.js +181 -16
  17. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  18. package/dist/bin/sync-runner-watch-routes.d.ts +16 -1
  19. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  20. package/dist/bin/sync-runner-watch-routes.js +40 -2
  21. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  22. package/dist/bin/sync-runner-watch-routes.test.js +53 -1
  23. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  24. package/dist/bin/sync-runner.d.ts +15 -3
  25. package/dist/bin/sync-runner.d.ts.map +1 -1
  26. package/dist/bin/sync-runner.js +66 -13
  27. package/dist/bin/sync-runner.js.map +1 -1
  28. package/dist/bin/sync-runner.test.js +433 -13
  29. package/dist/bin/sync-runner.test.js.map +1 -1
  30. package/dist/cli/conflict.d.ts +25 -2
  31. package/dist/cli/conflict.d.ts.map +1 -1
  32. package/dist/cli/conflict.js +29 -4
  33. package/dist/cli/conflict.js.map +1 -1
  34. package/dist/cli/share.d.ts +40 -4
  35. package/dist/cli/share.d.ts.map +1 -1
  36. package/dist/cli/share.js +490 -90
  37. package/dist/cli/share.js.map +1 -1
  38. package/dist/cli/share.test.js +881 -150
  39. package/dist/cli/share.test.js.map +1 -1
  40. package/dist/cli/sync-scope.test.js +147 -18
  41. package/dist/cli/sync-scope.test.js.map +1 -1
  42. package/dist/cli/sync.d.ts +20 -0
  43. package/dist/cli/sync.d.ts.map +1 -1
  44. package/dist/cli/sync.js +261 -29
  45. package/dist/cli/sync.js.map +1 -1
  46. package/dist/cli/sync.test.js +519 -90
  47. package/dist/cli/sync.test.js.map +1 -1
  48. package/dist/journal-delta-writes.test.d.ts +2 -0
  49. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  50. package/dist/journal-delta-writes.test.js +140 -0
  51. package/dist/journal-delta-writes.test.js.map +1 -0
  52. package/dist/journal.d.ts +1 -0
  53. package/dist/journal.d.ts.map +1 -1
  54. package/dist/journal.js +155 -25
  55. package/dist/journal.js.map +1 -1
  56. package/dist/journal.test.js +29 -11
  57. package/dist/journal.test.js.map +1 -1
  58. package/dist/lib/describe-error.d.ts +0 -19
  59. package/dist/lib/describe-error.d.ts.map +1 -1
  60. package/dist/lib/describe-error.js +27 -3
  61. package/dist/lib/describe-error.js.map +1 -1
  62. package/dist/lib/describe-error.test.js +23 -0
  63. package/dist/lib/describe-error.test.js.map +1 -1
  64. package/dist/object-io.d.ts +34 -13
  65. package/dist/object-io.d.ts.map +1 -1
  66. package/dist/object-io.js +154 -29
  67. package/dist/object-io.js.map +1 -1
  68. package/dist/operation-lock.d.ts +12 -0
  69. package/dist/operation-lock.d.ts.map +1 -1
  70. package/dist/operation-lock.js +103 -21
  71. package/dist/operation-lock.js.map +1 -1
  72. package/dist/operation-lock.test.js +90 -0
  73. package/dist/operation-lock.test.js.map +1 -1
  74. package/dist/outcome-telemetry.d.ts +7 -1
  75. package/dist/outcome-telemetry.d.ts.map +1 -1
  76. package/dist/outcome-telemetry.js +15 -9
  77. package/dist/outcome-telemetry.js.map +1 -1
  78. package/dist/outcome-telemetry.test.js +15 -0
  79. package/dist/outcome-telemetry.test.js.map +1 -1
  80. package/dist/outposts/client.d.ts +2 -2
  81. package/dist/outposts/client.d.ts.map +1 -1
  82. package/dist/outposts/client.js +4 -3
  83. package/dist/outposts/client.js.map +1 -1
  84. package/dist/outposts/command.d.ts.map +1 -1
  85. package/dist/outposts/command.js +9 -6
  86. package/dist/outposts/command.js.map +1 -1
  87. package/dist/outposts/command.test.js +59 -2
  88. package/dist/outposts/command.test.js.map +1 -1
  89. package/dist/outposts/remote-command.d.ts +8 -8
  90. package/dist/outposts/remote-command.d.ts.map +1 -1
  91. package/dist/outposts/remote-command.js +10 -8
  92. package/dist/outposts/remote-command.js.map +1 -1
  93. package/dist/prefix-coalesce.d.ts +14 -0
  94. package/dist/prefix-coalesce.d.ts.map +1 -1
  95. package/dist/prefix-coalesce.js +29 -0
  96. package/dist/prefix-coalesce.js.map +1 -1
  97. package/dist/prefix-coalesce.test.js +22 -1
  98. package/dist/prefix-coalesce.test.js.map +1 -1
  99. package/dist/skill-telemetry.d.ts +6 -1
  100. package/dist/skill-telemetry.d.ts.map +1 -1
  101. package/dist/skill-telemetry.js +14 -8
  102. package/dist/skill-telemetry.js.map +1 -1
  103. package/dist/skill-telemetry.test.js +43 -0
  104. package/dist/skill-telemetry.test.js.map +1 -1
  105. package/dist/sts-credential-refresh.test.js +87 -2
  106. package/dist/sts-credential-refresh.test.js.map +1 -1
  107. package/dist/sync/state-store.d.ts.map +1 -1
  108. package/dist/sync/state-store.js +20 -1
  109. package/dist/sync/state-store.js.map +1 -1
  110. package/dist/sync/state-store.test.js +64 -0
  111. package/dist/sync/state-store.test.js.map +1 -1
  112. package/dist/sync-core.d.ts.map +1 -1
  113. package/dist/sync-core.js +7 -2
  114. package/dist/sync-core.js.map +1 -1
  115. package/dist/sync-core.test.js +36 -0
  116. package/dist/sync-core.test.js.map +1 -1
  117. package/dist/telemetry.d.ts +30 -6
  118. package/dist/telemetry.d.ts.map +1 -1
  119. package/dist/telemetry.js +331 -135
  120. package/dist/telemetry.js.map +1 -1
  121. package/dist/telemetry.test.js +403 -0
  122. package/dist/telemetry.test.js.map +1 -1
  123. package/dist/types.d.ts +26 -3
  124. package/dist/types.d.ts.map +1 -1
  125. package/dist/watcher.d.ts +76 -0
  126. package/dist/watcher.d.ts.map +1 -1
  127. package/dist/watcher.js +105 -3
  128. package/dist/watcher.js.map +1 -1
  129. package/dist/watcher.test.js +165 -1
  130. package/dist/watcher.test.js.map +1 -1
  131. package/package.json +1 -1
@@ -53,9 +53,49 @@ vi.mock("readline", () => ({
53
53
  }));
54
54
  import * as readline from "readline";
55
55
  import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
56
- import { deleteRemoteFile, downloadFile, headRemoteFile, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError } from "../s3.js";
56
+ import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
57
57
  import { VaultAuthError } from "../vault-client.js";
58
- import { hashFile } from "../journal.js";
58
+ import { hashFile, readJournal, writeJournal } from "../journal.js";
59
+ /**
60
+ * Read a journal the way production does.
61
+ *
62
+ * These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
63
+ * when that file held a full copy of the journal. It is now a small locator —
64
+ * the authority is the v3 state store — so the assertions below go through
65
+ * `readJournal`, which is what the sync engine itself uses. The slug is
66
+ * recovered from the path so each call site keeps naming the journal it means.
67
+ */
68
+ /**
69
+ * Seed a journal the way production does.
70
+ *
71
+ * These fixtures used to write `sync-journal.<slug>.json` by hand, which worked
72
+ * only while that file WAS the journal. It is a locator now, so a hand-written
73
+ * copy is simply ignored and the test silently runs against an empty journal.
74
+ * Going through `writeJournal` puts the state where the engine actually reads
75
+ * it, and migrates v1 fixtures on the way in exactly as a real upgrade would.
76
+ */
77
+ function seedJournalAt(journalPath, journal) {
78
+ const slug = path
79
+ .basename(journalPath)
80
+ .replace(/^sync-journal\./, "")
81
+ .replace(/\.json$/, "");
82
+ writeJournal(slug, journal);
83
+ }
84
+ function journalAt(journalPath) {
85
+ const slug = path
86
+ .basename(journalPath)
87
+ .replace(/^sync-journal\./, "")
88
+ .replace(/\.json$/, "");
89
+ return readJournal(slug);
90
+ }
91
+ function sidecarIn(dir) {
92
+ const found = fs.readdirSync(dir).filter((name) => name.includes(".conflict-"));
93
+ expect(found).toHaveLength(1);
94
+ return path.join(dir, found[0]);
95
+ }
96
+ function conflictIndexAt(hqRoot) {
97
+ return JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq-conflicts", "index.json"), "utf-8"));
98
+ }
59
99
  const mockConfig = {
60
100
  apiUrl: "https://vault-api.test",
61
101
  authToken: "test-jwt-token",
@@ -369,6 +409,74 @@ describe("share", () => {
369
409
  // Remote key must be company-relative, not hqRoot-relative
370
410
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
371
411
  });
412
+ it("replans an upload from the bytes present when its action starts", async () => {
413
+ const companyRoot = path.join(tmpDir, "companies", "acme");
414
+ fs.mkdirSync(companyRoot, { recursive: true });
415
+ const testFile = path.join(companyRoot, "changing.md");
416
+ fs.writeFileSync(testFile, "body seen by the whole-run preview");
417
+ let uploadedBody = "";
418
+ vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath) => {
419
+ uploadedBody = fs.readFileSync(localPath, "utf-8");
420
+ return { etag: '"fresh-upload-etag"' };
421
+ });
422
+ let changedAfterPlan = false;
423
+ let executionHeartbeatCalls = 0;
424
+ const result = await share({
425
+ paths: [testFile],
426
+ company: "acme",
427
+ vaultConfig: mockConfig,
428
+ hqRoot: tmpDir,
429
+ onEvent: (event) => {
430
+ if (event.type === "plan" && !changedAfterPlan) {
431
+ changedAfterPlan = true;
432
+ fs.writeFileSync(testFile, "body present when upload executes");
433
+ }
434
+ },
435
+ onHeartbeat: () => {
436
+ if (changedAfterPlan)
437
+ executionHeartbeatCalls++;
438
+ },
439
+ });
440
+ expect(changedAfterPlan).toBe(true);
441
+ expect(executionHeartbeatCalls).toBeGreaterThan(0);
442
+ expect(result.filesUploaded).toBe(1);
443
+ expect(uploadedBody).toBe("body present when upload executes");
444
+ expect(journalAt(path.join(stateDir, "sync-journal.acme.json")).files["changing.md"])
445
+ .toMatchObject({
446
+ hash: hashFile(testFile),
447
+ remoteEtag: "fresh-upload-etag",
448
+ direction: "up",
449
+ });
450
+ });
451
+ it("restarts the per-file decision when local bytes change during remote HEAD", async () => {
452
+ const companyRoot = path.join(tmpDir, "companies", "acme");
453
+ fs.mkdirSync(companyRoot, { recursive: true });
454
+ const testFile = path.join(companyRoot, "head-race.md");
455
+ fs.writeFileSync(testFile, "body before HEAD");
456
+ vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
457
+ fs.writeFileSync(testFile, "body written while HEAD was in flight");
458
+ return null;
459
+ });
460
+ let uploadedBody = "";
461
+ vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath) => {
462
+ uploadedBody = fs.readFileSync(localPath, "utf-8");
463
+ return { etag: '"head-race-etag"' };
464
+ });
465
+ const result = await share({
466
+ paths: [testFile],
467
+ company: "acme",
468
+ vaultConfig: mockConfig,
469
+ hqRoot: tmpDir,
470
+ });
471
+ expect(result.filesUploaded).toBe(1);
472
+ expect(uploadedBody).toBe("body written while HEAD was in flight");
473
+ expect(journalAt(path.join(stateDir, "sync-journal.acme.json")).files["head-race.md"])
474
+ .toMatchObject({
475
+ hash: hashFile(testFile),
476
+ remoteEtag: "head-race-etag",
477
+ direction: "up",
478
+ });
479
+ });
372
480
  it("propagates a per-file vault 401 to the runner auth boundary", async () => {
373
481
  const companyRoot = path.join(tmpDir, "companies", "acme");
374
482
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -382,6 +490,42 @@ describe("share", () => {
382
490
  hqRoot: tmpDir,
383
491
  })).rejects.toBeInstanceOf(VaultAuthError);
384
492
  });
493
+ it("surfaces a safe nested upload-worker cause without replacing the first error", async () => {
494
+ const companyRoot = path.join(tmpDir, "companies", "acme");
495
+ fs.mkdirSync(companyRoot, { recursive: true });
496
+ const firstFile = path.join(companyRoot, "first.md");
497
+ const secondFile = path.join(companyRoot, "second.md");
498
+ fs.writeFileSync(firstFile, "first");
499
+ fs.writeFileSync(secondFile, "second");
500
+ const networkCause = Object.assign(new Error("connect ECONNREFUSED sync.example.test Authorization: Bearer upload-secret"), { code: "ECONNREFUSED", syscall: "connect", hostname: "sync.example.test" });
501
+ const first = new VaultAuthError("UnknownError");
502
+ first.name = "UnknownError";
503
+ first.cause = networkCause;
504
+ const second = new VaultAuthError("second worker failed");
505
+ vi.mocked(uploadFile)
506
+ .mockRejectedValueOnce(first)
507
+ .mockRejectedValueOnce(second);
508
+ let thrown;
509
+ try {
510
+ await share({
511
+ paths: [firstFile, secondFile],
512
+ company: "acme",
513
+ vaultConfig: mockConfig,
514
+ hqRoot: tmpDir,
515
+ });
516
+ }
517
+ catch (err) {
518
+ thrown = err;
519
+ }
520
+ expect(thrown).toBe(first);
521
+ expect(first.cause).toBe(networkCause);
522
+ expect(first.message).toContain("cause=ECONNREFUSED");
523
+ expect(first.message).toContain("syscall=connect");
524
+ expect(first.message).toContain("host=sync.example.test");
525
+ expect(first.message).toContain("(and 1 more upload-worker errors)");
526
+ expect(first.message).not.toContain("upload-secret");
527
+ expect(first.message.length).toBeLessThanOrEqual(1_024);
528
+ });
385
529
  it("respects ignore rules", async () => {
386
530
  const companyRoot = path.join(tmpDir, "companies", "acme");
387
531
  fs.mkdirSync(path.join(companyRoot, ".git"), { recursive: true });
@@ -528,7 +672,7 @@ describe("share", () => {
528
672
  const { hashFile } = await import("../journal.js");
529
673
  const hash = hashFile(testFile);
530
674
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
531
- fs.writeFileSync(journalPath, JSON.stringify({
675
+ seedJournalAt(journalPath, ({
532
676
  version: "1",
533
677
  lastSync: new Date().toISOString(),
534
678
  files: {
@@ -559,7 +703,7 @@ describe("share", () => {
559
703
  // Journal has a stale hash for this path — simulating "local has been
560
704
  // edited since the last push".
561
705
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
562
- fs.writeFileSync(journalPath, JSON.stringify({
706
+ seedJournalAt(journalPath, ({
563
707
  version: "1",
564
708
  lastSync: new Date().toISOString(),
565
709
  files: {
@@ -581,6 +725,226 @@ describe("share", () => {
581
725
  expect(result.filesUploaded).toBe(1);
582
726
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md", undefined, expect.anything());
583
727
  });
728
+ // ── Stat fast path for change detection ─────────────────────────────────
729
+ // Push used to hash EVERY regular file every pass before deciding whether to
730
+ // skip it — half a million reads on a real vault — because stat metadata was
731
+ // considered untrustworthy: a tool can rewrite a file to the same length and
732
+ // restore its mtime, and a size+mtime check would then skip that edit
733
+ // forever.
734
+ //
735
+ // These tests pin the narrower claim the fast path actually rests on: size,
736
+ // mtime AND ctime together. ctime is the inode-change stamp, and userspace
737
+ // cannot restore it — `cp -p`, `touch -r` and `rsync --times` all reset mtime
738
+ // while ctime moves to now. So the same-length-plus-restored-mtime rewrite
739
+ // that the old comment warned about is still caught, without reading a byte.
740
+ //
741
+ // Differential proof: drop ctime from the comparison and the "restored mtime"
742
+ // test below fails — the edit is skipped and never uploaded.
743
+ describe("stat fast path for change detection", () => {
744
+ const CONTENT_A = "aaaaaaaaaa"; // 10 bytes
745
+ const CONTENT_B = "bbbbbbbbbb"; // 10 bytes — same length, different bytes
746
+ /** Write `body` at `key` under the acme company root; return its path. */
747
+ function writeFile(key, body) {
748
+ const companyRoot = path.join(tmpDir, "companies", "acme");
749
+ fs.mkdirSync(companyRoot, { recursive: true });
750
+ const abs = path.join(companyRoot, key);
751
+ fs.writeFileSync(abs, body);
752
+ return abs;
753
+ }
754
+ function seed(key, entry) {
755
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
756
+ version: "2",
757
+ lastSync: new Date().toISOString(),
758
+ files: { [key]: { syncedAt: new Date().toISOString(), direction: "up", ...entry } },
759
+ pulls: [],
760
+ });
761
+ }
762
+ function push(abs, onConflict) {
763
+ return share({
764
+ paths: [abs],
765
+ company: "acme",
766
+ vaultConfig: mockConfig,
767
+ hqRoot: tmpDir,
768
+ skipUnchanged: true,
769
+ ...(onConflict ? { onConflict } : {}),
770
+ });
771
+ }
772
+ it("skips a file whose size, mtime and ctime all match — without reading it", async () => {
773
+ const abs = writeFile("untouched.md", CONTENT_A);
774
+ const stat = fs.lstatSync(abs);
775
+ // The recorded hash is deliberately WRONG. Hashing the file would find a
776
+ // mismatch and upload, so a skip here can only mean the bytes were never
777
+ // read — which is the whole point of the fast path.
778
+ seed("untouched.md", {
779
+ hash: "deliberately-not-the-hash-of-this-file",
780
+ size: stat.size,
781
+ mtimeMs: stat.mtimeMs,
782
+ ctimeMs: stat.ctimeMs,
783
+ });
784
+ const result = await push(abs);
785
+ expect(result.filesSkipped).toBe(1);
786
+ expect(result.filesUploaded).toBe(0);
787
+ expect(uploadFile).not.toHaveBeenCalled();
788
+ });
789
+ it("re-reads when the size differs", async () => {
790
+ const abs = writeFile("resized.md", CONTENT_A + "extra");
791
+ const stat = fs.lstatSync(abs);
792
+ seed("resized.md", {
793
+ hash: hashFile(abs) + "-stale",
794
+ size: CONTENT_A.length, // shorter than what is on disk now
795
+ mtimeMs: stat.mtimeMs,
796
+ ctimeMs: stat.ctimeMs,
797
+ });
798
+ const result = await push(abs);
799
+ expect(result.filesUploaded).toBe(1);
800
+ expect(uploadFile).toHaveBeenCalled();
801
+ });
802
+ it("re-reads when the mtime differs", async () => {
803
+ const abs = writeFile("retimed.md", CONTENT_B);
804
+ const stat = fs.lstatSync(abs);
805
+ seed("retimed.md", {
806
+ hash: "hash-of-the-previous-body",
807
+ size: stat.size, // same length
808
+ mtimeMs: stat.mtimeMs - 60_000, // journal remembers an older edit time
809
+ ctimeMs: stat.ctimeMs,
810
+ });
811
+ const result = await push(abs);
812
+ expect(result.filesUploaded).toBe(1);
813
+ expect(uploadFile).toHaveBeenCalled();
814
+ });
815
+ it("re-reads a same-length rewrite whose mtime was restored — ctime still moves", async () => {
816
+ // The exact case the old hash-everything comment existed to defend
817
+ // against, as produced by `cp -p`, `touch -r` or `rsync --times`.
818
+ const abs = writeFile("mtime-restored.md", CONTENT_A);
819
+ const before = fs.lstatSync(abs);
820
+ await new Promise((resolve) => setTimeout(resolve, 5));
821
+ fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
822
+ fs.utimesSync(abs, before.atime, before.mtime); // put the old mtime back
823
+ const after = fs.lstatSync(abs);
824
+ // Preconditions. The disguise worked on size, and as closely on mtime as
825
+ // the OS allows — `utimes` takes whole milliseconds, so a restored stamp
826
+ // lands within 1ms of the original rather than exactly on it. It failed
827
+ // on ctime, which is the only signal left.
828
+ expect(after.size).toBe(before.size);
829
+ expect(Math.abs(after.mtimeMs - before.mtimeMs)).toBeLessThan(1);
830
+ expect(after.ctimeMs).not.toBe(before.ctimeMs);
831
+ // Seed the mtime that is actually on disk, so mtime matches EXACTLY and
832
+ // cannot be what triggers the re-read. Only the stale ctime can.
833
+ seed("mtime-restored.md", {
834
+ hash: "hash-of-content-a",
835
+ size: before.size,
836
+ mtimeMs: after.mtimeMs,
837
+ ctimeMs: before.ctimeMs,
838
+ });
839
+ const result = await push(abs);
840
+ expect(result.filesUploaded).toBe(1);
841
+ expect(uploadFile).toHaveBeenCalled();
842
+ });
843
+ it("falls back to reading when the journal predates stat tracking, then stamps it", async () => {
844
+ // Entries written before ctime was recorded carry no stat metadata. They
845
+ // must not be skipped on faith; they are read once and stamped so the
846
+ // next pass takes the fast path.
847
+ const abs = writeFile("legacy.md", CONTENT_A);
848
+ seed("legacy.md", { hash: hashFile(abs), size: CONTENT_A.length });
849
+ const result = await push(abs);
850
+ expect(result.filesSkipped).toBe(1);
851
+ expect(uploadFile).not.toHaveBeenCalled();
852
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["legacy.md"];
853
+ const stat = fs.lstatSync(abs);
854
+ expect(entry.mtimeMs).toBe(stat.mtimeMs);
855
+ expect(entry.ctimeMs).toBe(stat.ctimeMs);
856
+ });
857
+ it("never lets the fast path skip a localDiverges entry", async () => {
858
+ // A pull-held divergent entry stores the LOCAL hash and local stat, so
859
+ // every field matches by construction. Skipping on that asserts "local
860
+ // matches remote", which is exactly what the flag exists to deny.
861
+ const abs = writeFile("divergent.md", CONTENT_A);
862
+ const stat = fs.lstatSync(abs);
863
+ seed("divergent.md", {
864
+ hash: hashFile(abs),
865
+ size: stat.size,
866
+ mtimeMs: stat.mtimeMs,
867
+ ctimeMs: stat.ctimeMs,
868
+ localDiverges: true,
869
+ });
870
+ // `keep` so the divergence resolves without an interactive prompt. Under
871
+ // `keep` the file is deliberately NOT uploaded, so the proof that the
872
+ // fast path did not swallow it is that it reached Stage 2 at all: a
873
+ // fast-path skip returns before any remote HEAD is issued.
874
+ await push(abs, "keep");
875
+ expect(headRemoteFile).toHaveBeenCalled();
876
+ });
877
+ it("stamps size, mtime and ctime on upload so the next pass can skip", async () => {
878
+ const abs = writeFile("fresh.md", CONTENT_A);
879
+ const result = await push(abs);
880
+ expect(result.filesUploaded).toBe(1);
881
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["fresh.md"];
882
+ const stat = fs.lstatSync(abs);
883
+ expect(entry.size).toBe(stat.size);
884
+ expect(entry.mtimeMs).toBe(stat.mtimeMs);
885
+ expect(entry.ctimeMs).toBe(stat.ctimeMs);
886
+ });
887
+ it("replans and stamps the new body when the file changes during remote HEAD", async () => {
888
+ // The pass-level preview hashes CONTENT_A, then the asynchronous remote
889
+ // probe changes the file to CONTENT_B. The per-file execution seam must
890
+ // restart from CONTENT_B, upload it now, and journal that exact body — no
891
+ // stale hash and no redundant upload on the next pass.
892
+ const abs = writeFile("edited-mid-flight.md", CONTENT_A);
893
+ let mutated = false;
894
+ vi.mocked(headRemoteFile).mockImplementation(async () => {
895
+ if (!mutated) {
896
+ mutated = true;
897
+ fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
898
+ }
899
+ return null;
900
+ });
901
+ await push(abs);
902
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["edited-mid-flight.md"];
903
+ const currentStat = fs.lstatSync(abs);
904
+ expect(entry.hash).toBe(hashFile(abs));
905
+ expect(entry.mtimeMs).toBe(currentStat.mtimeMs);
906
+ expect(entry.ctimeMs).toBe(currentStat.ctimeMs);
907
+ // The current bytes were already shipped and journaled in this pass.
908
+ vi.mocked(uploadFile).mockClear();
909
+ const second = await push(abs);
910
+ expect(second.filesUploaded).toBe(0);
911
+ expect(uploadFile).not.toHaveBeenCalled();
912
+ });
913
+ it("distrusts whole-second timestamps as a coarse-filesystem signal", () => {
914
+ // Codex P2. FAT/exFAT, some FUSE mounts and cached network filesystems
915
+ // report timestamps rounded to whole seconds, so a same-size rewrite
916
+ // inside one tick can leave both mtime and ctime equal to the journal's
917
+ // and be skipped indefinitely. Every filesystem HQ can trust here (APFS,
918
+ // ext4, NTFS) records sub-second precision, so a pair that is exactly on
919
+ // the second is treated as untrustworthy and sent back to hashing.
920
+ const abs = writeFile("coarse.md", CONTENT_A);
921
+ const coarse = { size: CONTENT_A.length, mtimeMs: 1_700_000_000_000, ctimeMs: 1_700_000_000_000 };
922
+ const spy = vi.spyOn(fs, "lstatSync").mockReturnValue({
923
+ isFile: () => true,
924
+ ...coarse,
925
+ });
926
+ try {
927
+ expect(shareTesting.statMatchesJournal(abs, {
928
+ hash: "h",
929
+ direction: "up",
930
+ syncedAt: new Date().toISOString(),
931
+ ...coarse,
932
+ })).toBe(false);
933
+ // Same values, one of them sub-second: a real filesystem, so trusted.
934
+ const fine = { ...coarse, ctimeMs: coarse.ctimeMs + 0.5 };
935
+ spy.mockReturnValue({ isFile: () => true, ...fine });
936
+ expect(shareTesting.statMatchesJournal(abs, {
937
+ hash: "h",
938
+ direction: "up",
939
+ syncedAt: new Date().toISOString(),
940
+ ...fine,
941
+ })).toBe(true);
942
+ }
943
+ finally {
944
+ spy.mockRestore();
945
+ }
946
+ });
947
+ });
584
948
  // ── Push-side FILE_TOMBSTONE consult (delete-resync, 3B) ────────────────────
585
949
  // An authoritative delete (`hq files delete`) writes a FILE_TOMBSTONE and
586
950
  // removes the S3 object. Without a push-side consult, a behind peer that still
@@ -593,7 +957,7 @@ describe("share", () => {
593
957
  // share.ts makes the first test fail (uploadFile IS called → resurrection).
594
958
  describe("push-side tombstone consult", () => {
595
959
  function seedJournal(key, hash, size) {
596
- fs.writeFileSync(path.join(stateDir, "sync-journal.acme.json"), JSON.stringify({
960
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), ({
597
961
  version: "1",
598
962
  lastSync: new Date().toISOString(),
599
963
  files: {
@@ -630,6 +994,8 @@ describe("share", () => {
630
994
  });
631
995
  // The resurrection is blocked at the source: no upload for the tombstoned key.
632
996
  expect(uploadFile).not.toHaveBeenCalled();
997
+ expect(primeUploads).toHaveBeenCalledWith(expect.anything(), []);
998
+ expect(primeObjectTransport).toHaveBeenCalledWith(expect.anything(), "get", []);
633
999
  expect(result.filesUploaded).toBe(0);
634
1000
  expect(result.filesSuppressedByTombstone).toBe(1);
635
1001
  expect(events.some((e) => e.type === "upload-suppressed-tombstone" && e.path === "docs/shared.md")).toBe(true);
@@ -736,8 +1102,12 @@ describe("share", () => {
736
1102
  etag: '"remote-new-etag"',
737
1103
  size: 99,
738
1104
  });
1105
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1106
+ fs.writeFileSync(dest, "remote edit");
1107
+ return undefined;
1108
+ });
739
1109
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
740
- fs.writeFileSync(journalPath, JSON.stringify({
1110
+ seedJournalAt(journalPath, ({
741
1111
  version: "1",
742
1112
  lastSync: new Date().toISOString(),
743
1113
  files: {
@@ -768,6 +1138,24 @@ describe("share", () => {
768
1138
  direction: "push",
769
1139
  resolution: "keep",
770
1140
  });
1141
+ // `keep` is cloud-wins on the push leg too: remote takes the working
1142
+ // name, while the displaced local body remains recoverable beside it.
1143
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("remote edit");
1144
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local edit");
1145
+ const journal = journalAt(journalPath);
1146
+ expect(journal.files["drifted.md"]).toMatchObject({
1147
+ hash: hashFile(testFile),
1148
+ remoteEtag: "remote-new-etag",
1149
+ direction: "down",
1150
+ });
1151
+ const index = conflictIndexAt(tmpDir);
1152
+ expect(index.conflicts).toHaveLength(1);
1153
+ expect(index.conflicts[0]).toMatchObject({
1154
+ originalPath: "companies/acme/drifted.md",
1155
+ preserved: "local",
1156
+ });
1157
+ expect(fs.readFileSync(path.join(tmpDir, index.conflicts[0].conflictPath), "utf-8"))
1158
+ .toBe("local edit");
771
1159
  });
772
1160
  it("cloud-authoritative: never pushes a stale local ontology/.last-run over the server value (one-sided-change clobber guard)", async () => {
773
1161
  // Regression for the gardener watermark clobber. A second HQ root sharing this
@@ -789,7 +1177,7 @@ describe("share", () => {
789
1177
  // assert it is never consulted below — the skip is cheap, and a leftover
790
1178
  // mockResolvedValueOnce would otherwise leak into a later test's HEAD queue.
791
1179
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
792
- fs.writeFileSync(journalPath, JSON.stringify({
1180
+ seedJournalAt(journalPath, ({
793
1181
  version: "1",
794
1182
  lastSync: new Date().toISOString(),
795
1183
  files: {
@@ -835,9 +1223,9 @@ describe("share", () => {
835
1223
  //
836
1224
  // Fix: even on first-time upload, if HEAD shows remote exists and
837
1225
  // its content (etag) differs from what we'd put, emit a conflict.
838
- // Under keep, the existing pull-side mirror routine downloads the
839
- // remote bytes to \`<orig>.conflict-<ts>-<short>\` so both versions
840
- // survive on disk; under abort, the company aborts before PUT.
1226
+ // Under keep, cloud takes the original name and local moves to
1227
+ // \`<orig>.conflict-<ts>-<short>\` so both versions survive on disk;
1228
+ // under abort, the company aborts before PUT.
841
1229
  const companyRoot = path.join(tmpDir, "companies", "acme");
842
1230
  fs.mkdirSync(companyRoot, { recursive: true });
843
1231
  const testFile = path.join(companyRoot, "v7b-fresh.txt");
@@ -849,6 +1237,10 @@ describe("share", () => {
849
1237
  etag: '"remote-version-etag"',
850
1238
  size: 40,
851
1239
  });
1240
+ vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
1241
+ fs.writeFileSync(dest, "peer remote content");
1242
+ return undefined;
1243
+ });
852
1244
  // No journal file written — first-time upload contract.
853
1245
  const events = [];
854
1246
  const result = await share({
@@ -860,14 +1252,19 @@ describe("share", () => {
860
1252
  onEvent: (e) => events.push(e),
861
1253
  });
862
1254
  expect(result.conflictPaths).toEqual(["v7b-fresh.txt"]);
863
- // Under keep, local stays as-is (no upload) and the mirror records
864
- // the remote bytes so neither version is lost.
1255
+ // Under keep, no upload occurs: the remote bytes take the working name
1256
+ // and the local body is preserved beside them.
865
1257
  expect(result.filesUploaded).toBe(0);
866
1258
  const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
867
1259
  expect(conflicts).toHaveLength(1);
868
1260
  expect(conflicts[0].path).toBe("v7b-fresh.txt");
869
- // Local file unmodified — it must still contain the local content.
870
- expect(fs.readFileSync(testFile, "utf-8")).toBe("macos-fresh-collision-content");
1261
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote content");
1262
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("macos-fresh-collision-content");
1263
+ const index = conflictIndexAt(tmpDir);
1264
+ expect(index.conflicts[0]).toMatchObject({
1265
+ originalPath: "companies/acme/v7b-fresh.txt",
1266
+ preserved: "local",
1267
+ });
871
1268
  });
872
1269
  it("first-time-upload-with-cloud-collision: aborts company under --on-conflict abort (Bug #7)", async () => {
873
1270
  // Symmetric to the keep case: when --on-conflict abort is passed, a
@@ -895,11 +1292,11 @@ describe("share", () => {
895
1292
  expect(result.filesUploaded).toBe(0);
896
1293
  });
897
1294
  it("HQ-DESKTOP-54: a privilege refusal while writing the push conflict mirror is a skip, not an error", async () => {
898
- // The keep-mirror also materializes the remote object (to a .conflict-*
899
- // file). On an unprivileged Windows host that throws the typed privilege
1295
+ // Push keep also materializes the remote object (at the original name).
1296
+ // On an unprivileged Windows host that throws the typed privilege
900
1297
  // error; it must be downgraded to the deliberate skip so the push stays
901
1298
  // complete and the runner exits 0 — never type:error. The conflict itself
902
- // is still recorded and the local file is untouched.
1299
+ // is still surfaced and the local rename is rolled back.
903
1300
  const companyRoot = path.join(tmpDir, "companies", "acme");
904
1301
  fs.mkdirSync(companyRoot, { recursive: true });
905
1302
  const testFile = path.join(companyRoot, "v7b-priv.txt");
@@ -910,7 +1307,7 @@ describe("share", () => {
910
1307
  size: 40,
911
1308
  });
912
1309
  // On a real unprivileged host BOTH the fresh-collision reconciled probe and
913
- // the keep-mirror materialize the remote symlink, so both downloadFile calls
1310
+ // the keep adoption materialize the remote symlink, so both downloadFile calls
914
1311
  // refuse. The probe's catch returns "differs" (conflict) without erroring;
915
1312
  // the mirror must downgrade the refusal to the deliberate skip.
916
1313
  vi.mocked(downloadFile)
@@ -931,7 +1328,7 @@ describe("share", () => {
931
1328
  target: "../target.md",
932
1329
  }));
933
1330
  expect(events.some((e) => e.type === "error")).toBe(false);
934
- // The conflict is still recorded; the local file is preserved.
1331
+ // The conflict is still surfaced; the failed adoption restores local.
935
1332
  expect(result.conflictPaths).toEqual(["v7b-priv.txt"]);
936
1333
  expect(fs.readFileSync(testFile, "utf-8")).toBe("local-collision-content");
937
1334
  });
@@ -969,7 +1366,7 @@ describe("share", () => {
969
1366
  direction: "push",
970
1367
  });
971
1368
  expect(fs.readdirSync(companyRoot).filter((name) => name.includes(".conflict-"))).toEqual([]);
972
- const journal = JSON.parse(fs.readFileSync(path.join(stateDir, "sync-journal.acme.json"), "utf-8"));
1369
+ const journal = readJournal("acme");
973
1370
  expect(journal.files["kms-scaffold.md"]).toMatchObject({
974
1371
  direction: "up",
975
1372
  remoteEtag: "288bfa91684ae21f9ae27c061254d412",
@@ -1043,7 +1440,7 @@ describe("share", () => {
1043
1440
  etag: '"0123456789abcdef0123456789abcdef-3"',
1044
1441
  size: 32,
1045
1442
  });
1046
- // The probe (and the subsequent keep-mirror download) return DIFFERENT
1443
+ // The probe (and the subsequent keep adoption download) return DIFFERENT
1047
1444
  // remote bytes.
1048
1445
  vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
1049
1446
  fs.writeFileSync(dest, "a-peers-different-version");
@@ -1062,8 +1459,8 @@ describe("share", () => {
1062
1459
  expect(result.filesUploaded).toBe(0);
1063
1460
  const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
1064
1461
  expect(conflicts).toHaveLength(1);
1065
- // Local file untouched under keep.
1066
- expect(fs.readFileSync(testFile, "utf-8")).toBe("my-local-version");
1462
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("a-peers-different-version");
1463
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my-local-version");
1067
1464
  });
1068
1465
  describe("conditional-write fence (If-Match / If-None-Match / 412)", () => {
1069
1466
  const fence412 = () => Object.assign(new Error("Precondition failed"), {
@@ -1085,7 +1482,7 @@ describe("share", () => {
1085
1482
  size: 5,
1086
1483
  });
1087
1484
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1088
- fs.writeFileSync(journalPath, JSON.stringify({
1485
+ seedJournalAt(journalPath, ({
1089
1486
  version: "1",
1090
1487
  lastSync: syncedAt,
1091
1488
  files: {
@@ -1123,12 +1520,18 @@ describe("share", () => {
1123
1520
  });
1124
1521
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "brand-new.md", undefined, { ifNoneMatch: "*" });
1125
1522
  });
1126
- it("412 under keep: conflict event + remote mirrored + ledger entry + NO journal stamp", async () => {
1523
+ it("412 under keep: adopts the racing remote, preserves local, and converges the journal", async () => {
1127
1524
  const companyRoot = path.join(tmpDir, "companies", "acme");
1128
1525
  fs.mkdirSync(companyRoot, { recursive: true });
1129
1526
  const testFile = path.join(companyRoot, "raced.md");
1130
1527
  fs.writeFileSync(testFile, "my local bytes");
1131
- vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1528
+ vi.mocked(headRemoteFile)
1529
+ .mockResolvedValueOnce(null)
1530
+ .mockResolvedValueOnce({
1531
+ lastModified: new Date(),
1532
+ etag: '"racing-peer-etag"',
1533
+ size: 23,
1534
+ });
1132
1535
  vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
1133
1536
  vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1134
1537
  fs.writeFileSync(dest, "the racing peer version");
@@ -1148,19 +1551,22 @@ describe("share", () => {
1148
1551
  expect(result.filesUploaded).toBe(0);
1149
1552
  expect(result.aborted).toBe(false);
1150
1553
  expect(events.some((e) => e.type === "conflict" && e.path === "raced.md")).toBe(true);
1151
- // The racing remote bytes were preserved next to the local copy.
1152
- const mirrors = fs
1153
- .readdirSync(companyRoot)
1154
- .filter((f) => f.includes(".conflict-"));
1155
- expect(mirrors).toHaveLength(1);
1156
- // Local copy untouched.
1157
- expect(fs.readFileSync(testFile, "utf-8")).toBe("my local bytes");
1158
- // No journal stamp for the failed PUT — next pass re-evaluates fresh.
1554
+ // The racing remote wins the working name; local remains recoverable.
1555
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("the racing peer version");
1556
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my local bytes");
1557
+ // The pull-shaped result is journalled as the remote body, so the next
1558
+ // pass does not revive the same resolved conflict.
1159
1559
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1160
- if (fs.existsSync(journalPath)) {
1161
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1162
- expect(journal.files?.["raced.md"]).toBeUndefined();
1163
- }
1560
+ const journal = journalAt(journalPath);
1561
+ expect(journal.files["raced.md"]).toMatchObject({
1562
+ hash: hashFile(testFile),
1563
+ remoteEtag: "racing-peer-etag",
1564
+ direction: "down",
1565
+ });
1566
+ expect(conflictIndexAt(tmpDir).conflicts[0]).toMatchObject({
1567
+ originalPath: "companies/acme/raced.md",
1568
+ preserved: "local",
1569
+ });
1164
1570
  });
1165
1571
  it("412 under overwrite: retries exactly once WITHOUT the fence (explicit consent)", async () => {
1166
1572
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1239,8 +1645,12 @@ describe("share", () => {
1239
1645
  }
1240
1646
  return { etag: '"should-not-land"' };
1241
1647
  });
1648
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1649
+ fs.writeFileSync(dest, "reviewed peer body");
1650
+ return undefined;
1651
+ });
1242
1652
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1243
- fs.writeFileSync(journalPath, JSON.stringify({
1653
+ seedJournalAt(journalPath, ({
1244
1654
  version: "1",
1245
1655
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1246
1656
  files: {
@@ -1283,8 +1693,9 @@ describe("share", () => {
1283
1693
  }
1284
1694
  // No unfenced retry under keep.
1285
1695
  expect(vi.mocked(uploadFile).mock.calls.some((c) => c[4] === undefined)).toBe(false);
1286
- // Local stale bytes preserved on disk (keep does not pull).
1287
- expect(fs.readFileSync(testFile, "utf-8")).toBe(staleBytes);
1696
+ // Keep adopts the peer body but retains the stale local body beside it.
1697
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("reviewed peer body");
1698
+ expect(fs.readFileSync(sidecarIn(path.dirname(testFile)), "utf-8")).toBe(staleBytes);
1288
1699
  });
1289
1700
  it("legitimate local-ahead still uploads when remote remains at journal baseline", async () => {
1290
1701
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1300,7 +1711,7 @@ describe("share", () => {
1300
1711
  });
1301
1712
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
1302
1713
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1303
- fs.writeFileSync(journalPath, JSON.stringify({
1714
+ seedJournalAt(journalPath, ({
1304
1715
  version: "1",
1305
1716
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1306
1717
  files: {
@@ -1334,8 +1745,12 @@ describe("share", () => {
1334
1745
  etag: '"remote-concurrent"',
1335
1746
  size: 20,
1336
1747
  });
1748
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1749
+ fs.writeFileSync(dest, "remote concurrent");
1750
+ return undefined;
1751
+ });
1337
1752
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1338
- fs.writeFileSync(journalPath, JSON.stringify({
1753
+ seedJournalAt(journalPath, ({
1339
1754
  version: "1",
1340
1755
  lastSync: new Date().toISOString(),
1341
1756
  files: {
@@ -1358,12 +1773,14 @@ describe("share", () => {
1358
1773
  expect(result.conflictPaths).toEqual(["both-changed.md"]);
1359
1774
  expect(result.filesUploaded).toBe(0);
1360
1775
  expect(uploadFile).not.toHaveBeenCalled();
1361
- });
1362
- it("localDiverges under keep: pull-kept divergent local cannot silently become write authority", async () => {
1363
- // After pull --on-conflict keep, journal stamps remoteEtag of the peer
1364
- // version AND localDiverges=true (local never matched that etag). A later
1365
- // push must not treat "remote still at stamped etag" as license to PUT
1366
- // the divergent local body (journal-baseline If-Match would succeed).
1776
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("remote concurrent");
1777
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local concurrent");
1778
+ });
1779
+ it("localDiverges under keep: a pull-held divergent local yields to cloud without becoming write authority", async () => {
1780
+ // After a pull-side hold (`skip` or pull-leg `publish-local`), the journal
1781
+ // stamps the peer remoteEtag and localDiverges=true because local never
1782
+ // matched that etag. A later push must not treat "remote still at stamped
1783
+ // etag" as license to PUT the divergent local body.
1367
1784
  const companyRoot = path.join(tmpDir, "companies", "acme");
1368
1785
  fs.mkdirSync(companyRoot, { recursive: true });
1369
1786
  const testFile = path.join(companyRoot, "kept-divergent.md");
@@ -1376,8 +1793,12 @@ describe("share", () => {
1376
1793
  etag: `"${REMOTE}"`,
1377
1794
  size: 40,
1378
1795
  });
1796
+ vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1797
+ fs.writeFileSync(dest, "peer remote body");
1798
+ return undefined;
1799
+ });
1379
1800
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1380
- fs.writeFileSync(journalPath, JSON.stringify({
1801
+ seedJournalAt(journalPath, ({
1381
1802
  version: "1",
1382
1803
  lastSync: new Date().toISOString(),
1383
1804
  files: {
@@ -1407,7 +1828,10 @@ describe("share", () => {
1407
1828
  expect(events.some((e) => e.type === "conflict" &&
1408
1829
  e.path === "kept-divergent.md" &&
1409
1830
  e.resolution === "keep")).toBe(true);
1410
- expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1831
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote body");
1832
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe(localBytes);
1833
+ expect(journalAt(journalPath).files["kept-divergent.md"]?.localDiverges)
1834
+ .toBeFalsy();
1411
1835
  });
1412
1836
  it("localDiverges under overwrite: explicit intent may force-write", async () => {
1413
1837
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1423,7 +1847,7 @@ describe("share", () => {
1423
1847
  });
1424
1848
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"forced-etag"' });
1425
1849
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1426
- fs.writeFileSync(journalPath, JSON.stringify({
1850
+ seedJournalAt(journalPath, ({
1427
1851
  version: "1",
1428
1852
  lastSync: new Date().toISOString(),
1429
1853
  files: {
@@ -1450,6 +1874,304 @@ describe("share", () => {
1450
1874
  // fence then retry — either is consent. Must not leave the file unuploaded.
1451
1875
  expect(uploadFile).toHaveBeenCalled();
1452
1876
  });
1877
+ // ── publish-local: fenced promotion of a kept-local copy ────────────────
1878
+ //
1879
+ // `keep` leaves a file permanently unpublishable: push refuses it on the
1880
+ // localDiverges flag, pull never clears the flag (remote is unchanged), and
1881
+ // `keep` re-arms it every pass. The only existing escape — `overwrite` — is
1882
+ // a blunt force-write that will PUT unfenced after a 412, clobbering a
1883
+ // remote that genuinely advanced.
1884
+ //
1885
+ // `publish-local` is the missing middle: the operator explicitly elects
1886
+ // their local copy as the new authority, and the engine accepts it ONLY
1887
+ // while the remote still sits at the etag recorded when the keep happened.
1888
+ // If the remote moved, consent was given against a version that no longer
1889
+ // exists, so the write must fail closed instead of forcing.
1890
+ it("localDiverges under publish-local: promotes local under an If-Match fence on the journal etag", async () => {
1891
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1892
+ fs.mkdirSync(companyRoot, { recursive: true });
1893
+ const testFile = path.join(companyRoot, "publish-divergent.md");
1894
+ const localBytes = "my kept divergent local, now elected authority";
1895
+ fs.writeFileSync(testFile, localBytes);
1896
+ const localHash = hashFile(testFile);
1897
+ const REMOTE = "peer-version-etag";
1898
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1899
+ lastModified: new Date(),
1900
+ etag: `"${REMOTE}"`,
1901
+ size: 40,
1902
+ });
1903
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1904
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1905
+ fs.writeFileSync(journalPath, JSON.stringify({
1906
+ version: "1",
1907
+ lastSync: new Date().toISOString(),
1908
+ files: {
1909
+ "publish-divergent.md": {
1910
+ hash: localHash,
1911
+ size: localBytes.length,
1912
+ syncedAt: new Date().toISOString(),
1913
+ direction: "down",
1914
+ remoteEtag: REMOTE,
1915
+ localDiverges: true,
1916
+ },
1917
+ },
1918
+ }));
1919
+ const result = await share({
1920
+ paths: [testFile],
1921
+ company: "acme",
1922
+ vaultConfig: mockConfig,
1923
+ hqRoot: tmpDir,
1924
+ skipUnchanged: false,
1925
+ onConflict: "publish-local",
1926
+ });
1927
+ expect(result.filesUploaded).toBe(1);
1928
+ // Fenced on the etag the keep recorded — never unfenced, never live-HEAD.
1929
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1930
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1931
+ // The entry is rewritten as an ordinary upload: local is now authority,
1932
+ // so the journal-honesty flag must be gone rather than re-armed.
1933
+ const journal = journalAt(journalPath);
1934
+ expect(journal.files["publish-divergent.md"].direction).toBe("up");
1935
+ expect(journal.files["publish-divergent.md"].localDiverges).toBeFalsy();
1936
+ expect(journal.files["publish-divergent.md"].remoteEtag).toBe("published-etag");
1937
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1938
+ });
1939
+ it("localDiverges under publish-local: a 412 fails closed and never retries unfenced", async () => {
1940
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1941
+ fs.mkdirSync(companyRoot, { recursive: true });
1942
+ const testFile = path.join(companyRoot, "publish-raced.md");
1943
+ const localBytes = "my local, but the remote moved under me";
1944
+ fs.writeFileSync(testFile, localBytes);
1945
+ const localHash = hashFile(testFile);
1946
+ const REMOTE = "etag-at-keep-time";
1947
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1948
+ lastModified: new Date(),
1949
+ etag: `"${REMOTE}"`,
1950
+ size: 40,
1951
+ });
1952
+ const precondition = Object.assign(new Error("At least one of the pre-conditions you specified did not hold"), {
1953
+ name: "PreconditionFailed",
1954
+ });
1955
+ vi.mocked(uploadFile).mockRejectedValueOnce(precondition);
1956
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1957
+ fs.writeFileSync(journalPath, JSON.stringify({
1958
+ version: "1",
1959
+ lastSync: new Date().toISOString(),
1960
+ files: {
1961
+ "publish-raced.md": {
1962
+ hash: localHash,
1963
+ size: localBytes.length,
1964
+ syncedAt: new Date().toISOString(),
1965
+ direction: "down",
1966
+ remoteEtag: REMOTE,
1967
+ localDiverges: true,
1968
+ },
1969
+ },
1970
+ }));
1971
+ const result = await share({
1972
+ paths: [testFile],
1973
+ company: "acme",
1974
+ vaultConfig: mockConfig,
1975
+ hqRoot: tmpDir,
1976
+ skipUnchanged: false,
1977
+ onConflict: "publish-local",
1978
+ });
1979
+ // Exactly one attempt: the fenced one. The `overwrite` escape hatch of
1980
+ // re-PUTting with no precondition must NOT be reachable from here.
1981
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1982
+ expect(result.filesUploaded).toBe(0);
1983
+ expect(result.conflictPaths).toContain("publish-raced.md");
1984
+ // Consent was for a remote that no longer exists, so the flag stays put
1985
+ // and the operator is asked again rather than silently overruled.
1986
+ const journal = journalAt(journalPath);
1987
+ expect(journal.files["publish-raced.md"].localDiverges).toBe(true);
1988
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1989
+ });
1990
+ // Codex review (PR #356, P1). The sync runner's push leg always calls
1991
+ // share() with `skipUnchanged: true`, and a pull-held entry stores the
1992
+ // CURRENT local hash — that is exactly what the journal-honesty stamp
1993
+ // records. So `existing.hash === localHash` is true for every stuck file,
1994
+ // classifyPushCandidates() returns `skip-unchanged`, and executeUploads()
1995
+ // never runs: no HEAD, no conflict, no upload. Without this bypass the
1996
+ // whole recovery is a no-op in the one mode that can reach personal-vault
1997
+ // files, and the next pull leaves them stuck again. `hash === journal hash`
1998
+ // means "local is unchanged since the stamp", NOT "local matches remote" —
1999
+ // for a divergent entry those are different claims.
2000
+ it("localDiverges bypasses the skip-unchanged fast path so the recovery can run at all", async () => {
2001
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2002
+ fs.mkdirSync(companyRoot, { recursive: true });
2003
+ const testFile = path.join(companyRoot, "stuck-under-skip-unchanged.md");
2004
+ const localBytes = "the body a pull hold stamped and then refused to publish";
2005
+ fs.writeFileSync(testFile, localBytes);
2006
+ const localHash = hashFile(testFile);
2007
+ const REMOTE = "etag-at-keep-time";
2008
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
2009
+ lastModified: new Date(),
2010
+ etag: `"${REMOTE}"`,
2011
+ size: 40,
2012
+ });
2013
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
2014
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2015
+ fs.writeFileSync(journalPath, JSON.stringify({
2016
+ version: "1",
2017
+ lastSync: new Date().toISOString(),
2018
+ files: {
2019
+ "stuck-under-skip-unchanged.md": {
2020
+ // Deliberately the CURRENT local hash: this is what a pull hold
2021
+ // writes, and it is what makes the fast path swallow the file.
2022
+ hash: localHash,
2023
+ size: localBytes.length,
2024
+ syncedAt: new Date().toISOString(),
2025
+ direction: "down",
2026
+ remoteEtag: REMOTE,
2027
+ localDiverges: true,
2028
+ },
2029
+ },
2030
+ }));
2031
+ const result = await share({
2032
+ paths: [testFile],
2033
+ company: "acme",
2034
+ vaultConfig: mockConfig,
2035
+ hqRoot: tmpDir,
2036
+ skipUnchanged: true,
2037
+ onConflict: "publish-local",
2038
+ });
2039
+ expect(result.filesUploaded).toBe(1);
2040
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
2041
+ const journal = journalAt(journalPath);
2042
+ expect(journal.files["stuck-under-skip-unchanged.md"].direction).toBe("up");
2043
+ expect(journal.files["stuck-under-skip-unchanged.md"].localDiverges).toBeFalsy();
2044
+ });
2045
+ // Codex review (PR #356, P2). A genuine two-sided conflict is not the
2046
+ // localDiverges recovery: the journal etag is stale BY DEFINITION here
2047
+ // (that is what remoteChanged means), so fencing consent on it guarantees a
2048
+ // 412 and the file can never publish. Consent given at this prompt is
2049
+ // consent against the remote the operator was just shown, so the fence must
2050
+ // be the freshly observed etag.
2051
+ it("publish-local on a fresh two-sided conflict fences on the OBSERVED remote etag, not the stale journal one", async () => {
2052
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2053
+ fs.mkdirSync(companyRoot, { recursive: true });
2054
+ const testFile = path.join(companyRoot, "two-sided.md");
2055
+ fs.writeFileSync(testFile, "my newer local body");
2056
+ const STALE_JOURNAL_ETAG = "etag-from-last-sync";
2057
+ const OBSERVED_REMOTE_ETAG = "etag-the-peer-just-wrote";
2058
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
2059
+ lastModified: new Date(),
2060
+ etag: `"${OBSERVED_REMOTE_ETAG}"`,
2061
+ size: 40,
2062
+ });
2063
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
2064
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2065
+ fs.writeFileSync(journalPath, JSON.stringify({
2066
+ version: "1",
2067
+ lastSync: new Date().toISOString(),
2068
+ files: {
2069
+ "two-sided.md": {
2070
+ hash: "a-hash-that-is-neither-side",
2071
+ size: 10,
2072
+ syncedAt: new Date().toISOString(),
2073
+ direction: "up",
2074
+ remoteEtag: STALE_JOURNAL_ETAG,
2075
+ },
2076
+ },
2077
+ }));
2078
+ const result = await share({
2079
+ paths: [testFile],
2080
+ company: "acme",
2081
+ vaultConfig: mockConfig,
2082
+ hqRoot: tmpDir,
2083
+ skipUnchanged: false,
2084
+ onConflict: "publish-local",
2085
+ });
2086
+ expect(result.filesUploaded).toBe(1);
2087
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({
2088
+ ifMatch: OBSERVED_REMOTE_ETAG,
2089
+ });
2090
+ });
2091
+ // Codex review (PR #356, P2). HEAD returning null skips the whole conflict
2092
+ // block, so the precondition becomes If-None-Match:* and the object is
2093
+ // RECREATED. For a divergent entry that silently overrules a peer's
2094
+ // deletion — the remote is emphatically no longer at the recorded etag, so
2095
+ // the strategy's own fence says fail closed. `keep`'s recreate-once
2096
+ // behaviour for non-divergent entries is unaffected.
2097
+ it("publish-local does not resurrect a remote deleted after the keep", async () => {
2098
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2099
+ fs.mkdirSync(companyRoot, { recursive: true });
2100
+ const testFile = path.join(companyRoot, "peer-deleted.md");
2101
+ const localBytes = "my divergent local body";
2102
+ fs.writeFileSync(testFile, localBytes);
2103
+ const localHash = hashFile(testFile);
2104
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2105
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2106
+ fs.writeFileSync(journalPath, JSON.stringify({
2107
+ version: "1",
2108
+ lastSync: new Date().toISOString(),
2109
+ files: {
2110
+ "peer-deleted.md": {
2111
+ hash: localHash,
2112
+ size: localBytes.length,
2113
+ syncedAt: new Date().toISOString(),
2114
+ direction: "down",
2115
+ remoteEtag: "etag-before-the-peer-deleted-it",
2116
+ localDiverges: true,
2117
+ },
2118
+ },
2119
+ }));
2120
+ const result = await share({
2121
+ paths: [testFile],
2122
+ company: "acme",
2123
+ vaultConfig: mockConfig,
2124
+ hqRoot: tmpDir,
2125
+ skipUnchanged: false,
2126
+ onConflict: "publish-local",
2127
+ });
2128
+ expect(uploadFile).not.toHaveBeenCalled();
2129
+ expect(result.filesUploaded).toBe(0);
2130
+ expect(result.conflictPaths).toContain("peer-deleted.md");
2131
+ const journal = journalAt(journalPath);
2132
+ expect(journal.files["peer-deleted.md"].localDiverges).toBe(true);
2133
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
2134
+ });
2135
+ it("publish-local on a non-divergent file behaves like an ordinary fenced upload", async () => {
2136
+ const companyRoot = path.join(tmpDir, "companies", "acme");
2137
+ fs.mkdirSync(companyRoot, { recursive: true });
2138
+ const testFile = path.join(companyRoot, "ordinary.md");
2139
+ fs.writeFileSync(testFile, "locally edited since last sync");
2140
+ const REMOTE = "unchanged-remote-etag";
2141
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
2142
+ lastModified: new Date(),
2143
+ etag: `"${REMOTE}"`,
2144
+ size: 10,
2145
+ });
2146
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"next-etag"' });
2147
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
2148
+ fs.writeFileSync(journalPath, JSON.stringify({
2149
+ version: "1",
2150
+ lastSync: new Date().toISOString(),
2151
+ files: {
2152
+ "ordinary.md": {
2153
+ hash: "stale-hash-from-a-previous-body",
2154
+ size: 10,
2155
+ syncedAt: new Date().toISOString(),
2156
+ direction: "up",
2157
+ remoteEtag: REMOTE,
2158
+ },
2159
+ },
2160
+ }));
2161
+ const result = await share({
2162
+ paths: [testFile],
2163
+ company: "acme",
2164
+ vaultConfig: mockConfig,
2165
+ hqRoot: tmpDir,
2166
+ skipUnchanged: false,
2167
+ onConflict: "publish-local",
2168
+ });
2169
+ // No localDiverges flag means no special casing: the normal push fence
2170
+ // applies and the file uploads without being routed through conflict.
2171
+ expect(result.filesUploaded).toBe(1);
2172
+ expect(result.conflictPaths ?? []).not.toContain("ordinary.md");
2173
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
2174
+ });
1453
2175
  // Journal still carries remoteEtag from a prior sync, but the remote key
1454
2176
  // is gone (HEAD null) — e.g. peer delete or operator purge. Recreation
1455
2177
  // must use If-None-Match:* (create-only), NOT If-Match on the stale
@@ -1467,7 +2189,7 @@ describe("share", () => {
1467
2189
  vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1468
2190
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"recreated-etag"' });
1469
2191
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1470
- fs.writeFileSync(journalPath, JSON.stringify({
2192
+ seedJournalAt(journalPath, ({
1471
2193
  version: "1",
1472
2194
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1473
2195
  files: {
@@ -1497,28 +2219,34 @@ describe("share", () => {
1497
2219
  const pc = vi.mocked(uploadFile).mock.calls[0][4];
1498
2220
  expect(pc?.ifMatch).toBeUndefined();
1499
2221
  expect(pc?.ifNoneMatch).toBe("*");
1500
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2222
+ const journal = journalAt(journalPath);
1501
2223
  expect(journal.files["recreate-deleted.md"]).toMatchObject({
1502
2224
  hash: localHash,
1503
2225
  remoteEtag: "recreated-etag",
1504
2226
  direction: "up",
1505
2227
  });
1506
2228
  });
1507
- it("deleted journaled remote + concurrent create 412 under keep: existing conflict semantics", async () => {
2229
+ it("deleted journaled remote + concurrent create 412 under keep: adopts the winner and preserves local", async () => {
1508
2230
  // Same setup as recreation, but a peer creates the key between HEAD and
1509
- // PUT → If-None-Match:* 412s → keep surfaces conflict, no journal stamp.
2231
+ // PUT → If-None-Match:* 412s → keep adopts the peer's winning create.
1510
2232
  const companyRoot = path.join(tmpDir, "companies", "acme");
1511
2233
  fs.mkdirSync(companyRoot, { recursive: true });
1512
2234
  const testFile = path.join(companyRoot, "race-recreate.md");
1513
2235
  fs.writeFileSync(testFile, "my recreated local");
1514
- vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2236
+ vi.mocked(headRemoteFile)
2237
+ .mockResolvedValueOnce(null)
2238
+ .mockResolvedValueOnce({
2239
+ lastModified: new Date(),
2240
+ etag: '"peer-created-etag"',
2241
+ size: 18,
2242
+ });
1515
2243
  vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
1516
2244
  vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1517
2245
  fs.writeFileSync(dest, "peer created first");
1518
2246
  return undefined;
1519
2247
  });
1520
2248
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1521
- fs.writeFileSync(journalPath, JSON.stringify({
2249
+ seedJournalAt(journalPath, ({
1522
2250
  version: "1",
1523
2251
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1524
2252
  files: {
@@ -1548,12 +2276,15 @@ describe("share", () => {
1548
2276
  // First (only) attempt must be create-fenced; no unfenced retry under keep.
1549
2277
  expect(uploadFile).toHaveBeenCalledTimes(1);
1550
2278
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
1551
- // Journal not advanced to a successful upload etag (entry may still be
1552
- // the prior baseline — next pass re-evaluates).
1553
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1554
- expect(journal.files["race-recreate.md"]?.remoteEtag).toBe("etag-before-delete");
1555
- expect(journal.files["race-recreate.md"]?.hash).toBe("old-hash");
1556
- expect(fs.readFileSync(testFile, "utf-8")).toBe("my recreated local");
2279
+ // Journal now describes the peer body at the original name.
2280
+ const journal = journalAt(journalPath);
2281
+ expect(journal.files["race-recreate.md"]).toMatchObject({
2282
+ remoteEtag: "peer-created-etag",
2283
+ hash: hashFile(testFile),
2284
+ direction: "down",
2285
+ });
2286
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("peer created first");
2287
+ expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my recreated local");
1557
2288
  });
1558
2289
  });
1559
2290
  it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
@@ -1874,7 +2605,7 @@ describe("share", () => {
1874
2605
  size: 5,
1875
2606
  });
1876
2607
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1877
- fs.writeFileSync(journalPath, JSON.stringify({
2608
+ seedJournalAt(journalPath, ({
1878
2609
  version: "1",
1879
2610
  lastSync: syncedAt,
1880
2611
  files: {
@@ -1914,7 +2645,7 @@ describe("share", () => {
1914
2645
  size: 5,
1915
2646
  });
1916
2647
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1917
- fs.writeFileSync(journalPath, JSON.stringify({
2648
+ seedJournalAt(journalPath, ({
1918
2649
  version: "1",
1919
2650
  lastSync: syncedAt,
1920
2651
  files: {
@@ -1950,7 +2681,7 @@ describe("share", () => {
1950
2681
  hqRoot: tmpDir,
1951
2682
  });
1952
2683
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1953
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2684
+ const journal = journalAt(journalPath);
1954
2685
  expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
1955
2686
  });
1956
2687
  it("forwards UploadAuthor to uploadFile when present (created-by metadata)", async () => {
@@ -1995,7 +2726,7 @@ describe("share", () => {
1995
2726
  const { hashFile } = await import("../journal.js");
1996
2727
  const hash = hashFile(testFile);
1997
2728
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1998
- fs.writeFileSync(journalPath, JSON.stringify({
2729
+ seedJournalAt(journalPath, ({
1999
2730
  version: "1",
2000
2731
  lastSync: new Date().toISOString(),
2001
2732
  files: {
@@ -2215,7 +2946,7 @@ describe("share", () => {
2215
2946
  .update("stable content")
2216
2947
  .digest("hex");
2217
2948
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2218
- fs.writeFileSync(journalPath, JSON.stringify({
2949
+ seedJournalAt(journalPath, ({
2219
2950
  version: "1",
2220
2951
  lastSync: new Date().toISOString(),
2221
2952
  files: {
@@ -2276,7 +3007,7 @@ describe("share", () => {
2276
3007
  // not be queued for delete.
2277
3008
  fs.writeFileSync(path.join(tmpDir, ".hqinclude"), "companies/*/knowledge/\n");
2278
3009
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2279
- fs.writeFileSync(journalPath, JSON.stringify({
3010
+ seedJournalAt(journalPath, ({
2280
3011
  version: "1",
2281
3012
  lastSync: new Date().toISOString(),
2282
3013
  files: {
@@ -2313,7 +3044,7 @@ describe("share", () => {
2313
3044
  operation: "delete",
2314
3045
  });
2315
3046
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
2316
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3047
+ const journal = journalAt(journalPath);
2317
3048
  expect(journal.files["knowledge"]).toBeUndefined();
2318
3049
  });
2319
3050
  it("propagateDeletes: a dangling local symlink is NOT classified as gone (lstat, not existsSync)", async () => {
@@ -2330,7 +3061,7 @@ describe("share", () => {
2330
3061
  const danglingLink = path.join(companyRoot, "dangling-link.md");
2331
3062
  fs.symlinkSync("./missing-target.md", danglingLink);
2332
3063
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2333
- fs.writeFileSync(journalPath, JSON.stringify({
3064
+ seedJournalAt(journalPath, ({
2334
3065
  version: "1",
2335
3066
  lastSync: new Date().toISOString(),
2336
3067
  files: {
@@ -2360,7 +3091,7 @@ describe("share", () => {
2360
3091
  expect(result.filesDeleted).toBe(0);
2361
3092
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2362
3093
  // Journal entry must survive the run.
2363
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3094
+ const journal = journalAt(journalPath);
2364
3095
  expect(journal.files["dangling-link.md"]).toBeDefined();
2365
3096
  });
2366
3097
  it("vault-litter drain: a `.drift-*` marker with direction:'down' and NO delete intent tombstones " +
@@ -2425,7 +3156,7 @@ describe("share", () => {
2425
3156
  });
2426
3157
  expect(result.filesDeleted).toBe(1);
2427
3158
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/scripts/run-project.sh.drift-1779863862-42519");
2428
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3159
+ const journal = journalAt(journalPath);
2429
3160
  expect(journal.files["personal/scripts/run-project.sh.drift-1779863862-42519"]).toBeUndefined();
2430
3161
  }
2431
3162
  finally {
@@ -2498,7 +3229,7 @@ describe("share", () => {
2498
3229
  fs.mkdirSync(companyRoot, { recursive: true });
2499
3230
  // No file on disk under companies/acme/peer-uploaded.md → local missing.
2500
3231
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2501
- fs.writeFileSync(journalPath, JSON.stringify({
3232
+ seedJournalAt(journalPath, ({
2502
3233
  version: "1",
2503
3234
  lastSync: new Date().toISOString(),
2504
3235
  files: {
@@ -2523,7 +3254,7 @@ describe("share", () => {
2523
3254
  });
2524
3255
  expect(result.filesDeleted).toBe(0);
2525
3256
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2526
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3257
+ const journal = journalAt(journalPath);
2527
3258
  expect(journal.files["peer-uploaded.md"]).toBeDefined();
2528
3259
  });
2529
3260
  it("ROLLBACK KNOB: owned-only is honored on a prs_ personal vault — an intent-less entry with a " +
@@ -2614,7 +3345,7 @@ describe("share", () => {
2614
3345
  ]);
2615
3346
  // Journal entry survives, so a later run (or a watcher-minted intent)
2616
3347
  // can still authorize the delete properly.
2617
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3348
+ const journal = journalAt(journalPath);
2618
3349
  expect(journal.files["personal/knowledge/gone.md"]).toBeDefined();
2619
3350
  }
2620
3351
  finally {
@@ -2631,7 +3362,7 @@ describe("share", () => {
2631
3362
  // deleted by the user.
2632
3363
  fs.writeFileSync(path.join(companyRoot, "kept.md"), "still here");
2633
3364
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2634
- fs.writeFileSync(journalPath, JSON.stringify({
3365
+ seedJournalAt(journalPath, ({
2635
3366
  version: "1",
2636
3367
  lastSync: new Date().toISOString(),
2637
3368
  files: {
@@ -2667,7 +3398,7 @@ describe("share", () => {
2667
3398
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2668
3399
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "gone.md");
2669
3400
  // Journal entry for the gone file is removed; the kept entry stays.
2670
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3401
+ const journal = journalAt(journalPath);
2671
3402
  expect(journal.files["gone.md"]).toBeUndefined();
2672
3403
  expect(journal.files["kept.md"]).toBeDefined();
2673
3404
  });
@@ -2675,7 +3406,7 @@ describe("share", () => {
2675
3406
  const companyRoot = path.join(tmpDir, "companies", "acme");
2676
3407
  fs.mkdirSync(companyRoot, { recursive: true });
2677
3408
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2678
- fs.writeFileSync(journalPath, JSON.stringify({
3409
+ seedJournalAt(journalPath, ({
2679
3410
  version: "1",
2680
3411
  lastSync: new Date().toISOString(),
2681
3412
  files: {
@@ -2717,7 +3448,7 @@ describe("share", () => {
2717
3448
  const companyRoot = path.join(tmpDir, "companies", "acme");
2718
3449
  fs.mkdirSync(companyRoot, { recursive: true });
2719
3450
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2720
- fs.writeFileSync(journalPath, JSON.stringify({
3451
+ seedJournalAt(journalPath, ({
2721
3452
  version: "1",
2722
3453
  lastSync: new Date().toISOString(),
2723
3454
  files: {
@@ -2740,7 +3471,7 @@ describe("share", () => {
2740
3471
  expect(result.filesDeleted).toBe(0);
2741
3472
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2742
3473
  // Journal entry survives so the next opt-in run can still propagate.
2743
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3474
+ const journal = journalAt(journalPath);
2744
3475
  expect(journal.files["gone.md"]).toBeDefined();
2745
3476
  });
2746
3477
  it("propagateDeletes: scope is constrained to the supplied paths — sibling deletes are not swept", async () => {
@@ -2748,7 +3479,7 @@ describe("share", () => {
2748
3479
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
2749
3480
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
2750
3481
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2751
- fs.writeFileSync(journalPath, JSON.stringify({
3482
+ seedJournalAt(journalPath, ({
2752
3483
  version: "1",
2753
3484
  lastSync: new Date().toISOString(),
2754
3485
  files: {
@@ -2783,7 +3514,7 @@ describe("share", () => {
2783
3514
  expect(result.filesDeleted).toBe(1);
2784
3515
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2785
3516
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
2786
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3517
+ const journal = journalAt(journalPath);
2787
3518
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
2788
3519
  // Sibling tree's journal entry is untouched — `hq share <subtree>` must
2789
3520
  // not act on files outside the named scope.
@@ -2799,7 +3530,7 @@ describe("share", () => {
2799
3530
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
2800
3531
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
2801
3532
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2802
- fs.writeFileSync(journalPath, JSON.stringify({
3533
+ seedJournalAt(journalPath, ({
2803
3534
  version: "1",
2804
3535
  lastSync: new Date().toISOString(),
2805
3536
  files: {
@@ -2834,7 +3565,7 @@ describe("share", () => {
2834
3565
  });
2835
3566
  expect(result.filesDeleted).toBe(1);
2836
3567
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
2837
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3568
+ const journal = journalAt(journalPath);
2838
3569
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
2839
3570
  // Scope stays exactly as narrow as the absolute spelling's — no wider.
2840
3571
  expect(journal.files["other/also-gone.md"]).toBeDefined();
@@ -2851,7 +3582,7 @@ describe("share", () => {
2851
3582
  fs.mkdirSync(acmeRepo, { recursive: true });
2852
3583
  fs.symlinkSync(acmeRepo, path.join(companyRoot, "knowledge"));
2853
3584
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2854
- fs.writeFileSync(journalPath, JSON.stringify({
3585
+ seedJournalAt(journalPath, ({
2855
3586
  version: "1",
2856
3587
  lastSync: new Date().toISOString(),
2857
3588
  files: {
@@ -2881,14 +3612,14 @@ describe("share", () => {
2881
3612
  });
2882
3613
  expect(result.filesDeleted).toBe(0);
2883
3614
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2884
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3615
+ const journal = journalAt(journalPath);
2885
3616
  expect(journal.files["knowledge/agents/brief.md"]).toBeDefined();
2886
3617
  });
2887
3618
  it("propagateDeletes: a failed DeleteObject leaves the journal entry intact for retry", async () => {
2888
3619
  const companyRoot = path.join(tmpDir, "companies", "acme");
2889
3620
  fs.mkdirSync(companyRoot, { recursive: true });
2890
3621
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2891
- fs.writeFileSync(journalPath, JSON.stringify({
3622
+ seedJournalAt(journalPath, ({
2892
3623
  version: "1",
2893
3624
  lastSync: new Date().toISOString(),
2894
3625
  files: {
@@ -2921,7 +3652,7 @@ describe("share", () => {
2921
3652
  const errorEvent = events.find((e) => e.type === "error");
2922
3653
  expect(errorEvent).toMatchObject({ path: "flaky.md", message: expect.stringContaining("S3 down") });
2923
3654
  // Entry survives — next run will retry the delete.
2924
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3655
+ const journal = journalAt(journalPath);
2925
3656
  expect(journal.files["flaky.md"]).toBeDefined();
2926
3657
  });
2927
3658
  // ── Delete propagation safety: direction + filter guards (Bug B + C) ──────
@@ -2955,7 +3686,7 @@ describe("share", () => {
2955
3686
  // and one this machine uploaded (direction:'up'). Only the 'up' one should
2956
3687
  // be eligible for delete-propagation.
2957
3688
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2958
- fs.writeFileSync(journalPath, JSON.stringify({
3689
+ seedJournalAt(journalPath, ({
2959
3690
  version: "1",
2960
3691
  lastSync: new Date().toISOString(),
2961
3692
  files: {
@@ -2992,7 +3723,7 @@ describe("share", () => {
2992
3723
  const companyRoot = path.join(tmpDir, "companies", "acme");
2993
3724
  fs.mkdirSync(companyRoot, { recursive: true });
2994
3725
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2995
- fs.writeFileSync(journalPath, JSON.stringify({
3726
+ seedJournalAt(journalPath, ({
2996
3727
  version: "1",
2997
3728
  lastSync: new Date().toISOString(),
2998
3729
  files: {
@@ -3025,7 +3756,7 @@ describe("share", () => {
3025
3756
  // absence is a filter outcome, not a user-intended delete.
3026
3757
  fs.writeFileSync(path.join(tmpDir, ".hqignore"), "companies/acme/legacy/**\n");
3027
3758
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3028
- fs.writeFileSync(journalPath, JSON.stringify({
3759
+ seedJournalAt(journalPath, ({
3029
3760
  version: "1",
3030
3761
  lastSync: new Date().toISOString(),
3031
3762
  files: {
@@ -3084,7 +3815,7 @@ describe("share", () => {
3084
3815
  // Under owned-only this would be stuck on remote forever; currency-gated
3085
3816
  // checks the ETag and propagates the delete cleanly when match holds.
3086
3817
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3087
- fs.writeFileSync(journalPath, JSON.stringify({
3818
+ seedJournalAt(journalPath, ({
3088
3819
  version: "1",
3089
3820
  lastSync: new Date().toISOString(),
3090
3821
  files: {
@@ -3114,7 +3845,7 @@ describe("share", () => {
3114
3845
  });
3115
3846
  expect(result.filesDeleted).toBe(1);
3116
3847
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "core/policies/old.md");
3117
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3848
+ const journal = journalAt(journalPath);
3118
3849
  expect(journal.files["core/policies/old.md"]).toBeUndefined();
3119
3850
  });
3120
3851
  it("currency-gated: propagates the S3 delete for a local-delete tombstone (pull-leg producer hand-off)", async () => {
@@ -3125,7 +3856,7 @@ describe("share", () => {
3125
3856
  const companyRoot = path.join(tmpDir, "companies", "acme");
3126
3857
  fs.mkdirSync(companyRoot, { recursive: true });
3127
3858
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3128
- fs.writeFileSync(journalPath, JSON.stringify({
3859
+ seedJournalAt(journalPath, ({
3129
3860
  version: "2",
3130
3861
  lastSync: new Date().toISOString(),
3131
3862
  files: {
@@ -3161,7 +3892,7 @@ describe("share", () => {
3161
3892
  });
3162
3893
  expect(result.filesDeleted).toBe(1);
3163
3894
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/deleted.md");
3164
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3895
+ const journal = journalAt(journalPath);
3165
3896
  expect(journal.files["docs/deleted.md"]).toBeUndefined();
3166
3897
  });
3167
3898
  it("currency-gated: refuses delete + emits stale-etag event when remote moved since last sync", async () => {
@@ -3170,7 +3901,7 @@ describe("share", () => {
3170
3901
  // Another device modified `shared.md` after this device's last sync.
3171
3902
  // The journal still records the old etag; HEAD returns the new one.
3172
3903
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3173
- fs.writeFileSync(journalPath, JSON.stringify({
3904
+ seedJournalAt(journalPath, ({
3174
3905
  version: "1",
3175
3906
  lastSync: new Date().toISOString(),
3176
3907
  files: {
@@ -3202,7 +3933,7 @@ describe("share", () => {
3202
3933
  // No remote DELETE issued, journal entry preserved (pull will re-pull).
3203
3934
  expect(result.filesDeleted).toBe(0);
3204
3935
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3205
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3936
+ const journal = journalAt(journalPath);
3206
3937
  expect(journal.files["shared.md"]).toBeDefined();
3207
3938
  // Dedicated event fired so UIs surface the refusal. `reason` field
3208
3939
  // discriminates "stale-etag" (real drift) from "legacy-no-etag" so
@@ -3224,7 +3955,7 @@ describe("share", () => {
3224
3955
  fs.mkdirSync(companyRoot, { recursive: true });
3225
3956
  const recreatedPath = path.join(companyRoot, "racy.md");
3226
3957
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3227
- fs.writeFileSync(journalPath, JSON.stringify({
3958
+ seedJournalAt(journalPath, ({
3228
3959
  version: "2",
3229
3960
  lastSync: new Date().toISOString(),
3230
3961
  files: {
@@ -3260,7 +3991,7 @@ describe("share", () => {
3260
3991
  expect(result.filesDeleted).toBe(0);
3261
3992
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3262
3993
  expect(fs.readFileSync(recreatedPath, "utf-8")).toBe("recreated after planning");
3263
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3994
+ const journal = journalAt(journalPath);
3264
3995
  expect(journal.files["racy.md"]).toBeDefined();
3265
3996
  expect(events).toContainEqual(expect.objectContaining({
3266
3997
  type: "delete-refused-stale-etag",
@@ -3282,7 +4013,7 @@ describe("share", () => {
3282
4013
  // genuine local==remote sync, so propagating the delete would destroy the
3283
4014
  // (different) remote version. Must be REFUSED, not propagated.
3284
4015
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3285
- fs.writeFileSync(journalPath, JSON.stringify({
4016
+ seedJournalAt(journalPath, ({
3286
4017
  version: "2",
3287
4018
  lastSync: new Date().toISOString(),
3288
4019
  files: {
@@ -3317,7 +4048,7 @@ describe("share", () => {
3317
4048
  // Refusal short-circuits before the currency HEAD — no network needed.
3318
4049
  expect(headRemoteFile).not.toHaveBeenCalled();
3319
4050
  // Journal entry preserved, not silently dropped.
3320
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4051
+ const journal = journalAt(journalPath);
3321
4052
  expect(journal.files["core/policies/kept.md"]).toBeDefined();
3322
4053
  // Surfaced as a refusal with the divergent-local reason.
3323
4054
  const refused = events.find((e) => e.type === "delete-refused-stale-etag");
@@ -3334,7 +4065,7 @@ describe("share", () => {
3334
4065
  // S3 console). Local copy also missing. Currency-gated should drop the
3335
4066
  // journal entry without issuing a DELETE — the remote is already gone.
3336
4067
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3337
- fs.writeFileSync(journalPath, JSON.stringify({
4068
+ seedJournalAt(journalPath, ({
3338
4069
  version: "1",
3339
4070
  lastSync: new Date().toISOString(),
3340
4071
  files: {
@@ -3367,7 +4098,7 @@ describe("share", () => {
3367
4098
  // from real deletes without re-counting events.
3368
4099
  expect(result.filesTombstoned).toBe(1);
3369
4100
  expect(result.filesRefusedStale).toBe(0);
3370
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4101
+ const journal = journalAt(journalPath);
3371
4102
  expect(journal.files["removed-out-of-band.md"]).toBeUndefined();
3372
4103
  // Synthetic progress event carries the tombstone marker. Without this,
3373
4104
  // the tty stream renders tombstones identically to real deletes — operator
@@ -3388,7 +4119,7 @@ describe("share", () => {
3388
4119
  // against. Refuse the delete in the safe direction; a future sync with
3389
4120
  // a recorded etag can re-evaluate.
3390
4121
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3391
- fs.writeFileSync(journalPath, JSON.stringify({
4122
+ seedJournalAt(journalPath, ({
3392
4123
  version: "1",
3393
4124
  lastSync: new Date().toISOString(),
3394
4125
  files: {
@@ -3442,7 +4173,7 @@ describe("share", () => {
3442
4173
  const companyRoot = path.join(tmpDir, "companies", "acme");
3443
4174
  fs.mkdirSync(companyRoot, { recursive: true });
3444
4175
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3445
- fs.writeFileSync(journalPath, JSON.stringify({
4176
+ seedJournalAt(journalPath, ({
3446
4177
  version: "1",
3447
4178
  lastSync: new Date().toISOString(),
3448
4179
  files: {
@@ -3494,7 +4225,7 @@ describe("share", () => {
3494
4225
  const companyRoot = path.join(tmpDir, "companies", "acme");
3495
4226
  fs.mkdirSync(companyRoot, { recursive: true });
3496
4227
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3497
- fs.writeFileSync(journalPath, JSON.stringify({
4228
+ seedJournalAt(journalPath, ({
3498
4229
  version: "1",
3499
4230
  lastSync: new Date().toISOString(),
3500
4231
  files: {
@@ -3528,7 +4259,7 @@ describe("share", () => {
3528
4259
  const companyRoot = path.join(tmpDir, "companies", "acme");
3529
4260
  fs.mkdirSync(companyRoot, { recursive: true });
3530
4261
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3531
- fs.writeFileSync(journalPath, JSON.stringify({
4262
+ seedJournalAt(journalPath, ({
3532
4263
  version: "1",
3533
4264
  lastSync: new Date().toISOString(),
3534
4265
  files: {
@@ -3578,7 +4309,7 @@ describe("share", () => {
3578
4309
  fs.mkdirSync(companyRoot, { recursive: true });
3579
4310
  const recreatedPath = path.join(companyRoot, "notes", "gone.md");
3580
4311
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3581
- fs.writeFileSync(journalPath, JSON.stringify({
4312
+ seedJournalAt(journalPath, ({
3582
4313
  version: "1",
3583
4314
  lastSync: new Date().toISOString(),
3584
4315
  files: {
@@ -3626,7 +4357,7 @@ describe("share", () => {
3626
4357
  const companyRoot = path.join(tmpDir, "companies", "acme");
3627
4358
  fs.mkdirSync(companyRoot, { recursive: true });
3628
4359
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3629
- fs.writeFileSync(journalPath, JSON.stringify({
4360
+ seedJournalAt(journalPath, ({
3630
4361
  version: "1",
3631
4362
  lastSync: new Date().toISOString(),
3632
4363
  files: {
@@ -3656,14 +4387,14 @@ describe("share", () => {
3656
4387
  type: "delete-refused-stale-etag",
3657
4388
  reason: "legacy-no-etag",
3658
4389
  }));
3659
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4390
+ const journal = journalAt(journalPath);
3660
4391
  expect(journal.files["notes/legacy.md"]).toBeDefined();
3661
4392
  });
3662
4393
  it("owned-only still requires an intent — it is the documented rollback knob", async () => {
3663
4394
  const companyRoot = path.join(tmpDir, "companies", "acme");
3664
4395
  fs.mkdirSync(companyRoot, { recursive: true });
3665
4396
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3666
- fs.writeFileSync(journalPath, JSON.stringify({
4397
+ seedJournalAt(journalPath, ({
3667
4398
  version: "1",
3668
4399
  lastSync: new Date().toISOString(),
3669
4400
  files: {
@@ -3850,7 +4581,7 @@ describe("share", () => {
3850
4581
  const companyRoot = path.join(tmpDir, "companies", "acme");
3851
4582
  fs.mkdirSync(companyRoot, { recursive: true });
3852
4583
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3853
- fs.writeFileSync(journalPath, JSON.stringify({
4584
+ seedJournalAt(journalPath, ({
3854
4585
  version: "1",
3855
4586
  lastSync: new Date().toISOString(),
3856
4587
  files: {
@@ -3891,7 +4622,7 @@ describe("share", () => {
3891
4622
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "regular.md");
3892
4623
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md");
3893
4624
  // Both journal entries are removed by the delete loop's removeEntry call.
3894
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4625
+ const journal = journalAt(journalPath);
3895
4626
  expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeUndefined();
3896
4627
  expect(journal.files["regular.md"]).toBeUndefined();
3897
4628
  });
@@ -3906,7 +4637,7 @@ describe("share", () => {
3906
4637
  process.env.HQ_STATE_DIR = stateDirPersonal;
3907
4638
  try {
3908
4639
  const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
3909
- fs.writeFileSync(journalPath, JSON.stringify({
4640
+ seedJournalAt(journalPath, ({
3910
4641
  version: "1",
3911
4642
  lastSync: new Date().toISOString(),
3912
4643
  files: {
@@ -3932,7 +4663,7 @@ describe("share", () => {
3932
4663
  });
3933
4664
  expect(result.filesDeleted).toBe(1);
3934
4665
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/.obsidian/graph.json.drift-1779863862-42519");
3935
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4666
+ const journal = journalAt(journalPath);
3936
4667
  expect(journal.files["personal/.obsidian/graph.json.drift-1779863862-42519"]).toBeUndefined();
3937
4668
  }
3938
4669
  finally {
@@ -3955,7 +4686,7 @@ describe("share", () => {
3955
4686
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
3956
4687
  direction: "up",
3957
4688
  });
3958
- fs.writeFileSync(journalPath, JSON.stringify({
4689
+ seedJournalAt(journalPath, ({
3959
4690
  version: "1",
3960
4691
  lastSync: new Date().toISOString(),
3961
4692
  files: {
@@ -4003,7 +4734,7 @@ describe("share", () => {
4003
4734
  expect(pruned[0].byReason["ignore-filtered"]).toBe(3);
4004
4735
  expect(pruned[0].byReason["excluded-top-level"]).toBe(1);
4005
4736
  // The persisted journal converged: excluded rows gone, legit row kept.
4006
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4737
+ const journal = journalAt(journalPath);
4007
4738
  expect(Object.keys(journal.files)).toContain("personal/keep.md");
4008
4739
  expect(journal.files[".claude/worktrees/wt/a.ts"]).toBeUndefined();
4009
4740
  expect(journal.files["personal/node_modules/pkg/index.js"]).toBeUndefined();
@@ -4035,7 +4766,7 @@ describe("share", () => {
4035
4766
  const key = "deleted-top-level.md";
4036
4767
  const journalSlug = "personal-scoped-delete";
4037
4768
  const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
4038
- fs.writeFileSync(journalPath, JSON.stringify({
4769
+ seedJournalAt(journalPath, ({
4039
4770
  version: "2",
4040
4771
  lastSync: new Date().toISOString(),
4041
4772
  pulls: [],
@@ -4067,7 +4798,7 @@ describe("share", () => {
4067
4798
  });
4068
4799
  expect(result.filesDeleted).toBe(1);
4069
4800
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
4070
- expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
4801
+ expect(journalAt(journalPath).files[key]).toBeUndefined();
4071
4802
  });
4072
4803
  // ── Decommission prefixes ──────────────────────────────────────────────────
4073
4804
  //
@@ -4122,7 +4853,7 @@ describe("share", () => {
4122
4853
  // Local file remains on disk — decommission is a remote-only sweep.
4123
4854
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "notes.md"))).toBe(true);
4124
4855
  // Journal entry pruned in lockstep with the remote delete.
4125
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4856
+ const journalAfter = journalAt(personalJournalPath);
4126
4857
  expect(journalAfter.files["companies/foo/notes.md"]).toBeUndefined();
4127
4858
  expect(journalAfter.files["knowledge/x.md"]).toBeDefined();
4128
4859
  });
@@ -4158,7 +4889,7 @@ describe("share", () => {
4158
4889
  expect(deleteRemoteFile).not.toHaveBeenCalled();
4159
4890
  // Journal entry preserved — the pulled-from-peer record stays so a
4160
4891
  // future pull can still reconcile it correctly.
4161
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4892
+ const journalAfter = journalAt(personalJournalPath);
4162
4893
  expect(journalAfter.files["companies/foo/from-peer.md"]).toBeDefined();
4163
4894
  });
4164
4895
  it("decommissionPrefixes: dedupes with propagateDeletes plan — a key in both still emits one DeleteObject", async () => {
@@ -4278,7 +5009,7 @@ describe("share", () => {
4278
5009
  expect(result.filesTombstoned).toBe(1);
4279
5010
  expect(result.filesDeleted).toBe(0);
4280
5011
  // Journal entry is gone.
4281
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
5012
+ const journalAfter = journalAt(personalJournalPath);
4282
5013
  expect(journalAfter.files["companies/foo/gone.md"]).toBeUndefined();
4283
5014
  });
4284
5015
  describe("personalMode", () => {
@@ -4319,7 +5050,7 @@ describe("share", () => {
4319
5050
  const acmeJournalPath = path.join(stateDir, "sync-journal.acme.json");
4320
5051
  expect(fs.existsSync(personalJournalPath)).toBe(true);
4321
5052
  expect(fs.existsSync(acmeJournalPath)).toBe(false);
4322
- const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
5053
+ const journal = journalAt(personalJournalPath);
4323
5054
  expect(journal.files["knowledge/notes.md"]).toBeDefined();
4324
5055
  });
4325
5056
  it("personalMode=true accepts files outside companies/<slug>/ (companion to the company-folder rejection)", async () => {
@@ -4607,7 +5338,7 @@ describe("share", () => {
4607
5338
  .createHash("sha256")
4608
5339
  .update(target)
4609
5340
  .digest("hex");
4610
- fs.writeFileSync(journalPath, JSON.stringify({
5341
+ seedJournalAt(journalPath, ({
4611
5342
  version: "1",
4612
5343
  lastSync: new Date().toISOString(),
4613
5344
  files: {
@@ -5010,7 +5741,7 @@ describe("share", () => {
5010
5741
  });
5011
5742
  expect(pushed.filesUploaded).toBe(1);
5012
5743
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5013
- const afterPush = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5744
+ const afterPush = journalAt(journalPath);
5014
5745
  expect(Object.keys(afterPush.files)).toContain("knowledge/agents/brief.md");
5015
5746
  vi.mocked(deleteRemoteFile).mockClear();
5016
5747
  const walked = await share({
@@ -5024,7 +5755,7 @@ describe("share", () => {
5024
5755
  expect(walked.filesDeleted).toBe(0);
5025
5756
  const deletedKeys = vi.mocked(deleteRemoteFile).mock.calls.map((c) => c[1]);
5026
5757
  expect(deletedKeys).not.toContain("knowledge/agents/brief.md");
5027
- const afterWalk = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5758
+ const afterWalk = journalAt(journalPath);
5028
5759
  expect(Object.keys(afterWalk.files)).toContain("knowledge/agents/brief.md");
5029
5760
  });
5030
5761
  });
@@ -5068,7 +5799,7 @@ describe("share", () => {
5068
5799
  }
5069
5800
  }
5070
5801
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5071
- fs.writeFileSync(journalPath, JSON.stringify({
5802
+ seedJournalAt(journalPath, ({
5072
5803
  version: "1",
5073
5804
  lastSync: new Date().toISOString(),
5074
5805
  files,
@@ -5126,7 +5857,7 @@ describe("share", () => {
5126
5857
  expect(summary.samplePaths.length).toBeGreaterThan(0);
5127
5858
  expect(summary.samplePaths.length).toBeLessThanOrEqual(10);
5128
5859
  // Journal entries for the missing files SURVIVE — guard never mutates.
5129
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5860
+ const journal = journalAt(journalPath);
5130
5861
  expect(Object.keys(journal.files).length).toBe(100);
5131
5862
  // Per-key delete-refused-stale-etag events for each candidate.
5132
5863
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
@@ -5176,7 +5907,7 @@ describe("share", () => {
5176
5907
  expect(headedKeys).not.toContain(`f-${i.toString().padStart(4, "0")}.md`);
5177
5908
  }
5178
5909
  // Journal untouched — the entries stay recoverable.
5179
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5910
+ const journal = journalAt(journalPath);
5180
5911
  expect(Object.keys(journal.files).length).toBe(100);
5181
5912
  });
5182
5913
  // The core of the #227 regression, isolated: pre-#227 a trip emptied
@@ -5207,7 +5938,7 @@ describe("share", () => {
5207
5938
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" &&
5208
5939
  e.reason === "bulk-asymmetry");
5209
5940
  expect(perKeyRefusals.length).toBe(11);
5210
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5941
+ const journal = journalAt(journalPath);
5211
5942
  expect(Object.keys(journal.files).length).toBe(100);
5212
5943
  });
5213
5944
  it("vault litter still drains when the breaker trips", async () => {
@@ -5221,7 +5952,7 @@ describe("share", () => {
5221
5952
  withIntents: false,
5222
5953
  });
5223
5954
  const litterKey = "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md";
5224
- const seeded = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5955
+ const seeded = journalAt(journalPath);
5225
5956
  seeded.files[litterKey] = {
5226
5957
  hash: "h",
5227
5958
  size: 5,
@@ -5230,7 +5961,7 @@ describe("share", () => {
5230
5961
  remoteEtag: "litter-etag",
5231
5962
  kind: "file",
5232
5963
  };
5233
- fs.writeFileSync(journalPath, JSON.stringify(seeded));
5964
+ seedJournalAt(journalPath, seeded);
5234
5965
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
5235
5966
  etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
5236
5967
  lastModified: new Date(),
@@ -5370,7 +6101,7 @@ describe("share", () => {
5370
6101
  expect(summary).toBeDefined();
5371
6102
  expect(summary.ratio).toBeCloseTo(1.0, 5);
5372
6103
  // Journal untouched.
5373
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6104
+ const journal = journalAt(journalPath);
5374
6105
  expect(Object.keys(journal.files).length).toBe(50);
5375
6106
  });
5376
6107
  it("small absolute count (9/9 = 100% ratio but < MIN_ABS=10) does NOT trip: all 9 delete normally", async () => {
@@ -5414,7 +6145,7 @@ describe("share", () => {
5414
6145
  expect(deleteRemoteFile).not.toHaveBeenCalled();
5415
6146
  expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeDefined();
5416
6147
  // Journal entries survive.
5417
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6148
+ const journal = journalAt(journalPath);
5418
6149
  expect(Object.keys(journal.files).length).toBe(100);
5419
6150
  });
5420
6151
  it("direction:'down' entries don't count toward the bulk numerator (owned-only silently skips them)", async () => {
@@ -5442,7 +6173,7 @@ describe("share", () => {
5442
6173
  localDeleteIntent: deleteIntent("up-etag", "h"),
5443
6174
  };
5444
6175
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5445
- fs.writeFileSync(journalPath, JSON.stringify({ version: "1", lastSync: now, files }));
6176
+ seedJournalAt(journalPath, { version: "1", lastSync: now, files });
5446
6177
  const result = await share({
5447
6178
  paths: [companyRoot],
5448
6179
  company: "acme",
@@ -5565,7 +6296,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5565
6296
  fs.mkdirSync(companyRoot, { recursive: true });
5566
6297
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5567
6298
  // v2-shaped fixture — same field set the production journal uses.
5568
- fs.writeFileSync(journalPath, JSON.stringify({
6299
+ seedJournalAt(journalPath, ({
5569
6300
  version: "2",
5570
6301
  lastSync: new Date().toISOString(),
5571
6302
  files: {
@@ -5611,7 +6342,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5611
6342
  const { hashFile } = await import("../journal.js");
5612
6343
  const baselineHash = hashFile(testFile);
5613
6344
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5614
- fs.writeFileSync(journalPath, JSON.stringify({
6345
+ seedJournalAt(journalPath, ({
5615
6346
  version: "2",
5616
6347
  lastSync: new Date().toISOString(),
5617
6348
  files: {
@@ -5652,7 +6383,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5652
6383
  const originalLstat = fs.lstatSync(testFile);
5653
6384
  // The recorded hash intentionally differs from the current bytes.
5654
6385
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5655
- fs.writeFileSync(journalPath, JSON.stringify({
6386
+ seedJournalAt(journalPath, ({
5656
6387
  version: "2",
5657
6388
  lastSync: new Date().toISOString(),
5658
6389
  files: {
@@ -5699,7 +6430,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5699
6430
  const { hashFile } = await import("../journal.js");
5700
6431
  const realHash = hashFile(testFile);
5701
6432
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5702
- fs.writeFileSync(journalPath, JSON.stringify({
6433
+ seedJournalAt(journalPath, ({
5703
6434
  version: "2",
5704
6435
  lastSync: new Date().toISOString(),
5705
6436
  files: {
@@ -5749,7 +6480,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5749
6480
  const { hashFile } = await import("../journal.js");
5750
6481
  const realHash = hashFile(testFile);
5751
6482
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5752
- fs.writeFileSync(journalPath, JSON.stringify({
6483
+ seedJournalAt(journalPath, ({
5753
6484
  version: "2",
5754
6485
  lastSync: new Date().toISOString(),
5755
6486
  files: {
@@ -5781,7 +6512,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5781
6512
  expect(uploadFile).not.toHaveBeenCalled();
5782
6513
  // Journal mtimeMs is now the bumped value, while content hashing remains
5783
6514
  // authoritative on the next sync.
5784
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6515
+ const journalAfter = journalAt(journalPath);
5785
6516
  expect(journalAfter.files["touched-then-fast.md"].mtimeMs).toBe(bumpedLstat.mtimeMs);
5786
6517
  expect(journalAfter.files["touched-then-fast.md"].size).toBe(bumpedLstat.size);
5787
6518
  // Content hash untouched — the file genuinely didn't change.
@@ -5801,7 +6532,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5801
6532
  });
5802
6533
  expect(result.filesUploaded).toBe(1);
5803
6534
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5804
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6535
+ const journal = journalAt(journalPath);
5805
6536
  expect(journal.files["fresh.md"]).toBeDefined();
5806
6537
  expect(journal.files["fresh.md"].mtimeMs).toBe(lstat.mtimeMs);
5807
6538
  expect(journal.files["fresh.md"].size).toBe(lstat.size);
@@ -5818,7 +6549,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5818
6549
  const realSize = fs.statSync(testFile).size;
5819
6550
  // Real hash but NO mtimeMs. Hash matches → skip upload.
5820
6551
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5821
- fs.writeFileSync(journalPath, JSON.stringify({
6552
+ seedJournalAt(journalPath, ({
5822
6553
  version: "1",
5823
6554
  lastSync: new Date().toISOString(),
5824
6555
  files: {
@@ -6377,7 +7108,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6377
7108
  // AND lost their etag/content-hash/stat journal metadata.
6378
7109
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6379
7110
  expect(fs.existsSync(journalPath)).toBe(true);
6380
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7111
+ const journal = journalAt(journalPath);
6381
7112
  const stampedKeys = Object.keys(journal.files);
6382
7113
  expect(stampedKeys.length).toBe(FILE_COUNT - 1);
6383
7114
  // The failed file is NOT in the journal (no successful upload).
@@ -6525,7 +7256,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6525
7256
  const companyRoot = path.join(tmpDir, "companies", "acme");
6526
7257
  fs.mkdirSync(companyRoot, { recursive: true });
6527
7258
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6528
- fs.writeFileSync(journalPath, JSON.stringify({
7259
+ seedJournalAt(journalPath, ({
6529
7260
  version: "1",
6530
7261
  lastSync: new Date().toISOString(),
6531
7262
  files: {
@@ -6554,7 +7285,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6554
7285
  expect(headRemoteFile).not.toHaveBeenCalled();
6555
7286
  expect(deleteRemoteFile).not.toHaveBeenCalled();
6556
7287
  expect(uploadFile).not.toHaveBeenCalled();
6557
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7288
+ const journal = journalAt(journalPath);
6558
7289
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
6559
7290
  });
6560
7291
  it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
@@ -6566,7 +7297,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6566
7297
  fs.mkdirSync(targetDir, { recursive: true });
6567
7298
  fs.symlinkSync(targetDir, linkPath, "dir");
6568
7299
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6569
- fs.writeFileSync(journalPath, JSON.stringify({
7300
+ seedJournalAt(journalPath, ({
6570
7301
  version: "1",
6571
7302
  lastSync: new Date().toISOString(),
6572
7303
  files: {
@@ -6611,7 +7342,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6611
7342
  samplePaths: [linkKey],
6612
7343
  });
6613
7344
  expect(events.some((event) => event.type === "error")).toBe(false);
6614
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7345
+ const journal = journalAt(journalPath);
6615
7346
  expect(journal.files[linkKey]).toBeDefined();
6616
7347
  }
6617
7348
  finally {