@indigoai-us/hq-cloud 6.15.20 → 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 (38) 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.test.js +140 -108
  8. package/dist/cli/share.test.js.map +1 -1
  9. package/dist/cli/sync.test.js +59 -47
  10. package/dist/cli/sync.test.js.map +1 -1
  11. package/dist/journal-delta-writes.test.d.ts +2 -0
  12. package/dist/journal-delta-writes.test.d.ts.map +1 -0
  13. package/dist/journal-delta-writes.test.js +140 -0
  14. package/dist/journal-delta-writes.test.js.map +1 -0
  15. package/dist/journal.d.ts.map +1 -1
  16. package/dist/journal.js +151 -24
  17. package/dist/journal.js.map +1 -1
  18. package/dist/journal.test.js +29 -11
  19. package/dist/journal.test.js.map +1 -1
  20. package/dist/outcome-telemetry.d.ts +7 -1
  21. package/dist/outcome-telemetry.d.ts.map +1 -1
  22. package/dist/outcome-telemetry.js +15 -9
  23. package/dist/outcome-telemetry.js.map +1 -1
  24. package/dist/outcome-telemetry.test.js +15 -0
  25. package/dist/outcome-telemetry.test.js.map +1 -1
  26. package/dist/skill-telemetry.d.ts +6 -1
  27. package/dist/skill-telemetry.d.ts.map +1 -1
  28. package/dist/skill-telemetry.js +14 -8
  29. package/dist/skill-telemetry.js.map +1 -1
  30. package/dist/skill-telemetry.test.js +43 -0
  31. package/dist/skill-telemetry.test.js.map +1 -1
  32. package/dist/telemetry.d.ts +7 -1
  33. package/dist/telemetry.d.ts.map +1 -1
  34. package/dist/telemetry.js +67 -60
  35. package/dist/telemetry.js.map +1 -1
  36. package/dist/telemetry.test.js +12 -0
  37. package/dist/telemetry.test.js.map +1 -1
  38. package/package.json +1 -1
@@ -118,7 +118,23 @@ import * as s3Module from "../s3.js";
118
118
  import { reindex } from "./reindex.js";
119
119
  import { readSyncProgress } from "../sync-progress.js";
120
120
  import { VaultAuthError } from "../vault-client.js";
121
- import { readJournal, getEntry } from "../journal.js";
121
+ import { readJournal, getEntry, listJournals } from "../journal.js";
122
+ /**
123
+ * Read a journal the way production does.
124
+ *
125
+ * These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
126
+ * when that file held a full copy of the journal. It is now a small locator —
127
+ * the authority is the v3 state store — so the assertions below go through
128
+ * `readJournal`, which is what the sync engine itself uses. The slug is
129
+ * recovered from the path so each call site keeps naming the journal it means.
130
+ */
131
+ function journalAt(journalPath) {
132
+ const slug = path
133
+ .basename(journalPath)
134
+ .replace(/^sync-journal\./, "")
135
+ .replace(/\.json$/, "");
136
+ return readJournal(slug);
137
+ }
122
138
  const mockConfig = {
123
139
  apiUrl: "https://vault-api.test",
124
140
  authToken: "test-jwt-token",
@@ -516,7 +532,7 @@ describe("sync", () => {
516
532
  hqRoot: tmpDir,
517
533
  });
518
534
  expect(result.conflicts).toBe(1);
519
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
535
+ const journal = journalAt(journalPath);
520
536
  // remoteEtag still recorded so the conflict can't re-fire (#137 invariant)…
521
537
  expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
522
538
  // …but the kept-local copy is flagged divergent so a later currency-gated
@@ -556,7 +572,7 @@ describe("sync", () => {
556
572
  // Identical bookkeeping to `keep`: the etag is stamped so the conflict
557
573
  // cannot re-fire, and the honesty flag is armed so the push leg knows this
558
574
  // is the divergent body it may promote under an If-Match fence.
559
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
575
+ const journal = journalAt(journalPath);
560
576
  expect(journal.files["docs/handoff.md"].remoteEtag).toBeTruthy();
561
577
  expect(journal.files["docs/handoff.md"].localDiverges).toBe(true);
562
578
  });
@@ -585,7 +601,7 @@ describe("sync", () => {
585
601
  ]);
586
602
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
587
603
  expect(fs.existsSync(path.join(companyDocs, "handoff.md"))).toBe(true);
588
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
604
+ const journal = journalAt(journalPath);
589
605
  expect(journal.files["docs/handoff.md"]?.localDiverges).toBeFalsy();
590
606
  });
591
607
  it("leaves a downloaded unreadable link unjournaled without emitting a fatal error", async () => {
@@ -624,7 +640,7 @@ describe("sync", () => {
624
640
  samplePaths: [linkKey],
625
641
  });
626
642
  expect(events.some((event) => event.type === "error")).toBe(false);
627
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
643
+ const journal = journalAt(journalPath);
628
644
  expect(journal.files[linkKey]).toBeUndefined();
629
645
  }
630
646
  finally {
@@ -722,7 +738,7 @@ describe("sync", () => {
722
738
  expect(litter).toEqual([]);
723
739
  // Journal re-stamped off the stale baseline (hash now matches local,
724
740
  // remoteEtag recorded) so the phantom conflict can't re-fire.
725
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
741
+ const journalAfter = journalAt(journalPath);
726
742
  expect(journalAfter.files["docs/handoff.md"].hash).not.toBe("stale-hash");
727
743
  expect(journalAfter.files["docs/handoff.md"].remoteEtag).toBeTruthy();
728
744
  });
@@ -780,7 +796,7 @@ describe("sync", () => {
780
796
  });
781
797
  expect(events.some((event) => event.type === "error")).toBe(false);
782
798
  expect(fs.readFileSync(localPath, "utf-8")).toBe("local version");
783
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
799
+ const journal = journalAt(journalPath);
784
800
  expect(journal.files[linkKey].hash).toBe("stale-hash");
785
801
  }
786
802
  finally {
@@ -1168,7 +1184,7 @@ describe("sync", () => {
1168
1184
  .filter((f) => f.includes(".conflict-"));
1169
1185
  expect(litter).toHaveLength(0);
1170
1186
  // Journal stamped, so the next pass is a plain unchanged-skip.
1171
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1187
+ const journal = journalAt(journalPath);
1172
1188
  expect(journal.files["docs/handoff.md"]).toBeDefined();
1173
1189
  });
1174
1190
  it("RF-F02EXEC: refuses a download whose parent symlink appears after planning", async () => {
@@ -1328,7 +1344,7 @@ describe("sync", () => {
1328
1344
  });
1329
1345
  expect(fs.readFileSync(untrackedPath, "utf-8")).toBe("brand new local work");
1330
1346
  expect(fs.existsSync(trackedPath)).toBe(false);
1331
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1347
+ const journal = journalAt(journalPath);
1332
1348
  expect(journal.files[untrackedKey]).toBeUndefined();
1333
1349
  expect(journal.files[trackedKey]).toBeUndefined();
1334
1350
  });
@@ -1401,7 +1417,7 @@ describe("sync", () => {
1401
1417
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1402
1418
  expect(s3Module.downloadFile).toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1403
1419
  expect(fs.readFileSync(localPath, "utf-8")).toBe("mock file content");
1404
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1420
+ const journal = journalAt(journalPath);
1405
1421
  expect(journal.files[key]?.removedAt).toBeUndefined();
1406
1422
  });
1407
1423
  it("does not canonicalize ignored remote-missing journal keys in the tombstone sweep", async () => {
@@ -1449,7 +1465,7 @@ describe("sync", () => {
1449
1465
  // Equivalence: an ignored key is never tombstoned — the local file and its
1450
1466
  // journal entry both survive, exactly as before the fix.
1451
1467
  expect(fs.existsSync(ignoredLocal)).toBe(true);
1452
- const sweptJournal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1468
+ const sweptJournal = journalAt(journalPath);
1453
1469
  expect(sweptJournal.files[ignoredKey]).toBeDefined();
1454
1470
  });
1455
1471
  it("tombstone lookup failure does not resurrect a current intent-backed delete", async () => {
@@ -1483,7 +1499,7 @@ describe("sync", () => {
1483
1499
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1484
1500
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything(), expect.anything());
1485
1501
  expect(fs.existsSync(localPath)).toBe(false);
1486
- expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]
1502
+ expect(journalAt(journalPath).files[key]
1487
1503
  ?.removedReason).toBe("local-delete");
1488
1504
  });
1489
1505
  it("pull producer: a BULK disappearance is drift-restored, NOT tombstoned (ridge-incident guard in the pull leg)", async () => {
@@ -1519,7 +1535,7 @@ describe("sync", () => {
1519
1535
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1520
1536
  // All restored, none tombstoned.
1521
1537
  expect(s3Module.downloadFile).toHaveBeenCalledTimes(12);
1522
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1538
+ const journal = journalAt(journalPath);
1523
1539
  for (const k of keys) {
1524
1540
  expect(journal.files[k]?.removedAt).toBeUndefined();
1525
1541
  }
@@ -1560,7 +1576,7 @@ describe("sync", () => {
1560
1576
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1561
1577
  expect(s3Module.downloadFile).not.toHaveBeenCalledWith(expect.anything(), key, expect.anything());
1562
1578
  expect(fs.existsSync(localPath)).toBe(false);
1563
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1579
+ const journal = journalAt(journalPath);
1564
1580
  // Entry PRESERVED as a tombstone, not removed.
1565
1581
  expect(journal.files[key]?.removedAt).toBeTruthy();
1566
1582
  expect(journal.files[key]?.removedReason).toBe("local-delete");
@@ -1592,7 +1608,7 @@ describe("sync", () => {
1592
1608
  it("stamps journal.lastSync on every successful run, even when nothing transferred", async () => {
1593
1609
  // First run downloads both remote files and stamps lastSync.
1594
1610
  await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1595
- const firstStamp = JSON.parse(fs.readFileSync(journalPath, "utf-8")).lastSync;
1611
+ const firstStamp = journalAt(journalPath).lastSync;
1596
1612
  expect(firstStamp).not.toBe("");
1597
1613
  // Second run: same remote, same local — Stage-2 plan is all-skip, so
1598
1614
  // updateEntry never fires. lastSync must still advance, otherwise the
@@ -1600,7 +1616,7 @@ describe("sync", () => {
1600
1616
  await new Promise((r) => setTimeout(r, 5));
1601
1617
  const result = await sync({ company: "acme", vaultConfig: mockConfig, hqRoot: tmpDir });
1602
1618
  expect(result.filesDownloaded).toBe(0);
1603
- const secondStamp = JSON.parse(fs.readFileSync(journalPath, "utf-8")).lastSync;
1619
+ const secondStamp = journalAt(journalPath).lastSync;
1604
1620
  expect(new Date(secondStamp).getTime()).toBeGreaterThan(new Date(firstStamp).getTime());
1605
1621
  });
1606
1622
  it("journalSlug: 'personal' routes journal I/O to sync-journal.personal.json", async () => {
@@ -1691,13 +1707,9 @@ describe("sync", () => {
1691
1707
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "bar.md"))).toBe(false);
1692
1708
  // The whole point: it now gets a journal baseline, so the push side stops
1693
1709
  // re-firing a transient conflict every sync (the bug this fix closes).
1694
- const journaledManifest = fs
1695
- .readdirSync(stateDir)
1696
- .filter((f) => f.startsWith("sync-journal."))
1697
- .some((f) => {
1698
- const j = JSON.parse(fs.readFileSync(path.join(stateDir, f), "utf8"));
1699
- return j.files?.["companies/manifest.yaml"] != null;
1700
- });
1710
+ // Enumerate through the API: the per-slug JSON files are locators now, so
1711
+ // reading them directly finds no `files` map at all.
1712
+ const journaledManifest = listJournals().some((j) => j.journal.files?.["companies/manifest.yaml"] != null);
1701
1713
  expect(journaledManifest).toBe(true);
1702
1714
  });
1703
1715
  it("personalMode pull lands the session-continuity pointer + active thread file under <hqRoot>/workspace/threads/ so a handoff resumes on machine B (DEV-1778)", async () => {
@@ -1950,7 +1962,7 @@ describe("sync", () => {
1950
1962
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
1951
1963
  // Journal entry for the deleted path must be gone too — otherwise
1952
1964
  // the next sync would tombstone the same key on every run.
1953
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1965
+ const journal = journalAt(journalPath);
1954
1966
  expect(journal.files["docs/deleted-by-peer.md"]).toBeUndefined();
1955
1967
  expect(journal.files["docs/kept.md"]).toBeDefined();
1956
1968
  });
@@ -2015,7 +2027,7 @@ describe("sync", () => {
2015
2027
  expect(result.filesTombstoned).toBe(0);
2016
2028
  // The journal is healed: the malformed key is gone, the canonical POSIX key
2017
2029
  // points at the live file (so a future pull stays converged, not poisoned).
2018
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2030
+ const journal = journalAt(journalPath);
2019
2031
  expect(journal.files[backslashKey]).toBeUndefined();
2020
2032
  expect(journal.files[posixKey]).toBeDefined();
2021
2033
  expect(Object.keys(journal.files).some((k) => k.includes("\\"))).toBe(false);
@@ -2068,7 +2080,7 @@ describe("sync", () => {
2068
2080
  expect(result.filesTombstoned).toBe(0);
2069
2081
  // Journal entry retained — next sync (push) will re-evaluate and
2070
2082
  // either re-upload the divergent local or surface as a conflict.
2071
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2083
+ const journal = journalAt(journalPath);
2072
2084
  expect(journal.files["docs/edited-locally.md"]).toBeDefined();
2073
2085
  expect(journal.files["docs/edited-locally.md"].hash).toBe(baselineHash);
2074
2086
  });
@@ -2112,7 +2124,7 @@ describe("sync", () => {
2112
2124
  expect(result.filesTombstoned).toBe(0);
2113
2125
  expect(fs.existsSync(racedPath)).toBe(true);
2114
2126
  expect(fs.readFileSync(racedPath, "utf-8")).toBe("concurrent local edit");
2115
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2127
+ const journal = journalAt(journalPath);
2116
2128
  expect(journal.files["docs/racy-delete.md"]).toBeDefined();
2117
2129
  expect(journal.files["docs/racy-delete.md"].hash).toBe(baselineHash);
2118
2130
  });
@@ -2149,7 +2161,7 @@ describe("sync", () => {
2149
2161
  expect(result.filesTombstoned).toBe(0);
2150
2162
  expect(result.conflicts).toBe(1);
2151
2163
  expect(fs.readFileSync(localPath, "utf-8")).toBe(baseline);
2152
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2164
+ const journal = journalAt(journalPath);
2153
2165
  expect(journal.files["docs/created-after-plan.md"]).toBeDefined();
2154
2166
  });
2155
2167
  it("CAS: preserves a concurrent local edit when a staged download is ready to replace it", async () => {
@@ -2233,7 +2245,7 @@ describe("sync", () => {
2233
2245
  propagateDeletePolicy: "all",
2234
2246
  });
2235
2247
  expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2236
- const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
2248
+ const journal = journalAt(personalJournalPath);
2237
2249
  expect(journal.files["core/core.yaml"]).toBeDefined();
2238
2250
  expect(journal.files[".claude/skills/core:demo/SKILL.md"]).toBeDefined();
2239
2251
  });
@@ -2267,7 +2279,7 @@ describe("sync", () => {
2267
2279
  });
2268
2280
  expect(s3Module.deleteRemoteFile).not.toHaveBeenCalled();
2269
2281
  expect(result.filesRefusedStale).toBe(2);
2270
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2282
+ const journal = journalAt(journalPath);
2271
2283
  expect(journal.files["docs/a.md"]).toBeDefined();
2272
2284
  expect(journal.files["docs/b.md"]).toBeDefined();
2273
2285
  });
@@ -2359,7 +2371,7 @@ describe("sync", () => {
2359
2371
  expect(fs.lstatSync(linkPath).isSymbolicLink()).toBe(true);
2360
2372
  expect(fs.readlinkSync(linkPath)).toBe("new-target.md");
2361
2373
  expect(result.filesTombstoned).toBe(0);
2362
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2374
+ const journal = journalAt(journalPath);
2363
2375
  expect(journal.files["docs/link.md"]).toBeDefined();
2364
2376
  expect(journal.files["docs/link.md"].hash).toBe(baselineHash);
2365
2377
  });
@@ -2426,7 +2438,7 @@ describe("sync", () => {
2426
2438
  // The really-deleted file MUST be tombstoned.
2427
2439
  expect(fs.existsSync(reallyDeletedPath)).toBe(false);
2428
2440
  expect(result.filesTombstoned).toBe(1);
2429
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2441
+ const journal = journalAt(journalPath);
2430
2442
  // Journal entry retained for the invisible key (so next sync at
2431
2443
  // broader scope can re-evaluate).
2432
2444
  expect(journal.files["docs/invisible-to-list.md"]).toBeDefined();
@@ -2500,7 +2512,7 @@ describe("sync", () => {
2500
2512
  expect(result.filesDownloaded).toBe(0);
2501
2513
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
2502
2514
  // Journal entry dropped so it converges (no re-tombstone next run).
2503
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2515
+ const journal = journalAt(journalPath);
2504
2516
  expect(journal.files["docs/shared.md"]).toBeUndefined();
2505
2517
  });
2506
2518
  it("delete-resync: a re-created object NEWER than the tombstone DOES sync (tombstone is not permanent)", async () => {
@@ -2664,7 +2676,7 @@ describe("sync", () => {
2664
2676
  });
2665
2677
  expect(fs.existsSync(deniedPath)).toBe(true);
2666
2678
  expect(result.filesTombstoned).toBe(0);
2667
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2679
+ const journal = journalAt(journalPath);
2668
2680
  expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
2669
2681
  });
2670
2682
  it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
@@ -2712,7 +2724,7 @@ describe("sync", () => {
2712
2724
  // Tombstone NOT counted — the delete wasn't honored.
2713
2725
  expect(result.filesTombstoned).toBe(0);
2714
2726
  // Journal entry MUST be retained so the next sync retries.
2715
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2727
+ const journal = journalAt(journalPath);
2716
2728
  expect(journal.files["docs/locked-by-peer.md"]).toBeDefined();
2717
2729
  expect(journal.files["docs/locked-by-peer.md"].hash).toBe("locked-hash");
2718
2730
  }
@@ -2939,7 +2951,7 @@ describe("sync", () => {
2939
2951
  expect(s3Module.downloadFile).not.toHaveBeenCalled();
2940
2952
  expect(fs.existsSync(escapedPath)).toBe(false);
2941
2953
  expect(fs.existsSync(path.join(companyRoot, traversalKey))).toBe(false);
2942
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2954
+ const journal = journalAt(journalPath);
2943
2955
  expect(journal.files[traversalKey]).toBeUndefined();
2944
2956
  }
2945
2957
  finally {
@@ -2973,7 +2985,7 @@ describe("sync", () => {
2973
2985
  expect(s3Module.downloadFile).not.toHaveBeenCalled();
2974
2986
  expect(fs.existsSync(escapedPath)).toBe(false);
2975
2987
  expect(fs.existsSync(path.join(companyRoot, remoteKey))).toBe(false);
2976
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2988
+ const journal = journalAt(journalPath);
2977
2989
  expect(journal.files[remoteKey]).toBeUndefined();
2978
2990
  }
2979
2991
  finally {
@@ -3048,7 +3060,7 @@ describe("sync", () => {
3048
3060
  vaultConfig: mockConfig,
3049
3061
  hqRoot: tmpDir,
3050
3062
  });
3051
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3063
+ const journal = journalAt(journalPath);
3052
3064
  expect(journal.files["docs/handoff.md"].remoteEtag).toBe("abc123");
3053
3065
  expect(journal.files["knowledge/readme.md"].remoteEtag).toBe("def456");
3054
3066
  });
@@ -3069,7 +3081,7 @@ describe("sync", () => {
3069
3081
  if (key === "bulk/file-8.md") {
3070
3082
  // This models the next transfer crashing the process. The first
3071
3083
  // eight entries must already be durable before the final writer.
3072
- const checkpoint = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3084
+ const checkpoint = journalAt(journalPath);
3073
3085
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
3074
3086
  throw new Error("simulated crash after checkpoint");
3075
3087
  }
@@ -3083,7 +3095,7 @@ describe("sync", () => {
3083
3095
  onEvent: () => undefined,
3084
3096
  });
3085
3097
  expect(first.filesDownloaded).toBe(8);
3086
- const checkpoint = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3098
+ const checkpoint = journalAt(journalPath);
3087
3099
  expect(Object.keys(checkpoint.files)).toHaveLength(8);
3088
3100
  vi.mocked(s3Module.downloadFile).mockImplementation(defaultDownload);
3089
3101
  vi.mocked(s3Module.downloadFile).mockClear();
@@ -3534,7 +3546,7 @@ describe("sync", () => {
3534
3546
  // skip-unchanged works on the next tick.
3535
3547
  const journalMod = await import("../journal.js");
3536
3548
  const expectedHash = journalMod.hashSymlinkTarget(linkTarget);
3537
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3549
+ const journal = journalAt(journalPath);
3538
3550
  expect(journal.files[linkKey]).toMatchObject({
3539
3551
  hash: expectedHash,
3540
3552
  size: 0,
@@ -4181,7 +4193,7 @@ describe("sync", () => {
4181
4193
  // The journal MUST be updated to reflect the user's keep
4182
4194
  // decision. Pre-fix: catch swallowed the ENOENT, journal stayed
4183
4195
  // at oldHash + old etag, every subsequent sync re-fired.
4184
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4196
+ const journal = journalAt(journalPath);
4185
4197
  const entry = journal.files[linkKey];
4186
4198
  expect(entry.hash).toBe(expectedNewHash);
4187
4199
  expect(entry.size).toBe(0);
@@ -4480,7 +4492,7 @@ describe("scope-invalid key hardening (companies/ prefix in a company vault —
4480
4492
  // Cleaned, not re-uploaded: local doubled-tree copy gone, journal dropped.
4481
4493
  expect(fs.existsSync(path.join(doubled, "poison.md"))).toBe(false);
4482
4494
  expect(result.filesTombstoned).toBeGreaterThanOrEqual(1);
4483
- const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4495
+ const journal = journalAt(journalPath);
4484
4496
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
4485
4497
  // No HEAD verify against the invalid key — the server validator would
4486
4498
  // reject it (INVALID_KEY_COMPANIES_SCOPED) and defer the cleanup forever.
@@ -4608,7 +4620,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4608
4620
  expect(events.filter((e) => e.type === "error")).toHaveLength(0);
4609
4621
  expect(result.aborted).toBe(false);
4610
4622
  expect(result.filesSkipped).toBe(1);
4611
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4623
+ const journalAfter = journalAt(journalPath);
4612
4624
  expect(Object.keys(journalAfter.files)).not.toContain(junkKey);
4613
4625
  });
4614
4626
  it("still pulls a junk-spelling key that is ALREADY journaled under that exact spelling", async () => {
@@ -4653,7 +4665,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4653
4665
  // Junk twin still gated; the canonical journal entry tombstones out.
4654
4666
  expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
4655
4667
  expect(result.filesTombstoned).toBe(1);
4656
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4668
+ const journalAfter = journalAt(journalPath);
4657
4669
  expect(Object.keys(journalAfter.files)).not.toContain(canonicalKey);
4658
4670
  });
4659
4671
  it("always downloads a CANONICAL remote key even when the journal holds only its junk twin (post-doctor convergence)", async () => {
@@ -4670,7 +4682,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4670
4682
  expect(vi.mocked(s3Module.downloadFile)).toHaveBeenCalledTimes(1);
4671
4683
  expect(vi.mocked(s3Module.downloadFile).mock.calls[0][1]).toBe(canonicalKey);
4672
4684
  expect(result.filesDownloaded).toBe(1);
4673
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4685
+ const journalAfter = journalAt(journalPath);
4674
4686
  expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
4675
4687
  });
4676
4688
  it("does not tombstone a journaled canonical key when the LIST holds only its junk twin", async () => {
@@ -4686,7 +4698,7 @@ describe("junk key-spelling guard (US-002 mixed-version amplifier)", () => {
4686
4698
  const { events, result } = await runSync();
4687
4699
  // Junk twin skipped, canonical entry NOT tombstoned.
4688
4700
  expect(events.filter((e) => e.type === "skip-junk-key-spelling")).toHaveLength(1);
4689
- const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4701
+ const journalAfter = journalAt(journalPath);
4690
4702
  expect(Object.keys(journalAfter.files)).toContain(canonicalKey);
4691
4703
  expect(result.filesTombstoned ?? 0).toBe(0);
4692
4704
  expect(events.filter((e) => e.type === "error")).toHaveLength(0);