@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.
@@ -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
- * Regression test: AWS SDK v3 raises an opaque `UnknownError` (name and
1556
- * message both literally "UnknownError") when it cannot match a Node-side
1557
- * networking failure to a modeled exception. The actual root cause —
1558
- * ENOTFOUND from a stale DNS cache, ECONNRESET on a flapping link, etc.
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("per-company error event surfaces cause-chain (ENOTFOUND from SDK UnknownError)", async () => {
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(2);
1592
- const companyErr = deps.stderr
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((e) => e.type === "error" && e.company === "privy");
1595
- expect(companyErr).toBeDefined();
1596
- expect(companyErr?.path).toBe("(company)");
1597
- // The diagnostic must carry the original networking signal — without
1598
- // this the operator only sees "UnknownError" and the run is unactionable.
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