@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.
@@ -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 });