@indigoai-us/hq-cloud 6.16.58-alpha.3 → 6.16.59

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 (154) hide show
  1. package/dist/bin/sync-runner-company.d.ts +0 -14
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +89 -57
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner-company.test.js +313 -56
  6. package/dist/bin/sync-runner-company.test.js.map +1 -1
  7. package/dist/bin/sync-runner-planning.d.ts +3 -12
  8. package/dist/bin/sync-runner-planning.d.ts.map +1 -1
  9. package/dist/bin/sync-runner-planning.js +3 -65
  10. package/dist/bin/sync-runner-planning.js.map +1 -1
  11. package/dist/bin/sync-runner-planning.test.js +1 -172
  12. package/dist/bin/sync-runner-planning.test.js.map +1 -1
  13. package/dist/bin/sync-runner-watch-loop.d.ts +2 -51
  14. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  15. package/dist/bin/sync-runner-watch-loop.js +165 -409
  16. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  17. package/dist/bin/sync-runner-watch-routes.d.ts +2 -19
  18. package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
  19. package/dist/bin/sync-runner-watch-routes.js +5 -40
  20. package/dist/bin/sync-runner-watch-routes.js.map +1 -1
  21. package/dist/bin/sync-runner-watch-routes.test.js +0 -47
  22. package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
  23. package/dist/bin/sync-runner.d.ts +13 -59
  24. package/dist/bin/sync-runner.d.ts.map +1 -1
  25. package/dist/bin/sync-runner.js +4 -48
  26. package/dist/bin/sync-runner.js.map +1 -1
  27. package/dist/bin/sync-runner.test.js +229 -1499
  28. package/dist/bin/sync-runner.test.js.map +1 -1
  29. package/dist/cli/share.d.ts +0 -19
  30. package/dist/cli/share.d.ts.map +1 -1
  31. package/dist/cli/share.js +0 -55
  32. package/dist/cli/share.js.map +1 -1
  33. package/dist/cli/share.test.js +1 -87
  34. package/dist/cli/share.test.js.map +1 -1
  35. package/dist/cli/sync-scope.test.js +0 -8
  36. package/dist/cli/sync-scope.test.js.map +1 -1
  37. package/dist/cli/sync.d.ts +8 -12
  38. package/dist/cli/sync.d.ts.map +1 -1
  39. package/dist/cli/sync.js +152 -302
  40. package/dist/cli/sync.js.map +1 -1
  41. package/dist/cli/sync.test.js +126 -256
  42. package/dist/cli/sync.test.js.map +1 -1
  43. package/dist/cognito-auth.d.ts +34 -1
  44. package/dist/cognito-auth.d.ts.map +1 -1
  45. package/dist/cognito-auth.js +92 -21
  46. package/dist/cognito-auth.js.map +1 -1
  47. package/dist/context.d.ts +5 -0
  48. package/dist/context.d.ts.map +1 -1
  49. package/dist/context.js +92 -12
  50. package/dist/context.js.map +1 -1
  51. package/dist/context.test.js +77 -2
  52. package/dist/context.test.js.map +1 -1
  53. package/dist/ignore.d.ts +10 -0
  54. package/dist/ignore.d.ts.map +1 -1
  55. package/dist/ignore.js +47 -5
  56. package/dist/ignore.js.map +1 -1
  57. package/dist/ignore.test.js +30 -3
  58. package/dist/ignore.test.js.map +1 -1
  59. package/dist/journal-row-store.d.ts +5 -40
  60. package/dist/journal-row-store.d.ts.map +1 -1
  61. package/dist/journal-row-store.js +21 -240
  62. package/dist/journal-row-store.js.map +1 -1
  63. package/dist/journal-row-store.test.js +0 -54
  64. package/dist/journal-row-store.test.js.map +1 -1
  65. package/dist/journal-snapshot-v4.d.ts.map +1 -1
  66. package/dist/journal-snapshot-v4.js +6 -11
  67. package/dist/journal-snapshot-v4.js.map +1 -1
  68. package/dist/journal-store.test.js +7 -324
  69. package/dist/journal-store.test.js.map +1 -1
  70. package/dist/journal.d.ts +16 -51
  71. package/dist/journal.d.ts.map +1 -1
  72. package/dist/journal.js +79 -303
  73. package/dist/journal.js.map +1 -1
  74. package/dist/lib/exit-codes.d.ts +0 -15
  75. package/dist/lib/exit-codes.d.ts.map +1 -1
  76. package/dist/lib/exit-codes.js +0 -15
  77. package/dist/lib/exit-codes.js.map +1 -1
  78. package/dist/lib/net-errors.d.ts +12 -0
  79. package/dist/lib/net-errors.d.ts.map +1 -1
  80. package/dist/lib/net-errors.js +35 -0
  81. package/dist/lib/net-errors.js.map +1 -1
  82. package/dist/lib/net-errors.test.js +19 -1
  83. package/dist/lib/net-errors.test.js.map +1 -1
  84. package/dist/machine-auth.test.js +6 -4
  85. package/dist/machine-auth.test.js.map +1 -1
  86. package/dist/machine-token-cache-isolation.test.d.ts +24 -0
  87. package/dist/machine-token-cache-isolation.test.d.ts.map +1 -0
  88. package/dist/machine-token-cache-isolation.test.js +241 -0
  89. package/dist/machine-token-cache-isolation.test.js.map +1 -0
  90. package/dist/manifest/build-manifest-resident-parity.test.js +3 -3
  91. package/dist/manifest/build-manifest-resident-parity.test.js.map +1 -1
  92. package/dist/personal-vault.d.ts.map +1 -1
  93. package/dist/personal-vault.js +3 -10
  94. package/dist/personal-vault.js.map +1 -1
  95. package/dist/personal-vault.test.js +0 -35
  96. package/dist/personal-vault.test.js.map +1 -1
  97. package/dist/remote-pull.d.ts.map +1 -1
  98. package/dist/remote-pull.js +2 -9
  99. package/dist/remote-pull.js.map +1 -1
  100. package/dist/remote-pull.test.js +2 -59
  101. package/dist/remote-pull.test.js.map +1 -1
  102. package/dist/s3.d.ts +0 -1
  103. package/dist/s3.d.ts.map +1 -1
  104. package/dist/s3.js +2 -20
  105. package/dist/s3.js.map +1 -1
  106. package/dist/sync/area-ledger.d.ts +3 -27
  107. package/dist/sync/area-ledger.d.ts.map +1 -1
  108. package/dist/sync/area-ledger.js +39 -149
  109. package/dist/sync/area-ledger.js.map +1 -1
  110. package/dist/sync/area-ledger.test.js +5 -192
  111. package/dist/sync/area-ledger.test.js.map +1 -1
  112. package/dist/sync/parallel-sync-coordinator.test.js +0 -29
  113. package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
  114. package/dist/sync/realtime-telemetry.d.ts +1 -1
  115. package/dist/sync/realtime-telemetry.d.ts.map +1 -1
  116. package/dist/sync/realtime-telemetry.js +6 -14
  117. package/dist/sync/realtime-telemetry.js.map +1 -1
  118. package/dist/sync/realtime-telemetry.test.js +16 -136
  119. package/dist/sync/realtime-telemetry.test.js.map +1 -1
  120. package/dist/sync/state-store.d.ts +0 -6
  121. package/dist/sync/state-store.d.ts.map +1 -1
  122. package/dist/sync/state-store.js +2 -14
  123. package/dist/sync/state-store.js.map +1 -1
  124. package/dist/watcher.d.ts +0 -12
  125. package/dist/watcher.d.ts.map +1 -1
  126. package/dist/watcher.js +0 -131
  127. package/dist/watcher.js.map +1 -1
  128. package/dist/watcher.test.js +0 -63
  129. package/dist/watcher.test.js.map +1 -1
  130. package/package.json +1 -2
  131. package/dist/cli/onepass-pull-planner-regressions.test.d.ts +0 -2
  132. package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +0 -1
  133. package/dist/cli/onepass-pull-planner-regressions.test.js +0 -300
  134. package/dist/cli/onepass-pull-planner-regressions.test.js.map +0 -1
  135. package/dist/cli/onepass-pull-planner.test.d.ts +0 -2
  136. package/dist/cli/onepass-pull-planner.test.d.ts.map +0 -1
  137. package/dist/cli/onepass-pull-planner.test.js +0 -266
  138. package/dist/cli/onepass-pull-planner.test.js.map +0 -1
  139. package/dist/cli/streaming-pull-planner.bench.test.d.ts +0 -15
  140. package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +0 -1
  141. package/dist/cli/streaming-pull-planner.bench.test.js +0 -273
  142. package/dist/cli/streaming-pull-planner.bench.test.js.map +0 -1
  143. package/dist/s3.list-cost.test.d.ts +0 -2
  144. package/dist/s3.list-cost.test.d.ts.map +0 -1
  145. package/dist/s3.list-cost.test.js +0 -51
  146. package/dist/s3.list-cost.test.js.map +0 -1
  147. package/dist/sync/remote-file-spool.d.ts +0 -16
  148. package/dist/sync/remote-file-spool.d.ts.map +0 -1
  149. package/dist/sync/remote-file-spool.js +0 -91
  150. package/dist/sync/remote-file-spool.js.map +0 -1
  151. package/dist/sync/remote-file-spool.test.d.ts +0 -2
  152. package/dist/sync/remote-file-spool.test.d.ts.map +0 -1
  153. package/dist/sync/remote-file-spool.test.js +0 -77
  154. package/dist/sync/remote-file-spool.test.js.map +0 -1
@@ -95,15 +95,6 @@ vi.mock("../s3.js", async (importOriginal) => {
95
95
  { key: "docs/handoff.md", size: 42, lastModified: new Date(), etag: '"abc123"' },
96
96
  { key: "knowledge/readme.md", size: 100, lastModified: new Date(), etag: '"def456"' },
97
97
  ];
98
- const listRemoteFiles = innerVi.fn().mockResolvedValue(remoteFiles);
99
- // Each pull consumes one independent LIST snapshot; never reuse a prior
100
- // pull's rows, so watcher/list freshness tests observe the actual call.
101
- async function* streamRemoteFiles(...args) {
102
- const files = await listRemoteFiles(...args);
103
- for (const remoteFile of files) {
104
- yield remoteFile;
105
- }
106
- }
107
98
  return {
108
99
  ...actual,
109
100
  toPosixKey: (key) => key.split("\\").join("/"),
@@ -117,8 +108,7 @@ vi.mock("../s3.js", async (importOriginal) => {
117
108
  innerFs.writeFileSync(localPath, "mock file content");
118
109
  return { metadata: {} };
119
110
  }),
120
- listRemoteFiles,
121
- streamRemoteFiles,
111
+ listRemoteFiles: innerVi.fn().mockResolvedValue(remoteFiles),
122
112
  deleteRemoteFile: innerVi.fn().mockResolvedValue(undefined),
123
113
  headRemoteFile: innerVi.fn().mockResolvedValue(null),
124
114
  primeObjectTransport: innerVi.fn().mockResolvedValue(undefined),
@@ -135,12 +125,11 @@ vi.mock("../remote-pull.js", async (importOriginal) => {
135
125
  const { vi: innerVi } = await import("vitest");
136
126
  return { ...actual, listRemoteForScope: innerVi.fn(actual.listRemoteForScope) };
137
127
  });
138
- import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, FULL_PULL_COOPERATIVE_CHECKPOINT_BATCH_SIZE, targetedJournalSessionCensusSizes, win32SymlinkFlavorBroken, } from "./sync.js";
128
+ import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, targetedJournalSessionCensusSizes, win32SymlinkFlavorBroken, } from "./sync.js";
139
129
  import { share } from "./share.js";
140
130
  import * as s3Module from "../s3.js";
141
131
  import * as conflictModule from "./conflict.js";
142
132
  import * as remotePullModule from "../remote-pull.js";
143
- import { CooperativePassScheduler } from "../bin/sync-runner-watch-loop.js";
144
133
  import { reindex } from "./reindex.js";
145
134
  import { readSyncProgress } from "../sync-progress.js";
146
135
  import { VaultAuthError } from "../vault-client.js";
@@ -149,7 +138,7 @@ import { readJournal, writeJournal, getEntry, listJournals, } from "../journal.j
149
138
  import { SqsPushReceiver } from "../sync/push-receiver.js";
150
139
  import { buildTargetedPullArgv } from "../bin/sync-runner-watch-routes.js";
151
140
  import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
152
- import { setObjectIOFactory } from "../object-io.js";
141
+ import { RateLimitedError, setObjectIOFactory } from "../object-io.js";
153
142
  /**
154
143
  * Read a journal the way production does.
155
144
  *
@@ -631,248 +620,6 @@ describe("sync", () => {
631
620
  },
632
621
  });
633
622
  }, 60_000);
634
- it("hands queued realtime work the durable boundary inside a large full pull", async () => {
635
- const rowCount = 59_020;
636
- // Keep the transfer count fixed while tuning checkpoint cadence below; the
637
- // fixture remains a 59,020-row streamed journal at every candidate size.
638
- const changedRowCount = 2_000;
639
- const fixedNow = new Date("2026-09-17T12:00:00.000Z");
640
- const materializedHash = "5633d479dfae75ba7a78914ee380fa202bd6126e7c6b7c22e3ebc9e1a6ddc871";
641
- const remoteFiles = Array.from({ length: rowCount }, (_value, index) => ({
642
- key: `docs/full-pass/${index}.md`,
643
- size: 17,
644
- lastModified: fixedNow,
645
- etag: `"${index < changedRowCount ? "remote" : "stable"}-${index}"`,
646
- }));
647
- writeJournal("acme", {
648
- version: "2",
649
- lastSync: "",
650
- pulls: [],
651
- files: Object.fromEntries(remoteFiles.map((remoteFile, index) => [
652
- remoteFile.key,
653
- {
654
- hash: materializedHash,
655
- size: 17,
656
- syncedAt: "2026-09-16T00:00:00.000Z",
657
- direction: "down",
658
- remoteEtag: `${index < changedRowCount ? "stale" : "stable"}-${index}`,
659
- mtimeMs: fixedNow.getTime(),
660
- ctimeMs: fixedNow.getTime(),
661
- },
662
- ])),
663
- });
664
- const listRemoteFiles = vi.mocked(s3Module.listRemoteFiles);
665
- const originalListRemoteFiles = listRemoteFiles.getMockImplementation();
666
- listRemoteFiles.mockResolvedValue(remoteFiles);
667
- const materializedPaths = new Set(remoteFiles.map((remoteFile) => path.join(tmpDir, "companies", "acme", remoteFile.key)));
668
- const originalLstatSync = fs.lstatSync;
669
- const lstatSync = vi.spyOn(fs, "lstatSync").mockImplementation((filePath) => {
670
- if (typeof filePath === "string" && materializedPaths.has(filePath)) {
671
- return {
672
- isSymbolicLink: () => false,
673
- isFile: () => true,
674
- isDirectory: () => false,
675
- size: 17,
676
- mtimeMs: fixedNow.getTime(),
677
- ctimeMs: fixedNow.getTime(),
678
- };
679
- }
680
- return originalLstatSync(filePath);
681
- });
682
- const originalReadFileSync = fs.readFileSync;
683
- const readFileSync = vi.spyOn(fs, "readFileSync").mockImplementation((filePath, ...args) => {
684
- if (typeof filePath === "string" && materializedPaths.has(filePath)) {
685
- return Buffer.from("mock file content");
686
- }
687
- return Reflect.apply(originalReadFileSync, fs, [filePath, ...args]);
688
- });
689
- const downloadFile = vi.mocked(s3Module.downloadFile);
690
- const originalDownload = downloadFile.getMockImplementation();
691
- let downloadsCompleted = 0;
692
- let inFlightDownloads = 0;
693
- let maxInFlightDownloads = 0;
694
- let checkpointCalls = 0;
695
- let realtimeQueued = false;
696
- let realtimeDispatchedAt = -1;
697
- const scheduler = new CooperativePassScheduler(() => 0);
698
- let realtimePass;
699
- downloadFile.mockImplementation(async (_ctx, _key, localPath, options) => {
700
- inFlightDownloads += 1;
701
- maxInFlightDownloads = Math.max(maxInFlightDownloads, inFlightDownloads);
702
- await Promise.resolve();
703
- options?.beforeReplace?.();
704
- materializedPaths.add(localPath);
705
- downloadsCompleted += 1;
706
- if (downloadsCompleted === 1) {
707
- realtimeQueued = true;
708
- realtimePass = scheduler.submit(async () => {
709
- // The chore drained its transfer unit and committed it before the
710
- // scheduler admitted this realtime writer.
711
- expect(inFlightDownloads).toBe(0);
712
- realtimeDispatchedAt = downloadsCompleted;
713
- remoteFiles.push({
714
- key: "docs/realtime/during-full-pull.md",
715
- size: 17,
716
- lastModified: fixedNow,
717
- etag: '"realtime"',
718
- });
719
- materializedPaths.add(path.join(tmpDir, "companies", "acme", "docs", "realtime", "during-full-pull.md"));
720
- const realtimeJournal = readJournal("acme");
721
- realtimeJournal.files["docs/realtime/during-full-pull.md"] = {
722
- hash: materializedHash,
723
- size: 17,
724
- syncedAt: fixedNow.toISOString(),
725
- direction: "up",
726
- remoteEtag: "realtime",
727
- mtimeMs: fixedNow.getTime(),
728
- ctimeMs: fixedNow.getTime(),
729
- };
730
- writeJournal("acme", realtimeJournal);
731
- }, "realtime");
732
- }
733
- inFlightDownloads -= 1;
734
- return {
735
- metadata: {},
736
- contentHash: materializedHash,
737
- contentSize: 17,
738
- };
739
- });
740
- try {
741
- await scheduler.submit(async ({ checkpoint }) => {
742
- await sync({
743
- company: "acme",
744
- vaultConfig: mockConfig,
745
- hqRoot: tmpDir,
746
- skipReindex: true,
747
- onEvent: () => undefined,
748
- cooperativeCheckpoint: async () => {
749
- checkpointCalls += 1;
750
- return checkpoint();
751
- },
752
- });
753
- }, "chore");
754
- await realtimePass;
755
- }
756
- finally {
757
- readFileSync.mockRestore();
758
- lstatSync.mockRestore();
759
- if (originalDownload)
760
- downloadFile.mockImplementation(originalDownload);
761
- else
762
- downloadFile.mockReset();
763
- if (originalListRemoteFiles)
764
- listRemoteFiles.mockImplementation(originalListRemoteFiles);
765
- else
766
- listRemoteFiles.mockReset();
767
- }
768
- expect(maxInFlightDownloads).toBeGreaterThan(1);
769
- expect(realtimeQueued).toBe(true);
770
- expect(checkpointCalls).toBeGreaterThan(0);
771
- expect(realtimeDispatchedAt).toBeGreaterThan(0);
772
- expect(realtimeDispatchedAt).toBeLessThanOrEqual(FULL_PULL_COOPERATIVE_CHECKPOINT_BATCH_SIZE);
773
- const finalJournal = readJournal("acme");
774
- expect(finalJournal.files["docs/realtime/during-full-pull.md"]).toBeDefined();
775
- expect(Object.keys(finalJournal.files)).toHaveLength(rowCount + 1);
776
- expect(remoteFiles.every((remoteFile) => finalJournal.files[remoteFile.key] !== undefined)).toBe(true);
777
- }, 120_000);
778
- /**
779
- * A cooperative hand-off restarts the pull from a freshly built context. The
780
- * downloads that already completed are durable in the journal, so the
781
- * restarted plan correctly sees them as unchanged — which means the run's
782
- * own counters are the ONLY remaining record of them. Discarding those
783
- * counters loses the reindex paths, the new-file notifications, and the
784
- * published-content list the watch loop needs before it processes the
785
- * filesystem events those downloads generated.
786
- */
787
- it("keeps downloads completed before a cooperative hand-off in the run's result", async () => {
788
- let checkpointCalls = 0;
789
- const result = await sync({
790
- company: "acme",
791
- vaultConfig: mockConfig,
792
- hqRoot: tmpDir,
793
- skipReindex: true,
794
- onEvent: () => undefined,
795
- // Queued realtime work takes the writer exactly once.
796
- cooperativeCheckpoint: async () => {
797
- checkpointCalls += 1;
798
- return checkpointCalls === 1;
799
- },
800
- });
801
- expect(checkpointCalls).toBe(1);
802
- expect(s3Module.downloadFile).toHaveBeenCalledTimes(2);
803
- expect(result.filesDownloaded).toBe(2);
804
- expect([...(result.changedPaths ?? [])].sort()).toEqual([
805
- "docs/handoff.md",
806
- "knowledge/readme.md",
807
- ]);
808
- expect((result.appliedUpserts ?? []).map((upsert) => upsert.path).sort()).toEqual([
809
- "docs/handoff.md",
810
- "knowledge/readme.md",
811
- ]);
812
- expect(result.newFiles.map((newFile) => newFile.path).sort()).toEqual([
813
- "docs/handoff.md",
814
- "knowledge/readme.md",
815
- ]);
816
- expect(result.newFilesCount).toBe(2);
817
- });
818
- it("keeps every no-op full-pull journal row and emits no local deletes", async () => {
819
- const fixedNow = new Date("2026-09-17T12:00:00.000Z");
820
- const materialized = "mock file content";
821
- const materializedHash = "5633d479dfae75ba7a78914ee380fa202bd6126e7c6b7c22e3ebc9e1a6ddc871";
822
- const remoteFiles = Array.from({ length: 8 }, (_value, index) => ({
823
- key: `docs/noop-full-pull/${index}.md`,
824
- size: Buffer.byteLength(materialized),
825
- lastModified: fixedNow,
826
- etag: `"stable-${index}"`,
827
- }));
828
- for (const remoteFile of remoteFiles) {
829
- const localPath = path.join(tmpDir, "companies", "acme", remoteFile.key);
830
- fs.mkdirSync(path.dirname(localPath), { recursive: true });
831
- fs.writeFileSync(localPath, materialized);
832
- }
833
- writeJournal("acme", {
834
- version: "2",
835
- lastSync: "",
836
- pulls: [],
837
- files: Object.fromEntries(remoteFiles.map((remoteFile) => [
838
- remoteFile.key,
839
- {
840
- hash: materializedHash,
841
- size: remoteFile.size,
842
- syncedAt: "2026-09-16T00:00:00.000Z",
843
- direction: "down",
844
- remoteEtag: remoteFile.etag.slice(1, -1),
845
- },
846
- ])),
847
- });
848
- const listRemoteFiles = vi.mocked(s3Module.listRemoteFiles);
849
- const originalListRemoteFiles = listRemoteFiles.getMockImplementation();
850
- listRemoteFiles.mockResolvedValue(remoteFiles);
851
- const unlinkSync = vi.spyOn(fs, "unlinkSync");
852
- try {
853
- await sync({
854
- company: "acme",
855
- vaultConfig: mockConfig,
856
- hqRoot: tmpDir,
857
- skipReindex: true,
858
- onEvent: () => undefined,
859
- });
860
- const companyRoot = path.join(tmpDir, "companies", "acme") + path.sep;
861
- const localDeletes = unlinkSync.mock.calls.filter(([candidate]) => typeof candidate === "string" && candidate.startsWith(companyRoot));
862
- expect(localDeletes).toEqual([]);
863
- }
864
- finally {
865
- unlinkSync.mockRestore();
866
- if (originalListRemoteFiles)
867
- listRemoteFiles.mockImplementation(originalListRemoteFiles);
868
- else
869
- listRemoteFiles.mockReset();
870
- }
871
- // Re-open through the on-disk journal reader rather than the in-memory
872
- // object that planned this pass: omitted rows would otherwise look safe.
873
- const reopened = readJournal("acme");
874
- expect(Object.keys(reopened.files).sort()).toEqual(remoteFiles.map((file) => file.key).sort());
875
- });
876
623
  it("retains targeted-pull baselines as row fingerprints rather than entry objects", async () => {
877
624
  writeJournal("acme", {
878
625
  version: "2",
@@ -4370,6 +4117,129 @@ describe("sync", () => {
4370
4117
  const journal = journalAt(journalPath);
4371
4118
  expect(journal.files["docs/out-of-scope.md"]).toBeDefined();
4372
4119
  });
4120
+ it("defers a tombstone as a diagnostic error when HEAD throws RateLimitedError", async () => {
4121
+ // Production (2026-09-18): vault 100/hr throttle during HEAD verify
4122
+ // was emitted as a hard type:"error", so the company landed in
4123
+ // errors[] and hq-sync-runner exited 2 even though the tombstone
4124
+ // was already dropped from this pass. Rate-limit deferrals reuse
4125
+ // the diagnostic marker; the file stays, the journal stays, and
4126
+ // no hard error is emitted.
4127
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4128
+ const skillDir = path.join(companyRoot, "skills", "wbr-finalize");
4129
+ fs.mkdirSync(skillDir, { recursive: true });
4130
+ const skillPath = path.join(skillDir, "SKILL.md");
4131
+ const body = "skill body";
4132
+ fs.writeFileSync(skillPath, body);
4133
+ const crypto = await import("node:crypto");
4134
+ const baselineHash = crypto.createHash("sha256").update(body).digest("hex");
4135
+ fs.writeFileSync(journalPath, JSON.stringify({
4136
+ version: "1",
4137
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4138
+ files: {
4139
+ "skills/wbr-finalize/SKILL.md": {
4140
+ hash: baselineHash,
4141
+ size: body.length,
4142
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4143
+ direction: "down",
4144
+ remoteEtag: "e-skill",
4145
+ },
4146
+ },
4147
+ }));
4148
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([]);
4149
+ // Drop sibling Once/implementation leftovers so verify HEAD is this throw.
4150
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4151
+ let headVerified = false;
4152
+ vi.mocked(s3Module.headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
4153
+ expect(key).toBe("skills/wbr-finalize/SKILL.md");
4154
+ headVerified = true;
4155
+ throw new RateLimitedError("skills/wbr-finalize/SKILL.md", "get");
4156
+ });
4157
+ try {
4158
+ const events = [];
4159
+ const result = await sync({
4160
+ company: "acme",
4161
+ vaultConfig: mockConfig,
4162
+ hqRoot: tmpDir,
4163
+ onEvent: (e) => events.push(e),
4164
+ });
4165
+ expect(headVerified).toBe(true);
4166
+ expect(s3Module.headRemoteFile).toHaveBeenCalledTimes(1);
4167
+ expect(s3Module.headRemoteFile).toHaveBeenCalledWith(expect.anything(), "skills/wbr-finalize/SKILL.md");
4168
+ expect(fs.existsSync(skillPath)).toBe(true);
4169
+ expect(result.filesTombstoned).toBe(0);
4170
+ expect(journalAt(journalPath).files["skills/wbr-finalize/SKILL.md"]).toBeDefined();
4171
+ const errorEvents = events.filter((e) => e.type === "error");
4172
+ expect(errorEvents).toHaveLength(1);
4173
+ expect(errorEvents[0]).toMatchObject({
4174
+ type: "error",
4175
+ diagnostic: true,
4176
+ path: "skills/wbr-finalize/SKILL.md",
4177
+ message: expect.stringMatching(/tombstone HEAD verify failed \(deferring\):.*RateLimited/),
4178
+ });
4179
+ expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
4180
+ }
4181
+ finally {
4182
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4183
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValue(null);
4184
+ }
4185
+ });
4186
+ it("still emits a hard error when tombstone HEAD throws a generic Error", async () => {
4187
+ const companyRoot = path.join(tmpDir, "companies", "acme");
4188
+ fs.mkdirSync(path.join(companyRoot, "docs"), { recursive: true });
4189
+ const localPath = path.join(companyRoot, "docs", "peer-deleted.md");
4190
+ const body = "still here";
4191
+ fs.writeFileSync(localPath, body);
4192
+ const crypto = await import("node:crypto");
4193
+ const baselineHash = crypto.createHash("sha256").update(body).digest("hex");
4194
+ fs.writeFileSync(journalPath, JSON.stringify({
4195
+ version: "1",
4196
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
4197
+ files: {
4198
+ "docs/peer-deleted.md": {
4199
+ hash: baselineHash,
4200
+ size: body.length,
4201
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
4202
+ direction: "down",
4203
+ remoteEtag: "e-del",
4204
+ },
4205
+ },
4206
+ }));
4207
+ vi.mocked(s3Module.listRemoteFiles).mockResolvedValueOnce([]);
4208
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4209
+ let headVerified = false;
4210
+ vi.mocked(s3Module.headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
4211
+ expect(key).toBe("docs/peer-deleted.md");
4212
+ headVerified = true;
4213
+ throw new Error("S3 boom");
4214
+ });
4215
+ try {
4216
+ const events = [];
4217
+ const result = await sync({
4218
+ company: "acme",
4219
+ vaultConfig: mockConfig,
4220
+ hqRoot: tmpDir,
4221
+ onEvent: (e) => events.push(e),
4222
+ });
4223
+ expect(headVerified).toBe(true);
4224
+ expect(s3Module.headRemoteFile).toHaveBeenCalledTimes(1);
4225
+ expect(s3Module.headRemoteFile).toHaveBeenCalledWith(expect.anything(), "docs/peer-deleted.md");
4226
+ expect(fs.existsSync(localPath)).toBe(true);
4227
+ expect(result.filesTombstoned).toBe(0);
4228
+ expect(journalAt(journalPath).files["docs/peer-deleted.md"]).toBeDefined();
4229
+ const errorEvents = events.filter((e) => e.type === "error");
4230
+ expect(errorEvents).toHaveLength(1);
4231
+ expect(errorEvents[0]).toMatchObject({
4232
+ type: "error",
4233
+ path: "docs/peer-deleted.md",
4234
+ message: expect.stringContaining("tombstone HEAD verify failed (deferring):"),
4235
+ });
4236
+ expect(errorEvents[0]).not.toHaveProperty("diagnostic");
4237
+ }
4238
+ finally {
4239
+ vi.mocked(s3Module.headRemoteFile).mockReset();
4240
+ vi.mocked(s3Module.headRemoteFile).mockResolvedValue(null);
4241
+ }
4242
+ });
4373
4243
  it("retains the journal entry when tombstone unlink fails with EACCES (Codex P1 — Bug #9 follow-up)", async () => {
4374
4244
  // Codex review on PR #24 caught this: pre-fix the tombstone branch
4375
4245
  // dropped the journal entry unconditionally even when the local