@indigoai-us/hq-cloud 6.14.41 → 6.14.43
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 +4 -0
- package/dist/bin/sync-runner-company.d.ts.map +1 -1
- package/dist/bin/sync-runner-company.js +38 -9
- package/dist/bin/sync-runner-company.js.map +1 -1
- package/dist/bin/sync-runner-rollup.d.ts +1 -1
- package/dist/bin/sync-runner-rollup.d.ts.map +1 -1
- package/dist/bin/sync-runner.d.ts +30 -5
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +9 -6
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +180 -21
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/share.d.ts.map +1 -1
- package/dist/cli/share.js +25 -3
- package/dist/cli/share.js.map +1 -1
- package/dist/cli/share.test.js +357 -1
- package/dist/cli/share.test.js.map +1 -1
- package/dist/lib/net-errors.d.ts +9 -6
- package/dist/lib/net-errors.d.ts.map +1 -1
- package/dist/lib/net-errors.js +9 -6
- package/dist/lib/net-errors.js.map +1 -1
- package/dist/lib/net-errors.test.js +9 -1
- package/dist/lib/net-errors.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner-company.ts +36 -9
- package/src/bin/sync-runner-rollup.ts +1 -1
- package/src/bin/sync-runner.test.ts +246 -23
- package/src/bin/sync-runner.ts +34 -8
- package/src/cli/share.test.ts +446 -1
- package/src/cli/share.ts +27 -3
- package/src/lib/net-errors.test.ts +13 -1
- package/src/lib/net-errors.ts +9 -6
- package/test/e2e/sync/transient-company-leg.test.ts +380 -0
|
@@ -1552,17 +1552,12 @@ describe("per-company fanout", () => {
|
|
|
1552
1552
|
expect(deps.stdout.events().some((event) => event.type === "all-complete")).toBe(false);
|
|
1553
1553
|
});
|
|
1554
1554
|
/**
|
|
1555
|
-
*
|
|
1556
|
-
*
|
|
1557
|
-
*
|
|
1558
|
-
*
|
|
1559
|
-
* is parked on `err.cause`. Pre-fix the per-company catch extracted only
|
|
1560
|
-
* `err.message`, so an unactionable bare "UnknownError" reached operators
|
|
1561
|
-
* and the underlying syscall/hostname were lost. The catch now runs the
|
|
1562
|
-
* thrown value through `describeError`, which walks the cause chain and
|
|
1563
|
-
* stitches a one-line diagnostic the operator can act on.
|
|
1555
|
+
* AWS SDK v3 can wrap a Node-side DNS error in an opaque `UnknownError`.
|
|
1556
|
+
* The cause-chain classifier must treat this exactly like the direct
|
|
1557
|
+
* ENOTFOUND shape from HQ-SYNC-X: visible and retryable, never an exit-2
|
|
1558
|
+
* company error.
|
|
1564
1559
|
*/
|
|
1565
|
-
it("
|
|
1560
|
+
it("classifies an SDK UnknownError whose cause is ENOTFOUND as transient (HQ-SYNC-X)", async () => {
|
|
1566
1561
|
const deps = makeDeps({
|
|
1567
1562
|
createVaultClient: () => makeVaultStub({
|
|
1568
1563
|
memberships: [{ companyUid: "cmp_x" }],
|
|
@@ -1588,18 +1583,119 @@ describe("per-company fanout", () => {
|
|
|
1588
1583
|
}),
|
|
1589
1584
|
});
|
|
1590
1585
|
const code = await runRunner(["--companies"], deps);
|
|
1591
|
-
expect(code).toBe(
|
|
1592
|
-
|
|
1586
|
+
expect(code).toBe(TRANSIENT_NETWORK_EXIT);
|
|
1587
|
+
expect(deps.stderr
|
|
1588
|
+
.events()
|
|
1589
|
+
.some((e) => e.type === "error" && e.company === "privy")).toBe(false);
|
|
1590
|
+
const transient = deps.stdout
|
|
1591
|
+
.events()
|
|
1592
|
+
.find((e) => e.type === "transient-network");
|
|
1593
|
+
expect(transient).toMatchObject({
|
|
1594
|
+
company: "privy",
|
|
1595
|
+
path: "(company)",
|
|
1596
|
+
});
|
|
1597
|
+
expect(transient?.message).toContain("UnknownError");
|
|
1598
|
+
expect(transient?.message).toContain("ENOTFOUND");
|
|
1599
|
+
expect(transient?.message).toContain("getaddrinfo");
|
|
1600
|
+
expect(transient?.message).toContain("privy.s3.us-east-1.amazonaws.com");
|
|
1601
|
+
});
|
|
1602
|
+
it("returns 75, preserves the aborted complete event, and reports a direct ENOTFOUND leg as transient", async () => {
|
|
1603
|
+
const transportError = Object.assign(new Error("getaddrinfo ENOTFOUND hq-vault-prs-test.s3.us-east-1.amazonaws.com"), { code: "ENOTFOUND" });
|
|
1604
|
+
const deps = makeDeps({
|
|
1605
|
+
createVaultClient: () => makeVaultStub({
|
|
1606
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
1607
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1608
|
+
}),
|
|
1609
|
+
sync: vi.fn().mockRejectedValue(transportError),
|
|
1610
|
+
});
|
|
1611
|
+
const code = await runRunner(["--companies"], deps);
|
|
1612
|
+
expect(code).toBe(TRANSIENT_NETWORK_EXIT);
|
|
1613
|
+
expect(deps.stderr
|
|
1614
|
+
.events()
|
|
1615
|
+
.some((e) => e.type === "error" && e.path === "(company)")).toBe(false);
|
|
1616
|
+
expect(deps.stdout.events().find((e) => e.type === "transient-network")).toMatchObject({
|
|
1617
|
+
company: "acme",
|
|
1618
|
+
path: "(company)",
|
|
1619
|
+
message: expect.stringContaining("code=ENOTFOUND"),
|
|
1620
|
+
});
|
|
1621
|
+
expect(deps.stdout.events().find((e) => e.type === "complete" && e.company === "acme")).toMatchObject({ aborted: true });
|
|
1622
|
+
const all = deps.stdout
|
|
1623
|
+
.events()
|
|
1624
|
+
.find((e) => e.type === "all-complete");
|
|
1625
|
+
expect(all.errors).toEqual([]);
|
|
1626
|
+
expect(all.transient).toEqual([
|
|
1627
|
+
{
|
|
1628
|
+
company: "acme",
|
|
1629
|
+
message: expect.stringContaining("code=ENOTFOUND"),
|
|
1630
|
+
},
|
|
1631
|
+
]);
|
|
1632
|
+
expect(all.companies).toEqual([
|
|
1633
|
+
expect.objectContaining({ company: "acme", status: "transient-network" }),
|
|
1634
|
+
]);
|
|
1635
|
+
});
|
|
1636
|
+
it("keeps deterministic company failures as exit 2 errors", async () => {
|
|
1637
|
+
const deps = makeDeps({
|
|
1638
|
+
createVaultClient: () => makeVaultStub({
|
|
1639
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
1640
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1641
|
+
}),
|
|
1642
|
+
sync: vi.fn().mockRejectedValue(new Error("AccessDenied: forbidden")),
|
|
1643
|
+
});
|
|
1644
|
+
expect(await runRunner(["--companies"], deps)).toBe(PARTIAL_SYNC_EXIT);
|
|
1645
|
+
expect(deps.stderr.events().find((e) => e.type === "error" && e.company === "acme" && e.path === "(company)")).toBeDefined();
|
|
1646
|
+
expect(deps.stdout
|
|
1647
|
+
.events()
|
|
1648
|
+
.some((e) => e.type === "transient-network")).toBe(false);
|
|
1649
|
+
});
|
|
1650
|
+
it("lets a real company error win over a transient company leg", async () => {
|
|
1651
|
+
const dnsError = Object.assign(new Error("getaddrinfo ENOTFOUND vault"), {
|
|
1652
|
+
code: "ENOTFOUND",
|
|
1653
|
+
});
|
|
1654
|
+
const slugs = { cmp_acme: "acme", cmp_beta: "beta" };
|
|
1655
|
+
const deps = makeDeps({
|
|
1656
|
+
createVaultClient: () => makeVaultStub({
|
|
1657
|
+
memberships: [{ companyUid: "cmp_acme" }, { companyUid: "cmp_beta" }],
|
|
1658
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: slugs[uid] ?? uid }),
|
|
1659
|
+
}),
|
|
1660
|
+
sync: vi
|
|
1661
|
+
.fn()
|
|
1662
|
+
.mockRejectedValueOnce(dnsError)
|
|
1663
|
+
.mockRejectedValueOnce(new Error("AccessDenied: forbidden")),
|
|
1664
|
+
});
|
|
1665
|
+
expect(await runRunner(["--companies"], deps)).toBe(PARTIAL_SYNC_EXIT);
|
|
1666
|
+
expect(deps.stdout
|
|
1667
|
+
.events()
|
|
1668
|
+
.filter((e) => e.type === "transient-network")).toHaveLength(1);
|
|
1669
|
+
expect(deps.stderr
|
|
1670
|
+
.events()
|
|
1671
|
+
.filter((e) => e.type === "error" && e.path === "(company)")).toHaveLength(1);
|
|
1672
|
+
});
|
|
1673
|
+
it("keeps a leg errored when a path error precedes a transient throw", async () => {
|
|
1674
|
+
const dnsError = Object.assign(new Error("getaddrinfo ENOTFOUND vault"), {
|
|
1675
|
+
code: "ENOTFOUND",
|
|
1676
|
+
});
|
|
1677
|
+
const deps = makeDeps({
|
|
1678
|
+
createVaultClient: () => makeVaultStub({
|
|
1679
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
1680
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1681
|
+
}),
|
|
1682
|
+
sync: vi.fn().mockImplementation(async (opts) => {
|
|
1683
|
+
opts.onEvent?.({
|
|
1684
|
+
type: "error",
|
|
1685
|
+
path: "locked.md",
|
|
1686
|
+
message: "AccessDenied: forbidden",
|
|
1687
|
+
});
|
|
1688
|
+
throw dnsError;
|
|
1689
|
+
}),
|
|
1690
|
+
});
|
|
1691
|
+
expect(await runRunner(["--companies"], deps)).toBe(PARTIAL_SYNC_EXIT);
|
|
1692
|
+
const all = deps.stdout
|
|
1593
1693
|
.events()
|
|
1594
|
-
.find((
|
|
1595
|
-
expect(
|
|
1596
|
-
expect(
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
expect(companyErr?.message).toContain("UnknownError");
|
|
1600
|
-
expect(companyErr?.message).toContain("ENOTFOUND");
|
|
1601
|
-
expect(companyErr?.message).toContain("getaddrinfo");
|
|
1602
|
-
expect(companyErr?.message).toContain("privy.s3.us-east-1.amazonaws.com");
|
|
1694
|
+
.find((event) => event.type === "all-complete");
|
|
1695
|
+
expect(all.transient).toEqual([expect.objectContaining({ company: "acme" })]);
|
|
1696
|
+
expect(all.companies).toEqual([
|
|
1697
|
+
expect.objectContaining({ company: "acme", status: "errored" }),
|
|
1698
|
+
]);
|
|
1603
1699
|
});
|
|
1604
1700
|
/**
|
|
1605
1701
|
* Regression test for the rollup-bug from the personal-sync 401 incident.
|
|
@@ -1890,6 +1986,33 @@ describe("all-complete aggregate", () => {
|
|
|
1890
1986
|
expect(events.find((event) => event.eventName === "sync_runner_completed"))
|
|
1891
1987
|
.toMatchObject({ properties: { status: "partial" } });
|
|
1892
1988
|
});
|
|
1989
|
+
it("records a transient company leg without adding an errored-leg telemetry count", async () => {
|
|
1990
|
+
const posts = [];
|
|
1991
|
+
const client = {
|
|
1992
|
+
...makeVaultStub({
|
|
1993
|
+
memberships: [{ companyUid: "cmp_a" }],
|
|
1994
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
1995
|
+
}),
|
|
1996
|
+
postTelemetryEvents: vi.fn(async (batch) => {
|
|
1997
|
+
posts.push(batch);
|
|
1998
|
+
return { ok: true, written: batch.events.length, skipped: [] };
|
|
1999
|
+
}),
|
|
2000
|
+
};
|
|
2001
|
+
const dnsError = Object.assign(new Error("getaddrinfo ENOTFOUND vault"), {
|
|
2002
|
+
code: "ENOTFOUND",
|
|
2003
|
+
});
|
|
2004
|
+
const deps = makeDeps({
|
|
2005
|
+
createVaultClient: () => client,
|
|
2006
|
+
sync: vi.fn().mockRejectedValue(dnsError),
|
|
2007
|
+
});
|
|
2008
|
+
expect(await runRunner(["--companies"], deps)).toBe(TRANSIENT_NETWORK_EXIT);
|
|
2009
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2010
|
+
const events = posts.flatMap((post) => post.events);
|
|
2011
|
+
expect(events.find((event) => event.eventName === "company_sync_leg_completed"))
|
|
2012
|
+
.toMatchObject({ properties: { status: "transient-network" } });
|
|
2013
|
+
expect(events.find((event) => event.eventName === "sync_runner_completed"))
|
|
2014
|
+
.toMatchObject({ properties: { status: "partial", errorCount: 0 } });
|
|
2015
|
+
});
|
|
1893
2016
|
it("retains terminal failure telemetry when post-client discovery fails", async () => {
|
|
1894
2017
|
const posts = [];
|
|
1895
2018
|
const client = {
|
|
@@ -1937,6 +2060,7 @@ describe("all-complete aggregate", () => {
|
|
|
1937
2060
|
bytesUploaded: 0,
|
|
1938
2061
|
conflictPaths: [],
|
|
1939
2062
|
errors: [],
|
|
2063
|
+
transient: [],
|
|
1940
2064
|
partial: false,
|
|
1941
2065
|
companies: [
|
|
1942
2066
|
{
|
|
@@ -2475,6 +2599,7 @@ describe("--direction", () => {
|
|
|
2475
2599
|
bytesUploaded: 125,
|
|
2476
2600
|
conflictPaths: [],
|
|
2477
2601
|
errors: [],
|
|
2602
|
+
transient: [],
|
|
2478
2603
|
partial: false,
|
|
2479
2604
|
companies: [
|
|
2480
2605
|
{
|
|
@@ -3633,6 +3758,40 @@ describe("runRunnerWithLoop — event-push wiring", () => {
|
|
|
3633
3758
|
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
3634
3759
|
expect(watcher.disposed).toBe(true);
|
|
3635
3760
|
});
|
|
3761
|
+
it("keeps the watch loop alive when a company leg returns transient exit 75 (HQ-SYNC-X)", async () => {
|
|
3762
|
+
const watcher = makeWatcherStub();
|
|
3763
|
+
const dnsError = Object.assign(new Error("getaddrinfo ENOTFOUND vault"), {
|
|
3764
|
+
code: "ENOTFOUND",
|
|
3765
|
+
});
|
|
3766
|
+
const transientPass = makeDeps({
|
|
3767
|
+
createVaultClient: () => makeVaultStub({
|
|
3768
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
3769
|
+
entityGet: (uid) => Promise.resolve({ uid, slug: "acme" }),
|
|
3770
|
+
}),
|
|
3771
|
+
sync: vi.fn().mockRejectedValue(dnsError),
|
|
3772
|
+
});
|
|
3773
|
+
let passes = 0;
|
|
3774
|
+
const runPass = vi.fn().mockImplementation(async () => {
|
|
3775
|
+
passes += 1;
|
|
3776
|
+
// End the otherwise-open watch loop with a later hard failure. The first
|
|
3777
|
+
// pass uses the real company-leg fanout path, not a synthetic exit code.
|
|
3778
|
+
return passes === 1 ? runRunner(["--companies"], transientPass) : 1;
|
|
3779
|
+
});
|
|
3780
|
+
const code = await runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
|
|
3781
|
+
runPass,
|
|
3782
|
+
clock: new FakeClock(),
|
|
3783
|
+
createWatcher: () => watcher,
|
|
3784
|
+
sleep: () => Promise.resolve(),
|
|
3785
|
+
onShutdownSignal: () => () => { },
|
|
3786
|
+
});
|
|
3787
|
+
expect(runPass).toHaveBeenCalledTimes(2);
|
|
3788
|
+
expect(code).toBe(1);
|
|
3789
|
+
expect(code).not.toBe(TRANSIENT_NETWORK_EXIT);
|
|
3790
|
+
expect(transientPass.stderr
|
|
3791
|
+
.events()
|
|
3792
|
+
.some((event) => event.type === "error" && event.path === "(company)")).toBe(false);
|
|
3793
|
+
expect(watcher.disposed).toBe(true);
|
|
3794
|
+
});
|
|
3636
3795
|
it("a PARTIAL_SYNC_EXIT poll pass does NOT surface as a crash — the loop retries", async () => {
|
|
3637
3796
|
// A partial pass (some per-file transfers threw: oversized object on an
|
|
3638
3797
|
// old runner, presign denial, corrupt vault key) must keep
|