@indigoai-us/hq-cloud 6.15.20 → 6.15.22

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 (48) hide show
  1. package/dist/bin/sync-runner.d.ts +1 -1
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +37 -5
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +77 -4
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/share.d.ts +36 -1
  8. package/dist/cli/share.d.ts.map +1 -1
  9. package/dist/cli/share.js +139 -20
  10. package/dist/cli/share.js.map +1 -1
  11. package/dist/cli/share.test.js +360 -108
  12. package/dist/cli/share.test.js.map +1 -1
  13. package/dist/cli/sync.d.ts.map +1 -1
  14. package/dist/cli/sync.js +6 -0
  15. package/dist/cli/sync.js.map +1 -1
  16. package/dist/cli/sync.test.js +59 -47
  17. package/dist/cli/sync.test.js.map +1 -1
  18. package/dist/journal-delta-writes.test.d.ts +2 -0
  19. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  20. package/dist/journal-delta-writes.test.js +140 -0
  21. package/dist/journal-delta-writes.test.js.map +1 -0
  22. package/dist/journal.d.ts +1 -0
  23. package/dist/journal.d.ts.map +1 -1
  24. package/dist/journal.js +155 -25
  25. package/dist/journal.js.map +1 -1
  26. package/dist/journal.test.js +29 -11
  27. package/dist/journal.test.js.map +1 -1
  28. package/dist/outcome-telemetry.d.ts +7 -1
  29. package/dist/outcome-telemetry.d.ts.map +1 -1
  30. package/dist/outcome-telemetry.js +15 -9
  31. package/dist/outcome-telemetry.js.map +1 -1
  32. package/dist/outcome-telemetry.test.js +15 -0
  33. package/dist/outcome-telemetry.test.js.map +1 -1
  34. package/dist/skill-telemetry.d.ts +6 -1
  35. package/dist/skill-telemetry.d.ts.map +1 -1
  36. package/dist/skill-telemetry.js +14 -8
  37. package/dist/skill-telemetry.js.map +1 -1
  38. package/dist/skill-telemetry.test.js +43 -0
  39. package/dist/skill-telemetry.test.js.map +1 -1
  40. package/dist/telemetry.d.ts +7 -1
  41. package/dist/telemetry.d.ts.map +1 -1
  42. package/dist/telemetry.js +67 -60
  43. package/dist/telemetry.js.map +1 -1
  44. package/dist/telemetry.test.js +12 -0
  45. package/dist/telemetry.test.js.map +1 -1
  46. package/dist/types.d.ts +17 -3
  47. package/dist/types.d.ts.map +1 -1
  48. package/package.json +1 -1
@@ -55,7 +55,39 @@ import * as readline from "readline";
55
55
  import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
56
56
  import { deleteRemoteFile, downloadFile, headRemoteFile, 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
+ }
59
91
  const mockConfig = {
60
92
  apiUrl: "https://vault-api.test",
61
93
  authToken: "test-jwt-token",
@@ -528,7 +560,7 @@ describe("share", () => {
528
560
  const { hashFile } = await import("../journal.js");
529
561
  const hash = hashFile(testFile);
530
562
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
531
- fs.writeFileSync(journalPath, JSON.stringify({
563
+ seedJournalAt(journalPath, ({
532
564
  version: "1",
533
565
  lastSync: new Date().toISOString(),
534
566
  files: {
@@ -559,7 +591,7 @@ describe("share", () => {
559
591
  // Journal has a stale hash for this path — simulating "local has been
560
592
  // edited since the last push".
561
593
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
562
- fs.writeFileSync(journalPath, JSON.stringify({
594
+ seedJournalAt(journalPath, ({
563
595
  version: "1",
564
596
  lastSync: new Date().toISOString(),
565
597
  files: {
@@ -581,6 +613,226 @@ describe("share", () => {
581
613
  expect(result.filesUploaded).toBe(1);
582
614
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md", undefined, expect.anything());
583
615
  });
616
+ // ── Stat fast path for change detection ─────────────────────────────────
617
+ // Push used to hash EVERY regular file every pass before deciding whether to
618
+ // skip it — half a million reads on a real vault — because stat metadata was
619
+ // considered untrustworthy: a tool can rewrite a file to the same length and
620
+ // restore its mtime, and a size+mtime check would then skip that edit
621
+ // forever.
622
+ //
623
+ // These tests pin the narrower claim the fast path actually rests on: size,
624
+ // mtime AND ctime together. ctime is the inode-change stamp, and userspace
625
+ // cannot restore it — `cp -p`, `touch -r` and `rsync --times` all reset mtime
626
+ // while ctime moves to now. So the same-length-plus-restored-mtime rewrite
627
+ // that the old comment warned about is still caught, without reading a byte.
628
+ //
629
+ // Differential proof: drop ctime from the comparison and the "restored mtime"
630
+ // test below fails — the edit is skipped and never uploaded.
631
+ describe("stat fast path for change detection", () => {
632
+ const CONTENT_A = "aaaaaaaaaa"; // 10 bytes
633
+ const CONTENT_B = "bbbbbbbbbb"; // 10 bytes — same length, different bytes
634
+ /** Write `body` at `key` under the acme company root; return its path. */
635
+ function writeFile(key, body) {
636
+ const companyRoot = path.join(tmpDir, "companies", "acme");
637
+ fs.mkdirSync(companyRoot, { recursive: true });
638
+ const abs = path.join(companyRoot, key);
639
+ fs.writeFileSync(abs, body);
640
+ return abs;
641
+ }
642
+ function seed(key, entry) {
643
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
644
+ version: "2",
645
+ lastSync: new Date().toISOString(),
646
+ files: { [key]: { syncedAt: new Date().toISOString(), direction: "up", ...entry } },
647
+ pulls: [],
648
+ });
649
+ }
650
+ function push(abs, onConflict) {
651
+ return share({
652
+ paths: [abs],
653
+ company: "acme",
654
+ vaultConfig: mockConfig,
655
+ hqRoot: tmpDir,
656
+ skipUnchanged: true,
657
+ ...(onConflict ? { onConflict } : {}),
658
+ });
659
+ }
660
+ it("skips a file whose size, mtime and ctime all match — without reading it", async () => {
661
+ const abs = writeFile("untouched.md", CONTENT_A);
662
+ const stat = fs.lstatSync(abs);
663
+ // The recorded hash is deliberately WRONG. Hashing the file would find a
664
+ // mismatch and upload, so a skip here can only mean the bytes were never
665
+ // read — which is the whole point of the fast path.
666
+ seed("untouched.md", {
667
+ hash: "deliberately-not-the-hash-of-this-file",
668
+ size: stat.size,
669
+ mtimeMs: stat.mtimeMs,
670
+ ctimeMs: stat.ctimeMs,
671
+ });
672
+ const result = await push(abs);
673
+ expect(result.filesSkipped).toBe(1);
674
+ expect(result.filesUploaded).toBe(0);
675
+ expect(uploadFile).not.toHaveBeenCalled();
676
+ });
677
+ it("re-reads when the size differs", async () => {
678
+ const abs = writeFile("resized.md", CONTENT_A + "extra");
679
+ const stat = fs.lstatSync(abs);
680
+ seed("resized.md", {
681
+ hash: hashFile(abs) + "-stale",
682
+ size: CONTENT_A.length, // shorter than what is on disk now
683
+ mtimeMs: stat.mtimeMs,
684
+ ctimeMs: stat.ctimeMs,
685
+ });
686
+ const result = await push(abs);
687
+ expect(result.filesUploaded).toBe(1);
688
+ expect(uploadFile).toHaveBeenCalled();
689
+ });
690
+ it("re-reads when the mtime differs", async () => {
691
+ const abs = writeFile("retimed.md", CONTENT_B);
692
+ const stat = fs.lstatSync(abs);
693
+ seed("retimed.md", {
694
+ hash: "hash-of-the-previous-body",
695
+ size: stat.size, // same length
696
+ mtimeMs: stat.mtimeMs - 60_000, // journal remembers an older edit time
697
+ ctimeMs: stat.ctimeMs,
698
+ });
699
+ const result = await push(abs);
700
+ expect(result.filesUploaded).toBe(1);
701
+ expect(uploadFile).toHaveBeenCalled();
702
+ });
703
+ it("re-reads a same-length rewrite whose mtime was restored — ctime still moves", async () => {
704
+ // The exact case the old hash-everything comment existed to defend
705
+ // against, as produced by `cp -p`, `touch -r` or `rsync --times`.
706
+ const abs = writeFile("mtime-restored.md", CONTENT_A);
707
+ const before = fs.lstatSync(abs);
708
+ await new Promise((resolve) => setTimeout(resolve, 5));
709
+ fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
710
+ fs.utimesSync(abs, before.atime, before.mtime); // put the old mtime back
711
+ const after = fs.lstatSync(abs);
712
+ // Preconditions. The disguise worked on size, and as closely on mtime as
713
+ // the OS allows — `utimes` takes whole milliseconds, so a restored stamp
714
+ // lands within 1ms of the original rather than exactly on it. It failed
715
+ // on ctime, which is the only signal left.
716
+ expect(after.size).toBe(before.size);
717
+ expect(Math.abs(after.mtimeMs - before.mtimeMs)).toBeLessThan(1);
718
+ expect(after.ctimeMs).not.toBe(before.ctimeMs);
719
+ // Seed the mtime that is actually on disk, so mtime matches EXACTLY and
720
+ // cannot be what triggers the re-read. Only the stale ctime can.
721
+ seed("mtime-restored.md", {
722
+ hash: "hash-of-content-a",
723
+ size: before.size,
724
+ mtimeMs: after.mtimeMs,
725
+ ctimeMs: before.ctimeMs,
726
+ });
727
+ const result = await push(abs);
728
+ expect(result.filesUploaded).toBe(1);
729
+ expect(uploadFile).toHaveBeenCalled();
730
+ });
731
+ it("falls back to reading when the journal predates stat tracking, then stamps it", async () => {
732
+ // Entries written before ctime was recorded carry no stat metadata. They
733
+ // must not be skipped on faith; they are read once and stamped so the
734
+ // next pass takes the fast path.
735
+ const abs = writeFile("legacy.md", CONTENT_A);
736
+ seed("legacy.md", { hash: hashFile(abs), size: CONTENT_A.length });
737
+ const result = await push(abs);
738
+ expect(result.filesSkipped).toBe(1);
739
+ expect(uploadFile).not.toHaveBeenCalled();
740
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["legacy.md"];
741
+ const stat = fs.lstatSync(abs);
742
+ expect(entry.mtimeMs).toBe(stat.mtimeMs);
743
+ expect(entry.ctimeMs).toBe(stat.ctimeMs);
744
+ });
745
+ it("never lets the fast path skip a localDiverges entry", async () => {
746
+ // A pull-kept divergent entry stores the LOCAL hash and local stat, so
747
+ // every field matches by construction. Skipping on that asserts "local
748
+ // matches remote", which is exactly what the flag exists to deny.
749
+ const abs = writeFile("divergent.md", CONTENT_A);
750
+ const stat = fs.lstatSync(abs);
751
+ seed("divergent.md", {
752
+ hash: hashFile(abs),
753
+ size: stat.size,
754
+ mtimeMs: stat.mtimeMs,
755
+ ctimeMs: stat.ctimeMs,
756
+ localDiverges: true,
757
+ });
758
+ // `keep` so the divergence resolves without an interactive prompt. Under
759
+ // `keep` the file is deliberately NOT uploaded, so the proof that the
760
+ // fast path did not swallow it is that it reached Stage 2 at all: a
761
+ // fast-path skip returns before any remote HEAD is issued.
762
+ await push(abs, "keep");
763
+ expect(headRemoteFile).toHaveBeenCalled();
764
+ });
765
+ it("stamps size, mtime and ctime on upload so the next pass can skip", async () => {
766
+ const abs = writeFile("fresh.md", CONTENT_A);
767
+ const result = await push(abs);
768
+ expect(result.filesUploaded).toBe(1);
769
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["fresh.md"];
770
+ const stat = fs.lstatSync(abs);
771
+ expect(entry.size).toBe(stat.size);
772
+ expect(entry.mtimeMs).toBe(stat.mtimeMs);
773
+ expect(entry.ctimeMs).toBe(stat.ctimeMs);
774
+ });
775
+ it("does not stamp ctime when the file changes between the hash and the stamp", async () => {
776
+ // Codex P1. Stage 1 hashes the file; the stamp happens later, after the
777
+ // upload. If the bytes change in between, pairing the OLD hash with the
778
+ // NEW timestamps would make every later pass skip the new body on stat
779
+ // alone — the edit would never upload. Withholding ctime forces the next
780
+ // pass to hash, which is the pre-fast-path behaviour.
781
+ const abs = writeFile("edited-mid-flight.md", CONTENT_A);
782
+ // Land the edit inside the exact window: Stage 1 has hashed the file, and
783
+ // the asynchronous remote probe runs before the stamp is taken.
784
+ let mutated = false;
785
+ vi.mocked(headRemoteFile).mockImplementation(async () => {
786
+ if (!mutated) {
787
+ mutated = true;
788
+ fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
789
+ }
790
+ return null;
791
+ });
792
+ await push(abs);
793
+ const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["edited-mid-flight.md"];
794
+ expect(entry.ctimeMs).toBeUndefined();
795
+ // Proof it is not stranded: the next pass reads it and ships the new body.
796
+ vi.mocked(uploadFile).mockClear();
797
+ const second = await push(abs);
798
+ expect(second.filesUploaded).toBe(1);
799
+ expect(uploadFile).toHaveBeenCalled();
800
+ });
801
+ it("distrusts whole-second timestamps as a coarse-filesystem signal", () => {
802
+ // Codex P2. FAT/exFAT, some FUSE mounts and cached network filesystems
803
+ // report timestamps rounded to whole seconds, so a same-size rewrite
804
+ // inside one tick can leave both mtime and ctime equal to the journal's
805
+ // and be skipped indefinitely. Every filesystem HQ can trust here (APFS,
806
+ // ext4, NTFS) records sub-second precision, so a pair that is exactly on
807
+ // the second is treated as untrustworthy and sent back to hashing.
808
+ const abs = writeFile("coarse.md", CONTENT_A);
809
+ const coarse = { size: CONTENT_A.length, mtimeMs: 1_700_000_000_000, ctimeMs: 1_700_000_000_000 };
810
+ const spy = vi.spyOn(fs, "lstatSync").mockReturnValue({
811
+ isFile: () => true,
812
+ ...coarse,
813
+ });
814
+ try {
815
+ expect(shareTesting.statMatchesJournal(abs, {
816
+ hash: "h",
817
+ direction: "up",
818
+ syncedAt: new Date().toISOString(),
819
+ ...coarse,
820
+ })).toBe(false);
821
+ // Same values, one of them sub-second: a real filesystem, so trusted.
822
+ const fine = { ...coarse, ctimeMs: coarse.ctimeMs + 0.5 };
823
+ spy.mockReturnValue({ isFile: () => true, ...fine });
824
+ expect(shareTesting.statMatchesJournal(abs, {
825
+ hash: "h",
826
+ direction: "up",
827
+ syncedAt: new Date().toISOString(),
828
+ ...fine,
829
+ })).toBe(true);
830
+ }
831
+ finally {
832
+ spy.mockRestore();
833
+ }
834
+ });
835
+ });
584
836
  // ── Push-side FILE_TOMBSTONE consult (delete-resync, 3B) ────────────────────
585
837
  // An authoritative delete (`hq files delete`) writes a FILE_TOMBSTONE and
586
838
  // removes the S3 object. Without a push-side consult, a behind peer that still
@@ -593,7 +845,7 @@ describe("share", () => {
593
845
  // share.ts makes the first test fail (uploadFile IS called → resurrection).
594
846
  describe("push-side tombstone consult", () => {
595
847
  function seedJournal(key, hash, size) {
596
- fs.writeFileSync(path.join(stateDir, "sync-journal.acme.json"), JSON.stringify({
848
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), ({
597
849
  version: "1",
598
850
  lastSync: new Date().toISOString(),
599
851
  files: {
@@ -737,7 +989,7 @@ describe("share", () => {
737
989
  size: 99,
738
990
  });
739
991
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
740
- fs.writeFileSync(journalPath, JSON.stringify({
992
+ seedJournalAt(journalPath, ({
741
993
  version: "1",
742
994
  lastSync: new Date().toISOString(),
743
995
  files: {
@@ -789,7 +1041,7 @@ describe("share", () => {
789
1041
  // assert it is never consulted below — the skip is cheap, and a leftover
790
1042
  // mockResolvedValueOnce would otherwise leak into a later test's HEAD queue.
791
1043
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
792
- fs.writeFileSync(journalPath, JSON.stringify({
1044
+ seedJournalAt(journalPath, ({
793
1045
  version: "1",
794
1046
  lastSync: new Date().toISOString(),
795
1047
  files: {
@@ -969,7 +1221,7 @@ describe("share", () => {
969
1221
  direction: "push",
970
1222
  });
971
1223
  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"));
1224
+ const journal = readJournal("acme");
973
1225
  expect(journal.files["kms-scaffold.md"]).toMatchObject({
974
1226
  direction: "up",
975
1227
  remoteEtag: "288bfa91684ae21f9ae27c061254d412",
@@ -1085,7 +1337,7 @@ describe("share", () => {
1085
1337
  size: 5,
1086
1338
  });
1087
1339
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1088
- fs.writeFileSync(journalPath, JSON.stringify({
1340
+ seedJournalAt(journalPath, ({
1089
1341
  version: "1",
1090
1342
  lastSync: syncedAt,
1091
1343
  files: {
@@ -1158,7 +1410,7 @@ describe("share", () => {
1158
1410
  // No journal stamp for the failed PUT — next pass re-evaluates fresh.
1159
1411
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1160
1412
  if (fs.existsSync(journalPath)) {
1161
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1413
+ const journal = journalAt(journalPath);
1162
1414
  expect(journal.files?.["raced.md"]).toBeUndefined();
1163
1415
  }
1164
1416
  });
@@ -1240,7 +1492,7 @@ describe("share", () => {
1240
1492
  return { etag: '"should-not-land"' };
1241
1493
  });
1242
1494
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1243
- fs.writeFileSync(journalPath, JSON.stringify({
1495
+ seedJournalAt(journalPath, ({
1244
1496
  version: "1",
1245
1497
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1246
1498
  files: {
@@ -1300,7 +1552,7 @@ describe("share", () => {
1300
1552
  });
1301
1553
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
1302
1554
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1303
- fs.writeFileSync(journalPath, JSON.stringify({
1555
+ seedJournalAt(journalPath, ({
1304
1556
  version: "1",
1305
1557
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1306
1558
  files: {
@@ -1335,7 +1587,7 @@ describe("share", () => {
1335
1587
  size: 20,
1336
1588
  });
1337
1589
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1338
- fs.writeFileSync(journalPath, JSON.stringify({
1590
+ seedJournalAt(journalPath, ({
1339
1591
  version: "1",
1340
1592
  lastSync: new Date().toISOString(),
1341
1593
  files: {
@@ -1377,7 +1629,7 @@ describe("share", () => {
1377
1629
  size: 40,
1378
1630
  });
1379
1631
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1380
- fs.writeFileSync(journalPath, JSON.stringify({
1632
+ seedJournalAt(journalPath, ({
1381
1633
  version: "1",
1382
1634
  lastSync: new Date().toISOString(),
1383
1635
  files: {
@@ -1423,7 +1675,7 @@ describe("share", () => {
1423
1675
  });
1424
1676
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"forced-etag"' });
1425
1677
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1426
- fs.writeFileSync(journalPath, JSON.stringify({
1678
+ seedJournalAt(journalPath, ({
1427
1679
  version: "1",
1428
1680
  lastSync: new Date().toISOString(),
1429
1681
  files: {
@@ -1506,7 +1758,7 @@ describe("share", () => {
1506
1758
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1507
1759
  // The entry is rewritten as an ordinary upload: local is now authority,
1508
1760
  // so the journal-honesty flag must be gone rather than re-armed.
1509
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1761
+ const journal = journalAt(journalPath);
1510
1762
  expect(journal.files["publish-divergent.md"].direction).toBe("up");
1511
1763
  expect(journal.files["publish-divergent.md"].localDiverges).toBeFalsy();
1512
1764
  expect(journal.files["publish-divergent.md"].remoteEtag).toBe("published-etag");
@@ -1559,7 +1811,7 @@ describe("share", () => {
1559
1811
  expect(result.conflictPaths).toContain("publish-raced.md");
1560
1812
  // Consent was for a remote that no longer exists, so the flag stays put
1561
1813
  // and the operator is asked again rather than silently overruled.
1562
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1814
+ const journal = journalAt(journalPath);
1563
1815
  expect(journal.files["publish-raced.md"].localDiverges).toBe(true);
1564
1816
  expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1565
1817
  });
@@ -1614,7 +1866,7 @@ describe("share", () => {
1614
1866
  });
1615
1867
  expect(result.filesUploaded).toBe(1);
1616
1868
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1617
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1869
+ const journal = journalAt(journalPath);
1618
1870
  expect(journal.files["stuck-under-skip-unchanged.md"].direction).toBe("up");
1619
1871
  expect(journal.files["stuck-under-skip-unchanged.md"].localDiverges).toBeFalsy();
1620
1872
  });
@@ -1704,7 +1956,7 @@ describe("share", () => {
1704
1956
  expect(uploadFile).not.toHaveBeenCalled();
1705
1957
  expect(result.filesUploaded).toBe(0);
1706
1958
  expect(result.conflictPaths).toContain("peer-deleted.md");
1707
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1959
+ const journal = journalAt(journalPath);
1708
1960
  expect(journal.files["peer-deleted.md"].localDiverges).toBe(true);
1709
1961
  expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1710
1962
  });
@@ -1765,7 +2017,7 @@ describe("share", () => {
1765
2017
  vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1766
2018
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"recreated-etag"' });
1767
2019
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1768
- fs.writeFileSync(journalPath, JSON.stringify({
2020
+ seedJournalAt(journalPath, ({
1769
2021
  version: "1",
1770
2022
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1771
2023
  files: {
@@ -1795,7 +2047,7 @@ describe("share", () => {
1795
2047
  const pc = vi.mocked(uploadFile).mock.calls[0][4];
1796
2048
  expect(pc?.ifMatch).toBeUndefined();
1797
2049
  expect(pc?.ifNoneMatch).toBe("*");
1798
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2050
+ const journal = journalAt(journalPath);
1799
2051
  expect(journal.files["recreate-deleted.md"]).toMatchObject({
1800
2052
  hash: localHash,
1801
2053
  remoteEtag: "recreated-etag",
@@ -1816,7 +2068,7 @@ describe("share", () => {
1816
2068
  return undefined;
1817
2069
  });
1818
2070
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1819
- fs.writeFileSync(journalPath, JSON.stringify({
2071
+ seedJournalAt(journalPath, ({
1820
2072
  version: "1",
1821
2073
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1822
2074
  files: {
@@ -1848,7 +2100,7 @@ describe("share", () => {
1848
2100
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
1849
2101
  // Journal not advanced to a successful upload etag (entry may still be
1850
2102
  // the prior baseline — next pass re-evaluates).
1851
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2103
+ const journal = journalAt(journalPath);
1852
2104
  expect(journal.files["race-recreate.md"]?.remoteEtag).toBe("etag-before-delete");
1853
2105
  expect(journal.files["race-recreate.md"]?.hash).toBe("old-hash");
1854
2106
  expect(fs.readFileSync(testFile, "utf-8")).toBe("my recreated local");
@@ -2172,7 +2424,7 @@ describe("share", () => {
2172
2424
  size: 5,
2173
2425
  });
2174
2426
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2175
- fs.writeFileSync(journalPath, JSON.stringify({
2427
+ seedJournalAt(journalPath, ({
2176
2428
  version: "1",
2177
2429
  lastSync: syncedAt,
2178
2430
  files: {
@@ -2212,7 +2464,7 @@ describe("share", () => {
2212
2464
  size: 5,
2213
2465
  });
2214
2466
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2215
- fs.writeFileSync(journalPath, JSON.stringify({
2467
+ seedJournalAt(journalPath, ({
2216
2468
  version: "1",
2217
2469
  lastSync: syncedAt,
2218
2470
  files: {
@@ -2248,7 +2500,7 @@ describe("share", () => {
2248
2500
  hqRoot: tmpDir,
2249
2501
  });
2250
2502
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2251
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2503
+ const journal = journalAt(journalPath);
2252
2504
  expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
2253
2505
  });
2254
2506
  it("forwards UploadAuthor to uploadFile when present (created-by metadata)", async () => {
@@ -2293,7 +2545,7 @@ describe("share", () => {
2293
2545
  const { hashFile } = await import("../journal.js");
2294
2546
  const hash = hashFile(testFile);
2295
2547
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2296
- fs.writeFileSync(journalPath, JSON.stringify({
2548
+ seedJournalAt(journalPath, ({
2297
2549
  version: "1",
2298
2550
  lastSync: new Date().toISOString(),
2299
2551
  files: {
@@ -2513,7 +2765,7 @@ describe("share", () => {
2513
2765
  .update("stable content")
2514
2766
  .digest("hex");
2515
2767
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2516
- fs.writeFileSync(journalPath, JSON.stringify({
2768
+ seedJournalAt(journalPath, ({
2517
2769
  version: "1",
2518
2770
  lastSync: new Date().toISOString(),
2519
2771
  files: {
@@ -2574,7 +2826,7 @@ describe("share", () => {
2574
2826
  // not be queued for delete.
2575
2827
  fs.writeFileSync(path.join(tmpDir, ".hqinclude"), "companies/*/knowledge/\n");
2576
2828
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2577
- fs.writeFileSync(journalPath, JSON.stringify({
2829
+ seedJournalAt(journalPath, ({
2578
2830
  version: "1",
2579
2831
  lastSync: new Date().toISOString(),
2580
2832
  files: {
@@ -2611,7 +2863,7 @@ describe("share", () => {
2611
2863
  operation: "delete",
2612
2864
  });
2613
2865
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
2614
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2866
+ const journal = journalAt(journalPath);
2615
2867
  expect(journal.files["knowledge"]).toBeUndefined();
2616
2868
  });
2617
2869
  it("propagateDeletes: a dangling local symlink is NOT classified as gone (lstat, not existsSync)", async () => {
@@ -2628,7 +2880,7 @@ describe("share", () => {
2628
2880
  const danglingLink = path.join(companyRoot, "dangling-link.md");
2629
2881
  fs.symlinkSync("./missing-target.md", danglingLink);
2630
2882
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2631
- fs.writeFileSync(journalPath, JSON.stringify({
2883
+ seedJournalAt(journalPath, ({
2632
2884
  version: "1",
2633
2885
  lastSync: new Date().toISOString(),
2634
2886
  files: {
@@ -2658,7 +2910,7 @@ describe("share", () => {
2658
2910
  expect(result.filesDeleted).toBe(0);
2659
2911
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2660
2912
  // Journal entry must survive the run.
2661
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2913
+ const journal = journalAt(journalPath);
2662
2914
  expect(journal.files["dangling-link.md"]).toBeDefined();
2663
2915
  });
2664
2916
  it("vault-litter drain: a `.drift-*` marker with direction:'down' and NO delete intent tombstones " +
@@ -2723,7 +2975,7 @@ describe("share", () => {
2723
2975
  });
2724
2976
  expect(result.filesDeleted).toBe(1);
2725
2977
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/scripts/run-project.sh.drift-1779863862-42519");
2726
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2978
+ const journal = journalAt(journalPath);
2727
2979
  expect(journal.files["personal/scripts/run-project.sh.drift-1779863862-42519"]).toBeUndefined();
2728
2980
  }
2729
2981
  finally {
@@ -2796,7 +3048,7 @@ describe("share", () => {
2796
3048
  fs.mkdirSync(companyRoot, { recursive: true });
2797
3049
  // No file on disk under companies/acme/peer-uploaded.md → local missing.
2798
3050
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2799
- fs.writeFileSync(journalPath, JSON.stringify({
3051
+ seedJournalAt(journalPath, ({
2800
3052
  version: "1",
2801
3053
  lastSync: new Date().toISOString(),
2802
3054
  files: {
@@ -2821,7 +3073,7 @@ describe("share", () => {
2821
3073
  });
2822
3074
  expect(result.filesDeleted).toBe(0);
2823
3075
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2824
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3076
+ const journal = journalAt(journalPath);
2825
3077
  expect(journal.files["peer-uploaded.md"]).toBeDefined();
2826
3078
  });
2827
3079
  it("ROLLBACK KNOB: owned-only is honored on a prs_ personal vault — an intent-less entry with a " +
@@ -2912,7 +3164,7 @@ describe("share", () => {
2912
3164
  ]);
2913
3165
  // Journal entry survives, so a later run (or a watcher-minted intent)
2914
3166
  // can still authorize the delete properly.
2915
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3167
+ const journal = journalAt(journalPath);
2916
3168
  expect(journal.files["personal/knowledge/gone.md"]).toBeDefined();
2917
3169
  }
2918
3170
  finally {
@@ -2929,7 +3181,7 @@ describe("share", () => {
2929
3181
  // deleted by the user.
2930
3182
  fs.writeFileSync(path.join(companyRoot, "kept.md"), "still here");
2931
3183
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2932
- fs.writeFileSync(journalPath, JSON.stringify({
3184
+ seedJournalAt(journalPath, ({
2933
3185
  version: "1",
2934
3186
  lastSync: new Date().toISOString(),
2935
3187
  files: {
@@ -2965,7 +3217,7 @@ describe("share", () => {
2965
3217
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2966
3218
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "gone.md");
2967
3219
  // Journal entry for the gone file is removed; the kept entry stays.
2968
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3220
+ const journal = journalAt(journalPath);
2969
3221
  expect(journal.files["gone.md"]).toBeUndefined();
2970
3222
  expect(journal.files["kept.md"]).toBeDefined();
2971
3223
  });
@@ -2973,7 +3225,7 @@ describe("share", () => {
2973
3225
  const companyRoot = path.join(tmpDir, "companies", "acme");
2974
3226
  fs.mkdirSync(companyRoot, { recursive: true });
2975
3227
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2976
- fs.writeFileSync(journalPath, JSON.stringify({
3228
+ seedJournalAt(journalPath, ({
2977
3229
  version: "1",
2978
3230
  lastSync: new Date().toISOString(),
2979
3231
  files: {
@@ -3015,7 +3267,7 @@ describe("share", () => {
3015
3267
  const companyRoot = path.join(tmpDir, "companies", "acme");
3016
3268
  fs.mkdirSync(companyRoot, { recursive: true });
3017
3269
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3018
- fs.writeFileSync(journalPath, JSON.stringify({
3270
+ seedJournalAt(journalPath, ({
3019
3271
  version: "1",
3020
3272
  lastSync: new Date().toISOString(),
3021
3273
  files: {
@@ -3038,7 +3290,7 @@ describe("share", () => {
3038
3290
  expect(result.filesDeleted).toBe(0);
3039
3291
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3040
3292
  // Journal entry survives so the next opt-in run can still propagate.
3041
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3293
+ const journal = journalAt(journalPath);
3042
3294
  expect(journal.files["gone.md"]).toBeDefined();
3043
3295
  });
3044
3296
  it("propagateDeletes: scope is constrained to the supplied paths — sibling deletes are not swept", async () => {
@@ -3046,7 +3298,7 @@ describe("share", () => {
3046
3298
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
3047
3299
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
3048
3300
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3049
- fs.writeFileSync(journalPath, JSON.stringify({
3301
+ seedJournalAt(journalPath, ({
3050
3302
  version: "1",
3051
3303
  lastSync: new Date().toISOString(),
3052
3304
  files: {
@@ -3081,7 +3333,7 @@ describe("share", () => {
3081
3333
  expect(result.filesDeleted).toBe(1);
3082
3334
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
3083
3335
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
3084
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3336
+ const journal = journalAt(journalPath);
3085
3337
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
3086
3338
  // Sibling tree's journal entry is untouched — `hq share <subtree>` must
3087
3339
  // not act on files outside the named scope.
@@ -3097,7 +3349,7 @@ describe("share", () => {
3097
3349
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
3098
3350
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
3099
3351
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3100
- fs.writeFileSync(journalPath, JSON.stringify({
3352
+ seedJournalAt(journalPath, ({
3101
3353
  version: "1",
3102
3354
  lastSync: new Date().toISOString(),
3103
3355
  files: {
@@ -3132,7 +3384,7 @@ describe("share", () => {
3132
3384
  });
3133
3385
  expect(result.filesDeleted).toBe(1);
3134
3386
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
3135
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3387
+ const journal = journalAt(journalPath);
3136
3388
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
3137
3389
  // Scope stays exactly as narrow as the absolute spelling's — no wider.
3138
3390
  expect(journal.files["other/also-gone.md"]).toBeDefined();
@@ -3149,7 +3401,7 @@ describe("share", () => {
3149
3401
  fs.mkdirSync(acmeRepo, { recursive: true });
3150
3402
  fs.symlinkSync(acmeRepo, path.join(companyRoot, "knowledge"));
3151
3403
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3152
- fs.writeFileSync(journalPath, JSON.stringify({
3404
+ seedJournalAt(journalPath, ({
3153
3405
  version: "1",
3154
3406
  lastSync: new Date().toISOString(),
3155
3407
  files: {
@@ -3179,14 +3431,14 @@ describe("share", () => {
3179
3431
  });
3180
3432
  expect(result.filesDeleted).toBe(0);
3181
3433
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3182
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3434
+ const journal = journalAt(journalPath);
3183
3435
  expect(journal.files["knowledge/agents/brief.md"]).toBeDefined();
3184
3436
  });
3185
3437
  it("propagateDeletes: a failed DeleteObject leaves the journal entry intact for retry", async () => {
3186
3438
  const companyRoot = path.join(tmpDir, "companies", "acme");
3187
3439
  fs.mkdirSync(companyRoot, { recursive: true });
3188
3440
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3189
- fs.writeFileSync(journalPath, JSON.stringify({
3441
+ seedJournalAt(journalPath, ({
3190
3442
  version: "1",
3191
3443
  lastSync: new Date().toISOString(),
3192
3444
  files: {
@@ -3219,7 +3471,7 @@ describe("share", () => {
3219
3471
  const errorEvent = events.find((e) => e.type === "error");
3220
3472
  expect(errorEvent).toMatchObject({ path: "flaky.md", message: expect.stringContaining("S3 down") });
3221
3473
  // Entry survives — next run will retry the delete.
3222
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3474
+ const journal = journalAt(journalPath);
3223
3475
  expect(journal.files["flaky.md"]).toBeDefined();
3224
3476
  });
3225
3477
  // ── Delete propagation safety: direction + filter guards (Bug B + C) ──────
@@ -3253,7 +3505,7 @@ describe("share", () => {
3253
3505
  // and one this machine uploaded (direction:'up'). Only the 'up' one should
3254
3506
  // be eligible for delete-propagation.
3255
3507
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3256
- fs.writeFileSync(journalPath, JSON.stringify({
3508
+ seedJournalAt(journalPath, ({
3257
3509
  version: "1",
3258
3510
  lastSync: new Date().toISOString(),
3259
3511
  files: {
@@ -3290,7 +3542,7 @@ describe("share", () => {
3290
3542
  const companyRoot = path.join(tmpDir, "companies", "acme");
3291
3543
  fs.mkdirSync(companyRoot, { recursive: true });
3292
3544
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3293
- fs.writeFileSync(journalPath, JSON.stringify({
3545
+ seedJournalAt(journalPath, ({
3294
3546
  version: "1",
3295
3547
  lastSync: new Date().toISOString(),
3296
3548
  files: {
@@ -3323,7 +3575,7 @@ describe("share", () => {
3323
3575
  // absence is a filter outcome, not a user-intended delete.
3324
3576
  fs.writeFileSync(path.join(tmpDir, ".hqignore"), "companies/acme/legacy/**\n");
3325
3577
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3326
- fs.writeFileSync(journalPath, JSON.stringify({
3578
+ seedJournalAt(journalPath, ({
3327
3579
  version: "1",
3328
3580
  lastSync: new Date().toISOString(),
3329
3581
  files: {
@@ -3382,7 +3634,7 @@ describe("share", () => {
3382
3634
  // Under owned-only this would be stuck on remote forever; currency-gated
3383
3635
  // checks the ETag and propagates the delete cleanly when match holds.
3384
3636
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3385
- fs.writeFileSync(journalPath, JSON.stringify({
3637
+ seedJournalAt(journalPath, ({
3386
3638
  version: "1",
3387
3639
  lastSync: new Date().toISOString(),
3388
3640
  files: {
@@ -3412,7 +3664,7 @@ describe("share", () => {
3412
3664
  });
3413
3665
  expect(result.filesDeleted).toBe(1);
3414
3666
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "core/policies/old.md");
3415
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3667
+ const journal = journalAt(journalPath);
3416
3668
  expect(journal.files["core/policies/old.md"]).toBeUndefined();
3417
3669
  });
3418
3670
  it("currency-gated: propagates the S3 delete for a local-delete tombstone (pull-leg producer hand-off)", async () => {
@@ -3423,7 +3675,7 @@ describe("share", () => {
3423
3675
  const companyRoot = path.join(tmpDir, "companies", "acme");
3424
3676
  fs.mkdirSync(companyRoot, { recursive: true });
3425
3677
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3426
- fs.writeFileSync(journalPath, JSON.stringify({
3678
+ seedJournalAt(journalPath, ({
3427
3679
  version: "2",
3428
3680
  lastSync: new Date().toISOString(),
3429
3681
  files: {
@@ -3459,7 +3711,7 @@ describe("share", () => {
3459
3711
  });
3460
3712
  expect(result.filesDeleted).toBe(1);
3461
3713
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/deleted.md");
3462
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3714
+ const journal = journalAt(journalPath);
3463
3715
  expect(journal.files["docs/deleted.md"]).toBeUndefined();
3464
3716
  });
3465
3717
  it("currency-gated: refuses delete + emits stale-etag event when remote moved since last sync", async () => {
@@ -3468,7 +3720,7 @@ describe("share", () => {
3468
3720
  // Another device modified `shared.md` after this device's last sync.
3469
3721
  // The journal still records the old etag; HEAD returns the new one.
3470
3722
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3471
- fs.writeFileSync(journalPath, JSON.stringify({
3723
+ seedJournalAt(journalPath, ({
3472
3724
  version: "1",
3473
3725
  lastSync: new Date().toISOString(),
3474
3726
  files: {
@@ -3500,7 +3752,7 @@ describe("share", () => {
3500
3752
  // No remote DELETE issued, journal entry preserved (pull will re-pull).
3501
3753
  expect(result.filesDeleted).toBe(0);
3502
3754
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3503
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3755
+ const journal = journalAt(journalPath);
3504
3756
  expect(journal.files["shared.md"]).toBeDefined();
3505
3757
  // Dedicated event fired so UIs surface the refusal. `reason` field
3506
3758
  // discriminates "stale-etag" (real drift) from "legacy-no-etag" so
@@ -3522,7 +3774,7 @@ describe("share", () => {
3522
3774
  fs.mkdirSync(companyRoot, { recursive: true });
3523
3775
  const recreatedPath = path.join(companyRoot, "racy.md");
3524
3776
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3525
- fs.writeFileSync(journalPath, JSON.stringify({
3777
+ seedJournalAt(journalPath, ({
3526
3778
  version: "2",
3527
3779
  lastSync: new Date().toISOString(),
3528
3780
  files: {
@@ -3558,7 +3810,7 @@ describe("share", () => {
3558
3810
  expect(result.filesDeleted).toBe(0);
3559
3811
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3560
3812
  expect(fs.readFileSync(recreatedPath, "utf-8")).toBe("recreated after planning");
3561
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3813
+ const journal = journalAt(journalPath);
3562
3814
  expect(journal.files["racy.md"]).toBeDefined();
3563
3815
  expect(events).toContainEqual(expect.objectContaining({
3564
3816
  type: "delete-refused-stale-etag",
@@ -3580,7 +3832,7 @@ describe("share", () => {
3580
3832
  // genuine local==remote sync, so propagating the delete would destroy the
3581
3833
  // (different) remote version. Must be REFUSED, not propagated.
3582
3834
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3583
- fs.writeFileSync(journalPath, JSON.stringify({
3835
+ seedJournalAt(journalPath, ({
3584
3836
  version: "2",
3585
3837
  lastSync: new Date().toISOString(),
3586
3838
  files: {
@@ -3615,7 +3867,7 @@ describe("share", () => {
3615
3867
  // Refusal short-circuits before the currency HEAD — no network needed.
3616
3868
  expect(headRemoteFile).not.toHaveBeenCalled();
3617
3869
  // Journal entry preserved, not silently dropped.
3618
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3870
+ const journal = journalAt(journalPath);
3619
3871
  expect(journal.files["core/policies/kept.md"]).toBeDefined();
3620
3872
  // Surfaced as a refusal with the divergent-local reason.
3621
3873
  const refused = events.find((e) => e.type === "delete-refused-stale-etag");
@@ -3632,7 +3884,7 @@ describe("share", () => {
3632
3884
  // S3 console). Local copy also missing. Currency-gated should drop the
3633
3885
  // journal entry without issuing a DELETE — the remote is already gone.
3634
3886
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3635
- fs.writeFileSync(journalPath, JSON.stringify({
3887
+ seedJournalAt(journalPath, ({
3636
3888
  version: "1",
3637
3889
  lastSync: new Date().toISOString(),
3638
3890
  files: {
@@ -3665,7 +3917,7 @@ describe("share", () => {
3665
3917
  // from real deletes without re-counting events.
3666
3918
  expect(result.filesTombstoned).toBe(1);
3667
3919
  expect(result.filesRefusedStale).toBe(0);
3668
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3920
+ const journal = journalAt(journalPath);
3669
3921
  expect(journal.files["removed-out-of-band.md"]).toBeUndefined();
3670
3922
  // Synthetic progress event carries the tombstone marker. Without this,
3671
3923
  // the tty stream renders tombstones identically to real deletes — operator
@@ -3686,7 +3938,7 @@ describe("share", () => {
3686
3938
  // against. Refuse the delete in the safe direction; a future sync with
3687
3939
  // a recorded etag can re-evaluate.
3688
3940
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3689
- fs.writeFileSync(journalPath, JSON.stringify({
3941
+ seedJournalAt(journalPath, ({
3690
3942
  version: "1",
3691
3943
  lastSync: new Date().toISOString(),
3692
3944
  files: {
@@ -3740,7 +3992,7 @@ describe("share", () => {
3740
3992
  const companyRoot = path.join(tmpDir, "companies", "acme");
3741
3993
  fs.mkdirSync(companyRoot, { recursive: true });
3742
3994
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3743
- fs.writeFileSync(journalPath, JSON.stringify({
3995
+ seedJournalAt(journalPath, ({
3744
3996
  version: "1",
3745
3997
  lastSync: new Date().toISOString(),
3746
3998
  files: {
@@ -3792,7 +4044,7 @@ describe("share", () => {
3792
4044
  const companyRoot = path.join(tmpDir, "companies", "acme");
3793
4045
  fs.mkdirSync(companyRoot, { recursive: true });
3794
4046
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3795
- fs.writeFileSync(journalPath, JSON.stringify({
4047
+ seedJournalAt(journalPath, ({
3796
4048
  version: "1",
3797
4049
  lastSync: new Date().toISOString(),
3798
4050
  files: {
@@ -3826,7 +4078,7 @@ describe("share", () => {
3826
4078
  const companyRoot = path.join(tmpDir, "companies", "acme");
3827
4079
  fs.mkdirSync(companyRoot, { recursive: true });
3828
4080
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3829
- fs.writeFileSync(journalPath, JSON.stringify({
4081
+ seedJournalAt(journalPath, ({
3830
4082
  version: "1",
3831
4083
  lastSync: new Date().toISOString(),
3832
4084
  files: {
@@ -3876,7 +4128,7 @@ describe("share", () => {
3876
4128
  fs.mkdirSync(companyRoot, { recursive: true });
3877
4129
  const recreatedPath = path.join(companyRoot, "notes", "gone.md");
3878
4130
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3879
- fs.writeFileSync(journalPath, JSON.stringify({
4131
+ seedJournalAt(journalPath, ({
3880
4132
  version: "1",
3881
4133
  lastSync: new Date().toISOString(),
3882
4134
  files: {
@@ -3924,7 +4176,7 @@ describe("share", () => {
3924
4176
  const companyRoot = path.join(tmpDir, "companies", "acme");
3925
4177
  fs.mkdirSync(companyRoot, { recursive: true });
3926
4178
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3927
- fs.writeFileSync(journalPath, JSON.stringify({
4179
+ seedJournalAt(journalPath, ({
3928
4180
  version: "1",
3929
4181
  lastSync: new Date().toISOString(),
3930
4182
  files: {
@@ -3954,14 +4206,14 @@ describe("share", () => {
3954
4206
  type: "delete-refused-stale-etag",
3955
4207
  reason: "legacy-no-etag",
3956
4208
  }));
3957
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4209
+ const journal = journalAt(journalPath);
3958
4210
  expect(journal.files["notes/legacy.md"]).toBeDefined();
3959
4211
  });
3960
4212
  it("owned-only still requires an intent — it is the documented rollback knob", async () => {
3961
4213
  const companyRoot = path.join(tmpDir, "companies", "acme");
3962
4214
  fs.mkdirSync(companyRoot, { recursive: true });
3963
4215
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3964
- fs.writeFileSync(journalPath, JSON.stringify({
4216
+ seedJournalAt(journalPath, ({
3965
4217
  version: "1",
3966
4218
  lastSync: new Date().toISOString(),
3967
4219
  files: {
@@ -4148,7 +4400,7 @@ describe("share", () => {
4148
4400
  const companyRoot = path.join(tmpDir, "companies", "acme");
4149
4401
  fs.mkdirSync(companyRoot, { recursive: true });
4150
4402
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4151
- fs.writeFileSync(journalPath, JSON.stringify({
4403
+ seedJournalAt(journalPath, ({
4152
4404
  version: "1",
4153
4405
  lastSync: new Date().toISOString(),
4154
4406
  files: {
@@ -4189,7 +4441,7 @@ describe("share", () => {
4189
4441
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "regular.md");
4190
4442
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md");
4191
4443
  // Both journal entries are removed by the delete loop's removeEntry call.
4192
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4444
+ const journal = journalAt(journalPath);
4193
4445
  expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeUndefined();
4194
4446
  expect(journal.files["regular.md"]).toBeUndefined();
4195
4447
  });
@@ -4204,7 +4456,7 @@ describe("share", () => {
4204
4456
  process.env.HQ_STATE_DIR = stateDirPersonal;
4205
4457
  try {
4206
4458
  const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
4207
- fs.writeFileSync(journalPath, JSON.stringify({
4459
+ seedJournalAt(journalPath, ({
4208
4460
  version: "1",
4209
4461
  lastSync: new Date().toISOString(),
4210
4462
  files: {
@@ -4230,7 +4482,7 @@ describe("share", () => {
4230
4482
  });
4231
4483
  expect(result.filesDeleted).toBe(1);
4232
4484
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/.obsidian/graph.json.drift-1779863862-42519");
4233
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4485
+ const journal = journalAt(journalPath);
4234
4486
  expect(journal.files["personal/.obsidian/graph.json.drift-1779863862-42519"]).toBeUndefined();
4235
4487
  }
4236
4488
  finally {
@@ -4253,7 +4505,7 @@ describe("share", () => {
4253
4505
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
4254
4506
  direction: "up",
4255
4507
  });
4256
- fs.writeFileSync(journalPath, JSON.stringify({
4508
+ seedJournalAt(journalPath, ({
4257
4509
  version: "1",
4258
4510
  lastSync: new Date().toISOString(),
4259
4511
  files: {
@@ -4301,7 +4553,7 @@ describe("share", () => {
4301
4553
  expect(pruned[0].byReason["ignore-filtered"]).toBe(3);
4302
4554
  expect(pruned[0].byReason["excluded-top-level"]).toBe(1);
4303
4555
  // The persisted journal converged: excluded rows gone, legit row kept.
4304
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4556
+ const journal = journalAt(journalPath);
4305
4557
  expect(Object.keys(journal.files)).toContain("personal/keep.md");
4306
4558
  expect(journal.files[".claude/worktrees/wt/a.ts"]).toBeUndefined();
4307
4559
  expect(journal.files["personal/node_modules/pkg/index.js"]).toBeUndefined();
@@ -4333,7 +4585,7 @@ describe("share", () => {
4333
4585
  const key = "deleted-top-level.md";
4334
4586
  const journalSlug = "personal-scoped-delete";
4335
4587
  const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
4336
- fs.writeFileSync(journalPath, JSON.stringify({
4588
+ seedJournalAt(journalPath, ({
4337
4589
  version: "2",
4338
4590
  lastSync: new Date().toISOString(),
4339
4591
  pulls: [],
@@ -4365,7 +4617,7 @@ describe("share", () => {
4365
4617
  });
4366
4618
  expect(result.filesDeleted).toBe(1);
4367
4619
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
4368
- expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
4620
+ expect(journalAt(journalPath).files[key]).toBeUndefined();
4369
4621
  });
4370
4622
  // ── Decommission prefixes ──────────────────────────────────────────────────
4371
4623
  //
@@ -4420,7 +4672,7 @@ describe("share", () => {
4420
4672
  // Local file remains on disk — decommission is a remote-only sweep.
4421
4673
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "notes.md"))).toBe(true);
4422
4674
  // Journal entry pruned in lockstep with the remote delete.
4423
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4675
+ const journalAfter = journalAt(personalJournalPath);
4424
4676
  expect(journalAfter.files["companies/foo/notes.md"]).toBeUndefined();
4425
4677
  expect(journalAfter.files["knowledge/x.md"]).toBeDefined();
4426
4678
  });
@@ -4456,7 +4708,7 @@ describe("share", () => {
4456
4708
  expect(deleteRemoteFile).not.toHaveBeenCalled();
4457
4709
  // Journal entry preserved — the pulled-from-peer record stays so a
4458
4710
  // future pull can still reconcile it correctly.
4459
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4711
+ const journalAfter = journalAt(personalJournalPath);
4460
4712
  expect(journalAfter.files["companies/foo/from-peer.md"]).toBeDefined();
4461
4713
  });
4462
4714
  it("decommissionPrefixes: dedupes with propagateDeletes plan — a key in both still emits one DeleteObject", async () => {
@@ -4576,7 +4828,7 @@ describe("share", () => {
4576
4828
  expect(result.filesTombstoned).toBe(1);
4577
4829
  expect(result.filesDeleted).toBe(0);
4578
4830
  // Journal entry is gone.
4579
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4831
+ const journalAfter = journalAt(personalJournalPath);
4580
4832
  expect(journalAfter.files["companies/foo/gone.md"]).toBeUndefined();
4581
4833
  });
4582
4834
  describe("personalMode", () => {
@@ -4617,7 +4869,7 @@ describe("share", () => {
4617
4869
  const acmeJournalPath = path.join(stateDir, "sync-journal.acme.json");
4618
4870
  expect(fs.existsSync(personalJournalPath)).toBe(true);
4619
4871
  expect(fs.existsSync(acmeJournalPath)).toBe(false);
4620
- const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4872
+ const journal = journalAt(personalJournalPath);
4621
4873
  expect(journal.files["knowledge/notes.md"]).toBeDefined();
4622
4874
  });
4623
4875
  it("personalMode=true accepts files outside companies/<slug>/ (companion to the company-folder rejection)", async () => {
@@ -4905,7 +5157,7 @@ describe("share", () => {
4905
5157
  .createHash("sha256")
4906
5158
  .update(target)
4907
5159
  .digest("hex");
4908
- fs.writeFileSync(journalPath, JSON.stringify({
5160
+ seedJournalAt(journalPath, ({
4909
5161
  version: "1",
4910
5162
  lastSync: new Date().toISOString(),
4911
5163
  files: {
@@ -5308,7 +5560,7 @@ describe("share", () => {
5308
5560
  });
5309
5561
  expect(pushed.filesUploaded).toBe(1);
5310
5562
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5311
- const afterPush = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5563
+ const afterPush = journalAt(journalPath);
5312
5564
  expect(Object.keys(afterPush.files)).toContain("knowledge/agents/brief.md");
5313
5565
  vi.mocked(deleteRemoteFile).mockClear();
5314
5566
  const walked = await share({
@@ -5322,7 +5574,7 @@ describe("share", () => {
5322
5574
  expect(walked.filesDeleted).toBe(0);
5323
5575
  const deletedKeys = vi.mocked(deleteRemoteFile).mock.calls.map((c) => c[1]);
5324
5576
  expect(deletedKeys).not.toContain("knowledge/agents/brief.md");
5325
- const afterWalk = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5577
+ const afterWalk = journalAt(journalPath);
5326
5578
  expect(Object.keys(afterWalk.files)).toContain("knowledge/agents/brief.md");
5327
5579
  });
5328
5580
  });
@@ -5366,7 +5618,7 @@ describe("share", () => {
5366
5618
  }
5367
5619
  }
5368
5620
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5369
- fs.writeFileSync(journalPath, JSON.stringify({
5621
+ seedJournalAt(journalPath, ({
5370
5622
  version: "1",
5371
5623
  lastSync: new Date().toISOString(),
5372
5624
  files,
@@ -5424,7 +5676,7 @@ describe("share", () => {
5424
5676
  expect(summary.samplePaths.length).toBeGreaterThan(0);
5425
5677
  expect(summary.samplePaths.length).toBeLessThanOrEqual(10);
5426
5678
  // Journal entries for the missing files SURVIVE — guard never mutates.
5427
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5679
+ const journal = journalAt(journalPath);
5428
5680
  expect(Object.keys(journal.files).length).toBe(100);
5429
5681
  // Per-key delete-refused-stale-etag events for each candidate.
5430
5682
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
@@ -5474,7 +5726,7 @@ describe("share", () => {
5474
5726
  expect(headedKeys).not.toContain(`f-${i.toString().padStart(4, "0")}.md`);
5475
5727
  }
5476
5728
  // Journal untouched — the entries stay recoverable.
5477
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5729
+ const journal = journalAt(journalPath);
5478
5730
  expect(Object.keys(journal.files).length).toBe(100);
5479
5731
  });
5480
5732
  // The core of the #227 regression, isolated: pre-#227 a trip emptied
@@ -5505,7 +5757,7 @@ describe("share", () => {
5505
5757
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" &&
5506
5758
  e.reason === "bulk-asymmetry");
5507
5759
  expect(perKeyRefusals.length).toBe(11);
5508
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5760
+ const journal = journalAt(journalPath);
5509
5761
  expect(Object.keys(journal.files).length).toBe(100);
5510
5762
  });
5511
5763
  it("vault litter still drains when the breaker trips", async () => {
@@ -5519,7 +5771,7 @@ describe("share", () => {
5519
5771
  withIntents: false,
5520
5772
  });
5521
5773
  const litterKey = "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md";
5522
- const seeded = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5774
+ const seeded = journalAt(journalPath);
5523
5775
  seeded.files[litterKey] = {
5524
5776
  hash: "h",
5525
5777
  size: 5,
@@ -5528,7 +5780,7 @@ describe("share", () => {
5528
5780
  remoteEtag: "litter-etag",
5529
5781
  kind: "file",
5530
5782
  };
5531
- fs.writeFileSync(journalPath, JSON.stringify(seeded));
5783
+ seedJournalAt(journalPath, seeded);
5532
5784
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
5533
5785
  etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
5534
5786
  lastModified: new Date(),
@@ -5668,7 +5920,7 @@ describe("share", () => {
5668
5920
  expect(summary).toBeDefined();
5669
5921
  expect(summary.ratio).toBeCloseTo(1.0, 5);
5670
5922
  // Journal untouched.
5671
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5923
+ const journal = journalAt(journalPath);
5672
5924
  expect(Object.keys(journal.files).length).toBe(50);
5673
5925
  });
5674
5926
  it("small absolute count (9/9 = 100% ratio but < MIN_ABS=10) does NOT trip: all 9 delete normally", async () => {
@@ -5712,7 +5964,7 @@ describe("share", () => {
5712
5964
  expect(deleteRemoteFile).not.toHaveBeenCalled();
5713
5965
  expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeDefined();
5714
5966
  // Journal entries survive.
5715
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5967
+ const journal = journalAt(journalPath);
5716
5968
  expect(Object.keys(journal.files).length).toBe(100);
5717
5969
  });
5718
5970
  it("direction:'down' entries don't count toward the bulk numerator (owned-only silently skips them)", async () => {
@@ -5740,7 +5992,7 @@ describe("share", () => {
5740
5992
  localDeleteIntent: deleteIntent("up-etag", "h"),
5741
5993
  };
5742
5994
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5743
- fs.writeFileSync(journalPath, JSON.stringify({ version: "1", lastSync: now, files }));
5995
+ seedJournalAt(journalPath, { version: "1", lastSync: now, files });
5744
5996
  const result = await share({
5745
5997
  paths: [companyRoot],
5746
5998
  company: "acme",
@@ -5863,7 +6115,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5863
6115
  fs.mkdirSync(companyRoot, { recursive: true });
5864
6116
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5865
6117
  // v2-shaped fixture — same field set the production journal uses.
5866
- fs.writeFileSync(journalPath, JSON.stringify({
6118
+ seedJournalAt(journalPath, ({
5867
6119
  version: "2",
5868
6120
  lastSync: new Date().toISOString(),
5869
6121
  files: {
@@ -5909,7 +6161,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5909
6161
  const { hashFile } = await import("../journal.js");
5910
6162
  const baselineHash = hashFile(testFile);
5911
6163
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5912
- fs.writeFileSync(journalPath, JSON.stringify({
6164
+ seedJournalAt(journalPath, ({
5913
6165
  version: "2",
5914
6166
  lastSync: new Date().toISOString(),
5915
6167
  files: {
@@ -5950,7 +6202,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5950
6202
  const originalLstat = fs.lstatSync(testFile);
5951
6203
  // The recorded hash intentionally differs from the current bytes.
5952
6204
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5953
- fs.writeFileSync(journalPath, JSON.stringify({
6205
+ seedJournalAt(journalPath, ({
5954
6206
  version: "2",
5955
6207
  lastSync: new Date().toISOString(),
5956
6208
  files: {
@@ -5997,7 +6249,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5997
6249
  const { hashFile } = await import("../journal.js");
5998
6250
  const realHash = hashFile(testFile);
5999
6251
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6000
- fs.writeFileSync(journalPath, JSON.stringify({
6252
+ seedJournalAt(journalPath, ({
6001
6253
  version: "2",
6002
6254
  lastSync: new Date().toISOString(),
6003
6255
  files: {
@@ -6047,7 +6299,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6047
6299
  const { hashFile } = await import("../journal.js");
6048
6300
  const realHash = hashFile(testFile);
6049
6301
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6050
- fs.writeFileSync(journalPath, JSON.stringify({
6302
+ seedJournalAt(journalPath, ({
6051
6303
  version: "2",
6052
6304
  lastSync: new Date().toISOString(),
6053
6305
  files: {
@@ -6079,7 +6331,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6079
6331
  expect(uploadFile).not.toHaveBeenCalled();
6080
6332
  // Journal mtimeMs is now the bumped value, while content hashing remains
6081
6333
  // authoritative on the next sync.
6082
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6334
+ const journalAfter = journalAt(journalPath);
6083
6335
  expect(journalAfter.files["touched-then-fast.md"].mtimeMs).toBe(bumpedLstat.mtimeMs);
6084
6336
  expect(journalAfter.files["touched-then-fast.md"].size).toBe(bumpedLstat.size);
6085
6337
  // Content hash untouched — the file genuinely didn't change.
@@ -6099,7 +6351,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6099
6351
  });
6100
6352
  expect(result.filesUploaded).toBe(1);
6101
6353
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6102
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6354
+ const journal = journalAt(journalPath);
6103
6355
  expect(journal.files["fresh.md"]).toBeDefined();
6104
6356
  expect(journal.files["fresh.md"].mtimeMs).toBe(lstat.mtimeMs);
6105
6357
  expect(journal.files["fresh.md"].size).toBe(lstat.size);
@@ -6116,7 +6368,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6116
6368
  const realSize = fs.statSync(testFile).size;
6117
6369
  // Real hash but NO mtimeMs. Hash matches → skip upload.
6118
6370
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6119
- fs.writeFileSync(journalPath, JSON.stringify({
6371
+ seedJournalAt(journalPath, ({
6120
6372
  version: "1",
6121
6373
  lastSync: new Date().toISOString(),
6122
6374
  files: {
@@ -6675,7 +6927,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6675
6927
  // AND lost their etag/content-hash/stat journal metadata.
6676
6928
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6677
6929
  expect(fs.existsSync(journalPath)).toBe(true);
6678
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6930
+ const journal = journalAt(journalPath);
6679
6931
  const stampedKeys = Object.keys(journal.files);
6680
6932
  expect(stampedKeys.length).toBe(FILE_COUNT - 1);
6681
6933
  // The failed file is NOT in the journal (no successful upload).
@@ -6823,7 +7075,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6823
7075
  const companyRoot = path.join(tmpDir, "companies", "acme");
6824
7076
  fs.mkdirSync(companyRoot, { recursive: true });
6825
7077
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6826
- fs.writeFileSync(journalPath, JSON.stringify({
7078
+ seedJournalAt(journalPath, ({
6827
7079
  version: "1",
6828
7080
  lastSync: new Date().toISOString(),
6829
7081
  files: {
@@ -6852,7 +7104,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6852
7104
  expect(headRemoteFile).not.toHaveBeenCalled();
6853
7105
  expect(deleteRemoteFile).not.toHaveBeenCalled();
6854
7106
  expect(uploadFile).not.toHaveBeenCalled();
6855
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7107
+ const journal = journalAt(journalPath);
6856
7108
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
6857
7109
  });
6858
7110
  it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
@@ -6864,7 +7116,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6864
7116
  fs.mkdirSync(targetDir, { recursive: true });
6865
7117
  fs.symlinkSync(targetDir, linkPath, "dir");
6866
7118
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6867
- fs.writeFileSync(journalPath, JSON.stringify({
7119
+ seedJournalAt(journalPath, ({
6868
7120
  version: "1",
6869
7121
  lastSync: new Date().toISOString(),
6870
7122
  files: {
@@ -6909,7 +7161,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6909
7161
  samplePaths: [linkKey],
6910
7162
  });
6911
7163
  expect(events.some((event) => event.type === "error")).toBe(false);
6912
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7164
+ const journal = journalAt(journalPath);
6913
7165
  expect(journal.files[linkKey]).toBeDefined();
6914
7166
  }
6915
7167
  finally {