@indigoai-us/hq-cloud 6.16.59 → 6.16.61

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.
@@ -68,6 +68,7 @@ import { share, ServerOwnedPushPathsError, UnreachablePushPathsError, Unregister
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";
71
+ import { RateLimitedError } from "../object-io.js";
71
72
  import { AreaLedgerMigration, closeCachedAreaJournalMigration, hashFile, openJournalStateStoreForMigration, readJournal, readJournalScoped, setAreaLedgerMigrationTestHooksForTest, writeJournal, } from "../journal.js";
72
73
  import { ReconcileCursorStore } from "../sync/reconcile-cursor.js";
73
74
  import { AreaResolver } from "../sync/area-resolver.js";
@@ -1674,6 +1675,125 @@ describe("share", () => {
1674
1675
  // Remote key must be company-relative, not hqRoot-relative
1675
1676
  expect(uploadFile).toHaveBeenCalledWith(expect.anything(), testFile, "test.md", undefined, expect.anything());
1676
1677
  });
1678
+ it("defers a rate-limited upload as a diagnostic error and leaves the journal row unchanged", async () => {
1679
+ // Production (2026-09-18): vault 100/hr throttle on a per-file PUT was
1680
+ // emitted as a hard type:"error", so the company landed in errors[] and
1681
+ // hq-sync-runner exited 2 even though the journal row is untouched and
1682
+ // the next pass re-plans the PUT. Rate-limit deferrals reuse the
1683
+ // diagnostic marker; the file is not counted as uploaded.
1684
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1685
+ fs.mkdirSync(companyRoot, { recursive: true });
1686
+ const testFile = path.join(companyRoot, "test.md");
1687
+ fs.writeFileSync(testFile, "# Hello World");
1688
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1689
+ const staleHash = "stale-hash-for-old-content";
1690
+ seedJournalAt(journalPath, {
1691
+ version: "1",
1692
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
1693
+ files: {
1694
+ "test.md": {
1695
+ hash: staleHash,
1696
+ size: 5,
1697
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1698
+ direction: "up",
1699
+ remoteEtag: "old-etag",
1700
+ },
1701
+ },
1702
+ });
1703
+ vi.mocked(uploadFile).mockReset();
1704
+ let putAttempted = false;
1705
+ vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, _localPath, key) => {
1706
+ expect(key).toBe("test.md");
1707
+ putAttempted = true;
1708
+ throw new RateLimitedError("test.md", "put");
1709
+ });
1710
+ try {
1711
+ const events = [];
1712
+ const result = await share({
1713
+ paths: [testFile],
1714
+ company: "acme",
1715
+ vaultConfig: mockConfig,
1716
+ hqRoot: tmpDir,
1717
+ onEvent: (e) => events.push(e),
1718
+ });
1719
+ expect(putAttempted).toBe(true);
1720
+ expect(result.filesUploaded).toBe(0);
1721
+ expect(result.filesSkipped).toBeGreaterThanOrEqual(1);
1722
+ expect(journalAt(journalPath).files["test.md"]).toMatchObject({
1723
+ hash: staleHash,
1724
+ remoteEtag: "old-etag",
1725
+ });
1726
+ const errorEvents = events.filter((e) => e.type === "error");
1727
+ expect(errorEvents).toHaveLength(1);
1728
+ expect(errorEvents[0]).toMatchObject({
1729
+ type: "error",
1730
+ diagnostic: true,
1731
+ path: "test.md",
1732
+ message: expect.stringMatching(/RateLimited.*put test.md deferred to next sync/),
1733
+ });
1734
+ expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
1735
+ }
1736
+ finally {
1737
+ vi.mocked(uploadFile).mockReset();
1738
+ vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
1739
+ }
1740
+ });
1741
+ it("still emits a hard error when upload throws a generic Error", async () => {
1742
+ const companyRoot = path.join(tmpDir, "companies", "acme");
1743
+ fs.mkdirSync(companyRoot, { recursive: true });
1744
+ const testFile = path.join(companyRoot, "test.md");
1745
+ fs.writeFileSync(testFile, "# Hello World");
1746
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
1747
+ const staleHash = "stale-hash-for-old-content";
1748
+ seedJournalAt(journalPath, {
1749
+ version: "1",
1750
+ lastSync: new Date(Date.now() - 60_000).toISOString(),
1751
+ files: {
1752
+ "test.md": {
1753
+ hash: staleHash,
1754
+ size: 5,
1755
+ syncedAt: new Date(Date.now() - 60_000).toISOString(),
1756
+ direction: "up",
1757
+ remoteEtag: "old-etag",
1758
+ },
1759
+ },
1760
+ });
1761
+ vi.mocked(uploadFile).mockReset();
1762
+ let putAttempted = false;
1763
+ vi.mocked(uploadFile).mockImplementationOnce(async (_ctx, _localPath, key) => {
1764
+ expect(key).toBe("test.md");
1765
+ putAttempted = true;
1766
+ throw new Error("S3 boom");
1767
+ });
1768
+ try {
1769
+ const events = [];
1770
+ const result = await share({
1771
+ paths: [testFile],
1772
+ company: "acme",
1773
+ vaultConfig: mockConfig,
1774
+ hqRoot: tmpDir,
1775
+ onEvent: (e) => events.push(e),
1776
+ });
1777
+ expect(putAttempted).toBe(true);
1778
+ expect(result.filesUploaded).toBe(0);
1779
+ expect(journalAt(journalPath).files["test.md"]).toMatchObject({
1780
+ hash: staleHash,
1781
+ remoteEtag: "old-etag",
1782
+ });
1783
+ const errorEvents = events.filter((e) => e.type === "error");
1784
+ expect(errorEvents).toHaveLength(1);
1785
+ expect(errorEvents[0]).toMatchObject({
1786
+ type: "error",
1787
+ path: "test.md",
1788
+ message: expect.stringContaining("S3 boom"),
1789
+ });
1790
+ expect(errorEvents[0]).not.toHaveProperty("diagnostic");
1791
+ }
1792
+ finally {
1793
+ vi.mocked(uploadFile).mockReset();
1794
+ vi.mocked(uploadFile).mockResolvedValue({ etag: '"upload-etag"' });
1795
+ }
1796
+ });
1677
1797
  it("replans an upload from the bytes present when its action starts", async () => {
1678
1798
  const companyRoot = path.join(tmpDir, "companies", "acme");
1679
1799
  fs.mkdirSync(companyRoot, { recursive: true });
@@ -5505,6 +5625,123 @@ describe("share", () => {
5505
5625
  const journal = journalAt(journalPath);
5506
5626
  expect(journal.files["flaky.md"]).toBeDefined();
5507
5627
  });
5628
+ it("defers a rate-limited remote delete as a diagnostic error and leaves the journal row unchanged", async () => {
5629
+ const companyRoot = path.join(tmpDir, "companies", "acme");
5630
+ fs.mkdirSync(companyRoot, { recursive: true });
5631
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
5632
+ seedJournalAt(journalPath, ({
5633
+ version: "1",
5634
+ lastSync: new Date().toISOString(),
5635
+ files: {
5636
+ "flaky.md": {
5637
+ hash: "h",
5638
+ size: 5,
5639
+ syncedAt: new Date().toISOString(),
5640
+ direction: "up",
5641
+ remoteEtag: "flaky-etag",
5642
+ kind: "file",
5643
+ localDeleteIntent: deleteIntent("flaky-etag", "h"),
5644
+ },
5645
+ },
5646
+ }));
5647
+ vi.mocked(deleteRemoteFile).mockReset();
5648
+ let deleteAttempted = false;
5649
+ vi.mocked(deleteRemoteFile).mockImplementationOnce(async (_ctx, key) => {
5650
+ expect(key).toBe("flaky.md");
5651
+ deleteAttempted = true;
5652
+ throw new RateLimitedError("flaky.md", "delete");
5653
+ });
5654
+ try {
5655
+ const events = [];
5656
+ const result = await share({
5657
+ paths: [companyRoot],
5658
+ company: "acme",
5659
+ vaultConfig: mockConfig,
5660
+ hqRoot: tmpDir,
5661
+ skipUnchanged: true,
5662
+ propagateDeletes: true,
5663
+ propagateDeletePolicy: "owned-only",
5664
+ onEvent: (e) => events.push(e),
5665
+ });
5666
+ expect(deleteAttempted).toBe(true);
5667
+ expect(result.filesDeleted).toBe(0);
5668
+ expect(journalAt(journalPath).files["flaky.md"]).toBeDefined();
5669
+ expect(result.pathResults).toContainEqual({
5670
+ path: "flaky.md",
5671
+ status: "refused",
5672
+ operation: "delete",
5673
+ reason: "transfer-error",
5674
+ });
5675
+ const errorEvents = events.filter((e) => e.type === "error");
5676
+ expect(errorEvents).toHaveLength(1);
5677
+ expect(errorEvents[0]).toMatchObject({
5678
+ type: "error",
5679
+ diagnostic: true,
5680
+ path: "flaky.md",
5681
+ message: expect.stringMatching(/RateLimited.*delete flaky.md deferred to next sync/),
5682
+ });
5683
+ expect(events.some((e) => e.type === "error" && e.diagnostic !== true)).toBe(false);
5684
+ }
5685
+ finally {
5686
+ vi.mocked(deleteRemoteFile).mockReset();
5687
+ vi.mocked(deleteRemoteFile).mockResolvedValue(undefined);
5688
+ }
5689
+ });
5690
+ it("still emits a hard error when remote delete throws a generic Error", async () => {
5691
+ const companyRoot = path.join(tmpDir, "companies", "acme");
5692
+ fs.mkdirSync(companyRoot, { recursive: true });
5693
+ const journalPath = path.join(stateDir, "sync-journal.acme.json");
5694
+ seedJournalAt(journalPath, ({
5695
+ version: "1",
5696
+ lastSync: new Date().toISOString(),
5697
+ files: {
5698
+ "flaky.md": {
5699
+ hash: "h",
5700
+ size: 5,
5701
+ syncedAt: new Date().toISOString(),
5702
+ direction: "up",
5703
+ remoteEtag: "flaky-etag",
5704
+ kind: "file",
5705
+ localDeleteIntent: deleteIntent("flaky-etag", "h"),
5706
+ },
5707
+ },
5708
+ }));
5709
+ vi.mocked(deleteRemoteFile).mockReset();
5710
+ let deleteAttempted = false;
5711
+ vi.mocked(deleteRemoteFile).mockImplementationOnce(async (_ctx, key) => {
5712
+ expect(key).toBe("flaky.md");
5713
+ deleteAttempted = true;
5714
+ throw new Error("S3 down");
5715
+ });
5716
+ try {
5717
+ const events = [];
5718
+ const result = await share({
5719
+ paths: [companyRoot],
5720
+ company: "acme",
5721
+ vaultConfig: mockConfig,
5722
+ hqRoot: tmpDir,
5723
+ skipUnchanged: true,
5724
+ propagateDeletes: true,
5725
+ propagateDeletePolicy: "owned-only",
5726
+ onEvent: (e) => events.push(e),
5727
+ });
5728
+ expect(deleteAttempted).toBe(true);
5729
+ expect(result.filesDeleted).toBe(0);
5730
+ expect(journalAt(journalPath).files["flaky.md"]).toBeDefined();
5731
+ const errorEvents = events.filter((e) => e.type === "error");
5732
+ expect(errorEvents).toHaveLength(1);
5733
+ expect(errorEvents[0]).toMatchObject({
5734
+ type: "error",
5735
+ path: "flaky.md",
5736
+ message: expect.stringContaining("S3 down"),
5737
+ });
5738
+ expect(errorEvents[0]).not.toHaveProperty("diagnostic");
5739
+ }
5740
+ finally {
5741
+ vi.mocked(deleteRemoteFile).mockReset();
5742
+ vi.mocked(deleteRemoteFile).mockResolvedValue(undefined);
5743
+ }
5744
+ });
5508
5745
  it("scoped push defense-in-depth: a 403 on DeleteObject of a nested session-log key skips as scope-excluded without an error event", async () => {
5509
5746
  const companyRoot = path.join(tmpDir, "companies", "acme");
5510
5747
  fs.mkdirSync(companyRoot, { recursive: true });