@indigoai-us/hq-cloud 6.16.62 → 6.16.63
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.test.js +65 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +36 -16
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +148 -0
- package/dist/cli/share.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.test.js
CHANGED
|
@@ -1794,6 +1794,154 @@ describe("share", () => {
|
|
|
1794
1794
|
vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
|
|
1795
1795
|
}
|
|
1796
1796
|
});
|
|
1797
|
+
it("defers a breaker-tripped upload batch as one diagnostic company error", async () => {
|
|
1798
|
+
// Production (2026-09-18 16:03Z brickcraft, 16:05Z mhi-media): the
|
|
1799
|
+
// RateLimitBreaker tripped, remaining GET presigns all threw
|
|
1800
|
+
// RateLimitedError, and throwUploadWorkerErrors rewrote the first into a
|
|
1801
|
+
// hard throw (`… (and 4915 more upload-worker errors)`). The runner then
|
|
1802
|
+
// put the company in errors[] and exited 2 even though every file is
|
|
1803
|
+
// retried next sync. An all-transient worker batch is one diagnostic
|
|
1804
|
+
// event at path "(company)", not a throw.
|
|
1805
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1806
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1807
|
+
const names = ["a.md", "b.md", "c.md"];
|
|
1808
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1809
|
+
const staleHash = "stale-hash-for-old-content";
|
|
1810
|
+
const files = {};
|
|
1811
|
+
for (const name of names) {
|
|
1812
|
+
fs.writeFileSync(path.join(companyRoot, name), `# ${name}`);
|
|
1813
|
+
files[name] = {
|
|
1814
|
+
hash: staleHash,
|
|
1815
|
+
size: 5,
|
|
1816
|
+
syncedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
1817
|
+
direction: "up",
|
|
1818
|
+
remoteEtag: "old-etag",
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
seedJournalAt(journalPath, {
|
|
1822
|
+
version: "1",
|
|
1823
|
+
lastSync: new Date(Date.now() - 60_000).toISOString(),
|
|
1824
|
+
files,
|
|
1825
|
+
});
|
|
1826
|
+
vi.mocked(headRemoteFile).mockReset();
|
|
1827
|
+
for (const _name of names) {
|
|
1828
|
+
vi.mocked(headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
|
|
1829
|
+
throw new RateLimitedError(key, "get");
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
try {
|
|
1833
|
+
const events = [];
|
|
1834
|
+
const result = await share({
|
|
1835
|
+
paths: names.map((name) => path.join(companyRoot, name)),
|
|
1836
|
+
company: "acme",
|
|
1837
|
+
vaultConfig: mockConfig,
|
|
1838
|
+
hqRoot: tmpDir,
|
|
1839
|
+
onEvent: (e) => events.push(e),
|
|
1840
|
+
});
|
|
1841
|
+
expect(result.filesUploaded).toBe(0);
|
|
1842
|
+
expect(uploadFile).not.toHaveBeenCalled();
|
|
1843
|
+
for (const name of names) {
|
|
1844
|
+
expect(journalAt(journalPath).files[name]).toMatchObject({
|
|
1845
|
+
hash: staleHash,
|
|
1846
|
+
remoteEtag: "old-etag",
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
const errorEvents = events.filter((e) => e.type === "error");
|
|
1850
|
+
expect(errorEvents).toHaveLength(1);
|
|
1851
|
+
expect(errorEvents[0]).toMatchObject({
|
|
1852
|
+
type: "error",
|
|
1853
|
+
diagnostic: true,
|
|
1854
|
+
path: "(company)",
|
|
1855
|
+
message: expect.stringMatching(/RateLimited.*\(and 2 more upload-worker errors\)/),
|
|
1856
|
+
});
|
|
1857
|
+
expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
|
|
1858
|
+
}
|
|
1859
|
+
finally {
|
|
1860
|
+
vi.mocked(headRemoteFile).mockReset();
|
|
1861
|
+
vi.mocked(headRemoteFile).mockResolvedValue(null);
|
|
1862
|
+
}
|
|
1863
|
+
});
|
|
1864
|
+
it("still throws when an upload-worker batch includes a hard error", async () => {
|
|
1865
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1866
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1867
|
+
const names = ["a.md", "b.md", "c.md"];
|
|
1868
|
+
const journalPath = path.join(stateDir, "sync-journal.acme.json");
|
|
1869
|
+
const staleHash = "stale-hash-for-old-content";
|
|
1870
|
+
const files = {};
|
|
1871
|
+
for (const name of names) {
|
|
1872
|
+
fs.writeFileSync(path.join(companyRoot, name), `# ${name}`);
|
|
1873
|
+
files[name] = {
|
|
1874
|
+
hash: staleHash,
|
|
1875
|
+
size: 5,
|
|
1876
|
+
syncedAt: new Date(Date.now() - 60_000).toISOString(),
|
|
1877
|
+
direction: "up",
|
|
1878
|
+
remoteEtag: "old-etag",
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
seedJournalAt(journalPath, {
|
|
1882
|
+
version: "1",
|
|
1883
|
+
lastSync: new Date(Date.now() - 60_000).toISOString(),
|
|
1884
|
+
files,
|
|
1885
|
+
});
|
|
1886
|
+
vi.mocked(headRemoteFile).mockReset();
|
|
1887
|
+
vi.mocked(headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
|
|
1888
|
+
throw new RateLimitedError(key, "get");
|
|
1889
|
+
});
|
|
1890
|
+
vi.mocked(headRemoteFile).mockImplementationOnce(async (_ctx, key) => {
|
|
1891
|
+
throw new RateLimitedError(key, "get");
|
|
1892
|
+
});
|
|
1893
|
+
vi.mocked(headRemoteFile).mockImplementationOnce(async () => {
|
|
1894
|
+
throw new Error("S3 boom");
|
|
1895
|
+
});
|
|
1896
|
+
const events = [];
|
|
1897
|
+
let thrown;
|
|
1898
|
+
try {
|
|
1899
|
+
await share({
|
|
1900
|
+
paths: names.map((name) => path.join(companyRoot, name)),
|
|
1901
|
+
company: "acme",
|
|
1902
|
+
vaultConfig: mockConfig,
|
|
1903
|
+
hqRoot: tmpDir,
|
|
1904
|
+
onEvent: (e) => events.push(e),
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
catch (err) {
|
|
1908
|
+
thrown = err;
|
|
1909
|
+
}
|
|
1910
|
+
finally {
|
|
1911
|
+
vi.mocked(headRemoteFile).mockReset();
|
|
1912
|
+
vi.mocked(headRemoteFile).mockResolvedValue(null);
|
|
1913
|
+
}
|
|
1914
|
+
expect(thrown).toBeInstanceOf(Error);
|
|
1915
|
+
expect(thrown.message).toMatch(/S3 boom \(and 2 more upload-worker errors\)/);
|
|
1916
|
+
expect(events.some((e) => e.type === "error")).toBe(false);
|
|
1917
|
+
for (const name of names) {
|
|
1918
|
+
expect(journalAt(journalPath).files[name]).toMatchObject({
|
|
1919
|
+
hash: staleHash,
|
|
1920
|
+
remoteEtag: "old-etag",
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
it("uploads a three-file batch when every worker succeeds", async () => {
|
|
1925
|
+
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1926
|
+
fs.mkdirSync(companyRoot, { recursive: true });
|
|
1927
|
+
const names = ["a.md", "b.md", "c.md"];
|
|
1928
|
+
for (const name of names) {
|
|
1929
|
+
fs.writeFileSync(path.join(companyRoot, name), `# ${name}`);
|
|
1930
|
+
}
|
|
1931
|
+
const events = [];
|
|
1932
|
+
const result = await share({
|
|
1933
|
+
paths: names.map((name) => path.join(companyRoot, name)),
|
|
1934
|
+
company: "acme",
|
|
1935
|
+
vaultConfig: mockConfig,
|
|
1936
|
+
hqRoot: tmpDir,
|
|
1937
|
+
onEvent: (e) => events.push(e),
|
|
1938
|
+
});
|
|
1939
|
+
expect(result.filesUploaded).toBe(3);
|
|
1940
|
+
expect(result.aborted).toBe(false);
|
|
1941
|
+
expect(events.some((e) => e.type === "error")).toBe(false);
|
|
1942
|
+
const putKeys = vi.mocked(uploadFile).mock.calls.map((c) => c[2]).sort();
|
|
1943
|
+
expect(putKeys).toEqual([...names]);
|
|
1944
|
+
});
|
|
1797
1945
|
it("replans an upload from the bytes present when its action starts", async () => {
|
|
1798
1946
|
const companyRoot = path.join(tmpDir, "companies", "acme");
|
|
1799
1947
|
fs.mkdirSync(companyRoot, { recursive: true });
|