@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.
- package/dist/bin/sync-runner-company.d.ts +0 -14
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +89 -57
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +313 -56
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +3 -12
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +3 -65
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +1 -172
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +2 -51
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +165 -409
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +2 -19
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +5 -40
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +0 -47
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +13 -59
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +4 -48
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +229 -1499
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts +0 -19
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +0 -55
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +1 -87
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync-scope.test.js +0 -8
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +8 -12
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +152 -302
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +126 -256
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +34 -1
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +92 -21
- package/dist/cognito-auth.js.map +1 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +92 -12
- package/dist/context.js.map +1 -1
- package/dist/context.test.js +77 -2
- package/dist/context.test.js.map +1 -1
- package/dist/ignore.d.ts +10 -0
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +47 -5
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +30 -3
- package/dist/ignore.test.js.map +1 -1
- package/dist/journal-row-store.d.ts +5 -40
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +21 -240
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-row-store.test.js +0 -54
- package/dist/journal-row-store.test.js.map +1 -1
- package/dist/journal-snapshot-v4.d.ts.map +1 -1
- package/dist/journal-snapshot-v4.js +6 -11
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal-store.test.js +7 -324
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +16 -51
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +79 -303
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +0 -15
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +0 -15
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/lib/net-errors.d.ts +12 -0
- package/dist/lib/net-errors.d.ts.map +1 -1
- package/dist/lib/net-errors.js +35 -0
- package/dist/lib/net-errors.js.map +1 -1
- package/dist/lib/net-errors.test.js +19 -1
- package/dist/lib/net-errors.test.js.map +1 -1
- package/dist/machine-auth.test.js +6 -4
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/machine-token-cache-isolation.test.d.ts +24 -0
- package/dist/machine-token-cache-isolation.test.d.ts.map +1 -0
- package/dist/machine-token-cache-isolation.test.js +241 -0
- package/dist/machine-token-cache-isolation.test.js.map +1 -0
- package/dist/manifest/build-manifest-resident-parity.test.js +3 -3
- package/dist/manifest/build-manifest-resident-parity.test.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +3 -10
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +0 -35
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +2 -9
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +2 -59
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +0 -1
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +2 -20
- package/dist/s3.js.map +1 -1
- package/dist/sync/area-ledger.d.ts +3 -27
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +39 -149
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/area-ledger.test.js +5 -192
- package/dist/sync/area-ledger.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +0 -29
- package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
- package/dist/sync/realtime-telemetry.d.ts +1 -1
- package/dist/sync/realtime-telemetry.d.ts.map +1 -1
- package/dist/sync/realtime-telemetry.js +6 -14
- package/dist/sync/realtime-telemetry.js.map +1 -1
- package/dist/sync/realtime-telemetry.test.js +16 -136
- package/dist/sync/realtime-telemetry.test.js.map +1 -1
- package/dist/sync/state-store.d.ts +0 -6
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +2 -14
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watcher.d.ts +0 -12
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +0 -131
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +0 -63
- package/dist/watcher.test.js.map +1 -1
- package/package.json +1 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner-regressions.test.js +0 -300
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +0 -1
- package/dist/cli/onepass-pull-planner.test.d.ts +0 -2
- package/dist/cli/onepass-pull-planner.test.d.ts.map +0 -1
- package/dist/cli/onepass-pull-planner.test.js +0 -266
- package/dist/cli/onepass-pull-planner.test.js.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +0 -15
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +0 -1
- package/dist/cli/streaming-pull-planner.bench.test.js +0 -273
- package/dist/cli/streaming-pull-planner.bench.test.js.map +0 -1
- package/dist/s3.list-cost.test.d.ts +0 -2
- package/dist/s3.list-cost.test.d.ts.map +0 -1
- package/dist/s3.list-cost.test.js +0 -51
- package/dist/s3.list-cost.test.js.map +0 -1
- package/dist/sync/remote-file-spool.d.ts +0 -16
- package/dist/sync/remote-file-spool.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.js +0 -91
- package/dist/sync/remote-file-spool.js.map +0 -1
- package/dist/sync/remote-file-spool.test.d.ts +0 -2
- package/dist/sync/remote-file-spool.test.d.ts.map +0 -1
- package/dist/sync/remote-file-spool.test.js +0 -77
- package/dist/sync/remote-file-spool.test.js.map +0 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -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,
|
|
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
|