@indigoai-us/hq-cloud 6.15.29 → 6.15.31

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 (106) hide show
  1. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-company.js +24 -13
  3. package/dist/bin/sync-runner-company.js.map +1 -1
  4. package/dist/bin/sync-runner-company.test.js +0 -71
  5. package/dist/bin/sync-runner-company.test.js.map +1 -1
  6. package/dist/bin/sync-runner-watch-loop.d.ts +0 -10
  7. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  8. package/dist/bin/sync-runner-watch-loop.js +13 -170
  9. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  10. package/dist/bin/sync-runner-watch-routes.d.ts +1 -16
  11. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  12. package/dist/bin/sync-runner-watch-routes.js +2 -40
  13. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  14. package/dist/bin/sync-runner-watch-routes.test.js +1 -53
  15. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  16. package/dist/bin/sync-runner.d.ts +3 -15
  17. package/dist/bin/sync-runner.d.ts.map +1 -1
  18. package/dist/bin/sync-runner.js +13 -66
  19. package/dist/bin/sync-runner.js.map +1 -1
  20. package/dist/bin/sync-runner.test.js +13 -433
  21. package/dist/bin/sync-runner.test.js.map +1 -1
  22. package/dist/cli/conflict.d.ts +2 -25
  23. package/dist/cli/conflict.d.ts.map +1 -1
  24. package/dist/cli/conflict.js +4 -29
  25. package/dist/cli/conflict.js.map +1 -1
  26. package/dist/cli/share.d.ts +4 -40
  27. package/dist/cli/share.d.ts.map +1 -1
  28. package/dist/cli/share.js +86 -478
  29. package/dist/cli/share.js.map +1 -1
  30. package/dist/cli/share.test.js +150 -845
  31. package/dist/cli/share.test.js.map +1 -1
  32. package/dist/cli/sync-scope.test.js +18 -147
  33. package/dist/cli/sync-scope.test.js.map +1 -1
  34. package/dist/cli/sync.d.ts +0 -20
  35. package/dist/cli/sync.d.ts.map +1 -1
  36. package/dist/cli/sync.js +29 -261
  37. package/dist/cli/sync.js.map +1 -1
  38. package/dist/cli/sync.test.js +90 -519
  39. package/dist/cli/sync.test.js.map +1 -1
  40. package/dist/journal.d.ts +0 -1
  41. package/dist/journal.d.ts.map +1 -1
  42. package/dist/journal.js +25 -155
  43. package/dist/journal.js.map +1 -1
  44. package/dist/journal.test.js +11 -29
  45. package/dist/journal.test.js.map +1 -1
  46. package/dist/object-io.d.ts +13 -34
  47. package/dist/object-io.d.ts.map +1 -1
  48. package/dist/object-io.js +29 -154
  49. package/dist/object-io.js.map +1 -1
  50. package/dist/outcome-telemetry.d.ts +1 -7
  51. package/dist/outcome-telemetry.d.ts.map +1 -1
  52. package/dist/outcome-telemetry.js +9 -15
  53. package/dist/outcome-telemetry.js.map +1 -1
  54. package/dist/outcome-telemetry.test.js +0 -15
  55. package/dist/outcome-telemetry.test.js.map +1 -1
  56. package/dist/prefix-coalesce.d.ts +0 -14
  57. package/dist/prefix-coalesce.d.ts.map +1 -1
  58. package/dist/prefix-coalesce.js +0 -29
  59. package/dist/prefix-coalesce.js.map +1 -1
  60. package/dist/prefix-coalesce.test.js +1 -22
  61. package/dist/prefix-coalesce.test.js.map +1 -1
  62. package/dist/skill-telemetry.d.ts +1 -6
  63. package/dist/skill-telemetry.d.ts.map +1 -1
  64. package/dist/skill-telemetry.js +8 -14
  65. package/dist/skill-telemetry.js.map +1 -1
  66. package/dist/skill-telemetry.test.js +0 -43
  67. package/dist/skill-telemetry.test.js.map +1 -1
  68. package/dist/sts-credential-refresh.test.js +2 -87
  69. package/dist/sts-credential-refresh.test.js.map +1 -1
  70. package/dist/sync/state-store.d.ts.map +1 -1
  71. package/dist/sync/state-store.js +1 -20
  72. package/dist/sync/state-store.js.map +1 -1
  73. package/dist/sync/state-store.test.js +0 -64
  74. package/dist/sync/state-store.test.js.map +1 -1
  75. package/dist/sync-core.d.ts.map +1 -1
  76. package/dist/sync-core.js +2 -7
  77. package/dist/sync-core.js.map +1 -1
  78. package/dist/sync-core.test.js +0 -36
  79. package/dist/sync-core.test.js.map +1 -1
  80. package/dist/telemetry.d.ts +6 -30
  81. package/dist/telemetry.d.ts.map +1 -1
  82. package/dist/telemetry.js +135 -331
  83. package/dist/telemetry.js.map +1 -1
  84. package/dist/telemetry.test.js +0 -403
  85. package/dist/telemetry.test.js.map +1 -1
  86. package/dist/types.d.ts +3 -26
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/watcher.d.ts +0 -76
  89. package/dist/watcher.d.ts.map +1 -1
  90. package/dist/watcher.js +3 -105
  91. package/dist/watcher.js.map +1 -1
  92. package/dist/watcher.test.js +1 -165
  93. package/dist/watcher.test.js.map +1 -1
  94. package/package.json +1 -1
  95. package/dist/bandwidth.d.ts +0 -128
  96. package/dist/bandwidth.d.ts.map +0 -1
  97. package/dist/bandwidth.js +0 -254
  98. package/dist/bandwidth.js.map +0 -1
  99. package/dist/bandwidth.test.d.ts +0 -2
  100. package/dist/bandwidth.test.d.ts.map +0 -1
  101. package/dist/bandwidth.test.js +0 -243
  102. package/dist/bandwidth.test.js.map +0 -1
  103. package/dist/journal-delta-writes.test.d.ts +0 -2
  104. package/dist/journal-delta-writes.test.d.ts.map +0 -1
  105. package/dist/journal-delta-writes.test.js +0 -140
  106. package/dist/journal-delta-writes.test.js.map +0 -1
@@ -53,49 +53,9 @@ vi.mock("readline", () => ({
53
53
  }));
54
54
  import * as readline from "readline";
55
55
  import { share, isForbiddenCompanyVaultKey, UnreachablePushPathsError, currentUploadBytes, _testing as shareTesting } from "./share.js";
56
- import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
56
+ import { deleteRemoteFile, downloadFile, headRemoteFile, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError } from "../s3.js";
57
57
  import { VaultAuthError } from "../vault-client.js";
58
- import { hashFile, readJournal, writeJournal } from "../journal.js";
59
- /**
60
- * Read a journal the way production does.
61
- *
62
- * These tests used to `JSON.parse` `sync-journal.<slug>.json` directly, back
63
- * when that file held a full copy of the journal. It is now a small locator —
64
- * the authority is the v3 state store — so the assertions below go through
65
- * `readJournal`, which is what the sync engine itself uses. The slug is
66
- * recovered from the path so each call site keeps naming the journal it means.
67
- */
68
- /**
69
- * Seed a journal the way production does.
70
- *
71
- * These fixtures used to write `sync-journal.<slug>.json` by hand, which worked
72
- * only while that file WAS the journal. It is a locator now, so a hand-written
73
- * copy is simply ignored and the test silently runs against an empty journal.
74
- * Going through `writeJournal` puts the state where the engine actually reads
75
- * it, and migrates v1 fixtures on the way in exactly as a real upgrade would.
76
- */
77
- function seedJournalAt(journalPath, journal) {
78
- const slug = path
79
- .basename(journalPath)
80
- .replace(/^sync-journal\./, "")
81
- .replace(/\.json$/, "");
82
- writeJournal(slug, journal);
83
- }
84
- function journalAt(journalPath) {
85
- const slug = path
86
- .basename(journalPath)
87
- .replace(/^sync-journal\./, "")
88
- .replace(/\.json$/, "");
89
- return readJournal(slug);
90
- }
91
- function sidecarIn(dir) {
92
- const found = fs.readdirSync(dir).filter((name) => name.includes(".conflict-"));
93
- expect(found).toHaveLength(1);
94
- return path.join(dir, found[0]);
95
- }
96
- function conflictIndexAt(hqRoot) {
97
- return JSON.parse(fs.readFileSync(path.join(hqRoot, ".hq-conflicts", "index.json"), "utf-8"));
98
- }
58
+ import { hashFile } from "../journal.js";
99
59
  const mockConfig = {
100
60
  apiUrl: "https://vault-api.test",
101
61
  authToken: "test-jwt-token",
@@ -409,74 +369,6 @@ describe("share", () => {
409
369
  // Remote key must be company-relative, not hqRoot-relative
410
370
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
411
371
  });
412
- it("replans an upload from the bytes present when its action starts", async () => {
413
- const companyRoot = path.join(tmpDir, "companies", "acme");
414
- fs.mkdirSync(companyRoot, { recursive: true });
415
- const testFile = path.join(companyRoot, "changing.md");
416
- fs.writeFileSync(testFile, "body seen by the whole-run preview");
417
- let uploadedBody = "";
418
- vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath) => {
419
- uploadedBody = fs.readFileSync(localPath, "utf-8");
420
- return { etag: '"fresh-upload-etag"' };
421
- });
422
- let changedAfterPlan = false;
423
- let executionHeartbeatCalls = 0;
424
- const result = await share({
425
- paths: [testFile],
426
- company: "acme",
427
- vaultConfig: mockConfig,
428
- hqRoot: tmpDir,
429
- onEvent: (event) => {
430
- if (event.type === "plan" && !changedAfterPlan) {
431
- changedAfterPlan = true;
432
- fs.writeFileSync(testFile, "body present when upload executes");
433
- }
434
- },
435
- onHeartbeat: () => {
436
- if (changedAfterPlan)
437
- executionHeartbeatCalls++;
438
- },
439
- });
440
- expect(changedAfterPlan).toBe(true);
441
- expect(executionHeartbeatCalls).toBeGreaterThan(0);
442
- expect(result.filesUploaded).toBe(1);
443
- expect(uploadedBody).toBe("body present when upload executes");
444
- expect(journalAt(path.join(stateDir, "sync-journal.acme.json")).files["changing.md"])
445
- .toMatchObject({
446
- hash: hashFile(testFile),
447
- remoteEtag: "fresh-upload-etag",
448
- direction: "up",
449
- });
450
- });
451
- it("restarts the per-file decision when local bytes change during remote HEAD", async () => {
452
- const companyRoot = path.join(tmpDir, "companies", "acme");
453
- fs.mkdirSync(companyRoot, { recursive: true });
454
- const testFile = path.join(companyRoot, "head-race.md");
455
- fs.writeFileSync(testFile, "body before HEAD");
456
- vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
457
- fs.writeFileSync(testFile, "body written while HEAD was in flight");
458
- return null;
459
- });
460
- let uploadedBody = "";
461
- vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath) => {
462
- uploadedBody = fs.readFileSync(localPath, "utf-8");
463
- return { etag: '"head-race-etag"' };
464
- });
465
- const result = await share({
466
- paths: [testFile],
467
- company: "acme",
468
- vaultConfig: mockConfig,
469
- hqRoot: tmpDir,
470
- });
471
- expect(result.filesUploaded).toBe(1);
472
- expect(uploadedBody).toBe("body written while HEAD was in flight");
473
- expect(journalAt(path.join(stateDir, "sync-journal.acme.json")).files["head-race.md"])
474
- .toMatchObject({
475
- hash: hashFile(testFile),
476
- remoteEtag: "head-race-etag",
477
- direction: "up",
478
- });
479
- });
480
372
  it("propagates a per-file vault 401 to the runner auth boundary", async () => {
481
373
  const companyRoot = path.join(tmpDir, "companies", "acme");
482
374
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -636,7 +528,7 @@ describe("share", () => {
636
528
  const { hashFile } = await import("../journal.js");
637
529
  const hash = hashFile(testFile);
638
530
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
639
- seedJournalAt(journalPath, ({
531
+ fs.writeFileSync(journalPath, JSON.stringify({
640
532
  version: "1",
641
533
  lastSync: new Date().toISOString(),
642
534
  files: {
@@ -667,7 +559,7 @@ describe("share", () => {
667
559
  // Journal has a stale hash for this path — simulating "local has been
668
560
  // edited since the last push".
669
561
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
670
- seedJournalAt(journalPath, ({
562
+ fs.writeFileSync(journalPath, JSON.stringify({
671
563
  version: "1",
672
564
  lastSync: new Date().toISOString(),
673
565
  files: {
@@ -689,226 +581,6 @@ describe("share", () => {
689
581
  expect(result.filesUploaded).toBe(1);
690
582
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "changed.md", undefined, expect.anything());
691
583
  });
692
- // ── Stat fast path for change detection ─────────────────────────────────
693
- // Push used to hash EVERY regular file every pass before deciding whether to
694
- // skip it — half a million reads on a real vault — because stat metadata was
695
- // considered untrustworthy: a tool can rewrite a file to the same length and
696
- // restore its mtime, and a size+mtime check would then skip that edit
697
- // forever.
698
- //
699
- // These tests pin the narrower claim the fast path actually rests on: size,
700
- // mtime AND ctime together. ctime is the inode-change stamp, and userspace
701
- // cannot restore it — `cp -p`, `touch -r` and `rsync --times` all reset mtime
702
- // while ctime moves to now. So the same-length-plus-restored-mtime rewrite
703
- // that the old comment warned about is still caught, without reading a byte.
704
- //
705
- // Differential proof: drop ctime from the comparison and the "restored mtime"
706
- // test below fails — the edit is skipped and never uploaded.
707
- describe("stat fast path for change detection", () => {
708
- const CONTENT_A = "aaaaaaaaaa"; // 10 bytes
709
- const CONTENT_B = "bbbbbbbbbb"; // 10 bytes — same length, different bytes
710
- /** Write `body` at `key` under the acme company root; return its path. */
711
- function writeFile(key, body) {
712
- const companyRoot = path.join(tmpDir, "companies", "acme");
713
- fs.mkdirSync(companyRoot, { recursive: true });
714
- const abs = path.join(companyRoot, key);
715
- fs.writeFileSync(abs, body);
716
- return abs;
717
- }
718
- function seed(key, entry) {
719
- seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
720
- version: "2",
721
- lastSync: new Date().toISOString(),
722
- files: { [key]: { syncedAt: new Date().toISOString(), direction: "up", ...entry } },
723
- pulls: [],
724
- });
725
- }
726
- function push(abs, onConflict) {
727
- return share({
728
- paths: [abs],
729
- company: "acme",
730
- vaultConfig: mockConfig,
731
- hqRoot: tmpDir,
732
- skipUnchanged: true,
733
- ...(onConflict ? { onConflict } : {}),
734
- });
735
- }
736
- it("skips a file whose size, mtime and ctime all match — without reading it", async () => {
737
- const abs = writeFile("untouched.md", CONTENT_A);
738
- const stat = fs.lstatSync(abs);
739
- // The recorded hash is deliberately WRONG. Hashing the file would find a
740
- // mismatch and upload, so a skip here can only mean the bytes were never
741
- // read — which is the whole point of the fast path.
742
- seed("untouched.md", {
743
- hash: "deliberately-not-the-hash-of-this-file",
744
- size: stat.size,
745
- mtimeMs: stat.mtimeMs,
746
- ctimeMs: stat.ctimeMs,
747
- });
748
- const result = await push(abs);
749
- expect(result.filesSkipped).toBe(1);
750
- expect(result.filesUploaded).toBe(0);
751
- expect(uploadFile).not.toHaveBeenCalled();
752
- });
753
- it("re-reads when the size differs", async () => {
754
- const abs = writeFile("resized.md", CONTENT_A + "extra");
755
- const stat = fs.lstatSync(abs);
756
- seed("resized.md", {
757
- hash: hashFile(abs) + "-stale",
758
- size: CONTENT_A.length, // shorter than what is on disk now
759
- mtimeMs: stat.mtimeMs,
760
- ctimeMs: stat.ctimeMs,
761
- });
762
- const result = await push(abs);
763
- expect(result.filesUploaded).toBe(1);
764
- expect(uploadFile).toHaveBeenCalled();
765
- });
766
- it("re-reads when the mtime differs", async () => {
767
- const abs = writeFile("retimed.md", CONTENT_B);
768
- const stat = fs.lstatSync(abs);
769
- seed("retimed.md", {
770
- hash: "hash-of-the-previous-body",
771
- size: stat.size, // same length
772
- mtimeMs: stat.mtimeMs - 60_000, // journal remembers an older edit time
773
- ctimeMs: stat.ctimeMs,
774
- });
775
- const result = await push(abs);
776
- expect(result.filesUploaded).toBe(1);
777
- expect(uploadFile).toHaveBeenCalled();
778
- });
779
- it("re-reads a same-length rewrite whose mtime was restored — ctime still moves", async () => {
780
- // The exact case the old hash-everything comment existed to defend
781
- // against, as produced by `cp -p`, `touch -r` or `rsync --times`.
782
- const abs = writeFile("mtime-restored.md", CONTENT_A);
783
- const before = fs.lstatSync(abs);
784
- await new Promise((resolve) => setTimeout(resolve, 5));
785
- fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
786
- fs.utimesSync(abs, before.atime, before.mtime); // put the old mtime back
787
- const after = fs.lstatSync(abs);
788
- // Preconditions. The disguise worked on size, and as closely on mtime as
789
- // the OS allows — `utimes` takes whole milliseconds, so a restored stamp
790
- // lands within 1ms of the original rather than exactly on it. It failed
791
- // on ctime, which is the only signal left.
792
- expect(after.size).toBe(before.size);
793
- expect(Math.abs(after.mtimeMs - before.mtimeMs)).toBeLessThan(1);
794
- expect(after.ctimeMs).not.toBe(before.ctimeMs);
795
- // Seed the mtime that is actually on disk, so mtime matches EXACTLY and
796
- // cannot be what triggers the re-read. Only the stale ctime can.
797
- seed("mtime-restored.md", {
798
- hash: "hash-of-content-a",
799
- size: before.size,
800
- mtimeMs: after.mtimeMs,
801
- ctimeMs: before.ctimeMs,
802
- });
803
- const result = await push(abs);
804
- expect(result.filesUploaded).toBe(1);
805
- expect(uploadFile).toHaveBeenCalled();
806
- });
807
- it("falls back to reading when the journal predates stat tracking, then stamps it", async () => {
808
- // Entries written before ctime was recorded carry no stat metadata. They
809
- // must not be skipped on faith; they are read once and stamped so the
810
- // next pass takes the fast path.
811
- const abs = writeFile("legacy.md", CONTENT_A);
812
- seed("legacy.md", { hash: hashFile(abs), size: CONTENT_A.length });
813
- const result = await push(abs);
814
- expect(result.filesSkipped).toBe(1);
815
- expect(uploadFile).not.toHaveBeenCalled();
816
- const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["legacy.md"];
817
- const stat = fs.lstatSync(abs);
818
- expect(entry.mtimeMs).toBe(stat.mtimeMs);
819
- expect(entry.ctimeMs).toBe(stat.ctimeMs);
820
- });
821
- it("never lets the fast path skip a localDiverges entry", async () => {
822
- // A pull-held divergent entry stores the LOCAL hash and local stat, so
823
- // every field matches by construction. Skipping on that asserts "local
824
- // matches remote", which is exactly what the flag exists to deny.
825
- const abs = writeFile("divergent.md", CONTENT_A);
826
- const stat = fs.lstatSync(abs);
827
- seed("divergent.md", {
828
- hash: hashFile(abs),
829
- size: stat.size,
830
- mtimeMs: stat.mtimeMs,
831
- ctimeMs: stat.ctimeMs,
832
- localDiverges: true,
833
- });
834
- // `keep` so the divergence resolves without an interactive prompt. Under
835
- // `keep` the file is deliberately NOT uploaded, so the proof that the
836
- // fast path did not swallow it is that it reached Stage 2 at all: a
837
- // fast-path skip returns before any remote HEAD is issued.
838
- await push(abs, "keep");
839
- expect(headRemoteFile).toHaveBeenCalled();
840
- });
841
- it("stamps size, mtime and ctime on upload so the next pass can skip", async () => {
842
- const abs = writeFile("fresh.md", CONTENT_A);
843
- const result = await push(abs);
844
- expect(result.filesUploaded).toBe(1);
845
- const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["fresh.md"];
846
- const stat = fs.lstatSync(abs);
847
- expect(entry.size).toBe(stat.size);
848
- expect(entry.mtimeMs).toBe(stat.mtimeMs);
849
- expect(entry.ctimeMs).toBe(stat.ctimeMs);
850
- });
851
- it("replans and stamps the new body when the file changes during remote HEAD", async () => {
852
- // The pass-level preview hashes CONTENT_A, then the asynchronous remote
853
- // probe changes the file to CONTENT_B. The per-file execution seam must
854
- // restart from CONTENT_B, upload it now, and journal that exact body — no
855
- // stale hash and no redundant upload on the next pass.
856
- const abs = writeFile("edited-mid-flight.md", CONTENT_A);
857
- let mutated = false;
858
- vi.mocked(headRemoteFile).mockImplementation(async () => {
859
- if (!mutated) {
860
- mutated = true;
861
- fs.writeFileSync(abs, CONTENT_B); // same length, different bytes
862
- }
863
- return null;
864
- });
865
- await push(abs);
866
- const entry = journalAt(path.join(stateDir, "sync-journal.acme.json")).files["edited-mid-flight.md"];
867
- const currentStat = fs.lstatSync(abs);
868
- expect(entry.hash).toBe(hashFile(abs));
869
- expect(entry.mtimeMs).toBe(currentStat.mtimeMs);
870
- expect(entry.ctimeMs).toBe(currentStat.ctimeMs);
871
- // The current bytes were already shipped and journaled in this pass.
872
- vi.mocked(uploadFile).mockClear();
873
- const second = await push(abs);
874
- expect(second.filesUploaded).toBe(0);
875
- expect(uploadFile).not.toHaveBeenCalled();
876
- });
877
- it("distrusts whole-second timestamps as a coarse-filesystem signal", () => {
878
- // Codex P2. FAT/exFAT, some FUSE mounts and cached network filesystems
879
- // report timestamps rounded to whole seconds, so a same-size rewrite
880
- // inside one tick can leave both mtime and ctime equal to the journal's
881
- // and be skipped indefinitely. Every filesystem HQ can trust here (APFS,
882
- // ext4, NTFS) records sub-second precision, so a pair that is exactly on
883
- // the second is treated as untrustworthy and sent back to hashing.
884
- const abs = writeFile("coarse.md", CONTENT_A);
885
- const coarse = { size: CONTENT_A.length, mtimeMs: 1_700_000_000_000, ctimeMs: 1_700_000_000_000 };
886
- const spy = vi.spyOn(fs, "lstatSync").mockReturnValue({
887
- isFile: () => true,
888
- ...coarse,
889
- });
890
- try {
891
- expect(shareTesting.statMatchesJournal(abs, {
892
- hash: "h",
893
- direction: "up",
894
- syncedAt: new Date().toISOString(),
895
- ...coarse,
896
- })).toBe(false);
897
- // Same values, one of them sub-second: a real filesystem, so trusted.
898
- const fine = { ...coarse, ctimeMs: coarse.ctimeMs + 0.5 };
899
- spy.mockReturnValue({ isFile: () => true, ...fine });
900
- expect(shareTesting.statMatchesJournal(abs, {
901
- hash: "h",
902
- direction: "up",
903
- syncedAt: new Date().toISOString(),
904
- ...fine,
905
- })).toBe(true);
906
- }
907
- finally {
908
- spy.mockRestore();
909
- }
910
- });
911
- });
912
584
  // ── Push-side FILE_TOMBSTONE consult (delete-resync, 3B) ────────────────────
913
585
  // An authoritative delete (`hq files delete`) writes a FILE_TOMBSTONE and
914
586
  // removes the S3 object. Without a push-side consult, a behind peer that still
@@ -921,7 +593,7 @@ describe("share", () => {
921
593
  // share.ts makes the first test fail (uploadFile IS called → resurrection).
922
594
  describe("push-side tombstone consult", () => {
923
595
  function seedJournal(key, hash, size) {
924
- seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), ({
596
+ fs.writeFileSync(path.join(stateDir, "sync-journal.acme.json"), JSON.stringify({
925
597
  version: "1",
926
598
  lastSync: new Date().toISOString(),
927
599
  files: {
@@ -958,8 +630,6 @@ describe("share", () => {
958
630
  });
959
631
  // The resurrection is blocked at the source: no upload for the tombstoned key.
960
632
  expect(uploadFile).not.toHaveBeenCalled();
961
- expect(primeUploads).toHaveBeenCalledWith(expect.anything(), []);
962
- expect(primeObjectTransport).toHaveBeenCalledWith(expect.anything(), "get", []);
963
633
  expect(result.filesUploaded).toBe(0);
964
634
  expect(result.filesSuppressedByTombstone).toBe(1);
965
635
  expect(events.some((e) => e.type === "upload-suppressed-tombstone" && e.path === "docs/shared.md")).toBe(true);
@@ -1066,12 +736,8 @@ describe("share", () => {
1066
736
  etag: '"remote-new-etag"',
1067
737
  size: 99,
1068
738
  });
1069
- vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1070
- fs.writeFileSync(dest, "remote edit");
1071
- return undefined;
1072
- });
1073
739
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1074
- seedJournalAt(journalPath, ({
740
+ fs.writeFileSync(journalPath, JSON.stringify({
1075
741
  version: "1",
1076
742
  lastSync: new Date().toISOString(),
1077
743
  files: {
@@ -1102,24 +768,6 @@ describe("share", () => {
1102
768
  direction: "push",
1103
769
  resolution: "keep",
1104
770
  });
1105
- // `keep` is cloud-wins on the push leg too: remote takes the working
1106
- // name, while the displaced local body remains recoverable beside it.
1107
- expect(fs.readFileSync(testFile, "utf-8")).toBe("remote edit");
1108
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local edit");
1109
- const journal = journalAt(journalPath);
1110
- expect(journal.files["drifted.md"]).toMatchObject({
1111
- hash: hashFile(testFile),
1112
- remoteEtag: "remote-new-etag",
1113
- direction: "down",
1114
- });
1115
- const index = conflictIndexAt(tmpDir);
1116
- expect(index.conflicts).toHaveLength(1);
1117
- expect(index.conflicts[0]).toMatchObject({
1118
- originalPath: "companies/acme/drifted.md",
1119
- preserved: "local",
1120
- });
1121
- expect(fs.readFileSync(path.join(tmpDir, index.conflicts[0].conflictPath), "utf-8"))
1122
- .toBe("local edit");
1123
771
  });
1124
772
  it("cloud-authoritative: never pushes a stale local ontology/.last-run over the server value (one-sided-change clobber guard)", async () => {
1125
773
  // Regression for the gardener watermark clobber. A second HQ root sharing this
@@ -1141,7 +789,7 @@ describe("share", () => {
1141
789
  // assert it is never consulted below — the skip is cheap, and a leftover
1142
790
  // mockResolvedValueOnce would otherwise leak into a later test's HEAD queue.
1143
791
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1144
- seedJournalAt(journalPath, ({
792
+ fs.writeFileSync(journalPath, JSON.stringify({
1145
793
  version: "1",
1146
794
  lastSync: new Date().toISOString(),
1147
795
  files: {
@@ -1187,9 +835,9 @@ describe("share", () => {
1187
835
  //
1188
836
  // Fix: even on first-time upload, if HEAD shows remote exists and
1189
837
  // its content (etag) differs from what we'd put, emit a conflict.
1190
- // Under keep, cloud takes the original name and local moves to
1191
- // \`<orig>.conflict-<ts>-<short>\` so both versions survive on disk;
1192
- // under abort, the company aborts before PUT.
838
+ // Under keep, the existing pull-side mirror routine downloads the
839
+ // remote bytes to \`<orig>.conflict-<ts>-<short>\` so both versions
840
+ // survive on disk; under abort, the company aborts before PUT.
1193
841
  const companyRoot = path.join(tmpDir, "companies", "acme");
1194
842
  fs.mkdirSync(companyRoot, { recursive: true });
1195
843
  const testFile = path.join(companyRoot, "v7b-fresh.txt");
@@ -1201,10 +849,6 @@ describe("share", () => {
1201
849
  etag: '"remote-version-etag"',
1202
850
  size: 40,
1203
851
  });
1204
- vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
1205
- fs.writeFileSync(dest, "peer remote content");
1206
- return undefined;
1207
- });
1208
852
  // No journal file written — first-time upload contract.
1209
853
  const events = [];
1210
854
  const result = await share({
@@ -1216,19 +860,14 @@ describe("share", () => {
1216
860
  onEvent: (e) => events.push(e),
1217
861
  });
1218
862
  expect(result.conflictPaths).toEqual(["v7b-fresh.txt"]);
1219
- // Under keep, no upload occurs: the remote bytes take the working name
1220
- // and the local body is preserved beside them.
863
+ // Under keep, local stays as-is (no upload) and the mirror records
864
+ // the remote bytes so neither version is lost.
1221
865
  expect(result.filesUploaded).toBe(0);
1222
866
  const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
1223
867
  expect(conflicts).toHaveLength(1);
1224
868
  expect(conflicts[0].path).toBe("v7b-fresh.txt");
1225
- expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote content");
1226
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("macos-fresh-collision-content");
1227
- const index = conflictIndexAt(tmpDir);
1228
- expect(index.conflicts[0]).toMatchObject({
1229
- originalPath: "companies/acme/v7b-fresh.txt",
1230
- preserved: "local",
1231
- });
869
+ // Local file unmodified — it must still contain the local content.
870
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("macos-fresh-collision-content");
1232
871
  });
1233
872
  it("first-time-upload-with-cloud-collision: aborts company under --on-conflict abort (Bug #7)", async () => {
1234
873
  // Symmetric to the keep case: when --on-conflict abort is passed, a
@@ -1256,11 +895,11 @@ describe("share", () => {
1256
895
  expect(result.filesUploaded).toBe(0);
1257
896
  });
1258
897
  it("HQ-DESKTOP-54: a privilege refusal while writing the push conflict mirror is a skip, not an error", async () => {
1259
- // Push keep also materializes the remote object (at the original name).
1260
- // On an unprivileged Windows host that throws the typed privilege
898
+ // The keep-mirror also materializes the remote object (to a .conflict-*
899
+ // file). On an unprivileged Windows host that throws the typed privilege
1261
900
  // error; it must be downgraded to the deliberate skip so the push stays
1262
901
  // complete and the runner exits 0 — never type:error. The conflict itself
1263
- // is still surfaced and the local rename is rolled back.
902
+ // is still recorded and the local file is untouched.
1264
903
  const companyRoot = path.join(tmpDir, "companies", "acme");
1265
904
  fs.mkdirSync(companyRoot, { recursive: true });
1266
905
  const testFile = path.join(companyRoot, "v7b-priv.txt");
@@ -1271,7 +910,7 @@ describe("share", () => {
1271
910
  size: 40,
1272
911
  });
1273
912
  // On a real unprivileged host BOTH the fresh-collision reconciled probe and
1274
- // the keep adoption materialize the remote symlink, so both downloadFile calls
913
+ // the keep-mirror materialize the remote symlink, so both downloadFile calls
1275
914
  // refuse. The probe's catch returns "differs" (conflict) without erroring;
1276
915
  // the mirror must downgrade the refusal to the deliberate skip.
1277
916
  vi.mocked(downloadFile)
@@ -1292,7 +931,7 @@ describe("share", () => {
1292
931
  target: "../target.md",
1293
932
  }));
1294
933
  expect(events.some((e) => e.type === "error")).toBe(false);
1295
- // The conflict is still surfaced; the failed adoption restores local.
934
+ // The conflict is still recorded; the local file is preserved.
1296
935
  expect(result.conflictPaths).toEqual(["v7b-priv.txt"]);
1297
936
  expect(fs.readFileSync(testFile, "utf-8")).toBe("local-collision-content");
1298
937
  });
@@ -1330,7 +969,7 @@ describe("share", () => {
1330
969
  direction: "push",
1331
970
  });
1332
971
  expect(fs.readdirSync(companyRoot).filter((name) => name.includes(".conflict-"))).toEqual([]);
1333
- const journal = readJournal("acme");
972
+ const journal = JSON.parse(fs.readFileSync(path.join(stateDir, "sync-journal.acme.json"), "utf-8"));
1334
973
  expect(journal.files["kms-scaffold.md"]).toMatchObject({
1335
974
  direction: "up",
1336
975
  remoteEtag: "288bfa91684ae21f9ae27c061254d412",
@@ -1404,7 +1043,7 @@ describe("share", () => {
1404
1043
  etag: '"0123456789abcdef0123456789abcdef-3"',
1405
1044
  size: 32,
1406
1045
  });
1407
- // The probe (and the subsequent keep adoption download) return DIFFERENT
1046
+ // The probe (and the subsequent keep-mirror download) return DIFFERENT
1408
1047
  // remote bytes.
1409
1048
  vi.mocked(downloadFile).mockImplementation(async (_ctx, _key, dest) => {
1410
1049
  fs.writeFileSync(dest, "a-peers-different-version");
@@ -1423,8 +1062,8 @@ describe("share", () => {
1423
1062
  expect(result.filesUploaded).toBe(0);
1424
1063
  const conflicts = events.filter((e) => typeof e === "object" && e !== null && e.type === "conflict");
1425
1064
  expect(conflicts).toHaveLength(1);
1426
- expect(fs.readFileSync(testFile, "utf-8")).toBe("a-peers-different-version");
1427
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my-local-version");
1065
+ // Local file untouched under keep.
1066
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("my-local-version");
1428
1067
  });
1429
1068
  describe("conditional-write fence (If-Match / If-None-Match / 412)", () => {
1430
1069
  const fence412 = () => Object.assign(new Error("Precondition failed"), {
@@ -1446,7 +1085,7 @@ describe("share", () => {
1446
1085
  size: 5,
1447
1086
  });
1448
1087
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1449
- seedJournalAt(journalPath, ({
1088
+ fs.writeFileSync(journalPath, JSON.stringify({
1450
1089
  version: "1",
1451
1090
  lastSync: syncedAt,
1452
1091
  files: {
@@ -1484,18 +1123,12 @@ describe("share", () => {
1484
1123
  });
1485
1124
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "brand-new.md", undefined, { ifNoneMatch: "*" });
1486
1125
  });
1487
- it("412 under keep: adopts the racing remote, preserves local, and converges the journal", async () => {
1126
+ it("412 under keep: conflict event + remote mirrored + ledger entry + NO journal stamp", async () => {
1488
1127
  const companyRoot = path.join(tmpDir, "companies", "acme");
1489
1128
  fs.mkdirSync(companyRoot, { recursive: true });
1490
1129
  const testFile = path.join(companyRoot, "raced.md");
1491
1130
  fs.writeFileSync(testFile, "my local bytes");
1492
- vi.mocked(headRemoteFile)
1493
- .mockResolvedValueOnce(null)
1494
- .mockResolvedValueOnce({
1495
- lastModified: new Date(),
1496
- etag: '"racing-peer-etag"',
1497
- size: 23,
1498
- });
1131
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
1499
1132
  vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
1500
1133
  vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1501
1134
  fs.writeFileSync(dest, "the racing peer version");
@@ -1515,22 +1148,19 @@ describe("share", () => {
1515
1148
  expect(result.filesUploaded).toBe(0);
1516
1149
  expect(result.aborted).toBe(false);
1517
1150
  expect(events.some((e) => e.type === "conflict" && e.path === "raced.md")).toBe(true);
1518
- // The racing remote wins the working name; local remains recoverable.
1519
- expect(fs.readFileSync(testFile, "utf-8")).toBe("the racing peer version");
1520
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my local bytes");
1521
- // The pull-shaped result is journalled as the remote body, so the next
1522
- // pass does not revive the same resolved conflict.
1151
+ // The racing remote bytes were preserved next to the local copy.
1152
+ const mirrors = fs
1153
+ .readdirSync(companyRoot)
1154
+ .filter((f) => f.includes(".conflict-"));
1155
+ expect(mirrors).toHaveLength(1);
1156
+ // Local copy untouched.
1157
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("my local bytes");
1158
+ // No journal stamp for the failed PUT — next pass re-evaluates fresh.
1523
1159
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1524
- const journal = journalAt(journalPath);
1525
- expect(journal.files["raced.md"]).toMatchObject({
1526
- hash: hashFile(testFile),
1527
- remoteEtag: "racing-peer-etag",
1528
- direction: "down",
1529
- });
1530
- expect(conflictIndexAt(tmpDir).conflicts[0]).toMatchObject({
1531
- originalPath: "companies/acme/raced.md",
1532
- preserved: "local",
1533
- });
1160
+ if (fs.existsSync(journalPath)) {
1161
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1162
+ expect(journal.files?.["raced.md"]).toBeUndefined();
1163
+ }
1534
1164
  });
1535
1165
  it("412 under overwrite: retries exactly once WITHOUT the fence (explicit consent)", async () => {
1536
1166
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1609,12 +1239,8 @@ describe("share", () => {
1609
1239
  }
1610
1240
  return { etag: '"should-not-land"' };
1611
1241
  });
1612
- vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1613
- fs.writeFileSync(dest, "reviewed peer body");
1614
- return undefined;
1615
- });
1616
1242
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1617
- seedJournalAt(journalPath, ({
1243
+ fs.writeFileSync(journalPath, JSON.stringify({
1618
1244
  version: "1",
1619
1245
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1620
1246
  files: {
@@ -1657,9 +1283,8 @@ describe("share", () => {
1657
1283
  }
1658
1284
  // No unfenced retry under keep.
1659
1285
  expect(vi.mocked(uploadFile).mock.calls.some((c) => c[4] === undefined)).toBe(false);
1660
- // Keep adopts the peer body but retains the stale local body beside it.
1661
- expect(fs.readFileSync(testFile, "utf-8")).toBe("reviewed peer body");
1662
- expect(fs.readFileSync(sidecarIn(path.dirname(testFile)), "utf-8")).toBe(staleBytes);
1286
+ // Local stale bytes preserved on disk (keep does not pull).
1287
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(staleBytes);
1663
1288
  });
1664
1289
  it("legitimate local-ahead still uploads when remote remains at journal baseline", async () => {
1665
1290
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1675,7 +1300,7 @@ describe("share", () => {
1675
1300
  });
1676
1301
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"new-upload-etag"' });
1677
1302
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1678
- seedJournalAt(journalPath, ({
1303
+ fs.writeFileSync(journalPath, JSON.stringify({
1679
1304
  version: "1",
1680
1305
  lastSync: new Date(Date.now() - 60_000).toISOString(),
1681
1306
  files: {
@@ -1709,12 +1334,8 @@ describe("share", () => {
1709
1334
  etag: '"remote-concurrent"',
1710
1335
  size: 20,
1711
1336
  });
1712
- vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1713
- fs.writeFileSync(dest, "remote concurrent");
1714
- return undefined;
1715
- });
1716
1337
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1717
- seedJournalAt(journalPath, ({
1338
+ fs.writeFileSync(journalPath, JSON.stringify({
1718
1339
  version: "1",
1719
1340
  lastSync: new Date().toISOString(),
1720
1341
  files: {
@@ -1737,14 +1358,12 @@ describe("share", () => {
1737
1358
  expect(result.conflictPaths).toEqual(["both-changed.md"]);
1738
1359
  expect(result.filesUploaded).toBe(0);
1739
1360
  expect(uploadFile).not.toHaveBeenCalled();
1740
- expect(fs.readFileSync(testFile, "utf-8")).toBe("remote concurrent");
1741
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("local concurrent");
1742
- });
1743
- it("localDiverges under keep: a pull-held divergent local yields to cloud without becoming write authority", async () => {
1744
- // After a pull-side hold (`skip` or pull-leg `publish-local`), the journal
1745
- // stamps the peer remoteEtag and localDiverges=true because local never
1746
- // matched that etag. A later push must not treat "remote still at stamped
1747
- // etag" as license to PUT the divergent local body.
1361
+ });
1362
+ it("localDiverges under keep: pull-kept divergent local cannot silently become write authority", async () => {
1363
+ // After pull --on-conflict keep, journal stamps remoteEtag of the peer
1364
+ // version AND localDiverges=true (local never matched that etag). A later
1365
+ // push must not treat "remote still at stamped etag" as license to PUT
1366
+ // the divergent local body (journal-baseline If-Match would succeed).
1748
1367
  const companyRoot = path.join(tmpDir, "companies", "acme");
1749
1368
  fs.mkdirSync(companyRoot, { recursive: true });
1750
1369
  const testFile = path.join(companyRoot, "kept-divergent.md");
@@ -1757,12 +1376,8 @@ describe("share", () => {
1757
1376
  etag: `"${REMOTE}"`,
1758
1377
  size: 40,
1759
1378
  });
1760
- vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
1761
- fs.writeFileSync(dest, "peer remote body");
1762
- return undefined;
1763
- });
1764
1379
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1765
- seedJournalAt(journalPath, ({
1380
+ fs.writeFileSync(journalPath, JSON.stringify({
1766
1381
  version: "1",
1767
1382
  lastSync: new Date().toISOString(),
1768
1383
  files: {
@@ -1792,10 +1407,7 @@ describe("share", () => {
1792
1407
  expect(events.some((e) => e.type === "conflict" &&
1793
1408
  e.path === "kept-divergent.md" &&
1794
1409
  e.resolution === "keep")).toBe(true);
1795
- expect(fs.readFileSync(testFile, "utf-8")).toBe("peer remote body");
1796
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe(localBytes);
1797
- expect(journalAt(journalPath).files["kept-divergent.md"]?.localDiverges)
1798
- .toBeFalsy();
1410
+ expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1799
1411
  });
1800
1412
  it("localDiverges under overwrite: explicit intent may force-write", async () => {
1801
1413
  const companyRoot = path.join(tmpDir, "companies", "acme");
@@ -1811,7 +1423,7 @@ describe("share", () => {
1811
1423
  });
1812
1424
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"forced-etag"' });
1813
1425
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
1814
- seedJournalAt(journalPath, ({
1426
+ fs.writeFileSync(journalPath, JSON.stringify({
1815
1427
  version: "1",
1816
1428
  lastSync: new Date().toISOString(),
1817
1429
  files: {
@@ -1838,304 +1450,6 @@ describe("share", () => {
1838
1450
  // fence then retry — either is consent. Must not leave the file unuploaded.
1839
1451
  expect(uploadFile).toHaveBeenCalled();
1840
1452
  });
1841
- // ── publish-local: fenced promotion of a kept-local copy ────────────────
1842
- //
1843
- // `keep` leaves a file permanently unpublishable: push refuses it on the
1844
- // localDiverges flag, pull never clears the flag (remote is unchanged), and
1845
- // `keep` re-arms it every pass. The only existing escape — `overwrite` — is
1846
- // a blunt force-write that will PUT unfenced after a 412, clobbering a
1847
- // remote that genuinely advanced.
1848
- //
1849
- // `publish-local` is the missing middle: the operator explicitly elects
1850
- // their local copy as the new authority, and the engine accepts it ONLY
1851
- // while the remote still sits at the etag recorded when the keep happened.
1852
- // If the remote moved, consent was given against a version that no longer
1853
- // exists, so the write must fail closed instead of forcing.
1854
- it("localDiverges under publish-local: promotes local under an If-Match fence on the journal etag", async () => {
1855
- const companyRoot = path.join(tmpDir, "companies", "acme");
1856
- fs.mkdirSync(companyRoot, { recursive: true });
1857
- const testFile = path.join(companyRoot, "publish-divergent.md");
1858
- const localBytes = "my kept divergent local, now elected authority";
1859
- fs.writeFileSync(testFile, localBytes);
1860
- const localHash = hashFile(testFile);
1861
- const REMOTE = "peer-version-etag";
1862
- vi.mocked(headRemoteFile).mockResolvedValueOnce({
1863
- lastModified: new Date(),
1864
- etag: `"${REMOTE}"`,
1865
- size: 40,
1866
- });
1867
- vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1868
- const journalPath = path.join(stateDir, "sync-journal.acme.json");
1869
- fs.writeFileSync(journalPath, JSON.stringify({
1870
- version: "1",
1871
- lastSync: new Date().toISOString(),
1872
- files: {
1873
- "publish-divergent.md": {
1874
- hash: localHash,
1875
- size: localBytes.length,
1876
- syncedAt: new Date().toISOString(),
1877
- direction: "down",
1878
- remoteEtag: REMOTE,
1879
- localDiverges: true,
1880
- },
1881
- },
1882
- }));
1883
- const result = await share({
1884
- paths: [testFile],
1885
- company: "acme",
1886
- vaultConfig: mockConfig,
1887
- hqRoot: tmpDir,
1888
- skipUnchanged: false,
1889
- onConflict: "publish-local",
1890
- });
1891
- expect(result.filesUploaded).toBe(1);
1892
- // Fenced on the etag the keep recorded — never unfenced, never live-HEAD.
1893
- expect(uploadFile).toHaveBeenCalledTimes(1);
1894
- expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
1895
- // The entry is rewritten as an ordinary upload: local is now authority,
1896
- // so the journal-honesty flag must be gone rather than re-armed.
1897
- const journal = journalAt(journalPath);
1898
- expect(journal.files["publish-divergent.md"].direction).toBe("up");
1899
- expect(journal.files["publish-divergent.md"].localDiverges).toBeFalsy();
1900
- expect(journal.files["publish-divergent.md"].remoteEtag).toBe("published-etag");
1901
- expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1902
- });
1903
- it("localDiverges under publish-local: a 412 fails closed and never retries unfenced", async () => {
1904
- const companyRoot = path.join(tmpDir, "companies", "acme");
1905
- fs.mkdirSync(companyRoot, { recursive: true });
1906
- const testFile = path.join(companyRoot, "publish-raced.md");
1907
- const localBytes = "my local, but the remote moved under me";
1908
- fs.writeFileSync(testFile, localBytes);
1909
- const localHash = hashFile(testFile);
1910
- const REMOTE = "etag-at-keep-time";
1911
- vi.mocked(headRemoteFile).mockResolvedValueOnce({
1912
- lastModified: new Date(),
1913
- etag: `"${REMOTE}"`,
1914
- size: 40,
1915
- });
1916
- const precondition = Object.assign(new Error("At least one of the pre-conditions you specified did not hold"), {
1917
- name: "PreconditionFailed",
1918
- });
1919
- vi.mocked(uploadFile).mockRejectedValueOnce(precondition);
1920
- const journalPath = path.join(stateDir, "sync-journal.acme.json");
1921
- fs.writeFileSync(journalPath, JSON.stringify({
1922
- version: "1",
1923
- lastSync: new Date().toISOString(),
1924
- files: {
1925
- "publish-raced.md": {
1926
- hash: localHash,
1927
- size: localBytes.length,
1928
- syncedAt: new Date().toISOString(),
1929
- direction: "down",
1930
- remoteEtag: REMOTE,
1931
- localDiverges: true,
1932
- },
1933
- },
1934
- }));
1935
- const result = await share({
1936
- paths: [testFile],
1937
- company: "acme",
1938
- vaultConfig: mockConfig,
1939
- hqRoot: tmpDir,
1940
- skipUnchanged: false,
1941
- onConflict: "publish-local",
1942
- });
1943
- // Exactly one attempt: the fenced one. The `overwrite` escape hatch of
1944
- // re-PUTting with no precondition must NOT be reachable from here.
1945
- expect(uploadFile).toHaveBeenCalledTimes(1);
1946
- expect(result.filesUploaded).toBe(0);
1947
- expect(result.conflictPaths).toContain("publish-raced.md");
1948
- // Consent was for a remote that no longer exists, so the flag stays put
1949
- // and the operator is asked again rather than silently overruled.
1950
- const journal = journalAt(journalPath);
1951
- expect(journal.files["publish-raced.md"].localDiverges).toBe(true);
1952
- expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
1953
- });
1954
- // Codex review (PR #356, P1). The sync runner's push leg always calls
1955
- // share() with `skipUnchanged: true`, and a pull-held entry stores the
1956
- // CURRENT local hash — that is exactly what the journal-honesty stamp
1957
- // records. So `existing.hash === localHash` is true for every stuck file,
1958
- // classifyPushCandidates() returns `skip-unchanged`, and executeUploads()
1959
- // never runs: no HEAD, no conflict, no upload. Without this bypass the
1960
- // whole recovery is a no-op in the one mode that can reach personal-vault
1961
- // files, and the next pull leaves them stuck again. `hash === journal hash`
1962
- // means "local is unchanged since the stamp", NOT "local matches remote" —
1963
- // for a divergent entry those are different claims.
1964
- it("localDiverges bypasses the skip-unchanged fast path so the recovery can run at all", async () => {
1965
- const companyRoot = path.join(tmpDir, "companies", "acme");
1966
- fs.mkdirSync(companyRoot, { recursive: true });
1967
- const testFile = path.join(companyRoot, "stuck-under-skip-unchanged.md");
1968
- const localBytes = "the body a pull hold stamped and then refused to publish";
1969
- fs.writeFileSync(testFile, localBytes);
1970
- const localHash = hashFile(testFile);
1971
- const REMOTE = "etag-at-keep-time";
1972
- vi.mocked(headRemoteFile).mockResolvedValueOnce({
1973
- lastModified: new Date(),
1974
- etag: `"${REMOTE}"`,
1975
- size: 40,
1976
- });
1977
- vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
1978
- const journalPath = path.join(stateDir, "sync-journal.acme.json");
1979
- fs.writeFileSync(journalPath, JSON.stringify({
1980
- version: "1",
1981
- lastSync: new Date().toISOString(),
1982
- files: {
1983
- "stuck-under-skip-unchanged.md": {
1984
- // Deliberately the CURRENT local hash: this is what a pull hold
1985
- // writes, and it is what makes the fast path swallow the file.
1986
- hash: localHash,
1987
- size: localBytes.length,
1988
- syncedAt: new Date().toISOString(),
1989
- direction: "down",
1990
- remoteEtag: REMOTE,
1991
- localDiverges: true,
1992
- },
1993
- },
1994
- }));
1995
- const result = await share({
1996
- paths: [testFile],
1997
- company: "acme",
1998
- vaultConfig: mockConfig,
1999
- hqRoot: tmpDir,
2000
- skipUnchanged: true,
2001
- onConflict: "publish-local",
2002
- });
2003
- expect(result.filesUploaded).toBe(1);
2004
- expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
2005
- const journal = journalAt(journalPath);
2006
- expect(journal.files["stuck-under-skip-unchanged.md"].direction).toBe("up");
2007
- expect(journal.files["stuck-under-skip-unchanged.md"].localDiverges).toBeFalsy();
2008
- });
2009
- // Codex review (PR #356, P2). A genuine two-sided conflict is not the
2010
- // localDiverges recovery: the journal etag is stale BY DEFINITION here
2011
- // (that is what remoteChanged means), so fencing consent on it guarantees a
2012
- // 412 and the file can never publish. Consent given at this prompt is
2013
- // consent against the remote the operator was just shown, so the fence must
2014
- // be the freshly observed etag.
2015
- it("publish-local on a fresh two-sided conflict fences on the OBSERVED remote etag, not the stale journal one", async () => {
2016
- const companyRoot = path.join(tmpDir, "companies", "acme");
2017
- fs.mkdirSync(companyRoot, { recursive: true });
2018
- const testFile = path.join(companyRoot, "two-sided.md");
2019
- fs.writeFileSync(testFile, "my newer local body");
2020
- const STALE_JOURNAL_ETAG = "etag-from-last-sync";
2021
- const OBSERVED_REMOTE_ETAG = "etag-the-peer-just-wrote";
2022
- vi.mocked(headRemoteFile).mockResolvedValueOnce({
2023
- lastModified: new Date(),
2024
- etag: `"${OBSERVED_REMOTE_ETAG}"`,
2025
- size: 40,
2026
- });
2027
- vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"published-etag"' });
2028
- const journalPath = path.join(stateDir, "sync-journal.acme.json");
2029
- fs.writeFileSync(journalPath, JSON.stringify({
2030
- version: "1",
2031
- lastSync: new Date().toISOString(),
2032
- files: {
2033
- "two-sided.md": {
2034
- hash: "a-hash-that-is-neither-side",
2035
- size: 10,
2036
- syncedAt: new Date().toISOString(),
2037
- direction: "up",
2038
- remoteEtag: STALE_JOURNAL_ETAG,
2039
- },
2040
- },
2041
- }));
2042
- const result = await share({
2043
- paths: [testFile],
2044
- company: "acme",
2045
- vaultConfig: mockConfig,
2046
- hqRoot: tmpDir,
2047
- skipUnchanged: false,
2048
- onConflict: "publish-local",
2049
- });
2050
- expect(result.filesUploaded).toBe(1);
2051
- expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({
2052
- ifMatch: OBSERVED_REMOTE_ETAG,
2053
- });
2054
- });
2055
- // Codex review (PR #356, P2). HEAD returning null skips the whole conflict
2056
- // block, so the precondition becomes If-None-Match:* and the object is
2057
- // RECREATED. For a divergent entry that silently overrules a peer's
2058
- // deletion — the remote is emphatically no longer at the recorded etag, so
2059
- // the strategy's own fence says fail closed. `keep`'s recreate-once
2060
- // behaviour for non-divergent entries is unaffected.
2061
- it("publish-local does not resurrect a remote deleted after the keep", async () => {
2062
- const companyRoot = path.join(tmpDir, "companies", "acme");
2063
- fs.mkdirSync(companyRoot, { recursive: true });
2064
- const testFile = path.join(companyRoot, "peer-deleted.md");
2065
- const localBytes = "my divergent local body";
2066
- fs.writeFileSync(testFile, localBytes);
2067
- const localHash = hashFile(testFile);
2068
- vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2069
- const journalPath = path.join(stateDir, "sync-journal.acme.json");
2070
- fs.writeFileSync(journalPath, JSON.stringify({
2071
- version: "1",
2072
- lastSync: new Date().toISOString(),
2073
- files: {
2074
- "peer-deleted.md": {
2075
- hash: localHash,
2076
- size: localBytes.length,
2077
- syncedAt: new Date().toISOString(),
2078
- direction: "down",
2079
- remoteEtag: "etag-before-the-peer-deleted-it",
2080
- localDiverges: true,
2081
- },
2082
- },
2083
- }));
2084
- const result = await share({
2085
- paths: [testFile],
2086
- company: "acme",
2087
- vaultConfig: mockConfig,
2088
- hqRoot: tmpDir,
2089
- skipUnchanged: false,
2090
- onConflict: "publish-local",
2091
- });
2092
- expect(uploadFile).not.toHaveBeenCalled();
2093
- expect(result.filesUploaded).toBe(0);
2094
- expect(result.conflictPaths).toContain("peer-deleted.md");
2095
- const journal = journalAt(journalPath);
2096
- expect(journal.files["peer-deleted.md"].localDiverges).toBe(true);
2097
- expect(fs.readFileSync(testFile, "utf-8")).toBe(localBytes);
2098
- });
2099
- it("publish-local on a non-divergent file behaves like an ordinary fenced upload", async () => {
2100
- const companyRoot = path.join(tmpDir, "companies", "acme");
2101
- fs.mkdirSync(companyRoot, { recursive: true });
2102
- const testFile = path.join(companyRoot, "ordinary.md");
2103
- fs.writeFileSync(testFile, "locally edited since last sync");
2104
- const REMOTE = "unchanged-remote-etag";
2105
- vi.mocked(headRemoteFile).mockResolvedValueOnce({
2106
- lastModified: new Date(),
2107
- etag: `"${REMOTE}"`,
2108
- size: 10,
2109
- });
2110
- vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"next-etag"' });
2111
- const journalPath = path.join(stateDir, "sync-journal.acme.json");
2112
- fs.writeFileSync(journalPath, JSON.stringify({
2113
- version: "1",
2114
- lastSync: new Date().toISOString(),
2115
- files: {
2116
- "ordinary.md": {
2117
- hash: "stale-hash-from-a-previous-body",
2118
- size: 10,
2119
- syncedAt: new Date().toISOString(),
2120
- direction: "up",
2121
- remoteEtag: REMOTE,
2122
- },
2123
- },
2124
- }));
2125
- const result = await share({
2126
- paths: [testFile],
2127
- company: "acme",
2128
- vaultConfig: mockConfig,
2129
- hqRoot: tmpDir,
2130
- skipUnchanged: false,
2131
- onConflict: "publish-local",
2132
- });
2133
- // No localDiverges flag means no special casing: the normal push fence
2134
- // applies and the file uploads without being routed through conflict.
2135
- expect(result.filesUploaded).toBe(1);
2136
- expect(result.conflictPaths ?? []).not.toContain("ordinary.md");
2137
- expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifMatch: REMOTE });
2138
- });
2139
1453
  // Journal still carries remoteEtag from a prior sync, but the remote key
2140
1454
  // is gone (HEAD null) — e.g. peer delete or operator purge. Recreation
2141
1455
  // must use If-None-Match:* (create-only), NOT If-Match on the stale
@@ -2153,7 +1467,7 @@ describe("share", () => {
2153
1467
  vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2154
1468
  vi.mocked(uploadFile).mockResolvedValueOnce({ etag: '"recreated-etag"' });
2155
1469
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2156
- seedJournalAt(journalPath, ({
1470
+ fs.writeFileSync(journalPath, JSON.stringify({
2157
1471
  version: "1",
2158
1472
  lastSync: new Date(Date.now() - 60_000).toISOString(),
2159
1473
  files: {
@@ -2183,34 +1497,28 @@ describe("share", () => {
2183
1497
  const pc = vi.mocked(uploadFile).mock.calls[0][4];
2184
1498
  expect(pc?.ifMatch).toBeUndefined();
2185
1499
  expect(pc?.ifNoneMatch).toBe("*");
2186
- const journal = journalAt(journalPath);
1500
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2187
1501
  expect(journal.files["recreate-deleted.md"]).toMatchObject({
2188
1502
  hash: localHash,
2189
1503
  remoteEtag: "recreated-etag",
2190
1504
  direction: "up",
2191
1505
  });
2192
1506
  });
2193
- it("deleted journaled remote + concurrent create 412 under keep: adopts the winner and preserves local", async () => {
1507
+ it("deleted journaled remote + concurrent create 412 under keep: existing conflict semantics", async () => {
2194
1508
  // Same setup as recreation, but a peer creates the key between HEAD and
2195
- // PUT → If-None-Match:* 412s → keep adopts the peer's winning create.
1509
+ // PUT → If-None-Match:* 412s → keep surfaces conflict, no journal stamp.
2196
1510
  const companyRoot = path.join(tmpDir, "companies", "acme");
2197
1511
  fs.mkdirSync(companyRoot, { recursive: true });
2198
1512
  const testFile = path.join(companyRoot, "race-recreate.md");
2199
1513
  fs.writeFileSync(testFile, "my recreated local");
2200
- vi.mocked(headRemoteFile)
2201
- .mockResolvedValueOnce(null)
2202
- .mockResolvedValueOnce({
2203
- lastModified: new Date(),
2204
- etag: '"peer-created-etag"',
2205
- size: 18,
2206
- });
1514
+ vi.mocked(headRemoteFile).mockResolvedValueOnce(null);
2207
1515
  vi.mocked(uploadFile).mockRejectedValueOnce(fence412());
2208
1516
  vi.mocked(downloadFile).mockImplementationOnce(async (_ctx, _key, dest) => {
2209
1517
  fs.writeFileSync(dest, "peer created first");
2210
1518
  return undefined;
2211
1519
  });
2212
1520
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2213
- seedJournalAt(journalPath, ({
1521
+ fs.writeFileSync(journalPath, JSON.stringify({
2214
1522
  version: "1",
2215
1523
  lastSync: new Date(Date.now() - 60_000).toISOString(),
2216
1524
  files: {
@@ -2240,15 +1548,12 @@ describe("share", () => {
2240
1548
  // First (only) attempt must be create-fenced; no unfenced retry under keep.
2241
1549
  expect(uploadFile).toHaveBeenCalledTimes(1);
2242
1550
  expect(vi.mocked(uploadFile).mock.calls[0][4]).toEqual({ ifNoneMatch: "*" });
2243
- // Journal now describes the peer body at the original name.
2244
- const journal = journalAt(journalPath);
2245
- expect(journal.files["race-recreate.md"]).toMatchObject({
2246
- remoteEtag: "peer-created-etag",
2247
- hash: hashFile(testFile),
2248
- direction: "down",
2249
- });
2250
- expect(fs.readFileSync(testFile, "utf-8")).toBe("peer created first");
2251
- expect(fs.readFileSync(sidecarIn(companyRoot), "utf-8")).toBe("my recreated local");
1551
+ // Journal not advanced to a successful upload etag (entry may still be
1552
+ // the prior baseline — next pass re-evaluates).
1553
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
1554
+ expect(journal.files["race-recreate.md"]?.remoteEtag).toBe("etag-before-delete");
1555
+ expect(journal.files["race-recreate.md"]?.hash).toBe("old-hash");
1556
+ expect(fs.readFileSync(testFile, "utf-8")).toBe("my recreated local");
2252
1557
  });
2253
1558
  });
2254
1559
  it("scoped push (plan-exceeds-grant): syncs the in-scope subset, skips out-of-scope paths, never aborts (feedback_ded09d56)", async () => {
@@ -2569,7 +1874,7 @@ describe("share", () => {
2569
1874
  size: 5,
2570
1875
  });
2571
1876
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2572
- seedJournalAt(journalPath, ({
1877
+ fs.writeFileSync(journalPath, JSON.stringify({
2573
1878
  version: "1",
2574
1879
  lastSync: syncedAt,
2575
1880
  files: {
@@ -2609,7 +1914,7 @@ describe("share", () => {
2609
1914
  size: 5,
2610
1915
  });
2611
1916
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2612
- seedJournalAt(journalPath, ({
1917
+ fs.writeFileSync(journalPath, JSON.stringify({
2613
1918
  version: "1",
2614
1919
  lastSync: syncedAt,
2615
1920
  files: {
@@ -2645,7 +1950,7 @@ describe("share", () => {
2645
1950
  hqRoot: tmpDir,
2646
1951
  });
2647
1952
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2648
- const journal = journalAt(journalPath);
1953
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
2649
1954
  expect(journal.files["fresh.md"].remoteEtag).toBe("new-upload-etag");
2650
1955
  });
2651
1956
  it("forwards UploadAuthor to uploadFile when present (created-by metadata)", async () => {
@@ -2690,7 +1995,7 @@ describe("share", () => {
2690
1995
  const { hashFile } = await import("../journal.js");
2691
1996
  const hash = hashFile(testFile);
2692
1997
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2693
- seedJournalAt(journalPath, ({
1998
+ fs.writeFileSync(journalPath, JSON.stringify({
2694
1999
  version: "1",
2695
2000
  lastSync: new Date().toISOString(),
2696
2001
  files: {
@@ -2910,7 +2215,7 @@ describe("share", () => {
2910
2215
  .update("stable content")
2911
2216
  .digest("hex");
2912
2217
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2913
- seedJournalAt(journalPath, ({
2218
+ fs.writeFileSync(journalPath, JSON.stringify({
2914
2219
  version: "1",
2915
2220
  lastSync: new Date().toISOString(),
2916
2221
  files: {
@@ -2971,7 +2276,7 @@ describe("share", () => {
2971
2276
  // not be queued for delete.
2972
2277
  fs.writeFileSync(path.join(tmpDir, ".hqinclude"), "companies/*/knowledge/\n");
2973
2278
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
2974
- seedJournalAt(journalPath, ({
2279
+ fs.writeFileSync(journalPath, JSON.stringify({
2975
2280
  version: "1",
2976
2281
  lastSync: new Date().toISOString(),
2977
2282
  files: {
@@ -3008,7 +2313,7 @@ describe("share", () => {
3008
2313
  operation: "delete",
3009
2314
  });
3010
2315
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "knowledge");
3011
- const journal = journalAt(journalPath);
2316
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3012
2317
  expect(journal.files["knowledge"]).toBeUndefined();
3013
2318
  });
3014
2319
  it("propagateDeletes: a dangling local symlink is NOT classified as gone (lstat, not existsSync)", async () => {
@@ -3025,7 +2330,7 @@ describe("share", () => {
3025
2330
  const danglingLink = path.join(companyRoot, "dangling-link.md");
3026
2331
  fs.symlinkSync("./missing-target.md", danglingLink);
3027
2332
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3028
- seedJournalAt(journalPath, ({
2333
+ fs.writeFileSync(journalPath, JSON.stringify({
3029
2334
  version: "1",
3030
2335
  lastSync: new Date().toISOString(),
3031
2336
  files: {
@@ -3055,7 +2360,7 @@ describe("share", () => {
3055
2360
  expect(result.filesDeleted).toBe(0);
3056
2361
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3057
2362
  // Journal entry must survive the run.
3058
- const journal = journalAt(journalPath);
2363
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3059
2364
  expect(journal.files["dangling-link.md"]).toBeDefined();
3060
2365
  });
3061
2366
  it("vault-litter drain: a `.drift-*` marker with direction:'down' and NO delete intent tombstones " +
@@ -3120,7 +2425,7 @@ describe("share", () => {
3120
2425
  });
3121
2426
  expect(result.filesDeleted).toBe(1);
3122
2427
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/scripts/run-project.sh.drift-1779863862-42519");
3123
- const journal = journalAt(journalPath);
2428
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3124
2429
  expect(journal.files["personal/scripts/run-project.sh.drift-1779863862-42519"]).toBeUndefined();
3125
2430
  }
3126
2431
  finally {
@@ -3193,7 +2498,7 @@ describe("share", () => {
3193
2498
  fs.mkdirSync(companyRoot, { recursive: true });
3194
2499
  // No file on disk under companies/acme/peer-uploaded.md → local missing.
3195
2500
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3196
- seedJournalAt(journalPath, ({
2501
+ fs.writeFileSync(journalPath, JSON.stringify({
3197
2502
  version: "1",
3198
2503
  lastSync: new Date().toISOString(),
3199
2504
  files: {
@@ -3218,7 +2523,7 @@ describe("share", () => {
3218
2523
  });
3219
2524
  expect(result.filesDeleted).toBe(0);
3220
2525
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3221
- const journal = journalAt(journalPath);
2526
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3222
2527
  expect(journal.files["peer-uploaded.md"]).toBeDefined();
3223
2528
  });
3224
2529
  it("ROLLBACK KNOB: owned-only is honored on a prs_ personal vault — an intent-less entry with a " +
@@ -3309,7 +2614,7 @@ describe("share", () => {
3309
2614
  ]);
3310
2615
  // Journal entry survives, so a later run (or a watcher-minted intent)
3311
2616
  // can still authorize the delete properly.
3312
- const journal = journalAt(journalPath);
2617
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3313
2618
  expect(journal.files["personal/knowledge/gone.md"]).toBeDefined();
3314
2619
  }
3315
2620
  finally {
@@ -3326,7 +2631,7 @@ describe("share", () => {
3326
2631
  // deleted by the user.
3327
2632
  fs.writeFileSync(path.join(companyRoot, "kept.md"), "still here");
3328
2633
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3329
- seedJournalAt(journalPath, ({
2634
+ fs.writeFileSync(journalPath, JSON.stringify({
3330
2635
  version: "1",
3331
2636
  lastSync: new Date().toISOString(),
3332
2637
  files: {
@@ -3362,7 +2667,7 @@ describe("share", () => {
3362
2667
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
3363
2668
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "gone.md");
3364
2669
  // Journal entry for the gone file is removed; the kept entry stays.
3365
- const journal = journalAt(journalPath);
2670
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3366
2671
  expect(journal.files["gone.md"]).toBeUndefined();
3367
2672
  expect(journal.files["kept.md"]).toBeDefined();
3368
2673
  });
@@ -3370,7 +2675,7 @@ describe("share", () => {
3370
2675
  const companyRoot = path.join(tmpDir, "companies", "acme");
3371
2676
  fs.mkdirSync(companyRoot, { recursive: true });
3372
2677
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3373
- seedJournalAt(journalPath, ({
2678
+ fs.writeFileSync(journalPath, JSON.stringify({
3374
2679
  version: "1",
3375
2680
  lastSync: new Date().toISOString(),
3376
2681
  files: {
@@ -3412,7 +2717,7 @@ describe("share", () => {
3412
2717
  const companyRoot = path.join(tmpDir, "companies", "acme");
3413
2718
  fs.mkdirSync(companyRoot, { recursive: true });
3414
2719
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3415
- seedJournalAt(journalPath, ({
2720
+ fs.writeFileSync(journalPath, JSON.stringify({
3416
2721
  version: "1",
3417
2722
  lastSync: new Date().toISOString(),
3418
2723
  files: {
@@ -3435,7 +2740,7 @@ describe("share", () => {
3435
2740
  expect(result.filesDeleted).toBe(0);
3436
2741
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3437
2742
  // Journal entry survives so the next opt-in run can still propagate.
3438
- const journal = journalAt(journalPath);
2743
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3439
2744
  expect(journal.files["gone.md"]).toBeDefined();
3440
2745
  });
3441
2746
  it("propagateDeletes: scope is constrained to the supplied paths — sibling deletes are not swept", async () => {
@@ -3443,7 +2748,7 @@ describe("share", () => {
3443
2748
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
3444
2749
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
3445
2750
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3446
- seedJournalAt(journalPath, ({
2751
+ fs.writeFileSync(journalPath, JSON.stringify({
3447
2752
  version: "1",
3448
2753
  lastSync: new Date().toISOString(),
3449
2754
  files: {
@@ -3478,7 +2783,7 @@ describe("share", () => {
3478
2783
  expect(result.filesDeleted).toBe(1);
3479
2784
  expect(deleteRemoteFile).toHaveBeenCalledTimes(1);
3480
2785
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
3481
- const journal = journalAt(journalPath);
2786
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3482
2787
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
3483
2788
  // Sibling tree's journal entry is untouched — `hq share <subtree>` must
3484
2789
  // not act on files outside the named scope.
@@ -3494,7 +2799,7 @@ describe("share", () => {
3494
2799
  fs.mkdirSync(path.join(companyRoot, "in-scope"), { recursive: true });
3495
2800
  fs.mkdirSync(path.join(companyRoot, "other"), { recursive: true });
3496
2801
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3497
- seedJournalAt(journalPath, ({
2802
+ fs.writeFileSync(journalPath, JSON.stringify({
3498
2803
  version: "1",
3499
2804
  lastSync: new Date().toISOString(),
3500
2805
  files: {
@@ -3529,7 +2834,7 @@ describe("share", () => {
3529
2834
  });
3530
2835
  expect(result.filesDeleted).toBe(1);
3531
2836
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "in-scope/gone.md");
3532
- const journal = journalAt(journalPath);
2837
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3533
2838
  expect(journal.files["in-scope/gone.md"]).toBeUndefined();
3534
2839
  // Scope stays exactly as narrow as the absolute spelling's — no wider.
3535
2840
  expect(journal.files["other/also-gone.md"]).toBeDefined();
@@ -3546,7 +2851,7 @@ describe("share", () => {
3546
2851
  fs.mkdirSync(acmeRepo, { recursive: true });
3547
2852
  fs.symlinkSync(acmeRepo, path.join(companyRoot, "knowledge"));
3548
2853
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3549
- seedJournalAt(journalPath, ({
2854
+ fs.writeFileSync(journalPath, JSON.stringify({
3550
2855
  version: "1",
3551
2856
  lastSync: new Date().toISOString(),
3552
2857
  files: {
@@ -3576,14 +2881,14 @@ describe("share", () => {
3576
2881
  });
3577
2882
  expect(result.filesDeleted).toBe(0);
3578
2883
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3579
- const journal = journalAt(journalPath);
2884
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3580
2885
  expect(journal.files["knowledge/agents/brief.md"]).toBeDefined();
3581
2886
  });
3582
2887
  it("propagateDeletes: a failed DeleteObject leaves the journal entry intact for retry", async () => {
3583
2888
  const companyRoot = path.join(tmpDir, "companies", "acme");
3584
2889
  fs.mkdirSync(companyRoot, { recursive: true });
3585
2890
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3586
- seedJournalAt(journalPath, ({
2891
+ fs.writeFileSync(journalPath, JSON.stringify({
3587
2892
  version: "1",
3588
2893
  lastSync: new Date().toISOString(),
3589
2894
  files: {
@@ -3616,7 +2921,7 @@ describe("share", () => {
3616
2921
  const errorEvent = events.find((e) => e.type === "error");
3617
2922
  expect(errorEvent).toMatchObject({ path: "flaky.md", message: expect.stringContaining("S3 down") });
3618
2923
  // Entry survives — next run will retry the delete.
3619
- const journal = journalAt(journalPath);
2924
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3620
2925
  expect(journal.files["flaky.md"]).toBeDefined();
3621
2926
  });
3622
2927
  // ── Delete propagation safety: direction + filter guards (Bug B + C) ──────
@@ -3650,7 +2955,7 @@ describe("share", () => {
3650
2955
  // and one this machine uploaded (direction:'up'). Only the 'up' one should
3651
2956
  // be eligible for delete-propagation.
3652
2957
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3653
- seedJournalAt(journalPath, ({
2958
+ fs.writeFileSync(journalPath, JSON.stringify({
3654
2959
  version: "1",
3655
2960
  lastSync: new Date().toISOString(),
3656
2961
  files: {
@@ -3687,7 +2992,7 @@ describe("share", () => {
3687
2992
  const companyRoot = path.join(tmpDir, "companies", "acme");
3688
2993
  fs.mkdirSync(companyRoot, { recursive: true });
3689
2994
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3690
- seedJournalAt(journalPath, ({
2995
+ fs.writeFileSync(journalPath, JSON.stringify({
3691
2996
  version: "1",
3692
2997
  lastSync: new Date().toISOString(),
3693
2998
  files: {
@@ -3720,7 +3025,7 @@ describe("share", () => {
3720
3025
  // absence is a filter outcome, not a user-intended delete.
3721
3026
  fs.writeFileSync(path.join(tmpDir, ".hqignore"), "companies/acme/legacy/**\n");
3722
3027
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3723
- seedJournalAt(journalPath, ({
3028
+ fs.writeFileSync(journalPath, JSON.stringify({
3724
3029
  version: "1",
3725
3030
  lastSync: new Date().toISOString(),
3726
3031
  files: {
@@ -3779,7 +3084,7 @@ describe("share", () => {
3779
3084
  // Under owned-only this would be stuck on remote forever; currency-gated
3780
3085
  // checks the ETag and propagates the delete cleanly when match holds.
3781
3086
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3782
- seedJournalAt(journalPath, ({
3087
+ fs.writeFileSync(journalPath, JSON.stringify({
3783
3088
  version: "1",
3784
3089
  lastSync: new Date().toISOString(),
3785
3090
  files: {
@@ -3809,7 +3114,7 @@ describe("share", () => {
3809
3114
  });
3810
3115
  expect(result.filesDeleted).toBe(1);
3811
3116
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "core/policies/old.md");
3812
- const journal = journalAt(journalPath);
3117
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3813
3118
  expect(journal.files["core/policies/old.md"]).toBeUndefined();
3814
3119
  });
3815
3120
  it("currency-gated: propagates the S3 delete for a local-delete tombstone (pull-leg producer hand-off)", async () => {
@@ -3820,7 +3125,7 @@ describe("share", () => {
3820
3125
  const companyRoot = path.join(tmpDir, "companies", "acme");
3821
3126
  fs.mkdirSync(companyRoot, { recursive: true });
3822
3127
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3823
- seedJournalAt(journalPath, ({
3128
+ fs.writeFileSync(journalPath, JSON.stringify({
3824
3129
  version: "2",
3825
3130
  lastSync: new Date().toISOString(),
3826
3131
  files: {
@@ -3856,7 +3161,7 @@ describe("share", () => {
3856
3161
  });
3857
3162
  expect(result.filesDeleted).toBe(1);
3858
3163
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/deleted.md");
3859
- const journal = journalAt(journalPath);
3164
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3860
3165
  expect(journal.files["docs/deleted.md"]).toBeUndefined();
3861
3166
  });
3862
3167
  it("currency-gated: refuses delete + emits stale-etag event when remote moved since last sync", async () => {
@@ -3865,7 +3170,7 @@ describe("share", () => {
3865
3170
  // Another device modified `shared.md` after this device's last sync.
3866
3171
  // The journal still records the old etag; HEAD returns the new one.
3867
3172
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3868
- seedJournalAt(journalPath, ({
3173
+ fs.writeFileSync(journalPath, JSON.stringify({
3869
3174
  version: "1",
3870
3175
  lastSync: new Date().toISOString(),
3871
3176
  files: {
@@ -3897,7 +3202,7 @@ describe("share", () => {
3897
3202
  // No remote DELETE issued, journal entry preserved (pull will re-pull).
3898
3203
  expect(result.filesDeleted).toBe(0);
3899
3204
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3900
- const journal = journalAt(journalPath);
3205
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3901
3206
  expect(journal.files["shared.md"]).toBeDefined();
3902
3207
  // Dedicated event fired so UIs surface the refusal. `reason` field
3903
3208
  // discriminates "stale-etag" (real drift) from "legacy-no-etag" so
@@ -3919,7 +3224,7 @@ describe("share", () => {
3919
3224
  fs.mkdirSync(companyRoot, { recursive: true });
3920
3225
  const recreatedPath = path.join(companyRoot, "racy.md");
3921
3226
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3922
- seedJournalAt(journalPath, ({
3227
+ fs.writeFileSync(journalPath, JSON.stringify({
3923
3228
  version: "2",
3924
3229
  lastSync: new Date().toISOString(),
3925
3230
  files: {
@@ -3955,7 +3260,7 @@ describe("share", () => {
3955
3260
  expect(result.filesDeleted).toBe(0);
3956
3261
  expect(deleteRemoteFile).not.toHaveBeenCalled();
3957
3262
  expect(fs.readFileSync(recreatedPath, "utf-8")).toBe("recreated after planning");
3958
- const journal = journalAt(journalPath);
3263
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
3959
3264
  expect(journal.files["racy.md"]).toBeDefined();
3960
3265
  expect(events).toContainEqual(expect.objectContaining({
3961
3266
  type: "delete-refused-stale-etag",
@@ -3977,7 +3282,7 @@ describe("share", () => {
3977
3282
  // genuine local==remote sync, so propagating the delete would destroy the
3978
3283
  // (different) remote version. Must be REFUSED, not propagated.
3979
3284
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
3980
- seedJournalAt(journalPath, ({
3285
+ fs.writeFileSync(journalPath, JSON.stringify({
3981
3286
  version: "2",
3982
3287
  lastSync: new Date().toISOString(),
3983
3288
  files: {
@@ -4012,7 +3317,7 @@ describe("share", () => {
4012
3317
  // Refusal short-circuits before the currency HEAD — no network needed.
4013
3318
  expect(headRemoteFile).not.toHaveBeenCalled();
4014
3319
  // Journal entry preserved, not silently dropped.
4015
- const journal = journalAt(journalPath);
3320
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4016
3321
  expect(journal.files["core/policies/kept.md"]).toBeDefined();
4017
3322
  // Surfaced as a refusal with the divergent-local reason.
4018
3323
  const refused = events.find((e) => e.type === "delete-refused-stale-etag");
@@ -4029,7 +3334,7 @@ describe("share", () => {
4029
3334
  // S3 console). Local copy also missing. Currency-gated should drop the
4030
3335
  // journal entry without issuing a DELETE — the remote is already gone.
4031
3336
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4032
- seedJournalAt(journalPath, ({
3337
+ fs.writeFileSync(journalPath, JSON.stringify({
4033
3338
  version: "1",
4034
3339
  lastSync: new Date().toISOString(),
4035
3340
  files: {
@@ -4062,7 +3367,7 @@ describe("share", () => {
4062
3367
  // from real deletes without re-counting events.
4063
3368
  expect(result.filesTombstoned).toBe(1);
4064
3369
  expect(result.filesRefusedStale).toBe(0);
4065
- const journal = journalAt(journalPath);
3370
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4066
3371
  expect(journal.files["removed-out-of-band.md"]).toBeUndefined();
4067
3372
  // Synthetic progress event carries the tombstone marker. Without this,
4068
3373
  // the tty stream renders tombstones identically to real deletes — operator
@@ -4083,7 +3388,7 @@ describe("share", () => {
4083
3388
  // against. Refuse the delete in the safe direction; a future sync with
4084
3389
  // a recorded etag can re-evaluate.
4085
3390
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4086
- seedJournalAt(journalPath, ({
3391
+ fs.writeFileSync(journalPath, JSON.stringify({
4087
3392
  version: "1",
4088
3393
  lastSync: new Date().toISOString(),
4089
3394
  files: {
@@ -4137,7 +3442,7 @@ describe("share", () => {
4137
3442
  const companyRoot = path.join(tmpDir, "companies", "acme");
4138
3443
  fs.mkdirSync(companyRoot, { recursive: true });
4139
3444
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4140
- seedJournalAt(journalPath, ({
3445
+ fs.writeFileSync(journalPath, JSON.stringify({
4141
3446
  version: "1",
4142
3447
  lastSync: new Date().toISOString(),
4143
3448
  files: {
@@ -4189,7 +3494,7 @@ describe("share", () => {
4189
3494
  const companyRoot = path.join(tmpDir, "companies", "acme");
4190
3495
  fs.mkdirSync(companyRoot, { recursive: true });
4191
3496
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4192
- seedJournalAt(journalPath, ({
3497
+ fs.writeFileSync(journalPath, JSON.stringify({
4193
3498
  version: "1",
4194
3499
  lastSync: new Date().toISOString(),
4195
3500
  files: {
@@ -4223,7 +3528,7 @@ describe("share", () => {
4223
3528
  const companyRoot = path.join(tmpDir, "companies", "acme");
4224
3529
  fs.mkdirSync(companyRoot, { recursive: true });
4225
3530
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4226
- seedJournalAt(journalPath, ({
3531
+ fs.writeFileSync(journalPath, JSON.stringify({
4227
3532
  version: "1",
4228
3533
  lastSync: new Date().toISOString(),
4229
3534
  files: {
@@ -4273,7 +3578,7 @@ describe("share", () => {
4273
3578
  fs.mkdirSync(companyRoot, { recursive: true });
4274
3579
  const recreatedPath = path.join(companyRoot, "notes", "gone.md");
4275
3580
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4276
- seedJournalAt(journalPath, ({
3581
+ fs.writeFileSync(journalPath, JSON.stringify({
4277
3582
  version: "1",
4278
3583
  lastSync: new Date().toISOString(),
4279
3584
  files: {
@@ -4321,7 +3626,7 @@ describe("share", () => {
4321
3626
  const companyRoot = path.join(tmpDir, "companies", "acme");
4322
3627
  fs.mkdirSync(companyRoot, { recursive: true });
4323
3628
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4324
- seedJournalAt(journalPath, ({
3629
+ fs.writeFileSync(journalPath, JSON.stringify({
4325
3630
  version: "1",
4326
3631
  lastSync: new Date().toISOString(),
4327
3632
  files: {
@@ -4351,14 +3656,14 @@ describe("share", () => {
4351
3656
  type: "delete-refused-stale-etag",
4352
3657
  reason: "legacy-no-etag",
4353
3658
  }));
4354
- const journal = journalAt(journalPath);
3659
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4355
3660
  expect(journal.files["notes/legacy.md"]).toBeDefined();
4356
3661
  });
4357
3662
  it("owned-only still requires an intent — it is the documented rollback knob", async () => {
4358
3663
  const companyRoot = path.join(tmpDir, "companies", "acme");
4359
3664
  fs.mkdirSync(companyRoot, { recursive: true });
4360
3665
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4361
- seedJournalAt(journalPath, ({
3666
+ fs.writeFileSync(journalPath, JSON.stringify({
4362
3667
  version: "1",
4363
3668
  lastSync: new Date().toISOString(),
4364
3669
  files: {
@@ -4545,7 +3850,7 @@ describe("share", () => {
4545
3850
  const companyRoot = path.join(tmpDir, "companies", "acme");
4546
3851
  fs.mkdirSync(companyRoot, { recursive: true });
4547
3852
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
4548
- seedJournalAt(journalPath, ({
3853
+ fs.writeFileSync(journalPath, JSON.stringify({
4549
3854
  version: "1",
4550
3855
  lastSync: new Date().toISOString(),
4551
3856
  files: {
@@ -4586,7 +3891,7 @@ describe("share", () => {
4586
3891
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "regular.md");
4587
3892
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md");
4588
3893
  // Both journal entries are removed by the delete loop's removeEntry call.
4589
- const journal = journalAt(journalPath);
3894
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4590
3895
  expect(journal.files["CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md"]).toBeUndefined();
4591
3896
  expect(journal.files["regular.md"]).toBeUndefined();
4592
3897
  });
@@ -4601,7 +3906,7 @@ describe("share", () => {
4601
3906
  process.env.HQ_STATE_DIR = stateDirPersonal;
4602
3907
  try {
4603
3908
  const journalPath = path.join(stateDirPersonal, "sync-journal.__hq_personal_vault__.json");
4604
- seedJournalAt(journalPath, ({
3909
+ fs.writeFileSync(journalPath, JSON.stringify({
4605
3910
  version: "1",
4606
3911
  lastSync: new Date().toISOString(),
4607
3912
  files: {
@@ -4627,7 +3932,7 @@ describe("share", () => {
4627
3932
  });
4628
3933
  expect(result.filesDeleted).toBe(1);
4629
3934
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), "personal/.obsidian/graph.json.drift-1779863862-42519");
4630
- const journal = journalAt(journalPath);
3935
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4631
3936
  expect(journal.files["personal/.obsidian/graph.json.drift-1779863862-42519"]).toBeUndefined();
4632
3937
  }
4633
3938
  finally {
@@ -4650,7 +3955,7 @@ describe("share", () => {
4650
3955
  hash: "h", size: 1, syncedAt: new Date().toISOString(),
4651
3956
  direction: "up",
4652
3957
  });
4653
- seedJournalAt(journalPath, ({
3958
+ fs.writeFileSync(journalPath, JSON.stringify({
4654
3959
  version: "1",
4655
3960
  lastSync: new Date().toISOString(),
4656
3961
  files: {
@@ -4698,7 +4003,7 @@ describe("share", () => {
4698
4003
  expect(pruned[0].byReason["ignore-filtered"]).toBe(3);
4699
4004
  expect(pruned[0].byReason["excluded-top-level"]).toBe(1);
4700
4005
  // The persisted journal converged: excluded rows gone, legit row kept.
4701
- const journal = journalAt(journalPath);
4006
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
4702
4007
  expect(Object.keys(journal.files)).toContain("personal/keep.md");
4703
4008
  expect(journal.files[".claude/worktrees/wt/a.ts"]).toBeUndefined();
4704
4009
  expect(journal.files["personal/node_modules/pkg/index.js"]).toBeUndefined();
@@ -4730,7 +4035,7 @@ describe("share", () => {
4730
4035
  const key = "deleted-top-level.md";
4731
4036
  const journalSlug = "personal-scoped-delete";
4732
4037
  const journalPath = path.join(stateDir, `sync-journal.${journalSlug}.json`);
4733
- seedJournalAt(journalPath, ({
4038
+ fs.writeFileSync(journalPath, JSON.stringify({
4734
4039
  version: "2",
4735
4040
  lastSync: new Date().toISOString(),
4736
4041
  pulls: [],
@@ -4762,7 +4067,7 @@ describe("share", () => {
4762
4067
  });
4763
4068
  expect(result.filesDeleted).toBe(1);
4764
4069
  expect(deleteRemoteFile).toHaveBeenCalledWith(expect.anything(), key);
4765
- expect(journalAt(journalPath).files[key]).toBeUndefined();
4070
+ expect(JSON.parse(fs.readFileSync(journalPath, "utf8")).files[key]).toBeUndefined();
4766
4071
  });
4767
4072
  // ── Decommission prefixes ──────────────────────────────────────────────────
4768
4073
  //
@@ -4817,7 +4122,7 @@ describe("share", () => {
4817
4122
  // Local file remains on disk — decommission is a remote-only sweep.
4818
4123
  expect(fs.existsSync(path.join(tmpDir, "companies", "foo", "notes.md"))).toBe(true);
4819
4124
  // Journal entry pruned in lockstep with the remote delete.
4820
- const journalAfter = journalAt(personalJournalPath);
4125
+ const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4821
4126
  expect(journalAfter.files["companies/foo/notes.md"]).toBeUndefined();
4822
4127
  expect(journalAfter.files["knowledge/x.md"]).toBeDefined();
4823
4128
  });
@@ -4853,7 +4158,7 @@ describe("share", () => {
4853
4158
  expect(deleteRemoteFile).not.toHaveBeenCalled();
4854
4159
  // Journal entry preserved — the pulled-from-peer record stays so a
4855
4160
  // future pull can still reconcile it correctly.
4856
- const journalAfter = journalAt(personalJournalPath);
4161
+ const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4857
4162
  expect(journalAfter.files["companies/foo/from-peer.md"]).toBeDefined();
4858
4163
  });
4859
4164
  it("decommissionPrefixes: dedupes with propagateDeletes plan — a key in both still emits one DeleteObject", async () => {
@@ -4973,7 +4278,7 @@ describe("share", () => {
4973
4278
  expect(result.filesTombstoned).toBe(1);
4974
4279
  expect(result.filesDeleted).toBe(0);
4975
4280
  // Journal entry is gone.
4976
- const journalAfter = journalAt(personalJournalPath);
4281
+ const journalAfter = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
4977
4282
  expect(journalAfter.files["companies/foo/gone.md"]).toBeUndefined();
4978
4283
  });
4979
4284
  describe("personalMode", () => {
@@ -5014,7 +4319,7 @@ describe("share", () => {
5014
4319
  const acmeJournalPath = path.join(stateDir, "sync-journal.acme.json");
5015
4320
  expect(fs.existsSync(personalJournalPath)).toBe(true);
5016
4321
  expect(fs.existsSync(acmeJournalPath)).toBe(false);
5017
- const journal = journalAt(personalJournalPath);
4322
+ const journal = JSON.parse(fs.readFileSync(personalJournalPath, "utf-8"));
5018
4323
  expect(journal.files["knowledge/notes.md"]).toBeDefined();
5019
4324
  });
5020
4325
  it("personalMode=true accepts files outside companies/<slug>/ (companion to the company-folder rejection)", async () => {
@@ -5302,7 +4607,7 @@ describe("share", () => {
5302
4607
  .createHash("sha256")
5303
4608
  .update(target)
5304
4609
  .digest("hex");
5305
- seedJournalAt(journalPath, ({
4610
+ fs.writeFileSync(journalPath, JSON.stringify({
5306
4611
  version: "1",
5307
4612
  lastSync: new Date().toISOString(),
5308
4613
  files: {
@@ -5705,7 +5010,7 @@ describe("share", () => {
5705
5010
  });
5706
5011
  expect(pushed.filesUploaded).toBe(1);
5707
5012
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5708
- const afterPush = journalAt(journalPath);
5013
+ const afterPush = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5709
5014
  expect(Object.keys(afterPush.files)).toContain("knowledge/agents/brief.md");
5710
5015
  vi.mocked(deleteRemoteFile).mockClear();
5711
5016
  const walked = await share({
@@ -5719,7 +5024,7 @@ describe("share", () => {
5719
5024
  expect(walked.filesDeleted).toBe(0);
5720
5025
  const deletedKeys = vi.mocked(deleteRemoteFile).mock.calls.map((c) => c[1]);
5721
5026
  expect(deletedKeys).not.toContain("knowledge/agents/brief.md");
5722
- const afterWalk = journalAt(journalPath);
5027
+ const afterWalk = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5723
5028
  expect(Object.keys(afterWalk.files)).toContain("knowledge/agents/brief.md");
5724
5029
  });
5725
5030
  });
@@ -5763,7 +5068,7 @@ describe("share", () => {
5763
5068
  }
5764
5069
  }
5765
5070
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
5766
- seedJournalAt(journalPath, ({
5071
+ fs.writeFileSync(journalPath, JSON.stringify({
5767
5072
  version: "1",
5768
5073
  lastSync: new Date().toISOString(),
5769
5074
  files,
@@ -5821,7 +5126,7 @@ describe("share", () => {
5821
5126
  expect(summary.samplePaths.length).toBeGreaterThan(0);
5822
5127
  expect(summary.samplePaths.length).toBeLessThanOrEqual(10);
5823
5128
  // Journal entries for the missing files SURVIVE — guard never mutates.
5824
- const journal = journalAt(journalPath);
5129
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5825
5130
  expect(Object.keys(journal.files).length).toBe(100);
5826
5131
  // Per-key delete-refused-stale-etag events for each candidate.
5827
5132
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" && e.reason === "bulk-asymmetry");
@@ -5871,7 +5176,7 @@ describe("share", () => {
5871
5176
  expect(headedKeys).not.toContain(`f-${i.toString().padStart(4, "0")}.md`);
5872
5177
  }
5873
5178
  // Journal untouched — the entries stay recoverable.
5874
- const journal = journalAt(journalPath);
5179
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5875
5180
  expect(Object.keys(journal.files).length).toBe(100);
5876
5181
  });
5877
5182
  // The core of the #227 regression, isolated: pre-#227 a trip emptied
@@ -5902,7 +5207,7 @@ describe("share", () => {
5902
5207
  const perKeyRefusals = events.filter((e) => e.type === "delete-refused-stale-etag" &&
5903
5208
  e.reason === "bulk-asymmetry");
5904
5209
  expect(perKeyRefusals.length).toBe(11);
5905
- const journal = journalAt(journalPath);
5210
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5906
5211
  expect(Object.keys(journal.files).length).toBe(100);
5907
5212
  });
5908
5213
  it("vault litter still drains when the breaker trips", async () => {
@@ -5916,7 +5221,7 @@ describe("share", () => {
5916
5221
  withIntents: false,
5917
5222
  });
5918
5223
  const litterKey = "CLAUDE.md.conflict-2026-05-13T19-40-40Z-e5797a.md";
5919
- const seeded = journalAt(journalPath);
5224
+ const seeded = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
5920
5225
  seeded.files[litterKey] = {
5921
5226
  hash: "h",
5922
5227
  size: 5,
@@ -5925,7 +5230,7 @@ describe("share", () => {
5925
5230
  remoteEtag: "litter-etag",
5926
5231
  kind: "file",
5927
5232
  };
5928
- seedJournalAt(journalPath, seeded);
5233
+ fs.writeFileSync(journalPath, JSON.stringify(seeded));
5929
5234
  vi.mocked(headRemoteFile).mockImplementation(async (_ctx, key) => ({
5930
5235
  etag: `etag-${parseInt(key.replace(/[^0-9]/g, ""), 10)}`,
5931
5236
  lastModified: new Date(),
@@ -6065,7 +5370,7 @@ describe("share", () => {
6065
5370
  expect(summary).toBeDefined();
6066
5371
  expect(summary.ratio).toBeCloseTo(1.0, 5);
6067
5372
  // Journal untouched.
6068
- const journal = journalAt(journalPath);
5373
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6069
5374
  expect(Object.keys(journal.files).length).toBe(50);
6070
5375
  });
6071
5376
  it("small absolute count (9/9 = 100% ratio but < MIN_ABS=10) does NOT trip: all 9 delete normally", async () => {
@@ -6109,7 +5414,7 @@ describe("share", () => {
6109
5414
  expect(deleteRemoteFile).not.toHaveBeenCalled();
6110
5415
  expect(events.find((e) => e.type === "delete-refused-bulk-asymmetry")).toBeDefined();
6111
5416
  // Journal entries survive.
6112
- const journal = journalAt(journalPath);
5417
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6113
5418
  expect(Object.keys(journal.files).length).toBe(100);
6114
5419
  });
6115
5420
  it("direction:'down' entries don't count toward the bulk numerator (owned-only silently skips them)", async () => {
@@ -6137,7 +5442,7 @@ describe("share", () => {
6137
5442
  localDeleteIntent: deleteIntent("up-etag", "h"),
6138
5443
  };
6139
5444
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6140
- seedJournalAt(journalPath, { version: "1", lastSync: now, files });
5445
+ fs.writeFileSync(journalPath, JSON.stringify({ version: "1", lastSync: now, files }));
6141
5446
  const result = await share({
6142
5447
  paths: [companyRoot],
6143
5448
  company: "acme",
@@ -6260,7 +5565,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6260
5565
  fs.mkdirSync(companyRoot, { recursive: true });
6261
5566
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6262
5567
  // v2-shaped fixture — same field set the production journal uses.
6263
- seedJournalAt(journalPath, ({
5568
+ fs.writeFileSync(journalPath, JSON.stringify({
6264
5569
  version: "2",
6265
5570
  lastSync: new Date().toISOString(),
6266
5571
  files: {
@@ -6306,7 +5611,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6306
5611
  const { hashFile } = await import("../journal.js");
6307
5612
  const baselineHash = hashFile(testFile);
6308
5613
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6309
- seedJournalAt(journalPath, ({
5614
+ fs.writeFileSync(journalPath, JSON.stringify({
6310
5615
  version: "2",
6311
5616
  lastSync: new Date().toISOString(),
6312
5617
  files: {
@@ -6347,7 +5652,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6347
5652
  const originalLstat = fs.lstatSync(testFile);
6348
5653
  // The recorded hash intentionally differs from the current bytes.
6349
5654
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6350
- seedJournalAt(journalPath, ({
5655
+ fs.writeFileSync(journalPath, JSON.stringify({
6351
5656
  version: "2",
6352
5657
  lastSync: new Date().toISOString(),
6353
5658
  files: {
@@ -6394,7 +5699,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6394
5699
  const { hashFile } = await import("../journal.js");
6395
5700
  const realHash = hashFile(testFile);
6396
5701
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6397
- seedJournalAt(journalPath, ({
5702
+ fs.writeFileSync(journalPath, JSON.stringify({
6398
5703
  version: "2",
6399
5704
  lastSync: new Date().toISOString(),
6400
5705
  files: {
@@ -6444,7 +5749,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6444
5749
  const { hashFile } = await import("../journal.js");
6445
5750
  const realHash = hashFile(testFile);
6446
5751
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6447
- seedJournalAt(journalPath, ({
5752
+ fs.writeFileSync(journalPath, JSON.stringify({
6448
5753
  version: "2",
6449
5754
  lastSync: new Date().toISOString(),
6450
5755
  files: {
@@ -6476,7 +5781,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6476
5781
  expect(uploadFile).not.toHaveBeenCalled();
6477
5782
  // Journal mtimeMs is now the bumped value, while content hashing remains
6478
5783
  // authoritative on the next sync.
6479
- const journalAfter = journalAt(journalPath);
5784
+ const journalAfter = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6480
5785
  expect(journalAfter.files["touched-then-fast.md"].mtimeMs).toBe(bumpedLstat.mtimeMs);
6481
5786
  expect(journalAfter.files["touched-then-fast.md"].size).toBe(bumpedLstat.size);
6482
5787
  // Content hash untouched — the file genuinely didn't change.
@@ -6496,7 +5801,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6496
5801
  });
6497
5802
  expect(result.filesUploaded).toBe(1);
6498
5803
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6499
- const journal = journalAt(journalPath);
5804
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
6500
5805
  expect(journal.files["fresh.md"]).toBeDefined();
6501
5806
  expect(journal.files["fresh.md"].mtimeMs).toBe(lstat.mtimeMs);
6502
5807
  expect(journal.files["fresh.md"].size).toBe(lstat.size);
@@ -6513,7 +5818,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
6513
5818
  const realSize = fs.statSync(testFile).size;
6514
5819
  // Real hash but NO mtimeMs. Hash matches → skip upload.
6515
5820
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
6516
- seedJournalAt(journalPath, ({
5821
+ fs.writeFileSync(journalPath, JSON.stringify({
6517
5822
  version: "1",
6518
5823
  lastSync: new Date().toISOString(),
6519
5824
  files: {
@@ -7072,7 +6377,7 @@ describe("currency-gated: journal version 2 fixtures", () => {
7072
6377
  // AND lost their etag/content-hash/stat journal metadata.
7073
6378
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
7074
6379
  expect(fs.existsSync(journalPath)).toBe(true);
7075
- const journal = journalAt(journalPath);
6380
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7076
6381
  const stampedKeys = Object.keys(journal.files);
7077
6382
  expect(stampedKeys.length).toBe(FILE_COUNT - 1);
7078
6383
  // The failed file is NOT in the journal (no successful upload).
@@ -7220,7 +6525,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7220
6525
  const companyRoot = path.join(tmpDir, "companies", "acme");
7221
6526
  fs.mkdirSync(companyRoot, { recursive: true });
7222
6527
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
7223
- seedJournalAt(journalPath, ({
6528
+ fs.writeFileSync(journalPath, JSON.stringify({
7224
6529
  version: "1",
7225
6530
  lastSync: new Date().toISOString(),
7226
6531
  files: {
@@ -7249,7 +6554,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7249
6554
  expect(headRemoteFile).not.toHaveBeenCalled();
7250
6555
  expect(deleteRemoteFile).not.toHaveBeenCalled();
7251
6556
  expect(uploadFile).not.toHaveBeenCalled();
7252
- const journal = journalAt(journalPath);
6557
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7253
6558
  expect(journal.files["companies/acme/knowledge/poison.md"]).toBeUndefined();
7254
6559
  });
7255
6560
  it("defers an unreadable scope-invalid symlink tombstone without a fatal error", async () => {
@@ -7261,7 +6566,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7261
6566
  fs.mkdirSync(targetDir, { recursive: true });
7262
6567
  fs.symlinkSync(targetDir, linkPath, "dir");
7263
6568
  const journalPath = path.join(stateDir, "sync-journal.acme.json");
7264
- seedJournalAt(journalPath, ({
6569
+ fs.writeFileSync(journalPath, JSON.stringify({
7265
6570
  version: "1",
7266
6571
  lastSync: new Date().toISOString(),
7267
6572
  files: {
@@ -7306,7 +6611,7 @@ describe("scope-invalid key hardening on push (doubled companies/ tree — incid
7306
6611
  samplePaths: [linkKey],
7307
6612
  });
7308
6613
  expect(events.some((event) => event.type === "error")).toBe(false);
7309
- const journal = journalAt(journalPath);
6614
+ const journal = JSON.parse(fs.readFileSync(journalPath, "utf-8"));
7310
6615
  expect(journal.files[linkKey]).toBeDefined();
7311
6616
  }
7312
6617
  finally {