@indigoai-us/hq-cloud 6.16.65 → 6.16.67
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 +14 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +42 -8
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +62 -7
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner-planning.d.ts +12 -3
- package/dist/bin/sync-runner-planning.d.ts.map +1 -1
- package/dist/bin/sync-runner-planning.js +65 -3
- package/dist/bin/sync-runner-planning.js.map +1 -1
- package/dist/bin/sync-runner-planning.test.js +172 -1
- package/dist/bin/sync-runner-planning.test.js.map +1 -1
- package/dist/bin/sync-runner-watch-loop.d.ts +51 -2
- package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-loop.js +409 -165
- package/dist/bin/sync-runner-watch-loop.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.d.ts +19 -2
- package/dist/bin/sync-runner-watch-routes.d.ts.map +1 -1
- package/dist/bin/sync-runner-watch-routes.js +40 -5
- package/dist/bin/sync-runner-watch-routes.js.map +1 -1
- package/dist/bin/sync-runner-watch-routes.test.js +47 -0
- package/dist/bin/sync-runner-watch-routes.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +59 -5
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +48 -4
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +1553 -192
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts +2 -0
- package/dist/cli/onepass-pull-planner-regressions.test.d.ts.map +1 -0
- package/dist/cli/onepass-pull-planner-regressions.test.js +300 -0
- package/dist/cli/onepass-pull-planner-regressions.test.js.map +1 -0
- package/dist/cli/onepass-pull-planner.test.d.ts +2 -0
- package/dist/cli/onepass-pull-planner.test.d.ts.map +1 -0
- package/dist/cli/onepass-pull-planner.test.js +266 -0
- package/dist/cli/onepass-pull-planner.test.js.map +1 -0
- package/dist/cli/push-scan-exclusions.test.js +4 -4
- package/dist/cli/push-scan-exclusions.test.js.map +1 -1
- package/dist/cli/share.d.ts +31 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +143 -8
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +350 -5
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/streaming-pull-planner.bench.test.d.ts +15 -0
- package/dist/cli/streaming-pull-planner.bench.test.d.ts.map +1 -0
- package/dist/cli/streaming-pull-planner.bench.test.js +273 -0
- package/dist/cli/streaming-pull-planner.bench.test.js.map +1 -0
- package/dist/cli/sync-scope.test.js +8 -0
- package/dist/cli/sync-scope.test.js.map +1 -1
- package/dist/cli/sync.d.ts +12 -0
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +297 -125
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +255 -2
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/journal-row-store.d.ts +40 -5
- package/dist/journal-row-store.d.ts.map +1 -1
- package/dist/journal-row-store.js +240 -21
- package/dist/journal-row-store.js.map +1 -1
- package/dist/journal-row-store.test.js +54 -0
- 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 +11 -6
- package/dist/journal-snapshot-v4.js.map +1 -1
- package/dist/journal-store.test.js +324 -7
- package/dist/journal-store.test.js.map +1 -1
- package/dist/journal.d.ts +51 -16
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +303 -79
- package/dist/journal.js.map +1 -1
- package/dist/lib/exit-codes.d.ts +15 -0
- package/dist/lib/exit-codes.d.ts.map +1 -1
- package/dist/lib/exit-codes.js +15 -0
- package/dist/lib/exit-codes.js.map +1 -1
- package/dist/personal-vault.d.ts.map +1 -1
- package/dist/personal-vault.js +10 -3
- package/dist/personal-vault.js.map +1 -1
- package/dist/personal-vault.test.js +35 -0
- package/dist/personal-vault.test.js.map +1 -1
- package/dist/remote-pull.d.ts.map +1 -1
- package/dist/remote-pull.js +9 -2
- package/dist/remote-pull.js.map +1 -1
- package/dist/remote-pull.test.js +59 -2
- package/dist/remote-pull.test.js.map +1 -1
- package/dist/s3.d.ts +1 -0
- package/dist/s3.d.ts.map +1 -1
- package/dist/s3.js +20 -2
- package/dist/s3.js.map +1 -1
- package/dist/s3.list-cost.test.d.ts +2 -0
- package/dist/s3.list-cost.test.d.ts.map +1 -0
- package/dist/s3.list-cost.test.js +51 -0
- package/dist/s3.list-cost.test.js.map +1 -0
- package/dist/sync/area-ledger.d.ts +27 -3
- package/dist/sync/area-ledger.d.ts.map +1 -1
- package/dist/sync/area-ledger.js +149 -39
- package/dist/sync/area-ledger.js.map +1 -1
- package/dist/sync/area-ledger.test.js +192 -5
- package/dist/sync/area-ledger.test.js.map +1 -1
- package/dist/sync/parallel-sync-coordinator.test.js +29 -0
- 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 +14 -6
- package/dist/sync/realtime-telemetry.js.map +1 -1
- package/dist/sync/realtime-telemetry.test.js +136 -16
- package/dist/sync/realtime-telemetry.test.js.map +1 -1
- package/dist/sync/remote-file-spool.d.ts +16 -0
- package/dist/sync/remote-file-spool.d.ts.map +1 -0
- package/dist/sync/remote-file-spool.js +91 -0
- package/dist/sync/remote-file-spool.js.map +1 -0
- package/dist/sync/remote-file-spool.test.d.ts +2 -0
- package/dist/sync/remote-file-spool.test.d.ts.map +1 -0
- package/dist/sync/remote-file-spool.test.js +77 -0
- package/dist/sync/remote-file-spool.test.js.map +1 -0
- package/dist/sync/state-store.d.ts +6 -0
- package/dist/sync/state-store.d.ts.map +1 -1
- package/dist/sync/state-store.js +14 -2
- package/dist/sync/state-store.js.map +1 -1
- package/dist/watcher.d.ts +12 -0
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +131 -0
- package/dist/watcher.js.map +1 -1
- package/dist/watcher.test.js +63 -0
- package/dist/watcher.test.js.map +1 -1
- package/package.json +2 -1
package/dist/cli/sync.test.js
CHANGED
|
@@ -95,6 +95,15 @@ 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
|
+
}
|
|
98
107
|
return {
|
|
99
108
|
...actual,
|
|
100
109
|
toPosixKey: (key) => key.split("\\").join("/"),
|
|
@@ -108,7 +117,8 @@ vi.mock("../s3.js", async (importOriginal) => {
|
|
|
108
117
|
innerFs.writeFileSync(localPath, "mock file content");
|
|
109
118
|
return { metadata: {} };
|
|
110
119
|
}),
|
|
111
|
-
listRemoteFiles
|
|
120
|
+
listRemoteFiles,
|
|
121
|
+
streamRemoteFiles,
|
|
112
122
|
deleteRemoteFile: innerVi.fn().mockResolvedValue(undefined),
|
|
113
123
|
headRemoteFile: innerVi.fn().mockResolvedValue(null),
|
|
114
124
|
primeObjectTransport: innerVi.fn().mockResolvedValue(undefined),
|
|
@@ -125,11 +135,12 @@ vi.mock("../remote-pull.js", async (importOriginal) => {
|
|
|
125
135
|
const { vi: innerVi } = await import("vitest");
|
|
126
136
|
return { ...actual, listRemoteForScope: innerVi.fn(actual.listRemoteForScope) };
|
|
127
137
|
});
|
|
128
|
-
import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, targetedJournalSessionCensusSizes, win32SymlinkFlavorBroken, } from "./sync.js";
|
|
138
|
+
import { sync, reportNewFilesToNotify, applyRealtimeDeltaPage, isSyncMutationStdinCommand, runSyncMutationStdinJson, SYNC_MUTATION_STDIN_COMMAND, FULL_PULL_COOPERATIVE_CHECKPOINT_BATCH_SIZE, targetedJournalSessionCensusSizes, win32SymlinkFlavorBroken, } from "./sync.js";
|
|
129
139
|
import { share } from "./share.js";
|
|
130
140
|
import * as s3Module from "../s3.js";
|
|
131
141
|
import * as conflictModule from "./conflict.js";
|
|
132
142
|
import * as remotePullModule from "../remote-pull.js";
|
|
143
|
+
import { CooperativePassScheduler } from "../bin/sync-runner-watch-loop.js";
|
|
133
144
|
import { reindex } from "./reindex.js";
|
|
134
145
|
import { readSyncProgress } from "../sync-progress.js";
|
|
135
146
|
import { VaultAuthError } from "../vault-client.js";
|
|
@@ -620,6 +631,248 @@ describe("sync", () => {
|
|
|
620
631
|
},
|
|
621
632
|
});
|
|
622
633
|
}, 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
|
+
});
|
|
623
876
|
it("retains targeted-pull baselines as row fingerprints rather than entry objects", async () => {
|
|
624
877
|
writeJournal("acme", {
|
|
625
878
|
version: "2",
|