@indigoai-us/hq-cloud 6.15.19 → 6.15.21

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 +2 -2
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +43 -8
  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/conflict.d.ts +25 -2
  8. package/dist/cli/conflict.d.ts.map +1 -1
  9. package/dist/cli/conflict.js +29 -4
  10. package/dist/cli/conflict.js.map +1 -1
  11. package/dist/cli/share.d.ts.map +1 -1
  12. package/dist/cli/share.js +90 -6
  13. package/dist/cli/share.js.map +1 -1
  14. package/dist/cli/share.test.js +434 -104
  15. package/dist/cli/share.test.js.map +1 -1
  16. package/dist/cli/sync.d.ts.map +1 -1
  17. package/dist/cli/sync.js +9 -1
  18. package/dist/cli/sync.js.map +1 -1
  19. package/dist/cli/sync.test.js +95 -46
  20. package/dist/cli/sync.test.js.map +1 -1
  21. package/dist/journal-delta-writes.test.d.ts +2 -0
  22. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  23. package/dist/journal-delta-writes.test.js +140 -0
  24. package/dist/journal-delta-writes.test.js.map +1 -0
  25. package/dist/journal.d.ts.map +1 -1
  26. package/dist/journal.js +151 -24
  27. package/dist/journal.js.map +1 -1
  28. package/dist/journal.test.js +29 -11
  29. package/dist/journal.test.js.map +1 -1
  30. package/dist/outcome-telemetry.d.ts +7 -1
  31. package/dist/outcome-telemetry.d.ts.map +1 -1
  32. package/dist/outcome-telemetry.js +15 -9
  33. package/dist/outcome-telemetry.js.map +1 -1
  34. package/dist/outcome-telemetry.test.js +15 -0
  35. package/dist/outcome-telemetry.test.js.map +1 -1
  36. package/dist/skill-telemetry.d.ts +6 -1
  37. package/dist/skill-telemetry.d.ts.map +1 -1
  38. package/dist/skill-telemetry.js +14 -8
  39. package/dist/skill-telemetry.js.map +1 -1
  40. package/dist/skill-telemetry.test.js +43 -0
  41. package/dist/skill-telemetry.test.js.map +1 -1
  42. package/dist/telemetry.d.ts +7 -1
  43. package/dist/telemetry.d.ts.map +1 -1
  44. package/dist/telemetry.js +67 -60
  45. package/dist/telemetry.js.map +1 -1
  46. package/dist/telemetry.test.js +12 -0
  47. package/dist/telemetry.test.js.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: {
@@ -593,7 +625,7 @@ describe("share", () => {
593
625
  // share.ts makes the first test fail (uploadFile IS called → resurrection).
594
626
  describe("push-side tombstone consult", () => {
595
627
  function seedJournal(key, hash, size) {
596
- fs.writeFileSync(path.join(stateDir, "sync-journal.acme.json"), JSON.stringify({
628
+ seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), ({
597
629
  version: "1",
598
630
  lastSync: new Date().toISOString(),
599
631
  files: {
@@ -737,7 +769,7 @@ describe("share", () => {
737
769
  size: 99,
738
770
  });
739
771
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
740
- fs.writeFileSync(journalPath, JSON.stringify({
772
+ seedJournalAt(journalPath, ({
741
773
  version: "1",
742
774
  lastSync: new Date().toISOString(),
743
775
  files: {
@@ -789,7 +821,7 @@ describe("share", () => {
789
821
  // assert it is never consulted below — the skip is cheap, and a leftover
790
822
  // mockResolvedValueOnce would otherwise leak into a later test's HEAD queue.
791
823
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
792
- fs.writeFileSync(journalPath, JSON.stringify({
824
+ seedJournalAt(journalPath, ({
793
825
  version: "1",
794
826
  lastSync: new Date().toISOString(),
795
827
  files: {
@@ -969,7 +1001,7 @@ describe("share", () => {
969
1001
  direction: "push",
970
1002
  });
971
1003
  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"));
1004
+ const journal = readJournal("acme");
973
1005
  expect(journal.files["kms-scaffold.md"]).toMatchObject({
974
1006
  direction: "up",
975
1007
  remoteEtag: "288bfa91684ae21f9ae27c061254d412",
@@ -1085,7 +1117,7 @@ describe("share", () => {
1085
1117
  size: 5,
1086
1118
  });
1087
1119
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1088
- fs.writeFileSync(journalPath, JSON.stringify({
1120
+ seedJournalAt(journalPath, ({
1089
1121
  version: "1",
1090
1122
  lastSync: syncedAt,
1091
1123
  files: {
@@ -1158,7 +1190,7 @@ describe("share", () => {
1158
1190
  // No journal stamp for the failed PUT — next pass re-evaluates fresh.
1159
1191
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1160
1192
  if (fs.existsSync(journalPath)) {
1161
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1193
+ const journal = journalAt(journalPath);
1162
1194
  expect(journal.files?.["raced.md"]).toBeUndefined();
1163
1195
  }
1164
1196
  });
@@ -1240,7 +1272,7 @@ describe("share", () => {
1240
1272
  return { etag: '"should-not-land"' };
1241
1273
  });
1242
1274
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1243
- fs.writeFileSync(journalPath, JSON.stringify({
1275
+ seedJournalAt(journalPath, ({
1244
1276
  version: "1",
1245
1277
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1246
1278
  files: {
@@ -1300,7 +1332,7 @@ describe("share", () => {
1300
1332
  });
1301
1333
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
1302
1334
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1303
- fs.writeFileSync(journalPath, JSON.stringify({
1335
+ seedJournalAt(journalPath, ({
1304
1336
  version: "1",
1305
1337
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1306
1338
  files: {
@@ -1335,7 +1367,7 @@ describe("share", () => {
1335
1367
  size: 20,
1336
1368
  });
1337
1369
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1338
- fs.writeFileSync(journalPath, JSON.stringify({
1370
+ seedJournalAt(journalPath, ({
1339
1371
  version: "1",
1340
1372
  lastSync: new Date().toISOString(),
1341
1373
  files: {
@@ -1377,7 +1409,7 @@ describe("share", () => {
1377
1409
  size: 40,
1378
1410
  });
1379
1411
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1380
- fs.writeFileSync(journalPath, JSON.stringify({
1412
+ seedJournalAt(journalPath, ({
1381
1413
  version: "1",
1382
1414
  lastSync: new Date().toISOString(),
1383
1415
  files: {
@@ -1423,7 +1455,7 @@ describe("share", () => {
1423
1455
  });
1424
1456
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"forced-etag"' });
1425
1457
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1426
- fs.writeFileSync(journalPath, JSON.stringify({
1458
+ seedJournalAt(journalPath, ({
1427
1459
  version: "1",
1428
1460
  lastSync: new Date().toISOString(),
1429
1461
  files: {
@@ -1450,6 +1482,304 @@ describe("share", () => {
1450
1482
  // fence then retry — either is consent. Must not leave the file unuploaded.
1451
1483
  expect(uploadFile).toHaveBeenCalled();
1452
1484
  });
1485
+ // ── publish-local: fenced promotion of a kept-local copy ────────────────
1486
+ //
1487
+ // `keep` leaves a file permanently unpublishable: push refuses it on the
1488
+ // localDiverges flag, pull never clears the flag (remote is unchanged), and
1489
+ // `keep` re-arms it every pass. The only existing escape — `overwrite` — is
1490
+ // a blunt force-write that will PUT unfenced after a 412, clobbering a
1491
+ // remote that genuinely advanced.
1492
+ //
1493
+ // `publish-local` is the missing middle: the operator explicitly elects
1494
+ // their local copy as the new authority, and the engine accepts it ONLY
1495
+ // while the remote still sits at the etag recorded when the keep happened.
1496
+ // If the remote moved, consent was given against a version that no longer
1497
+ // exists, so the write must fail closed instead of forcing.
1498
+ it("localDiverges under publish-local: promotes local under an If-Match fence on the journal etag", async () => {
1499
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1500
+ fs.mkdirSync(companyRoot, { recursive: true });
1501
+ const testFile = path.join(companyRoot, "publish-divergent.md");
1502
+ const localBytes = "my kept divergent local, now elected authority";
1503
+ fs.writeFileSync(testFile, localBytes);
1504
+ const localHash = hashFile(testFile);
1505
+ const REMOTE = "peer-version-etag";
1506
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1507
+ lastModified: new Date(),
1508
+ etag: `"${REMOTE}"`,
1509
+ size: 40,
1510
+ });
1511
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1512
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1513
+ fs.writeFileSync(journalPath, JSON.stringify({
1514
+ version: "1",
1515
+ lastSync: new Date().toISOString(),
1516
+ files: {
1517
+ "publish-divergent.md": {
1518
+ hash: localHash,
1519
+ size: localBytes.length,
1520
+ syncedAt: new Date().toISOString(),
1521
+ direction: "down",
1522
+ remoteEtag: REMOTE,
1523
+ localDiverges: true,
1524
+ },
1525
+ },
1526
+ }));
1527
+ const result = await share({
1528
+ paths: [testFile],
1529
+ company: "acme",
1530
+ vaultConfig: mockConfig,
1531
+ hqRoot: tmpDir,
1532
+ skipUnchanged: false,
1533
+ onConflict: "publish-local",
1534
+ });
1535
+ expect(result.filesUploaded).toBe(1);
1536
+ // Fenced on the etag the keep recorded — never unfenced, never live-HEAD.
1537
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1538
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1539
+ // The entry is rewritten as an ordinary upload: local is now authority,
1540
+ // so the journal-honesty flag must be gone rather than re-armed.
1541
+ const journal = journalAt(journalPath);
1542
+ expect(journal.files["publish-divergent.md"].direction).toBe("up");
1543
+ expect(journal.files["publish-divergent.md"].localDiverges).toBeFalsy();
1544
+ expect(journal.files["publish-divergent.md"].remoteEtag).toBe("published-etag");
1545
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1546
+ });
1547
+ it("localDiverges under publish-local: a 412 fails closed and never retries unfenced", async () => {
1548
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1549
+ fs.mkdirSync(companyRoot, { recursive: true });
1550
+ const testFile = path.join(companyRoot, "publish-raced.md");
1551
+ const localBytes = "my local, but the remote moved under me";
1552
+ fs.writeFileSync(testFile, localBytes);
1553
+ const localHash = hashFile(testFile);
1554
+ const REMOTE = "etag-at-keep-time";
1555
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1556
+ lastModified: new Date(),
1557
+ etag: `"${REMOTE}"`,
1558
+ size: 40,
1559
+ });
1560
+ const precondition = Object.assign(new Error("At least one of the pre-conditions you specified did not hold"), {
1561
+ name: "PreconditionFailed",
1562
+ });
1563
+ vi.mocked(uploadFile).mockRejectedValueOnce(precondition);
1564
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1565
+ fs.writeFileSync(journalPath, JSON.stringify({
1566
+ version: "1",
1567
+ lastSync: new Date().toISOString(),
1568
+ files: {
1569
+ "publish-raced.md": {
1570
+ hash: localHash,
1571
+ size: localBytes.length,
1572
+ syncedAt: new Date().toISOString(),
1573
+ direction: "down",
1574
+ remoteEtag: REMOTE,
1575
+ localDiverges: true,
1576
+ },
1577
+ },
1578
+ }));
1579
+ const result = await share({
1580
+ paths: [testFile],
1581
+ company: "acme",
1582
+ vaultConfig: mockConfig,
1583
+ hqRoot: tmpDir,
1584
+ skipUnchanged: false,
1585
+ onConflict: "publish-local",
1586
+ });
1587
+ // Exactly one attempt: the fenced one. The `overwrite` escape hatch of
1588
+ // re-PUTting with no precondition must NOT be reachable from here.
1589
+ expect(uploadFile).toHaveBeenCalledTimes(1);
1590
+ expect(result.filesUploaded).toBe(0);
1591
+ expect(result.conflictPaths).toContain("publish-raced.md");
1592
+ // Consent was for a remote that no longer exists, so the flag stays put
1593
+ // and the operator is asked again rather than silently overruled.
1594
+ const journal = journalAt(journalPath);
1595
+ expect(journal.files["publish-raced.md"].localDiverges).toBe(true);
1596
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1597
+ });
1598
+ // Codex review (PR #356, P1). The sync runner's push leg always calls
1599
+ // share() with `skipUnchanged: true`, and a pull-kept entry stores the
1600
+ // CURRENT local hash — that is exactly what the journal-honesty stamp
1601
+ // records. So `existing.hash === localHash` is true for every stuck file,
1602
+ // classifyPushCandidates() returns `skip-unchanged`, and executeUploads()
1603
+ // never runs: no HEAD, no conflict, no upload. Without this bypass the
1604
+ // whole recovery is a no-op in the one mode that can reach personal-vault
1605
+ // files, and the next pull leaves them stuck again. `hash === journal hash`
1606
+ // means "local is unchanged since the stamp", NOT "local matches remote" —
1607
+ // for a divergent entry those are different claims.
1608
+ it("localDiverges bypasses the skip-unchanged fast path so the recovery can run at all", async () => {
1609
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1610
+ fs.mkdirSync(companyRoot, { recursive: true });
1611
+ const testFile = path.join(companyRoot, "stuck-under-skip-unchanged.md");
1612
+ const localBytes = "the body a pull-keep stamped and then refused to publish";
1613
+ fs.writeFileSync(testFile, localBytes);
1614
+ const localHash = hashFile(testFile);
1615
+ const REMOTE = "etag-at-keep-time";
1616
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1617
+ lastModified: new Date(),
1618
+ etag: `"${REMOTE}"`,
1619
+ size: 40,
1620
+ });
1621
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1622
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1623
+ fs.writeFileSync(journalPath, JSON.stringify({
1624
+ version: "1",
1625
+ lastSync: new Date().toISOString(),
1626
+ files: {
1627
+ "stuck-under-skip-unchanged.md": {
1628
+ // Deliberately the CURRENT local hash: this is what a pull-keep
1629
+ // writes, and it is what makes the fast path swallow the file.
1630
+ hash: localHash,
1631
+ size: localBytes.length,
1632
+ syncedAt: new Date().toISOString(),
1633
+ direction: "down",
1634
+ remoteEtag: REMOTE,
1635
+ localDiverges: true,
1636
+ },
1637
+ },
1638
+ }));
1639
+ const result = await share({
1640
+ paths: [testFile],
1641
+ company: "acme",
1642
+ vaultConfig: mockConfig,
1643
+ hqRoot: tmpDir,
1644
+ skipUnchanged: true,
1645
+ onConflict: "publish-local",
1646
+ });
1647
+ expect(result.filesUploaded).toBe(1);
1648
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1649
+ const journal = journalAt(journalPath);
1650
+ expect(journal.files["stuck-under-skip-unchanged.md"].direction).toBe("up");
1651
+ expect(journal.files["stuck-under-skip-unchanged.md"].localDiverges).toBeFalsy();
1652
+ });
1653
+ // Codex review (PR #356, P2). A genuine two-sided conflict is not the
1654
+ // localDiverges recovery: the journal etag is stale BY DEFINITION here
1655
+ // (that is what remoteChanged means), so fencing consent on it guarantees a
1656
+ // 412 and the file can never publish. Consent given at this prompt is
1657
+ // consent against the remote the operator was just shown, so the fence must
1658
+ // be the freshly observed etag.
1659
+ it("publish-local on a fresh two-sided conflict fences on the OBSERVED remote etag, not the stale journal one", async () => {
1660
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1661
+ fs.mkdirSync(companyRoot, { recursive: true });
1662
+ const testFile = path.join(companyRoot, "two-sided.md");
1663
+ fs.writeFileSync(testFile, "my newer local body");
1664
+ const STALE_JOURNAL_ETAG = "etag-from-last-sync";
1665
+ const OBSERVED_REMOTE_ETAG = "etag-the-peer-just-wrote";
1666
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1667
+ lastModified: new Date(),
1668
+ etag: `"${OBSERVED_REMOTE_ETAG}"`,
1669
+ size: 40,
1670
+ });
1671
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1672
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1673
+ fs.writeFileSync(journalPath, JSON.stringify({
1674
+ version: "1",
1675
+ lastSync: new Date().toISOString(),
1676
+ files: {
1677
+ "two-sided.md": {
1678
+ hash: "a-hash-that-is-neither-side",
1679
+ size: 10,
1680
+ syncedAt: new Date().toISOString(),
1681
+ direction: "up",
1682
+ remoteEtag: STALE_JOURNAL_ETAG,
1683
+ },
1684
+ },
1685
+ }));
1686
+ const result = await share({
1687
+ paths: [testFile],
1688
+ company: "acme",
1689
+ vaultConfig: mockConfig,
1690
+ hqRoot: tmpDir,
1691
+ skipUnchanged: false,
1692
+ onConflict: "publish-local",
1693
+ });
1694
+ expect(result.filesUploaded).toBe(1);
1695
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({
1696
+ ifMatch: OBSERVED_REMOTE_ETAG,
1697
+ });
1698
+ });
1699
+ // Codex review (PR #356, P2). HEAD returning null skips the whole conflict
1700
+ // block, so the precondition becomes If-None-Match:* and the object is
1701
+ // RECREATED. For a divergent entry that silently overrules a peer's
1702
+ // deletion — the remote is emphatically no longer at the recorded etag, so
1703
+ // the strategy's own fence says fail closed. `keep`'s recreate-once
1704
+ // behaviour for non-divergent entries is unaffected.
1705
+ it("publish-local does not resurrect a remote deleted after the keep", async () => {
1706
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1707
+ fs.mkdirSync(companyRoot, { recursive: true });
1708
+ const testFile = path.join(companyRoot, "peer-deleted.md");
1709
+ const localBytes = "my divergent local body";
1710
+ fs.writeFileSync(testFile, localBytes);
1711
+ const localHash = hashFile(testFile);
1712
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1713
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1714
+ fs.writeFileSync(journalPath, JSON.stringify({
1715
+ version: "1",
1716
+ lastSync: new Date().toISOString(),
1717
+ files: {
1718
+ "peer-deleted.md": {
1719
+ hash: localHash,
1720
+ size: localBytes.length,
1721
+ syncedAt: new Date().toISOString(),
1722
+ direction: "down",
1723
+ remoteEtag: "etag-before-the-peer-deleted-it",
1724
+ localDiverges: true,
1725
+ },
1726
+ },
1727
+ }));
1728
+ const result = await share({
1729
+ paths: [testFile],
1730
+ company: "acme",
1731
+ vaultConfig: mockConfig,
1732
+ hqRoot: tmpDir,
1733
+ skipUnchanged: false,
1734
+ onConflict: "publish-local",
1735
+ });
1736
+ expect(uploadFile).not.toHaveBeenCalled();
1737
+ expect(result.filesUploaded).toBe(0);
1738
+ expect(result.conflictPaths).toContain("peer-deleted.md");
1739
+ const journal = journalAt(journalPath);
1740
+ expect(journal.files["peer-deleted.md"].localDiverges).toBe(true);
1741
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1742
+ });
1743
+ it("publish-local on a non-divergent file behaves like an ordinary fenced upload", async () => {
1744
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1745
+ fs.mkdirSync(companyRoot, { recursive: true });
1746
+ const testFile = path.join(companyRoot, "ordinary.md");
1747
+ fs.writeFileSync(testFile, "locally edited since last sync");
1748
+ const REMOTE = "unchanged-remote-etag";
1749
+ vi.mocked(headRemoteFile).mockResolvedValueOnce({
1750
+ lastModified: new Date(),
1751
+ etag: `"${REMOTE}"`,
1752
+ size: 10,
1753
+ });
1754
+ vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"next-etag"' });
1755
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1756
+ fs.writeFileSync(journalPath, JSON.stringify({
1757
+ version: "1",
1758
+ lastSync: new Date().toISOString(),
1759
+ files: {
1760
+ "ordinary.md": {
1761
+ hash: "stale-hash-from-a-previous-body",
1762
+ size: 10,
1763
+ syncedAt: new Date().toISOString(),
1764
+ direction: "up",
1765
+ remoteEtag: REMOTE,
1766
+ },
1767
+ },
1768
+ }));
1769
+ const result = await share({
1770
+ paths: [testFile],
1771
+ company: "acme",
1772
+ vaultConfig: mockConfig,
1773
+ hqRoot: tmpDir,
1774
+ skipUnchanged: false,
1775
+ onConflict: "publish-local",
1776
+ });
1777
+ // No localDiverges flag means no special casing: the normal push fence
1778
+ // applies and the file uploads without being routed through conflict.
1779
+ expect(result.filesUploaded).toBe(1);
1780
+ expect(result.conflictPaths ?? []).not.toContain("ordinary.md");
1781
+ expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1782
+ });
1453
1783
  // Journal still carries remoteEtag from a prior sync, but the remote key
1454
1784
  // is gone (HEAD null) — e.g. peer delete or operator purge. Recreation
1455
1785
  // must use If-None-Match:* (create-only), NOT If-Match on the stale
@@ -1467,7 +1797,7 @@ describe("share", () => {
1467
1797
  vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1468
1798
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"recreated-etag"' });
1469
1799
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1470
- fs.writeFileSync(journalPath, JSON.stringify({
1800
+ seedJournalAt(journalPath, ({
1471
1801
  version: "1",
1472
1802
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1473
1803
  files: {
@@ -1497,7 +1827,7 @@ describe("share", () => {
1497
1827
  const pc = vi.mocked(uploadFile).mock.calls[0][4];
1498
1828
  expect(pc?.ifMatch).toBeUndefined();
1499
1829
  expect(pc?.ifNoneMatch).toBe("*");
1500
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1830
+ const journal = journalAt(journalPath);
1501
1831
  expect(journal.files["recreate-deleted.md"]).toMatchObject({
1502
1832
  hash: localHash,
1503
1833
  remoteEtag: "recreated-etag",
@@ -1518,7 +1848,7 @@ describe("share", () => {
1518
1848
  return undefined;
1519
1849
  });
1520
1850
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1521
- fs.writeFileSync(journalPath, JSON.stringify({
1851
+ seedJournalAt(journalPath, ({
1522
1852
  version: "1",
1523
1853
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1524
1854
  files: {
@@ -1550,7 +1880,7 @@ describe("share", () => {
1550
1880
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
1551
1881
  // Journal not advanced to a successful upload etag (entry may still be
1552
1882
  // the prior baseline — next pass re-evaluates).
1553
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1883
+ const journal = journalAt(journalPath);
1554
1884
  expect(journal.files["race-recreate.md"]?.remoteEtag).toBe("etag-before-delete");
1555
1885
  expect(journal.files["race-recreate.md"]?.hash).toBe("old-hash");
1556
1886
  expect(fs.readFileSync(testFile, "utf-8")).toBe("my recreated local");
@@ -1874,7 +2204,7 @@ describe("share", () => {
1874
2204
  size: 5,
1875
2205
  });
1876
2206
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1877
- fs.writeFileSync(journalPath, JSON.stringify({
2207
+ seedJournalAt(journalPath, ({
1878
2208
  version: "1",
1879
2209
  lastSync: syncedAt,
1880
2210
  files: {
@@ -1914,7 +2244,7 @@ describe("share", () => {
1914
2244
  size: 5,
1915
2245
  });
1916
2246
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1917
- fs.writeFileSync(journalPath, JSON.stringify({
2247
+ seedJournalAt(journalPath, ({
1918
2248
  version: "1",
1919
2249
  lastSync: syncedAt,
1920
2250
  files: {
@@ -1950,7 +2280,7 @@ describe("share", () => {
1950
2280
  hqRoot: tmpDir,
1951
2281
  });
1952
2282
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1953
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2283
+ const journal = journalAt(journalPath);
1954
2284
  expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
1955
2285
  });
1956
2286
  it("forwards UploadAuthor to uploadFile when present (created-by metadata)", async () => {
@@ -1995,7 +2325,7 @@ describe("share", () => {
1995
2325
  const { hashFile } = await import("../journal.js");
1996
2326
  const hash = hashFile(testFile);
1997
2327
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1998
- fs.writeFileSync(journalPath, JSON.stringify({
2328
+ seedJournalAt(journalPath, ({
1999
2329
  version: "1",
2000
2330
  lastSync: new Date().toISOString(),
2001
2331
  files: {
@@ -2215,7 +2545,7 @@ describe("share", () => {
2215
2545
  .update("stable content")
2216
2546
  .digest("hex");
2217
2547
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2218
- fs.writeFileSync(journalPath, JSON.stringify({
2548
+ seedJournalAt(journalPath, ({
2219
2549
  version: "1",
2220
2550
  lastSync: new Date().toISOString(),
2221
2551
  files: {
@@ -2276,7 +2606,7 @@ describe("share", () => {
2276
2606
  // not be queued for delete.
2277
2607
  fs.writeFileSync(path.join(tmpDir, ".hqinclude"), "companies/*/knowledge/\n");
2278
2608
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2279
- fs.writeFileSync(journalPath, JSON.stringify({
2609
+ seedJournalAt(journalPath, ({
2280
2610
  version: "1",
2281
2611
  lastSync: new Date().toISOString(),
2282
2612
  files: {
@@ -2313,7 +2643,7 @@ describe("share", () => {
2313
2643
  operation: "delete",
2314
2644
  });
2315
2645
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
2316
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2646
+ const journal = journalAt(journalPath);
2317
2647
  expect(journal.files["knowledge"]).toBeUndefined();
2318
2648
  });
2319
2649
  it("propagateDeletes: a dangling local symlink is NOT classified as gone (lstat, not existsSync)", async () => {
@@ -2330,7 +2660,7 @@ describe("share", () => {
2330
2660
  const danglingLink = path.join(companyRoot, "dangling-link.md");
2331
2661
  fs.symlinkSync("./missing-target.md", danglingLink);
2332
2662
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2333
- fs.writeFileSync(journalPath, JSON.stringify({
2663
+ seedJournalAt(journalPath, ({
2334
2664
  version: "1",
2335
2665
  lastSync: new Date().toISOString(),
2336
2666
  files: {
@@ -2360,7 +2690,7 @@ describe("share", () => {
2360
2690
  expect(result.filesDeleted).toBe(0);
2361
2691
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2362
2692
  // Journal entry must survive the run.
2363
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2693
+ const journal = journalAt(journalPath);
2364
2694
  expect(journal.files["dangling-link.md"]).toBeDefined();
2365
2695
  });
2366
2696
  it("vault-litter drain: a `.drift-*` marker with direction:'down' and NO delete intent tombstones " +
@@ -2425,7 +2755,7 @@ describe("share", () => {
2425
2755
  });
2426
2756
  expect(result.filesDeleted).toBe(1);
2427
2757
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/scripts/run-project.sh.drift-1779863862-42519");
2428
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2758
+ const journal = journalAt(journalPath);
2429
2759
  expect(journal.files["personal/scripts/run-project.sh.drift-1779863862-42519"]).toBeUndefined();
2430
2760
  }
2431
2761
  finally {
@@ -2498,7 +2828,7 @@ describe("share", () => {
2498
2828
  fs.mkdirSync(companyRoot, { recursive: true });
2499
2829
  // No file on disk under companies/acme/peer-uploaded.md → local missing.
2500
2830
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2501
- fs.writeFileSync(journalPath, JSON.stringify({
2831
+ seedJournalAt(journalPath, ({
2502
2832
  version: "1",
2503
2833
  lastSync: new Date().toISOString(),
2504
2834
  files: {
@@ -2523,7 +2853,7 @@ describe("share", () => {
2523
2853
  });
2524
2854
  expect(result.filesDeleted).toBe(0);
2525
2855
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2526
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2856
+ const journal = journalAt(journalPath);
2527
2857
  expect(journal.files["peer-uploaded.md"]).toBeDefined();
2528
2858
  });
2529
2859
  it("ROLLBACK KNOB: owned-only is honored on a prs_ personal vault — an intent-less entry with a " +
@@ -2614,7 +2944,7 @@ describe("share", () => {
2614
2944
  ]);
2615
2945
  // Journal entry survives, so a later run (or a watcher-minted intent)
2616
2946
  // can still authorize the delete properly.
2617
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2947
+ const journal = journalAt(journalPath);
2618
2948
  expect(journal.files["personal/knowledge/gone.md"]).toBeDefined();
2619
2949
  }
2620
2950
  finally {
@@ -2631,7 +2961,7 @@ describe("share", () => {
2631
2961
  // deleted by the user.
2632
2962
  fs.writeFileSync(path.join(companyRoot, "kept.md"), "still here");
2633
2963
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2634
- fs.writeFileSync(journalPath, JSON.stringify({
2964
+ seedJournalAt(journalPath, ({
2635
2965
  version: "1",
2636
2966
  lastSync: new Date().toISOString(),
2637
2967
  files: {
@@ -2667,7 +2997,7 @@ describe("share", () => {
2667
2997
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2668
2998
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "gone.md");
2669
2999
  // Journal entry for the gone file is removed; the kept entry stays.
2670
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3000
+ const journal = journalAt(journalPath);
2671
3001
  expect(journal.files["gone.md"]).toBeUndefined();
2672
3002
  expect(journal.files["kept.md"]).toBeDefined();
2673
3003
  });
@@ -2675,7 +3005,7 @@ describe("share", () => {
2675
3005
  const companyRoot = path.join(tmpDir, "companies", "acme");
2676
3006
  fs.mkdirSync(companyRoot, { recursive: true });
2677
3007
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2678
- fs.writeFileSync(journalPath, JSON.stringify({
3008
+ seedJournalAt(journalPath, ({
2679
3009
  version: "1",
2680
3010
  lastSync: new Date().toISOString(),
2681
3011
  files: {
@@ -2717,7 +3047,7 @@ describe("share", () => {
2717
3047
  const companyRoot = path.join(tmpDir, "companies", "acme");
2718
3048
  fs.mkdirSync(companyRoot, { recursive: true });
2719
3049
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2720
- fs.writeFileSync(journalPath, JSON.stringify({
3050
+ seedJournalAt(journalPath, ({
2721
3051
  version: "1",
2722
3052
  lastSync: new Date().toISOString(),
2723
3053
  files: {
@@ -2740,7 +3070,7 @@ describe("share", () => {
2740
3070
  expect(result.filesDeleted).toBe(0);
2741
3071
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2742
3072
  // Journal entry survives so the next opt-in run can still propagate.
2743
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3073
+ const journal = journalAt(journalPath);
2744
3074
  expect(journal.files["gone.md"]).toBeDefined();
2745
3075
  });
2746
3076
  it("propagateDeletes: scope is constrained to the supplied paths — sibling deletes are not swept", async () => {
@@ -2748,7 +3078,7 @@ describe("share", () => {
2748
3078
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
2749
3079
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
2750
3080
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2751
- fs.writeFileSync(journalPath, JSON.stringify({
3081
+ seedJournalAt(journalPath, ({
2752
3082
  version: "1",
2753
3083
  lastSync: new Date().toISOString(),
2754
3084
  files: {
@@ -2783,7 +3113,7 @@ describe("share", () => {
2783
3113
  expect(result.filesDeleted).toBe(1);
2784
3114
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
2785
3115
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
2786
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3116
+ const journal = journalAt(journalPath);
2787
3117
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
2788
3118
  // Sibling tree's journal entry is untouched — `hq share <subtree>` must
2789
3119
  // not act on files outside the named scope.
@@ -2799,7 +3129,7 @@ describe("share", () => {
2799
3129
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
2800
3130
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
2801
3131
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2802
- fs.writeFileSync(journalPath, JSON.stringify({
3132
+ seedJournalAt(journalPath, ({
2803
3133
  version: "1",
2804
3134
  lastSync: new Date().toISOString(),
2805
3135
  files: {
@@ -2834,7 +3164,7 @@ describe("share", () => {
2834
3164
  });
2835
3165
  expect(result.filesDeleted).toBe(1);
2836
3166
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
2837
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3167
+ const journal = journalAt(journalPath);
2838
3168
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
2839
3169
  // Scope stays exactly as narrow as the absolute spelling's — no wider.
2840
3170
  expect(journal.files["other/also-gone.md"]).toBeDefined();
@@ -2851,7 +3181,7 @@ describe("share", () => {
2851
3181
  fs.mkdirSync(acmeRepo, { recursive: true });
2852
3182
  fs.symlinkSync(acmeRepo, path.join(companyRoot, "knowledge"));
2853
3183
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2854
- fs.writeFileSync(journalPath, JSON.stringify({
3184
+ seedJournalAt(journalPath, ({
2855
3185
  version: "1",
2856
3186
  lastSync: new Date().toISOString(),
2857
3187
  files: {
@@ -2881,14 +3211,14 @@ describe("share", () => {
2881
3211
  });
2882
3212
  expect(result.filesDeleted).toBe(0);
2883
3213
  expect(deleteRemoteFile).not.toHaveBeenCalled();
2884
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3214
+ const journal = journalAt(journalPath);
2885
3215
  expect(journal.files["knowledge/agents/brief.md"]).toBeDefined();
2886
3216
  });
2887
3217
  it("propagateDeletes: a failed DeleteObject leaves the journal entry intact for retry", async () => {
2888
3218
  const companyRoot = path.join(tmpDir, "companies", "acme");
2889
3219
  fs.mkdirSync(companyRoot, { recursive: true });
2890
3220
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2891
- fs.writeFileSync(journalPath, JSON.stringify({
3221
+ seedJournalAt(journalPath, ({
2892
3222
  version: "1",
2893
3223
  lastSync: new Date().toISOString(),
2894
3224
  files: {
@@ -2921,7 +3251,7 @@ describe("share", () => {
2921
3251
  const errorEvent = events.find((e) => e.type === "error");
2922
3252
  expect(errorEvent).toMatchObject({ path: "flaky.md", message: expect.stringContaining("S3 down") });
2923
3253
  // Entry survives — next run will retry the delete.
2924
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3254
+ const journal = journalAt(journalPath);
2925
3255
  expect(journal.files["flaky.md"]).toBeDefined();
2926
3256
  });
2927
3257
  // ── Delete propagation safety: direction + filter guards (Bug B + C) ──────
@@ -2955,7 +3285,7 @@ describe("share", () => {
2955
3285
  // and one this machine uploaded (direction:'up'). Only the 'up' one should
2956
3286
  // be eligible for delete-propagation.
2957
3287
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2958
- fs.writeFileSync(journalPath, JSON.stringify({
3288
+ seedJournalAt(journalPath, ({
2959
3289
  version: "1",
2960
3290
  lastSync: new Date().toISOString(),
2961
3291
  files: {
@@ -2992,7 +3322,7 @@ describe("share", () => {
2992
3322
  const companyRoot = path.join(tmpDir, "companies", "acme");
2993
3323
  fs.mkdirSync(companyRoot, { recursive: true });
2994
3324
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2995
- fs.writeFileSync(journalPath, JSON.stringify({
3325
+ seedJournalAt(journalPath, ({
2996
3326
  version: "1",
2997
3327
  lastSync: new Date().toISOString(),
2998
3328
  files: {
@@ -3025,7 +3355,7 @@ describe("share", () => {
3025
3355
  // absence is a filter outcome, not a user-intended delete.
3026
3356
  fs.writeFileSync(path.join(tmpDir, ".hqignore"), "companies/acme/legacy/**\n");
3027
3357
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3028
- fs.writeFileSync(journalPath, JSON.stringify({
3358
+ seedJournalAt(journalPath, ({
3029
3359
  version: "1",
3030
3360
  lastSync: new Date().toISOString(),
3031
3361
  files: {
@@ -3084,7 +3414,7 @@ describe("share", () => {
3084
3414
  // Under owned-only this would be stuck on remote forever; currency-gated
3085
3415
  // checks the ETag and propagates the delete cleanly when match holds.
3086
3416
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3087
- fs.writeFileSync(journalPath, JSON.stringify({
3417
+ seedJournalAt(journalPath, ({
3088
3418
  version: "1",
3089
3419
  lastSync: new Date().toISOString(),
3090
3420
  files: {
@@ -3114,7 +3444,7 @@ describe("share", () => {
3114
3444
  });
3115
3445
  expect(result.filesDeleted).toBe(1);
3116
3446
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "core/policies/old.md");
3117
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3447
+ const journal = journalAt(journalPath);
3118
3448
  expect(journal.files["core/policies/old.md"]).toBeUndefined();
3119
3449
  });
3120
3450
  it("currency-gated: propagates the S3 delete for a local-delete tombstone (pull-leg producer hand-off)", async () => {
@@ -3125,7 +3455,7 @@ describe("share", () => {
3125
3455
  const companyRoot = path.join(tmpDir, "companies", "acme");
3126
3456
  fs.mkdirSync(companyRoot, { recursive: true });
3127
3457
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3128
- fs.writeFileSync(journalPath, JSON.stringify({
3458
+ seedJournalAt(journalPath, ({
3129
3459
  version: "2",
3130
3460
  lastSync: new Date().toISOString(),
3131
3461
  files: {
@@ -3161,7 +3491,7 @@ describe("share", () => {
3161
3491
  });
3162
3492
  expect(result.filesDeleted).toBe(1);
3163
3493
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/deleted.md");
3164
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3494
+ const journal = journalAt(journalPath);
3165
3495
  expect(journal.files["docs/deleted.md"]).toBeUndefined();
3166
3496
  });
3167
3497
  it("currency-gated: refuses delete + emits stale-etag event when remote moved since last sync", async () => {
@@ -3170,7 +3500,7 @@ describe("share", () => {
3170
3500
  // Another device modified `shared.md` after this device's last sync.
3171
3501
  // The journal still records the old etag; HEAD returns the new one.
3172
3502
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3173
- fs.writeFileSync(journalPath, JSON.stringify({
3503
+ seedJournalAt(journalPath, ({
3174
3504
  version: "1",
3175
3505
  lastSync: new Date().toISOString(),
3176
3506
  files: {
@@ -3202,7 +3532,7 @@ describe("share", () => {
3202
3532
  // No remote DELETE issued, journal entry preserved (pull will re-pull).
3203
3533
  expect(result.filesDeleted).toBe(0);
3204
3534
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3205
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3535
+ const journal = journalAt(journalPath);
3206
3536
  expect(journal.files["shared.md"]).toBeDefined();
3207
3537
  // Dedicated event fired so UIs surface the refusal. `reason` field
3208
3538
  // discriminates "stale-etag" (real drift) from "legacy-no-etag" so
@@ -3224,7 +3554,7 @@ describe("share", () => {
3224
3554
  fs.mkdirSync(companyRoot, { recursive: true });
3225
3555
  const recreatedPath = path.join(companyRoot, "racy.md");
3226
3556
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3227
- fs.writeFileSync(journalPath, JSON.stringify({
3557
+ seedJournalAt(journalPath, ({
3228
3558
  version: "2",
3229
3559
  lastSync: new Date().toISOString(),
3230
3560
  files: {
@@ -3260,7 +3590,7 @@ describe("share", () => {
3260
3590
  expect(result.filesDeleted).toBe(0);
3261
3591
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3262
3592
  expect(fs.readFileSync(recreatedPath, "utf-8")).toBe("recreated after planning");
3263
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3593
+ const journal = journalAt(journalPath);
3264
3594
  expect(journal.files["racy.md"]).toBeDefined();
3265
3595
  expect(events).toContainEqual(expect.objectContaining({
3266
3596
  type: "delete-refused-stale-etag",
@@ -3282,7 +3612,7 @@ describe("share", () => {
3282
3612
  // genuine local==remote sync, so propagating the delete would destroy the
3283
3613
  // (different) remote version. Must be REFUSED, not propagated.
3284
3614
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3285
- fs.writeFileSync(journalPath, JSON.stringify({
3615
+ seedJournalAt(journalPath, ({
3286
3616
  version: "2",
3287
3617
  lastSync: new Date().toISOString(),
3288
3618
  files: {
@@ -3317,7 +3647,7 @@ describe("share", () => {
3317
3647
  // Refusal short-circuits before the currency HEAD — no network needed.
3318
3648
  expect(headRemoteFile).not.toHaveBeenCalled();
3319
3649
  // Journal entry preserved, not silently dropped.
3320
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3650
+ const journal = journalAt(journalPath);
3321
3651
  expect(journal.files["core/policies/kept.md"]).toBeDefined();
3322
3652
  // Surfaced as a refusal with the divergent-local reason.
3323
3653
  const refused = events.find((e) => e.type === "delete-refused-stale-etag");
@@ -3334,7 +3664,7 @@ describe("share", () => {
3334
3664
  // S3 console). Local copy also missing. Currency-gated should drop the
3335
3665
  // journal entry without issuing a DELETE — the remote is already gone.
3336
3666
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3337
- fs.writeFileSync(journalPath, JSON.stringify({
3667
+ seedJournalAt(journalPath, ({
3338
3668
  version: "1",
3339
3669
  lastSync: new Date().toISOString(),
3340
3670
  files: {
@@ -3367,7 +3697,7 @@ describe("share", () => {
3367
3697
  // from real deletes without re-counting events.
3368
3698
  expect(result.filesTombstoned).toBe(1);
3369
3699
  expect(result.filesRefusedStale).toBe(0);
3370
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3700
+ const journal = journalAt(journalPath);
3371
3701
  expect(journal.files["removed-out-of-band.md"]).toBeUndefined();
3372
3702
  // Synthetic progress event carries the tombstone marker. Without this,
3373
3703
  // the tty stream renders tombstones identically to real deletes — operator
@@ -3388,7 +3718,7 @@ describe("share", () => {
3388
3718
  // against. Refuse the delete in the safe direction; a future sync with
3389
3719
  // a recorded etag can re-evaluate.
3390
3720
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3391
- fs.writeFileSync(journalPath, JSON.stringify({
3721
+ seedJournalAt(journalPath, ({
3392
3722
  version: "1",
3393
3723
  lastSync: new Date().toISOString(),
3394
3724
  files: {
@@ -3442,7 +3772,7 @@ describe("share", () => {
3442
3772
  const companyRoot = path.join(tmpDir, "companies", "acme");
3443
3773
  fs.mkdirSync(companyRoot, { recursive: true });
3444
3774
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3445
- fs.writeFileSync(journalPath, JSON.stringify({
3775
+ seedJournalAt(journalPath, ({
3446
3776
  version: "1",
3447
3777
  lastSync: new Date().toISOString(),
3448
3778
  files: {
@@ -3494,7 +3824,7 @@ describe("share", () => {
3494
3824
  const companyRoot = path.join(tmpDir, "companies", "acme");
3495
3825
  fs.mkdirSync(companyRoot, { recursive: true });
3496
3826
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3497
- fs.writeFileSync(journalPath, JSON.stringify({
3827
+ seedJournalAt(journalPath, ({
3498
3828
  version: "1",
3499
3829
  lastSync: new Date().toISOString(),
3500
3830
  files: {
@@ -3528,7 +3858,7 @@ describe("share", () => {
3528
3858
  const companyRoot = path.join(tmpDir, "companies", "acme");
3529
3859
  fs.mkdirSync(companyRoot, { recursive: true });
3530
3860
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3531
- fs.writeFileSync(journalPath, JSON.stringify({
3861
+ seedJournalAt(journalPath, ({
3532
3862
  version: "1",
3533
3863
  lastSync: new Date().toISOString(),
3534
3864
  files: {
@@ -3578,7 +3908,7 @@ describe("share", () => {
3578
3908
  fs.mkdirSync(companyRoot, { recursive: true });
3579
3909
  const recreatedPath = path.join(companyRoot, "notes", "gone.md");
3580
3910
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3581
- fs.writeFileSync(journalPath, JSON.stringify({
3911
+ seedJournalAt(journalPath, ({
3582
3912
  version: "1",
3583
3913
  lastSync: new Date().toISOString(),
3584
3914
  files: {
@@ -3626,7 +3956,7 @@ describe("share", () => {
3626
3956
  const companyRoot = path.join(tmpDir, "companies", "acme");
3627
3957
  fs.mkdirSync(companyRoot, { recursive: true });
3628
3958
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3629
- fs.writeFileSync(journalPath, JSON.stringify({
3959
+ seedJournalAt(journalPath, ({
3630
3960
  version: "1",
3631
3961
  lastSync: new Date().toISOString(),
3632
3962
  files: {
@@ -3656,14 +3986,14 @@ describe("share", () => {
3656
3986
  type: "delete-refused-stale-etag",
3657
3987
  reason: "legacy-no-etag",
3658
3988
  }));
3659
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3989
+ const journal = journalAt(journalPath);
3660
3990
  expect(journal.files["notes/legacy.md"]).toBeDefined();
3661
3991
  });
3662
3992
  it("owned-only still requires an intent — it is the documented rollback knob", async () => {
3663
3993
  const companyRoot = path.join(tmpDir, "companies", "acme");
3664
3994
  fs.mkdirSync(companyRoot, { recursive: true });
3665
3995
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3666
- fs.writeFileSync(journalPath, JSON.stringify({
3996
+ seedJournalAt(journalPath, ({
3667
3997
  version: "1",
3668
3998
  lastSync: new Date().toISOString(),
3669
3999
  files: {
@@ -3850,7 +4180,7 @@ describe("share", () => {
3850
4180
  const companyRoot = path.join(tmpDir, "companies", "acme");
3851
4181
  fs.mkdirSync(companyRoot, { recursive: true });
3852
4182
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3853
- fs.writeFileSync(journalPath, JSON.stringify({
4183
+ seedJournalAt(journalPath, ({
3854
4184
  version: "1",
3855
4185
  lastSync: new Date().toISOString(),
3856
4186
  files: {
@@ -3891,7 +4221,7 @@ describe("share", () => {
3891
4221
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "regular.md");
3892
4222
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md");
3893
4223
  // Both journal entries are removed by the delete loop's removeEntry call.
3894
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4224
+ const journal = journalAt(journalPath);
3895
4225
  expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeUndefined();
3896
4226
  expect(journal.files["regular.md"]).toBeUndefined();
3897
4227
  });
@@ -3906,7 +4236,7 @@ describe("share", () => {
3906
4236
  process.env.HQ_STATE_DIR = stateDirPersonal;
3907
4237
  try {
3908
4238
  const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
3909
- fs.writeFileSync(journalPath, JSON.stringify({
4239
+ seedJournalAt(journalPath, ({
3910
4240
  version: "1",
3911
4241
  lastSync: new Date().toISOString(),
3912
4242
  files: {
@@ -3932,7 +4262,7 @@ describe("share", () => {
3932
4262
  });
3933
4263
  expect(result.filesDeleted).toBe(1);
3934
4264
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/.obsidian/graph.json.drift-1779863862-42519");
3935
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4265
+ const journal = journalAt(journalPath);
3936
4266
  expect(journal.files["personal/.obsidian/graph.json.drift-1779863862-42519"]).toBeUndefined();
3937
4267
  }
3938
4268
  finally {
@@ -3955,7 +4285,7 @@ describe("share", () => {
3955
4285
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
3956
4286
  direction: "up",
3957
4287
  });
3958
- fs.writeFileSync(journalPath, JSON.stringify({
4288
+ seedJournalAt(journalPath, ({
3959
4289
  version: "1",
3960
4290
  lastSync: new Date().toISOString(),
3961
4291
  files: {
@@ -4003,7 +4333,7 @@ describe("share", () => {
4003
4333
  expect(pruned[0].byReason["ignore-filtered"]).toBe(3);
4004
4334
  expect(pruned[0].byReason["excluded-top-level"]).toBe(1);
4005
4335
  // The persisted journal converged: excluded rows gone, legit row kept.
4006
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4336
+ const journal = journalAt(journalPath);
4007
4337
  expect(Object.keys(journal.files)).toContain("personal/keep.md");
4008
4338
  expect(journal.files[".claude/worktrees/wt/a.ts"]).toBeUndefined();
4009
4339
  expect(journal.files["personal/node_modules/pkg/index.js"]).toBeUndefined();
@@ -4035,7 +4365,7 @@ describe("share", () => {
4035
4365
  const key = "deleted-top-level.md";
4036
4366
  const journalSlug = "personal-scoped-delete";
4037
4367
  const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
4038
- fs.writeFileSync(journalPath, JSON.stringify({
4368
+ seedJournalAt(journalPath, ({
4039
4369
  version: "2",
4040
4370
  lastSync: new Date().toISOString(),
4041
4371
  pulls: [],
@@ -4067,7 +4397,7 @@ describe("share", () => {
4067
4397
  });
4068
4398
  expect(result.filesDeleted).toBe(1);
4069
4399
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
4070
- expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
4400
+ expect(journalAt(journalPath).files[key]).toBeUndefined();
4071
4401
  });
4072
4402
  // ── Decommission prefixes ──────────────────────────────────────────────────
4073
4403
  //
@@ -4122,7 +4452,7 @@ describe("share", () => {
4122
4452
  // Local file remains on disk — decommission is a remote-only sweep.
4123
4453
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "notes.md"))).toBe(true);
4124
4454
  // Journal entry pruned in lockstep with the remote delete.
4125
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4455
+ const journalAfter = journalAt(personalJournalPath);
4126
4456
  expect(journalAfter.files["companies/foo/notes.md"]).toBeUndefined();
4127
4457
  expect(journalAfter.files["knowledge/x.md"]).toBeDefined();
4128
4458
  });
@@ -4158,7 +4488,7 @@ describe("share", () => {
4158
4488
  expect(deleteRemoteFile).not.toHaveBeenCalled();
4159
4489
  // Journal entry preserved — the pulled-from-peer record stays so a
4160
4490
  // future pull can still reconcile it correctly.
4161
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4491
+ const journalAfter = journalAt(personalJournalPath);
4162
4492
  expect(journalAfter.files["companies/foo/from-peer.md"]).toBeDefined();
4163
4493
  });
4164
4494
  it("decommissionPrefixes: dedupes with propagateDeletes plan — a key in both still emits one DeleteObject", async () => {
@@ -4278,7 +4608,7 @@ describe("share", () => {
4278
4608
  expect(result.filesTombstoned).toBe(1);
4279
4609
  expect(result.filesDeleted).toBe(0);
4280
4610
  // Journal entry is gone.
4281
- const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4611
+ const journalAfter = journalAt(personalJournalPath);
4282
4612
  expect(journalAfter.files["companies/foo/gone.md"]).toBeUndefined();
4283
4613
  });
4284
4614
  describe("personalMode", () => {
@@ -4319,7 +4649,7 @@ describe("share", () => {
4319
4649
  const acmeJournalPath = path.join(stateDir, "sync-journal.acme.json");
4320
4650
  expect(fs.existsSync(personalJournalPath)).toBe(true);
4321
4651
  expect(fs.existsSync(acmeJournalPath)).toBe(false);
4322
- const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4652
+ const journal = journalAt(personalJournalPath);
4323
4653
  expect(journal.files["knowledge/notes.md"]).toBeDefined();
4324
4654
  });
4325
4655
  it("personalMode=true accepts files outside companies/<slug>/ (companion to the company-folder rejection)", async () => {
@@ -4607,7 +4937,7 @@ describe("share", () => {
4607
4937
  .createHash("sha256")
4608
4938
  .update(target)
4609
4939
  .digest("hex");
4610
- fs.writeFileSync(journalPath, JSON.stringify({
4940
+ seedJournalAt(journalPath, ({
4611
4941
  version: "1",
4612
4942
  lastSync: new Date().toISOString(),
4613
4943
  files: {
@@ -5010,7 +5340,7 @@ describe("share", () => {
5010
5340
  });
5011
5341
  expect(pushed.filesUploaded).toBe(1);
5012
5342
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5013
- const afterPush = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5343
+ const afterPush = journalAt(journalPath);
5014
5344
  expect(Object.keys(afterPush.files)).toContain("knowledge/agents/brief.md");
5015
5345
  vi.mocked(deleteRemoteFile).mockClear();
5016
5346
  const walked = await share({
@@ -5024,7 +5354,7 @@ describe("share", () => {
5024
5354
  expect(walked.filesDeleted).toBe(0);
5025
5355
  const deletedKeys = vi.mocked(deleteRemoteFile).mock.calls.map((c) => c[1]);
5026
5356
  expect(deletedKeys).not.toContain("knowledge/agents/brief.md");
5027
- const afterWalk = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5357
+ const afterWalk = journalAt(journalPath);
5028
5358
  expect(Object.keys(afterWalk.files)).toContain("knowledge/agents/brief.md");
5029
5359
  });
5030
5360
  });
@@ -5068,7 +5398,7 @@ describe("share", () => {
5068
5398
  }
5069
5399
  }
5070
5400
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5071
- fs.writeFileSync(journalPath, JSON.stringify({
5401
+ seedJournalAt(journalPath, ({
5072
5402
  version: "1",
5073
5403
  lastSync: new Date().toISOString(),
5074
5404
  files,
@@ -5126,7 +5456,7 @@ describe("share", () => {
5126
5456
  expect(summary.samplePaths.length).toBeGreaterThan(0);
5127
5457
  expect(summary.samplePaths.length).toBeLessThanOrEqual(10);
5128
5458
  // Journal entries for the missing files SURVIVE — guard never mutates.
5129
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5459
+ const journal = journalAt(journalPath);
5130
5460
  expect(Object.keys(journal.files).length).toBe(100);
5131
5461
  // Per-key delete-refused-stale-etag events for each candidate.
5132
5462
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
@@ -5176,7 +5506,7 @@ describe("share", () => {
5176
5506
  expect(headedKeys).not.toContain(`f-${i.toString().padStart(4, "0")}.md`);
5177
5507
  }
5178
5508
  // Journal untouched — the entries stay recoverable.
5179
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5509
+ const journal = journalAt(journalPath);
5180
5510
  expect(Object.keys(journal.files).length).toBe(100);
5181
5511
  });
5182
5512
  // The core of the #227 regression, isolated: pre-#227 a trip emptied
@@ -5207,7 +5537,7 @@ describe("share", () => {
5207
5537
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" &&
5208
5538
  e.reason === "bulk-asymmetry");
5209
5539
  expect(perKeyRefusals.length).toBe(11);
5210
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5540
+ const journal = journalAt(journalPath);
5211
5541
  expect(Object.keys(journal.files).length).toBe(100);
5212
5542
  });
5213
5543
  it("vault litter still drains when the breaker trips", async () => {
@@ -5221,7 +5551,7 @@ describe("share", () => {
5221
5551
  withIntents: false,
5222
5552
  });
5223
5553
  const litterKey = "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md";
5224
- const seeded = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5554
+ const seeded = journalAt(journalPath);
5225
5555
  seeded.files[litterKey] = {
5226
5556
  hash: "h",
5227
5557
  size: 5,
@@ -5230,7 +5560,7 @@ describe("share", () => {
5230
5560
  remoteEtag: "litter-etag",
5231
5561
  kind: "file",
5232
5562
  };
5233
- fs.writeFileSync(journalPath, JSON.stringify(seeded));
5563
+ seedJournalAt(journalPath, seeded);
5234
5564
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
5235
5565
  etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
5236
5566
  lastModified: new Date(),
@@ -5370,7 +5700,7 @@ describe("share", () => {
5370
5700
  expect(summary).toBeDefined();
5371
5701
  expect(summary.ratio).toBeCloseTo(1.0, 5);
5372
5702
  // Journal untouched.
5373
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5703
+ const journal = journalAt(journalPath);
5374
5704
  expect(Object.keys(journal.files).length).toBe(50);
5375
5705
  });
5376
5706
  it("small absolute count (9/9 = 100% ratio but < MIN_ABS=10) does NOT trip: all 9 delete normally", async () => {
@@ -5414,7 +5744,7 @@ describe("share", () => {
5414
5744
  expect(deleteRemoteFile).not.toHaveBeenCalled();
5415
5745
  expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeDefined();
5416
5746
  // Journal entries survive.
5417
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5747
+ const journal = journalAt(journalPath);
5418
5748
  expect(Object.keys(journal.files).length).toBe(100);
5419
5749
  });
5420
5750
  it("direction:'down' entries don't count toward the bulk numerator (owned-only silently skips them)", async () => {
@@ -5442,7 +5772,7 @@ describe("share", () => {
5442
5772
  localDeleteIntent: deleteIntent("up-etag", "h"),
5443
5773
  };
5444
5774
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5445
- fs.writeFileSync(journalPath, JSON.stringify({ version: "1", lastSync: now, files }));
5775
+ seedJournalAt(journalPath, { version: "1", lastSync: now, files });
5446
5776
  const result = await share({
5447
5777
  paths: [companyRoot],
5448
5778
  company: "acme",
@@ -5565,7 +5895,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5565
5895
  fs.mkdirSync(companyRoot, { recursive: true });
5566
5896
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5567
5897
  // v2-shaped fixture — same field set the production journal uses.
5568
- fs.writeFileSync(journalPath, JSON.stringify({
5898
+ seedJournalAt(journalPath, ({
5569
5899
  version: "2",
5570
5900
  lastSync: new Date().toISOString(),
5571
5901
  files: {
@@ -5611,7 +5941,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5611
5941
  const { hashFile } = await import("../journal.js");
5612
5942
  const baselineHash = hashFile(testFile);
5613
5943
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5614
- fs.writeFileSync(journalPath, JSON.stringify({
5944
+ seedJournalAt(journalPath, ({
5615
5945
  version: "2",
5616
5946
  lastSync: new Date().toISOString(),
5617
5947
  files: {
@@ -5652,7 +5982,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5652
5982
  const originalLstat = fs.lstatSync(testFile);
5653
5983
  // The recorded hash intentionally differs from the current bytes.
5654
5984
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5655
- fs.writeFileSync(journalPath, JSON.stringify({
5985
+ seedJournalAt(journalPath, ({
5656
5986
  version: "2",
5657
5987
  lastSync: new Date().toISOString(),
5658
5988
  files: {
@@ -5699,7 +6029,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5699
6029
  const { hashFile } = await import("../journal.js");
5700
6030
  const realHash = hashFile(testFile);
5701
6031
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5702
- fs.writeFileSync(journalPath, JSON.stringify({
6032
+ seedJournalAt(journalPath, ({
5703
6033
  version: "2",
5704
6034
  lastSync: new Date().toISOString(),
5705
6035
  files: {
@@ -5749,7 +6079,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5749
6079
  const { hashFile } = await import("../journal.js");
5750
6080
  const realHash = hashFile(testFile);
5751
6081
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5752
- fs.writeFileSync(journalPath, JSON.stringify({
6082
+ seedJournalAt(journalPath, ({
5753
6083
  version: "2",
5754
6084
  lastSync: new Date().toISOString(),
5755
6085
  files: {
@@ -5781,7 +6111,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5781
6111
  expect(uploadFile).not.toHaveBeenCalled();
5782
6112
  // Journal mtimeMs is now the bumped value, while content hashing remains
5783
6113
  // authoritative on the next sync.
5784
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6114
+ const journalAfter = journalAt(journalPath);
5785
6115
  expect(journalAfter.files["touched-then-fast.md"].mtimeMs).toBe(bumpedLstat.mtimeMs);
5786
6116
  expect(journalAfter.files["touched-then-fast.md"].size).toBe(bumpedLstat.size);
5787
6117
  // Content hash untouched — the file genuinely didn't change.
@@ -5801,7 +6131,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5801
6131
  });
5802
6132
  expect(result.filesUploaded).toBe(1);
5803
6133
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5804
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6134
+ const journal = journalAt(journalPath);
5805
6135
  expect(journal.files["fresh.md"]).toBeDefined();
5806
6136
  expect(journal.files["fresh.md"].mtimeMs).toBe(lstat.mtimeMs);
5807
6137
  expect(journal.files["fresh.md"].size).toBe(lstat.size);
@@ -5818,7 +6148,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
5818
6148
  const realSize = fs.statSync(testFile).size;
5819
6149
  // Real hash but NO mtimeMs. Hash matches → skip upload.
5820
6150
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5821
- fs.writeFileSync(journalPath, JSON.stringify({
6151
+ seedJournalAt(journalPath, ({
5822
6152
  version: "1",
5823
6153
  lastSync: new Date().toISOString(),
5824
6154
  files: {
@@ -6377,7 +6707,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6377
6707
  // AND lost their etag/content-hash/stat journal metadata.
6378
6708
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6379
6709
  expect(fs.existsSync(journalPath)).toBe(true);
6380
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6710
+ const journal = journalAt(journalPath);
6381
6711
  const stampedKeys = Object.keys(journal.files);
6382
6712
  expect(stampedKeys.length).toBe(FILE_COUNT - 1);
6383
6713
  // The failed file is NOT in the journal (no successful upload).
@@ -6525,7 +6855,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6525
6855
  const companyRoot = path.join(tmpDir, "companies", "acme");
6526
6856
  fs.mkdirSync(companyRoot, { recursive: true });
6527
6857
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6528
- fs.writeFileSync(journalPath, JSON.stringify({
6858
+ seedJournalAt(journalPath, ({
6529
6859
  version: "1",
6530
6860
  lastSync: new Date().toISOString(),
6531
6861
  files: {
@@ -6554,7 +6884,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6554
6884
  expect(headRemoteFile).not.toHaveBeenCalled();
6555
6885
  expect(deleteRemoteFile).not.toHaveBeenCalled();
6556
6886
  expect(uploadFile).not.toHaveBeenCalled();
6557
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6887
+ const journal = journalAt(journalPath);
6558
6888
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
6559
6889
  });
6560
6890
  it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
@@ -6566,7 +6896,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6566
6896
  fs.mkdirSync(targetDir, { recursive: true });
6567
6897
  fs.symlinkSync(targetDir, linkPath, "dir");
6568
6898
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6569
- fs.writeFileSync(journalPath, JSON.stringify({
6899
+ seedJournalAt(journalPath, ({
6570
6900
  version: "1",
6571
6901
  lastSync: new Date().toISOString(),
6572
6902
  files: {
@@ -6611,7 +6941,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
6611
6941
  samplePaths: [linkKey],
6612
6942
  });
6613
6943
  expect(events.some((event) => event.type === "error")).toBe(false);
6614
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6944
+ const journal = journalAt(journalPath);
6615
6945
  expect(journal.files[linkKey]).toBeDefined();
6616
6946
  }
6617
6947
  finally {