@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/share.test.js
CHANGED
|
@@ -64,7 +64,7 @@ vi.mock("readline", () => ({
|
|
|
64
64
|
})),
|
|
65
65
|
}));
|
|
66
66
|
import * as readline from "readline";
|
|
67
|
-
import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, UnregisteredCompanySkillError, currentUploadBytes, parseRegisteredCompanySkillUid, _testing as shareTesting, } from "./share.js";
|
|
67
|
+
import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, UnregisteredCompanySkillError, currentUploadBytes, FULL_PUSH_COOPERATIVE_CHECKPOINT_BATCH_SIZE, parseRegisteredCompanySkillUid, _testing as shareTesting, } from "./share.js";
|
|
68
68
|
import { deleteRemoteFile, downloadFile, headRemoteFile, primeObjectTransport, primeUploads, uploadFile, uploadSymlink, WindowsSymlinkPrivilegeError, } from "../s3.js";
|
|
69
69
|
import { VaultAuthError, VaultClientError, VaultConflictError, VaultPermissionDeniedError, } from "../vault-client.js";
|
|
70
70
|
import { CognitoRefreshError } from "../cognito-auth.js";
|
|
@@ -72,6 +72,7 @@ import { RateLimitedError } from "../object-io.js";
|
|
|
72
72
|
import { AreaLedgerMigration, closeCachedAreaJournalMigration, hashFile, openJournalStateStoreForMigration, readJournal, readJournalScoped, setAreaLedgerMigrationTestHooksForTest, writeJournal, } from "../journal.js";
|
|
73
73
|
import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
|
|
74
74
|
import { AreaResolver } from "../sync/area-resolver.js";
|
|
75
|
+
import { CooperativePassScheduler } from "../bin/sync-runner-watch-loop.js";
|
|
75
76
|
/**
|
|
76
77
|
* Read a journal the way production does.
|
|
77
78
|
*
|
|
@@ -1658,6 +1659,7 @@ describe("share", () => {
|
|
|
1658
1659
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
1659
1660
|
fs.rmSync(stateDir, { recursive: true, force: true });
|
|
1660
1661
|
delete process.env.HQ_STATE_DIR;
|
|
1662
|
+
shareTesting.disableCooperativeRowVisits();
|
|
1661
1663
|
});
|
|
1662
1664
|
it("shares a single file keyed relative to the company root", async () => {
|
|
1663
1665
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
@@ -1675,6 +1677,285 @@ describe("share", () => {
|
|
|
1675
1677
|
// Remote key must be company-relative, not hqRoot-relative
|
|
1676
1678
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
|
|
1677
1679
|
});
|
|
1680
|
+
it("hands queued realtime work a settled full-push boundary without losing journal rows", async () => {
|
|
1681
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1682
|
+
const fileCount = FULL_PUSH_COOPERATIVE_CHECKPOINT_BATCH_SIZE + 1;
|
|
1683
|
+
for (let index = 0; index < fileCount; index++) {
|
|
1684
|
+
const file = path.join(companyRoot, "docs", "full-push", `${index}.md`);
|
|
1685
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1686
|
+
fs.writeFileSync(file, "full-push body");
|
|
1687
|
+
}
|
|
1688
|
+
const scheduler = new CooperativePassScheduler(() => 0);
|
|
1689
|
+
let uploadsCompleted = 0;
|
|
1690
|
+
let uploadsInFlight = 0;
|
|
1691
|
+
let maxUploadsInFlight = 0;
|
|
1692
|
+
let realtimeQueued = false;
|
|
1693
|
+
let realtimeDispatchedAt = -1;
|
|
1694
|
+
let checkpointCalls = 0;
|
|
1695
|
+
let realtimePass;
|
|
1696
|
+
const upload = vi.mocked(uploadFile);
|
|
1697
|
+
const originalUpload = upload.getMockImplementation();
|
|
1698
|
+
upload.mockImplementation(async () => {
|
|
1699
|
+
uploadsInFlight++;
|
|
1700
|
+
maxUploadsInFlight = Math.max(maxUploadsInFlight, uploadsInFlight);
|
|
1701
|
+
await Promise.resolve();
|
|
1702
|
+
uploadsCompleted++;
|
|
1703
|
+
if (uploadsCompleted === 1) {
|
|
1704
|
+
realtimeQueued = true;
|
|
1705
|
+
realtimePass = scheduler.submit(async () => {
|
|
1706
|
+
// `executeUploads` drains the full unit before the scheduler grants
|
|
1707
|
+
// realtime the root lock: no two durable writers overlap.
|
|
1708
|
+
expect(uploadsInFlight).toBe(0);
|
|
1709
|
+
realtimeDispatchedAt = uploadsCompleted;
|
|
1710
|
+
const realtimePath = path.join(companyRoot, "docs", "realtime.md");
|
|
1711
|
+
fs.writeFileSync(realtimePath, "realtime body");
|
|
1712
|
+
const journal = readJournal("acme");
|
|
1713
|
+
journal.files["docs/realtime.md"] = {
|
|
1714
|
+
hash: hashFile(realtimePath),
|
|
1715
|
+
size: fs.statSync(realtimePath).size,
|
|
1716
|
+
syncedAt: "2026-09-17T12:00:00.000Z",
|
|
1717
|
+
direction: "up",
|
|
1718
|
+
remoteEtag: "realtime",
|
|
1719
|
+
mtimeMs: fs.statSync(realtimePath).mtimeMs,
|
|
1720
|
+
ctimeMs: fs.statSync(realtimePath).ctimeMs,
|
|
1721
|
+
};
|
|
1722
|
+
writeJournal("acme", journal);
|
|
1723
|
+
}, "realtime");
|
|
1724
|
+
}
|
|
1725
|
+
uploadsInFlight--;
|
|
1726
|
+
return { etag: '"full-push-etag"' };
|
|
1727
|
+
});
|
|
1728
|
+
try {
|
|
1729
|
+
await scheduler.submit(async ({ checkpoint }) => {
|
|
1730
|
+
await share({
|
|
1731
|
+
paths: [companyRoot],
|
|
1732
|
+
company: "acme",
|
|
1733
|
+
vaultConfig: mockConfig,
|
|
1734
|
+
hqRoot: tmpDir,
|
|
1735
|
+
skipUnchanged: true,
|
|
1736
|
+
propagateDeletes: true,
|
|
1737
|
+
cooperativeCheckpoint: async () => {
|
|
1738
|
+
checkpointCalls++;
|
|
1739
|
+
return checkpoint();
|
|
1740
|
+
},
|
|
1741
|
+
});
|
|
1742
|
+
}, "chore");
|
|
1743
|
+
await realtimePass;
|
|
1744
|
+
}
|
|
1745
|
+
finally {
|
|
1746
|
+
if (originalUpload)
|
|
1747
|
+
upload.mockImplementation(originalUpload);
|
|
1748
|
+
else
|
|
1749
|
+
upload.mockReset();
|
|
1750
|
+
}
|
|
1751
|
+
expect(maxUploadsInFlight).toBeGreaterThan(1);
|
|
1752
|
+
expect(realtimeQueued).toBe(true);
|
|
1753
|
+
expect(checkpointCalls).toBeGreaterThan(0);
|
|
1754
|
+
expect(realtimeDispatchedAt).toBeGreaterThan(0);
|
|
1755
|
+
expect(realtimeDispatchedAt).toBeLessThanOrEqual(FULL_PUSH_COOPERATIVE_CHECKPOINT_BATCH_SIZE);
|
|
1756
|
+
// Reopen the durable journal, rather than inspecting the stale first-pass
|
|
1757
|
+
// object: every pre-handoff key and the realtime key must survive.
|
|
1758
|
+
const durable = readJournal("acme");
|
|
1759
|
+
expect(Object.keys(durable.files)).toHaveLength(fileCount + 1);
|
|
1760
|
+
expect(durable.files["docs/realtime.md"]).toBeDefined();
|
|
1761
|
+
for (let index = 0; index < fileCount; index++) {
|
|
1762
|
+
expect(durable.files[`docs/full-push/${index}.md`]).toBeDefined();
|
|
1763
|
+
}
|
|
1764
|
+
}, 120_000);
|
|
1765
|
+
it("hands an all-skipped full personal push to queued realtime work", async () => {
|
|
1766
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1767
|
+
const fileCount = FULL_PUSH_COOPERATIVE_CHECKPOINT_BATCH_SIZE + 1;
|
|
1768
|
+
const files = {};
|
|
1769
|
+
for (let index = 0; index < fileCount; index++) {
|
|
1770
|
+
const relativePath = `docs/unchanged/${index}.md`;
|
|
1771
|
+
const file = path.join(companyRoot, relativePath);
|
|
1772
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1773
|
+
fs.writeFileSync(file, "unchanged body");
|
|
1774
|
+
const stat = fs.statSync(file);
|
|
1775
|
+
files[relativePath] = {
|
|
1776
|
+
hash: hashFile(file),
|
|
1777
|
+
size: stat.size,
|
|
1778
|
+
mtimeMs: stat.mtimeMs,
|
|
1779
|
+
ctimeMs: stat.ctimeMs,
|
|
1780
|
+
syncedAt: "2026-09-17T12:00:00.000Z",
|
|
1781
|
+
direction: "up",
|
|
1782
|
+
remoteEtag: "unchanged",
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
|
|
1786
|
+
version: "2",
|
|
1787
|
+
lastSync: "2026-09-17T12:00:00.000Z",
|
|
1788
|
+
files,
|
|
1789
|
+
pulls: [],
|
|
1790
|
+
});
|
|
1791
|
+
const scheduler = new CooperativePassScheduler(() => 0);
|
|
1792
|
+
let fullPassFinished = false;
|
|
1793
|
+
let eventLoopTicked = false;
|
|
1794
|
+
let realtimeQueued = false;
|
|
1795
|
+
let realtimeRanBeforeFullPassFinished = false;
|
|
1796
|
+
let checkpointCalls = 0;
|
|
1797
|
+
let realtimePass;
|
|
1798
|
+
let resolveRealtimeScheduled;
|
|
1799
|
+
const realtimeScheduled = new Promise((resolve) => {
|
|
1800
|
+
resolveRealtimeScheduled = resolve;
|
|
1801
|
+
});
|
|
1802
|
+
const queueRealtime = () => {
|
|
1803
|
+
if (realtimeQueued)
|
|
1804
|
+
return;
|
|
1805
|
+
realtimeQueued = true;
|
|
1806
|
+
setImmediate(() => {
|
|
1807
|
+
eventLoopTicked = true;
|
|
1808
|
+
realtimePass = scheduler.submit(async () => {
|
|
1809
|
+
realtimeRanBeforeFullPassFinished = !fullPassFinished;
|
|
1810
|
+
}, "realtime");
|
|
1811
|
+
resolveRealtimeScheduled?.();
|
|
1812
|
+
});
|
|
1813
|
+
};
|
|
1814
|
+
await scheduler.submit(async ({ checkpoint }) => {
|
|
1815
|
+
try {
|
|
1816
|
+
const result = await share({
|
|
1817
|
+
paths: [companyRoot],
|
|
1818
|
+
company: "acme",
|
|
1819
|
+
vaultConfig: mockConfig,
|
|
1820
|
+
hqRoot: tmpDir,
|
|
1821
|
+
skipUnchanged: true,
|
|
1822
|
+
onEvent: (event) => {
|
|
1823
|
+
if (event.type === "plan")
|
|
1824
|
+
queueRealtime();
|
|
1825
|
+
},
|
|
1826
|
+
cooperativeCheckpoint: async () => {
|
|
1827
|
+
checkpointCalls++;
|
|
1828
|
+
return checkpoint();
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
expect(result.filesSkipped).toBe(fileCount);
|
|
1832
|
+
expect(result.filesUploaded).toBe(0);
|
|
1833
|
+
}
|
|
1834
|
+
finally {
|
|
1835
|
+
fullPassFinished = true;
|
|
1836
|
+
}
|
|
1837
|
+
}, "chore");
|
|
1838
|
+
await realtimeScheduled;
|
|
1839
|
+
await realtimePass;
|
|
1840
|
+
expect(eventLoopTicked).toBe(true);
|
|
1841
|
+
expect(realtimeQueued).toBe(true);
|
|
1842
|
+
expect(realtimeRanBeforeFullPassFinished).toBe(true);
|
|
1843
|
+
expect(checkpointCalls).toBeGreaterThan(0);
|
|
1844
|
+
expect(uploadFile).not.toHaveBeenCalled();
|
|
1845
|
+
expect(Object.keys(readJournal("acme").files)).toHaveLength(fileCount);
|
|
1846
|
+
}, 120_000);
|
|
1847
|
+
it("resumes a skip-only full push at the next unit after realtime hand-off", async () => {
|
|
1848
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1849
|
+
const fileCount = FULL_PUSH_COOPERATIVE_CHECKPOINT_BATCH_SIZE * 3;
|
|
1850
|
+
const files = {};
|
|
1851
|
+
for (let index = 0; index < fileCount; index++) {
|
|
1852
|
+
const relativePath = `docs/resume/${index}.md`;
|
|
1853
|
+
const file = path.join(companyRoot, relativePath);
|
|
1854
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
1855
|
+
fs.writeFileSync(file, "resume body");
|
|
1856
|
+
const stat = fs.statSync(file);
|
|
1857
|
+
files[relativePath] = {
|
|
1858
|
+
hash: hashFile(file),
|
|
1859
|
+
size: stat.size,
|
|
1860
|
+
mtimeMs: stat.mtimeMs,
|
|
1861
|
+
ctimeMs: stat.ctimeMs,
|
|
1862
|
+
syncedAt: "2026-09-17T12:00:00.000Z",
|
|
1863
|
+
direction: "up",
|
|
1864
|
+
remoteEtag: "unchanged",
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
seedJournalAt(path.join(stateDir, "sync-journal.acme.json"), {
|
|
1868
|
+
version: "2",
|
|
1869
|
+
lastSync: "2026-09-17T12:00:00.000Z",
|
|
1870
|
+
files,
|
|
1871
|
+
pulls: [],
|
|
1872
|
+
});
|
|
1873
|
+
const scheduler = new CooperativePassScheduler(() => 0);
|
|
1874
|
+
let fullPassFinished = false;
|
|
1875
|
+
let realtimeRanBeforeFullPassFinished = false;
|
|
1876
|
+
const planEvents = [];
|
|
1877
|
+
const realtimePhaseRuns = [];
|
|
1878
|
+
const realtimePasses = [];
|
|
1879
|
+
const queuedForPhase = new Set();
|
|
1880
|
+
let currentPhase = "collection";
|
|
1881
|
+
const queueRealtimeForCurrentPhase = () => {
|
|
1882
|
+
const phase = currentPhase;
|
|
1883
|
+
if (queuedForPhase.has(phase))
|
|
1884
|
+
return;
|
|
1885
|
+
queuedForPhase.add(phase);
|
|
1886
|
+
setImmediate(() => {
|
|
1887
|
+
realtimePasses.push(scheduler.submit(async () => {
|
|
1888
|
+
realtimePhaseRuns.push(phase);
|
|
1889
|
+
if (!fullPassFinished)
|
|
1890
|
+
realtimeRanBeforeFullPassFinished = true;
|
|
1891
|
+
}, "realtime"));
|
|
1892
|
+
});
|
|
1893
|
+
};
|
|
1894
|
+
shareTesting.enableCooperativeRowVisits();
|
|
1895
|
+
let visits = {
|
|
1896
|
+
collect: new Map(),
|
|
1897
|
+
plan: new Map(),
|
|
1898
|
+
};
|
|
1899
|
+
try {
|
|
1900
|
+
await scheduler.submit(async ({ checkpoint }) => {
|
|
1901
|
+
queueRealtimeForCurrentPhase();
|
|
1902
|
+
try {
|
|
1903
|
+
const result = await share({
|
|
1904
|
+
paths: [companyRoot],
|
|
1905
|
+
company: "acme",
|
|
1906
|
+
vaultConfig: mockConfig,
|
|
1907
|
+
hqRoot: tmpDir,
|
|
1908
|
+
skipUnchanged: true,
|
|
1909
|
+
propagateDeletes: true,
|
|
1910
|
+
onEvent: (event) => {
|
|
1911
|
+
if (event.type === "plan") {
|
|
1912
|
+
planEvents.push(event);
|
|
1913
|
+
currentPhase = "execution";
|
|
1914
|
+
queueRealtimeForCurrentPhase();
|
|
1915
|
+
}
|
|
1916
|
+
},
|
|
1917
|
+
cooperativeCheckpoint: async () => {
|
|
1918
|
+
if (currentPhase === "collection" &&
|
|
1919
|
+
shareTesting.cooperativeRowVisits().plan.size > 0) {
|
|
1920
|
+
currentPhase = "planning";
|
|
1921
|
+
}
|
|
1922
|
+
if (currentPhase === "planning")
|
|
1923
|
+
queueRealtimeForCurrentPhase();
|
|
1924
|
+
return checkpoint();
|
|
1925
|
+
},
|
|
1926
|
+
});
|
|
1927
|
+
expect(result.filesSkipped).toBe(fileCount);
|
|
1928
|
+
expect(result.filesUploaded).toBe(0);
|
|
1929
|
+
}
|
|
1930
|
+
finally {
|
|
1931
|
+
fullPassFinished = true;
|
|
1932
|
+
}
|
|
1933
|
+
}, "chore");
|
|
1934
|
+
await Promise.all(realtimePasses);
|
|
1935
|
+
visits = shareTesting.cooperativeRowVisits();
|
|
1936
|
+
}
|
|
1937
|
+
finally {
|
|
1938
|
+
shareTesting.disableCooperativeRowVisits();
|
|
1939
|
+
}
|
|
1940
|
+
expect(realtimeRanBeforeFullPassFinished).toBe(true);
|
|
1941
|
+
expect(realtimePhaseRuns).toEqual(["collection", "planning", "execution"]);
|
|
1942
|
+
expect(planEvents).toHaveLength(1);
|
|
1943
|
+
expect(planEvents[0]).toMatchObject({
|
|
1944
|
+
type: "plan",
|
|
1945
|
+
filesToUpload: 0,
|
|
1946
|
+
filesToSkip: fileCount,
|
|
1947
|
+
filesToDelete: 0,
|
|
1948
|
+
});
|
|
1949
|
+
expect(visits.collect.size).toBe(fileCount);
|
|
1950
|
+
expect(visits.plan.size).toBe(fileCount);
|
|
1951
|
+
for (let index = 0; index < fileCount; index++) {
|
|
1952
|
+
const relativePath = `docs/resume/${index}.md`;
|
|
1953
|
+
expect(visits.collect.get(relativePath)).toBe(1);
|
|
1954
|
+
expect(visits.plan.get(relativePath)).toBe(1);
|
|
1955
|
+
}
|
|
1956
|
+
expect(uploadFile).not.toHaveBeenCalled();
|
|
1957
|
+
expect(Object.keys(readJournal("acme").files)).toHaveLength(fileCount);
|
|
1958
|
+
}, 120_000);
|
|
1678
1959
|
it("defers a rate-limited upload as a diagnostic error and leaves the journal row unchanged", async () => {
|
|
1679
1960
|
// Production (2026-09-18): vault 100/hr throttle on a per-file PUT was
|
|
1680
1961
|
// emitted as a hard type:"error", so the company landed in errors[] and
|
|
@@ -1794,6 +2075,61 @@ describe("share", () => {
|
|
|
1794
2075
|
vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
|
|
1795
2076
|
}
|
|
1796
2077
|
});
|
|
2078
|
+
it("defers a file that vanishes between upload and journal as a diagnostic error", async () => {
|
|
2079
|
+
// Production (2026-09-19 02:02:57Z, agent Alpha, company equi):
|
|
2080
|
+
// workers/thread-police/data/run.lock was PUT, then deleted by the
|
|
2081
|
+
// worker before updateEntry's lstat. PrematureJournalEntryError is
|
|
2082
|
+
// correct (nothing journaled), but the catch used to emit a hard
|
|
2083
|
+
// type:"error" so hq-sync-runner exited 2. Same diagnostic deferral
|
|
2084
|
+
// as a rate-limited PUT; the next pass re-plans the key.
|
|
2085
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
2086
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
2087
|
+
const relativePath = "workers/thread-police/data/run.lock";
|
|
2088
|
+
const testFile = path.join(companyRoot, relativePath);
|
|
2089
|
+
fs.mkdirSync(path.dirname(testFile), { recursive: true });
|
|
2090
|
+
fs.writeFileSync(testFile, "pid");
|
|
2091
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
2092
|
+
seedJournalAt(journalPath, {
|
|
2093
|
+
version: "1",
|
|
2094
|
+
lastSync: new Date(Date.now() - 60_000).toISOString(),
|
|
2095
|
+
files: {},
|
|
2096
|
+
});
|
|
2097
|
+
vi.mocked(uploadFile).mockReset();
|
|
2098
|
+
let putAttempted = false;
|
|
2099
|
+
vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, localPath, key) => {
|
|
2100
|
+
expect(key).toBe(relativePath);
|
|
2101
|
+
putAttempted = true;
|
|
2102
|
+
fs.unlinkSync(localPath);
|
|
2103
|
+
return { etag: '"upload-etag"' };
|
|
2104
|
+
});
|
|
2105
|
+
try {
|
|
2106
|
+
const events = [];
|
|
2107
|
+
const result = await share({
|
|
2108
|
+
paths: [testFile],
|
|
2109
|
+
company: "acme",
|
|
2110
|
+
vaultConfig: mockConfig,
|
|
2111
|
+
hqRoot: tmpDir,
|
|
2112
|
+
onEvent: (e) => events.push(e),
|
|
2113
|
+
});
|
|
2114
|
+
expect(putAttempted).toBe(true);
|
|
2115
|
+
expect(result.filesUploaded).toBe(0);
|
|
2116
|
+
expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
|
|
2117
|
+
expect(journalAt(journalPath).files[relativePath]).toBeUndefined();
|
|
2118
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
2119
|
+
expect(errorEvents).toHaveLength(1);
|
|
2120
|
+
expect(errorEvents[0]).toMatchObject({
|
|
2121
|
+
type: "error",
|
|
2122
|
+
diagnostic: true,
|
|
2123
|
+
path: relativePath,
|
|
2124
|
+
message: expect.stringContaining("PrematureJournalEntryError"),
|
|
2125
|
+
});
|
|
2126
|
+
expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
|
|
2127
|
+
}
|
|
2128
|
+
finally {
|
|
2129
|
+
vi.mocked(uploadFile).mockReset();
|
|
2130
|
+
vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
1797
2133
|
it("defers a breaker-tripped upload batch as one diagnostic company error", async () => {
|
|
1798
2134
|
// Production (2026-09-18 16:03Z brickcraft, 16:05Z mhi-media): the
|
|
1799
2135
|
// RateLimitBreaker tripped, remaining GET presigns all threw
|
|
@@ -2085,7 +2421,7 @@ describe("share", () => {
|
|
|
2085
2421
|
});
|
|
2086
2422
|
expect(result.filesUploaded).toBe(2);
|
|
2087
2423
|
});
|
|
2088
|
-
it("walks a very large directory without using its entries as call arguments", () => {
|
|
2424
|
+
it("walks a very large directory without using its entries as call arguments", async () => {
|
|
2089
2425
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
2090
2426
|
fs.mkdirSync(companyRoot, { recursive: true });
|
|
2091
2427
|
const nestedDir = path.join(companyRoot, "nested");
|
|
@@ -2112,8 +2448,15 @@ describe("share", () => {
|
|
|
2112
2448
|
.spyOn(fs, "readdirSync")
|
|
2113
2449
|
.mockImplementation(((dir) => path.resolve(String(dir)) === nestedDir ? nestedEntries : rootEntries));
|
|
2114
2450
|
let heartbeatCalls = 0;
|
|
2451
|
+
let eventLoopTicked = false;
|
|
2452
|
+
const eventLoopTick = new Promise((resolve) => {
|
|
2453
|
+
setImmediate(() => {
|
|
2454
|
+
eventLoopTicked = true;
|
|
2455
|
+
resolve();
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2115
2458
|
try {
|
|
2116
|
-
const collected = shareTesting.collectFiles([companyRoot], tmpDir, companyRoot, () => true, { onHeartbeat: () => heartbeatCalls++ });
|
|
2459
|
+
const collected = await shareTesting.collectFiles([companyRoot], tmpDir, companyRoot, () => true, { onHeartbeat: () => heartbeatCalls++ });
|
|
2117
2460
|
expect(collected).toHaveLength(entryCount);
|
|
2118
2461
|
expect(collected[0]).toMatchObject({ relativePath: "before.md" });
|
|
2119
2462
|
expect(collected[1]).toMatchObject({ relativePath: "nested/child.md" });
|
|
@@ -2122,6 +2465,8 @@ describe("share", () => {
|
|
|
2122
2465
|
relativePath: `entry-${entryCount - 3}.md`,
|
|
2123
2466
|
});
|
|
2124
2467
|
expect(heartbeatCalls).toBeGreaterThan(entryCount);
|
|
2468
|
+
expect(eventLoopTicked).toBe(true);
|
|
2469
|
+
await eventLoopTick;
|
|
2125
2470
|
}
|
|
2126
2471
|
finally {
|
|
2127
2472
|
readdirSpy.mockRestore();
|
|
@@ -7851,7 +8196,7 @@ describe("share", () => {
|
|
|
7851
8196
|
expect(uploadFile).toHaveBeenCalledWith(expect.anything(), realPolicy, "policies/real.md", undefined, expect.anything());
|
|
7852
8197
|
expect(uploadSymlink).toHaveBeenCalledWith(expect.anything(), "real.md", "policies/link.md", undefined, expect.anything());
|
|
7853
8198
|
});
|
|
7854
|
-
it("skips and reports a top-level link whose win32 readlink raises EINVAL", () => {
|
|
8199
|
+
it("skips and reports a top-level link whose win32 readlink raises EINVAL", async () => {
|
|
7855
8200
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
7856
8201
|
const targetDir = path.join(tmpDir, "outside", "target");
|
|
7857
8202
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -7870,7 +8215,7 @@ describe("share", () => {
|
|
|
7870
8215
|
const warnings = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
7871
8216
|
const unreachable = [];
|
|
7872
8217
|
try {
|
|
7873
|
-
const collected = shareTesting.collectFiles([unreadableLink], tmpDir, companyRoot, () => true, {
|
|
8218
|
+
const collected = await shareTesting.collectFiles([unreadableLink], tmpDir, companyRoot, () => true, {
|
|
7874
8219
|
onUnreachablePath: (namedPath, reason) => unreachable.push([namedPath, reason]),
|
|
7875
8220
|
});
|
|
7876
8221
|
expect(collected).toEqual([]);
|