@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.
- package/dist/bin/sync-runner-company.js +3 -3
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-company.test.js +67 -0
- package/dist/bin/sync-runner-company.test.js.map +1 -1
- package/dist/bin/sync-runner.d.ts +2 -2
- package/dist/bin/sync-runner.test.js +29 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +50 -10
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +237 -0
- package/dist/cli/share.test.js.map +1 -1
- package/dist/cli/sync.d.ts +3 -2
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +25 -5
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/sync.test.js +87 -0
- package/dist/cli/sync.test.js.map +1 -1
- package/dist/cognito-auth.d.ts +31 -5
- package/dist/cognito-auth.d.ts.map +1 -1
- package/dist/cognito-auth.js +111 -76
- package/dist/cognito-auth.js.map +1 -1
- package/dist/machine-auth.test.js +13 -13
- package/dist/machine-auth.test.js.map +1 -1
- package/dist/machine-token-cache-isolation.test.js +47 -4
- package/dist/machine-token-cache-isolation.test.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/share.test.js
CHANGED
|
@@ -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 });
|