@indigoai-us/hq-cli 5.77.14 → 5.79.0

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.
Files changed (309) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/assets/scaffold/core/scripts/archive-old-threads.sh +86 -0
  3. package/assets/scaffold/core/scripts/backfill-company-skill-mirrors.sh +73 -0
  4. package/assets/scaffold/core/scripts/backfill-workspace-mirror.sh +51 -0
  5. package/assets/scaffold/core/scripts/codex-skill-bridge.sh +550 -0
  6. package/assets/scaffold/core/scripts/detect-stale-core-policy-mirror.sh +185 -0
  7. package/assets/scaffold/core/scripts/hq-status-summary.sh +187 -0
  8. package/assets/scaffold/core/scripts/ontology-readme-drift.sh +264 -0
  9. package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +93 -0
  10. package/assets/scaffold/core/scripts/rebuild-all-indexes.sh +69 -0
  11. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +206 -0
  12. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +127 -0
  13. package/assets/scaffold/core/scripts/rebuild-orchestrator-index.sh +86 -0
  14. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +105 -0
  15. package/assets/scaffold/core/scripts/rebuild-public-knowledge-index.sh +83 -0
  16. package/assets/scaffold/core/scripts/rebuild-reports-index.sh +114 -0
  17. package/assets/scaffold/core/scripts/rebuild-social-drafts-index.sh +135 -0
  18. package/assets/scaffold/core/scripts/rebuild-threads-index.sh +82 -0
  19. package/assets/scaffold/core/scripts/rebuild-workers-index.sh +97 -0
  20. package/assets/scaffold/core/scripts/resize-screenshot.sh +47 -0
  21. package/assets/scaffold/core/scripts/token-usage-report.sh +254 -0
  22. package/assets/scaffold/core/scripts/worktree.sh +136 -0
  23. package/dist/commands/agents.js +28 -12
  24. package/dist/commands/api-keys.js +100 -20
  25. package/dist/commands/core.d.ts +90 -0
  26. package/dist/commands/core.js +279 -0
  27. package/dist/commands/files-browse.d.ts +21 -0
  28. package/dist/commands/files-browse.js +145 -24
  29. package/dist/commands/files.js +33 -7
  30. package/dist/commands/meetings.js +16 -9
  31. package/dist/commands/members.js +16 -5
  32. package/dist/commands/pack-install.d.ts +14 -0
  33. package/dist/commands/pack-install.js +42 -2
  34. package/dist/commands/pkg-install.js +39 -2
  35. package/dist/commands/run.js +6 -2
  36. package/dist/commands/secrets.js +120 -65
  37. package/dist/main.js +6 -0
  38. package/dist/utils/api-key-command-gate.d.ts +45 -0
  39. package/dist/utils/api-key-command-gate.js +51 -0
  40. package/dist/utils/hq-roots.d.ts +91 -0
  41. package/dist/utils/hq-roots.js +212 -0
  42. package/dist/utils/resolve-vault-credential.d.ts +63 -3
  43. package/dist/utils/resolve-vault-credential.js +152 -6
  44. package/dist/utils/run-bundled-script.d.ts +66 -0
  45. package/dist/utils/run-bundled-script.js +80 -0
  46. package/dist/utils/vault-api.d.ts +21 -0
  47. package/dist/utils/vault-api.js +74 -3
  48. package/package.json +7 -1
  49. package/.claude/policies/hq-cli-interactive-reads-use-sts-vend.md +0 -49
  50. package/.github/workflows/ci.yml +0 -46
  51. package/.github/workflows/publish.yml +0 -36
  52. package/.github/workflows/scripts/smoke-test-pkg.sh +0 -97
  53. package/e2e/cli.test.ts +0 -128
  54. package/eslint.config.js +0 -37
  55. package/pnpm-workspace.yaml +0 -4
  56. package/scripts/chmod-bins.mjs +0 -49
  57. package/scripts/generate-dsn.mjs +0 -26
  58. package/scripts/generate-scan-packages-table.mjs +0 -113
  59. package/scripts/smoke-sources-signals.sh +0 -103
  60. package/src/bin/hq-auth-refresh.ts +0 -43
  61. package/src/cli-version.ts +0 -13
  62. package/src/commands/__fixtures__/make-tar.ts +0 -126
  63. package/src/commands/_patterns.ts +0 -10
  64. package/src/commands/add.ts +0 -75
  65. package/src/commands/agents.test.ts +0 -527
  66. package/src/commands/agents.ts +0 -923
  67. package/src/commands/api-keys.test.ts +0 -291
  68. package/src/commands/api-keys.ts +0 -382
  69. package/src/commands/artifact-verify.test.ts +0 -177
  70. package/src/commands/auth.test.ts +0 -82
  71. package/src/commands/auth.ts +0 -213
  72. package/src/commands/billing.test.ts +0 -158
  73. package/src/commands/billing.ts +0 -146
  74. package/src/commands/channels.test.ts +0 -74
  75. package/src/commands/channels.ts +0 -91
  76. package/src/commands/cloud-demote.test.ts +0 -401
  77. package/src/commands/cloud-demote.ts +0 -278
  78. package/src/commands/cloud-provision.test.ts +0 -1117
  79. package/src/commands/cloud-provision.ts +0 -942
  80. package/src/commands/cloud.pull-all.test.ts +0 -711
  81. package/src/commands/cloud.pull-per-company.test.ts +0 -188
  82. package/src/commands/cloud.push-all.test.ts +0 -485
  83. package/src/commands/cloud.scope-excluded-warning.test.ts +0 -22
  84. package/src/commands/cloud.selectors.test.ts +0 -208
  85. package/src/commands/cloud.status.test.ts +0 -160
  86. package/src/commands/cloud.test.ts +0 -41
  87. package/src/commands/cloud.ts +0 -2130
  88. package/src/commands/company.test.ts +0 -202
  89. package/src/commands/company.ts +0 -148
  90. package/src/commands/creators.test.ts +0 -60
  91. package/src/commands/creators.ts +0 -117
  92. package/src/commands/crm.test.ts +0 -179
  93. package/src/commands/crm.ts +0 -236
  94. package/src/commands/db-migrate.ts +0 -55
  95. package/src/commands/db-provision.ts +0 -114
  96. package/src/commands/db-sql.ts +0 -124
  97. package/src/commands/db-status.ts +0 -108
  98. package/src/commands/db.ts +0 -26
  99. package/src/commands/dm.test.ts +0 -748
  100. package/src/commands/dm.ts +0 -1226
  101. package/src/commands/feedback.test.ts +0 -413
  102. package/src/commands/feedback.ts +0 -169
  103. package/src/commands/files-browse.test.ts +0 -1249
  104. package/src/commands/files-browse.ts +0 -1506
  105. package/src/commands/files-delete.test.ts +0 -484
  106. package/src/commands/files-recovery.test.ts +0 -370
  107. package/src/commands/files.test.ts +0 -722
  108. package/src/commands/files.ts +0 -1446
  109. package/src/commands/group-grants.test.ts +0 -330
  110. package/src/commands/group-grants.ts +0 -455
  111. package/src/commands/groups.test.ts +0 -44
  112. package/src/commands/groups.ts +0 -368
  113. package/src/commands/integrations.test.ts +0 -633
  114. package/src/commands/integrations.ts +0 -507
  115. package/src/commands/list.ts +0 -85
  116. package/src/commands/login.ts +0 -52
  117. package/src/commands/logout.ts +0 -30
  118. package/src/commands/marketplace-install.test.ts +0 -543
  119. package/src/commands/marketplace-security.test.ts +0 -646
  120. package/src/commands/mcp-registration.test.ts +0 -2787
  121. package/src/commands/mcp-registration.ts +0 -2612
  122. package/src/commands/mcp-status.test.ts +0 -483
  123. package/src/commands/mcp-status.ts +0 -575
  124. package/src/commands/mcp-status.us011.test.ts +0 -243
  125. package/src/commands/meetings.test.ts +0 -522
  126. package/src/commands/meetings.ts +0 -789
  127. package/src/commands/members.test.ts +0 -1474
  128. package/src/commands/members.ts +0 -1181
  129. package/src/commands/onboard-identity-guard.test.ts +0 -99
  130. package/src/commands/onboard-identity-guard.ts +0 -103
  131. package/src/commands/onboard-join.test.ts +0 -96
  132. package/src/commands/onboard-join.ts +0 -101
  133. package/src/commands/onboard-warning.test.ts +0 -26
  134. package/src/commands/onboard-warning.ts +0 -12
  135. package/src/commands/onboard.ts +0 -267
  136. package/src/commands/outposts-heartbeat.test.ts +0 -299
  137. package/src/commands/outposts-heartbeat.ts +0 -310
  138. package/src/commands/outposts-replica-sync.test.ts +0 -272
  139. package/src/commands/outposts-self-deploy.test.ts +0 -243
  140. package/src/commands/outposts.test.ts +0 -1039
  141. package/src/commands/outposts.ts +0 -1935
  142. package/src/commands/pack-install-secret-authorization.test.ts +0 -115
  143. package/src/commands/pack-install.test.ts +0 -1469
  144. package/src/commands/pack-install.ts +0 -2256
  145. package/src/commands/pack-update-cache.test.ts +0 -149
  146. package/src/commands/packs-update-api-key.test.ts +0 -105
  147. package/src/commands/packs.test.ts +0 -88
  148. package/src/commands/packs.ts +0 -622
  149. package/src/commands/people.test.ts +0 -807
  150. package/src/commands/people.ts +0 -422
  151. package/src/commands/pkg-install.dispatch.test.ts +0 -103
  152. package/src/commands/pkg-install.ts +0 -221
  153. package/src/commands/pkg-list.test.ts +0 -79
  154. package/src/commands/pkg-list.ts +0 -13
  155. package/src/commands/pkg-remove.ts +0 -71
  156. package/src/commands/pkg-update.ts +0 -189
  157. package/src/commands/publish.test.ts +0 -650
  158. package/src/commands/publish.ts +0 -580
  159. package/src/commands/reindex.test.ts +0 -259
  160. package/src/commands/reindex.ts +0 -274
  161. package/src/commands/rescue.test.ts +0 -39
  162. package/src/commands/rescue.ts +0 -210
  163. package/src/commands/run.env-local.test.ts +0 -88
  164. package/src/commands/run.test.ts +0 -45
  165. package/src/commands/run.ts +0 -190
  166. package/src/commands/safe-extract.test.ts +0 -459
  167. package/src/commands/safe-extract.ts +0 -444
  168. package/src/commands/secrets-scope.test.ts +0 -56
  169. package/src/commands/secrets-scope.ts +0 -32
  170. package/src/commands/secrets.parse-destination.test.ts +0 -38
  171. package/src/commands/secrets.test.ts +0 -2050
  172. package/src/commands/secrets.ts +0 -2249
  173. package/src/commands/signals.ts +0 -351
  174. package/src/commands/skill.test.ts +0 -770
  175. package/src/commands/skill.ts +0 -796
  176. package/src/commands/sources.ts +0 -362
  177. package/src/commands/sync-mode.test.ts +0 -366
  178. package/src/commands/sync-mode.ts +0 -387
  179. package/src/commands/sync-narrow.test.ts +0 -633
  180. package/src/commands/sync-narrow.ts +0 -555
  181. package/src/commands/sync.ts +0 -165
  182. package/src/commands/team-sync.ts +0 -628
  183. package/src/commands/update.ts +0 -71
  184. package/src/commands/whoami.ts +0 -83
  185. package/src/commands/workers.test.ts +0 -158
  186. package/src/commands/workers.ts +0 -301
  187. package/src/index.test.ts +0 -32
  188. package/src/index.ts +0 -20
  189. package/src/lib/db/control-plane.test.ts +0 -59
  190. package/src/lib/db/control-plane.ts +0 -113
  191. package/src/lib/db/local.test.ts +0 -81
  192. package/src/lib/db/local.ts +0 -148
  193. package/src/lib/db/migrate.test.ts +0 -133
  194. package/src/lib/db/migrate.ts +0 -137
  195. package/src/lib/db/paths.test.ts +0 -112
  196. package/src/lib/db/paths.ts +0 -128
  197. package/src/lib/db/remote-engine.test.ts +0 -44
  198. package/src/lib/db/remote-engine.ts +0 -148
  199. package/src/lib/db/remote-sql.test.ts +0 -32
  200. package/src/lib/db/remote-sql.ts +0 -62
  201. package/src/lib/db/sql.test.ts +0 -106
  202. package/src/lib/db/sql.ts +0 -192
  203. package/src/lib/hq-cloud-dep.smoke.test.ts +0 -75
  204. package/src/lib/local-tree-diff.test.ts +0 -301
  205. package/src/lib/local-tree-diff.ts +0 -362
  206. package/src/lib/narrow-hint-banner.test.ts +0 -235
  207. package/src/lib/narrow-hint-banner.ts +0 -212
  208. package/src/lib/plan-limit-nag.test.ts +0 -317
  209. package/src/lib/plan-limit-nag.ts +0 -264
  210. package/src/main.ts +0 -371
  211. package/src/node-preflight.test.ts +0 -60
  212. package/src/node-preflight.ts +0 -67
  213. package/src/outpost/session-heartbeat-bounds.test.ts +0 -195
  214. package/src/outpost/session-heartbeat-guard.test.ts +0 -105
  215. package/src/outpost/session-heartbeat-publisher.test.ts +0 -178
  216. package/src/outpost/session-heartbeat-publisher.ts +0 -186
  217. package/src/outpost/session-heartbeat-retain-guard.test.ts +0 -126
  218. package/src/outpost/session-heartbeat.test.ts +0 -459
  219. package/src/outpost/session-heartbeat.ts +0 -877
  220. package/src/packaging.test.ts +0 -109
  221. package/src/run/__fixtures__/discover-schemas/example.env.schema +0 -4
  222. package/src/run/discover-schemas.test.ts +0 -153
  223. package/src/run/discover-schemas.ts +0 -79
  224. package/src/run/hq-plugin.test.ts +0 -371
  225. package/src/run/hq-plugin.ts +0 -277
  226. package/src/run/varlock-shape.test.ts +0 -57
  227. package/src/schemas/hq-package.schema.json +0 -155
  228. package/src/sentry-before-send.test.ts +0 -359
  229. package/src/sentry-before-send.ts +0 -221
  230. package/src/sentry-epipe.test.ts +0 -37
  231. package/src/sentry-release.test.ts +0 -54
  232. package/src/sentry.test.ts +0 -24
  233. package/src/sentry.ts +0 -54
  234. package/src/strategies/link.ts +0 -62
  235. package/src/strategies/merge.ts +0 -142
  236. package/src/types.ts +0 -146
  237. package/src/utils/__fixtures__/scan-packages.generated-block.sh +0 -23
  238. package/src/utils/auth-error.test.ts +0 -40
  239. package/src/utils/auth-error.ts +0 -42
  240. package/src/utils/billing-gate.test.ts +0 -95
  241. package/src/utils/billing-gate.ts +0 -228
  242. package/src/utils/breadcrumb-buffer.ts +0 -18
  243. package/src/utils/cli-telemetry.test.ts +0 -153
  244. package/src/utils/cli-telemetry.ts +0 -61
  245. package/src/utils/cognito-session.machine.test.ts +0 -109
  246. package/src/utils/cognito-session.refresh.test.ts +0 -84
  247. package/src/utils/cognito-session.test.ts +0 -328
  248. package/src/utils/cognito-session.ts +0 -448
  249. package/src/utils/company-selection-error.test.ts +0 -42
  250. package/src/utils/company-selection-error.ts +0 -45
  251. package/src/utils/contribution-table.ts +0 -83
  252. package/src/utils/environmental-error.test.ts +0 -45
  253. package/src/utils/environmental-error.ts +0 -39
  254. package/src/utils/epipe.test.ts +0 -28
  255. package/src/utils/epipe.ts +0 -29
  256. package/src/utils/expected-cli-error.test.ts +0 -28
  257. package/src/utils/expected-cli-error.ts +0 -39
  258. package/src/utils/feedback-diagnostics.test.ts +0 -183
  259. package/src/utils/feedback-diagnostics.ts +0 -123
  260. package/src/utils/feedback-screenshots.test.ts +0 -134
  261. package/src/utils/feedback-screenshots.ts +0 -124
  262. package/src/utils/feedback-versions.test.ts +0 -98
  263. package/src/utils/feedback-versions.ts +0 -68
  264. package/src/utils/git.ts +0 -74
  265. package/src/utils/integrity.ts +0 -59
  266. package/src/utils/intercepted-process-exit.test.ts +0 -37
  267. package/src/utils/intercepted-process-exit.ts +0 -36
  268. package/src/utils/login-provider.test.ts +0 -42
  269. package/src/utils/login-provider.ts +0 -30
  270. package/src/utils/manifest.test.ts +0 -167
  271. package/src/utils/manifest.ts +0 -131
  272. package/src/utils/pack-contributions.test.ts +0 -603
  273. package/src/utils/pack-contributions.ts +0 -450
  274. package/src/utils/people.ts +0 -215
  275. package/src/utils/registry-client.ts +0 -204
  276. package/src/utils/registry.ts +0 -90
  277. package/src/utils/resolve-vault-credential.test.ts +0 -69
  278. package/src/utils/resolve-vault-credential.ts +0 -60
  279. package/src/utils/sandbox-runner-client.test.ts +0 -279
  280. package/src/utils/sandbox-runner-client.ts +0 -260
  281. package/src/utils/secrets-cache.ts +0 -183
  282. package/src/utils/sentry-identity.ts +0 -45
  283. package/src/utils/settle-with-timeout.test.ts +0 -21
  284. package/src/utils/settle-with-timeout.ts +0 -22
  285. package/src/utils/user-yaml-error.test.ts +0 -24
  286. package/src/utils/user-yaml-error.ts +0 -30
  287. package/src/utils/vault-api.test.ts +0 -536
  288. package/src/utils/vault-api.ts +0 -382
  289. package/src/utils/version-check.test.ts +0 -176
  290. package/src/utils/version-check.ts +0 -155
  291. package/src/utils/version-gate.test.ts +0 -1024
  292. package/src/utils/version-gate.ts +0 -702
  293. package/test/commands/db-tenant-isolation.test.ts +0 -94
  294. package/test/commands/db.test.ts +0 -85
  295. package/test/commands/signals.test.ts +0 -207
  296. package/test/commands/sources.test.ts +0 -231
  297. package/test/e2e/smoke-install-mcp.sh +0 -113
  298. package/test/fixtures/hq-pack-smoke-mcp/mcp/smoke-http.json +0 -1
  299. package/test/fixtures/hq-pack-smoke-mcp/package.yaml +0 -11
  300. package/test/fixtures/signals/action_item/sample.md +0 -16
  301. package/test/fixtures/signals/summary/sample.md +0 -12
  302. package/test/fixtures/sources/meetings/sample.md +0 -25
  303. package/test/helpers/cli-runner.ts +0 -150
  304. package/test/helpers/s3-list-mock.ts +0 -79
  305. package/test/helpers/vault-service-mock.ts +0 -295
  306. package/test/sources-signals/smoke.test.ts +0 -228
  307. package/tsconfig.json +0 -20
  308. package/vitest.config.ts +0 -11
  309. package/vitest.e2e.config.ts +0 -7
@@ -1,633 +0,0 @@
1
- /**
2
- * Unit tests for `hq integrations list|tools|call|approve` (integrations.ts).
3
- *
4
- * Coverage:
5
- * - toolPrefixForProvider — factory-prefix stripping + slug normalization
6
- * (must mirror hq-pro's factoryToolPrefix so composed HQ tool names hit
7
- * registered gateway tools)
8
- * - selectConnection — by id, by provider (factory:-prefixed match),
9
- * single-active default, and the guiding errors for none/ambiguous
10
- * - unwrapGatewayResult — MCP text-content unwrapping + non-JSON fallback
11
- * - queued-outcome detection + the approve hint in `call`
12
- * - approve — posts to the confirm route and prints the unwrapped result
13
- */
14
-
15
- import { Command } from "commander";
16
- import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from "vitest";
17
-
18
- vi.mock("../utils/cognito-session.js", async (importOriginal) => {
19
- const original =
20
- await importOriginal<typeof import("../utils/cognito-session.js")>();
21
- return {
22
- ...original,
23
- ensureCognitoIdToken: vi.fn(async () => "test-id-token"),
24
- };
25
- });
26
-
27
- vi.mock("../utils/vault-api.js", async (importOriginal) => {
28
- const original = await importOriginal<typeof import("../utils/vault-api.js")>();
29
- return {
30
- ...original,
31
- getCompanyUid: vi.fn(async () => "cmp_acme"),
32
- vaultApiFetch: vi.fn(),
33
- };
34
- });
35
-
36
- import { vaultApiFetch } from "../utils/vault-api.js";
37
- import { AuthError, isAuthError } from "../utils/auth-error.js";
38
- import {
39
- IntegrationsCliError,
40
- queuedOutcome,
41
- registerIntegrationsCommand,
42
- selectConnection,
43
- toolPrefixForProvider,
44
- unwrapGatewayResult,
45
- } from "./integrations.js";
46
-
47
- const vaultApiFetchMock = vi.mocked(vaultApiFetch);
48
-
49
- function jsonResponse(body: unknown, status = 200): Response {
50
- return {
51
- ok: status >= 200 && status < 300,
52
- status,
53
- json: async () => body,
54
- } as unknown as Response;
55
- }
56
-
57
- function connectionsResponse() {
58
- return jsonResponse({
59
- connections: [
60
- {
61
- id: "acct_factory_linear",
62
- provider: "factory:linear",
63
- status: "connected",
64
- writePolicy: "confirm",
65
- installation: { displayName: "Linear MCP server", status: "installed" },
66
- },
67
- ],
68
- });
69
- }
70
-
71
- function gatewayTextResponse(payload: unknown) {
72
- return jsonResponse({
73
- jsonrpc: "2.0",
74
- id: "x",
75
- result: { content: [{ type: "text", text: JSON.stringify(payload) }] },
76
- });
77
- }
78
-
79
- async function runCli(argv: string[]): Promise<void> {
80
- const program = new Command();
81
- program.exitOverride();
82
- registerIntegrationsCommand(program);
83
- await program.parseAsync(["node", "hq", ...argv]);
84
- }
85
-
86
- let logSpy: MockInstance;
87
- beforeEach(() => {
88
- logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
89
- });
90
- afterEach(() => {
91
- vi.clearAllMocks();
92
- logSpy.mockRestore();
93
- });
94
-
95
- function logged(): string {
96
- return logSpy.mock.calls.map((call) => call.join(" ")).join("\n");
97
- }
98
-
99
- describe("toolPrefixForProvider", () => {
100
- it("strips the factory: prefix and normalizes to the gateway's tool prefix", () => {
101
- expect(toolPrefixForProvider("factory:linear")).toBe("linear");
102
- expect(toolPrefixForProvider("factory:google-workspace")).toBe("google.workspace");
103
- expect(toolPrefixForProvider("Linear")).toBe("linear");
104
- });
105
- });
106
-
107
- describe("selectConnection", () => {
108
- const linear = {
109
- id: "acct_1",
110
- provider: "factory:linear",
111
- status: "connected",
112
- };
113
- const notion = {
114
- id: "acct_2",
115
- provider: "factory:notion",
116
- status: "connected",
117
- };
118
-
119
- it("matches --provider against the bare factory slug", () => {
120
- expect(selectConnection([linear, notion], { provider: "linear" })).toBe(linear);
121
- });
122
-
123
- it("defaults to the single active connection", () => {
124
- expect(selectConnection([linear], {})).toBe(linear);
125
- });
126
-
127
- it("guides on ambiguity and on unknown providers", () => {
128
- expect(() => selectConnection([linear, notion], {})).toThrow(/--provider/);
129
- expect(() => selectConnection([linear], { provider: "jira" })).toThrow(
130
- /Connected: linear/,
131
- );
132
- expect(() => selectConnection([], {})).toThrow(/No connected apps yet/);
133
- });
134
-
135
- it("resolves an explicit connection id even when revoked", () => {
136
- const revoked = { ...linear, status: "revoked" };
137
- expect(selectConnection([revoked], { connection: "acct_1" })).toBe(revoked);
138
- expect(() => selectConnection([revoked], { connection: "acct_9" })).toThrow(
139
- IntegrationsCliError,
140
- );
141
- });
142
- });
143
-
144
- describe("unwrapGatewayResult + queuedOutcome", () => {
145
- it("unwraps MCP text content into the inner JSON payload", () => {
146
- expect(
147
- unwrapGatewayResult({
148
- content: [{ type: "text", text: '{"tools":[{"name":"list_issues"}]}' }],
149
- }),
150
- ).toEqual({ tools: [{ name: "list_issues" }] });
151
- });
152
-
153
- it("falls back to raw text / raw result on non-JSON shapes", () => {
154
- expect(
155
- unwrapGatewayResult({ content: [{ type: "text", text: "plain" }] }),
156
- ).toBe("plain");
157
- expect(unwrapGatewayResult({ other: 1 })).toEqual({ other: 1 });
158
- });
159
-
160
- it("detects the queued-for-approval outcome", () => {
161
- expect(
162
- queuedOutcome({ queuedForApproval: true, queueId: "cq_1" }),
163
- ).toMatchObject({ queueId: "cq_1" });
164
- expect(queuedOutcome({ issues: [] })).toBeNull();
165
- });
166
- });
167
-
168
- describe("hq integrations tools", () => {
169
- it("composes the read tool name from the connection's provider and lists upstream tools", async () => {
170
- vaultApiFetchMock
171
- .mockResolvedValueOnce(connectionsResponse())
172
- .mockResolvedValueOnce(
173
- gatewayTextResponse({
174
- tools: [{ name: "list_issues", title: "List issues" }],
175
- }),
176
- );
177
-
178
- await runCli(["integrations", "tools", "--provider", "linear"]);
179
-
180
- const gatewayCall = vaultApiFetchMock.mock.calls[1]![0];
181
- expect(gatewayCall).toMatchObject({
182
- path: "/v1/integrations/mcp",
183
- method: "POST",
184
- });
185
- expect(gatewayCall.body).toMatchObject({
186
- method: "tools/call",
187
- params: {
188
- companyUid: "cmp_acme",
189
- name: "linear.mcp.tools.list",
190
- arguments: { companyUid: "cmp_acme", connectionId: "acct_factory_linear" },
191
- },
192
- });
193
- expect(logged()).toContain("list_issues");
194
- });
195
- });
196
-
197
- describe("hq integrations call", () => {
198
- it("sends the upstream tool + args + idempotency key and prints the result", async () => {
199
- vaultApiFetchMock
200
- .mockResolvedValueOnce(connectionsResponse())
201
- .mockResolvedValueOnce(gatewayTextResponse({ issues: [{ id: "DEV-1" }] }));
202
-
203
- await runCli([
204
- "integrations",
205
- "call",
206
- "list_issues",
207
- "--provider",
208
- "linear",
209
- "--args",
210
- '{"assignee":"me"}',
211
- "--idempotency-key",
212
- "stable-key",
213
- ]);
214
-
215
- const gatewayCall = vaultApiFetchMock.mock.calls[1]![0];
216
- expect(gatewayCall.body).toMatchObject({
217
- params: {
218
- name: "linear.mcp.tools.call",
219
- arguments: {
220
- toolName: "list_issues",
221
- arguments: { assignee: "me" },
222
- idempotencyKey: "stable-key",
223
- },
224
- },
225
- });
226
- expect(logged()).toContain("DEV-1");
227
- });
228
-
229
- it("surfaces a queued outcome with the exact approve command", async () => {
230
- vaultApiFetchMock
231
- .mockResolvedValueOnce(connectionsResponse())
232
- .mockResolvedValueOnce(
233
- gatewayTextResponse({
234
- queuedForApproval: true,
235
- status: "queued",
236
- queueId: "cq_123",
237
- connectionId: "acct_factory_linear",
238
- }),
239
- );
240
-
241
- await runCli([
242
- "integrations",
243
- "call",
244
- "save_issue",
245
- "--provider",
246
- "linear",
247
- "--args",
248
- "{}",
249
- ]);
250
-
251
- const output = logged();
252
- expect(output).toContain("Queued for approval");
253
- expect(output).toContain("hq integrations approve cq_123 --provider linear");
254
- });
255
-
256
- it("rejects non-object --args with a usage hint", async () => {
257
- await expect(
258
- runCli(["integrations", "call", "t", "--provider", "linear", "--args", "[1]"]),
259
- ).rejects.toThrow(/--args must be a JSON object/);
260
- expect(vaultApiFetchMock).not.toHaveBeenCalled();
261
- });
262
-
263
- it("marks non-object --args as expected", async () => {
264
- try {
265
- await runCli(["integrations", "call", "t", "--provider", "linear", "--args", "[1]"]);
266
- throw new Error("expected runCli to throw");
267
- } catch (err) {
268
- expect(err).toBeInstanceOf(IntegrationsCliError);
269
- expect((err as IntegrationsCliError).expected).toBe(true);
270
- }
271
- });
272
- });
273
-
274
- describe("hq integrations approve", () => {
275
- it("posts the decision to the confirm route and prints the unwrapped result", async () => {
276
- vaultApiFetchMock
277
- .mockResolvedValueOnce(connectionsResponse())
278
- .mockResolvedValueOnce(
279
- jsonResponse({
280
- status: "resumed",
281
- result: { content: [{ type: "text", text: '{"issues":[]}' }] },
282
- }),
283
- );
284
-
285
- await runCli(["integrations", "approve", "cq_123", "--provider", "linear"]);
286
-
287
- const confirmCall = vaultApiFetchMock.mock.calls[1]![0];
288
- expect(confirmCall).toMatchObject({
289
- path: "/v1/integrations/confirm/cq_123/approve",
290
- method: "POST",
291
- body: { companyUid: "cmp_acme", connectionId: "acct_factory_linear" },
292
- });
293
- expect(logged()).toContain("Approved");
294
- });
295
- });
296
-
297
- describe("expected integrations errors", () => {
298
- const linear = {
299
- id: "acct_1",
300
- provider: "factory:linear",
301
- status: "connected",
302
- };
303
- const notion = {
304
- id: "acct_2",
305
- provider: "factory:notion",
306
- status: "connected",
307
- };
308
-
309
- it("marks a non-owner 403 approve as expected (skips Sentry) and preserves the message", async () => {
310
- vaultApiFetchMock
311
- .mockResolvedValueOnce(connectionsResponse())
312
- .mockResolvedValueOnce(
313
- jsonResponse(
314
- { error: "Only a company owner can approve or reject queued integration writes" },
315
- 403,
316
- ),
317
- );
318
-
319
- try {
320
- await runCli(["integrations", "approve", "cq_123", "--provider", "linear"]);
321
- throw new Error("expected runCli to throw");
322
- } catch (err) {
323
- expect(err).toBeInstanceOf(IntegrationsCliError);
324
- expect((err as IntegrationsCliError).expected).toBe(true);
325
- expect((err as Error).message).toContain("Only a company owner");
326
- }
327
- });
328
-
329
- it("still captures a genuine server 500 on approve (expected === false)", async () => {
330
- vaultApiFetchMock
331
- .mockResolvedValueOnce(connectionsResponse())
332
- .mockResolvedValueOnce(jsonResponse({ error: "boom" }, 500));
333
-
334
- try {
335
- await runCli(["integrations", "approve", "cq_123", "--provider", "linear"]);
336
- throw new Error("expected runCli to throw");
337
- } catch (err) {
338
- expect(err).toBeInstanceOf(IntegrationsCliError);
339
- expect((err as IntegrationsCliError).expected).toBe(false);
340
- }
341
- });
342
-
343
- it("marks reject 403 as expected too", async () => {
344
- vaultApiFetchMock
345
- .mockResolvedValueOnce(connectionsResponse())
346
- .mockResolvedValueOnce(
347
- jsonResponse(
348
- { error: "Only a company owner can approve or reject queued integration writes" },
349
- 403,
350
- ),
351
- );
352
-
353
- try {
354
- await runCli(["integrations", "reject", "cq_123", "--provider", "linear"]);
355
- throw new Error("expected runCli to throw");
356
- } catch (err) {
357
- expect(err).toBeInstanceOf(IntegrationsCliError);
358
- expect((err as IntegrationsCliError).expected).toBe(true);
359
- }
360
-
361
- expect(vaultApiFetchMock.mock.calls[1]![0].path).toBe(
362
- "/v1/integrations/confirm/cq_123/reject",
363
- );
364
- });
365
-
366
- it("marks local connection selection usage errors as expected", () => {
367
- for (const fn of [
368
- () => selectConnection([], {}),
369
- () => selectConnection([linear, notion], {}),
370
- () => selectConnection([linear], { provider: "jira" }),
371
- ]) {
372
- try {
373
- fn();
374
- throw new Error("expected selectConnection to throw");
375
- } catch (err) {
376
- expect(err).toBeInstanceOf(IntegrationsCliError);
377
- expect((err as IntegrationsCliError).expected).toBe(true);
378
- }
379
- }
380
- });
381
- });
382
-
383
- // HQ-CLI-9: `hq integrations call get_board_info --provider monday …` hit a 401
384
- // on POST /v1/integrations/mcp while the caller's HQ session was rejected. The
385
- // old `callGateway` threw a plain, opaque `IntegrationsCliError("Integration
386
- // gateway request failed (HTTP 401).")` that shipped to Sentry as an
387
- // unactionable fatal. A gateway 401 is an expired/missing session — the same
388
- // expected auth state the vault company-resolution paths already raise as
389
- // `AuthError` (HQ-CLI-8). The fix routes every integration-gateway 401 through
390
- // that typed AuthError so the user gets one actionable "run `hq login`" message
391
- // and Sentry is skipped, while genuine 4xx/5xx faults keep their behavior.
392
- describe("integration gateway 401 → AuthError (HQ-CLI-9)", () => {
393
- it("throws an actionable AuthError (not an opaque IntegrationsCliError) when callGateway 401s", async () => {
394
- vaultApiFetchMock
395
- .mockResolvedValueOnce(connectionsResponse())
396
- .mockResolvedValueOnce(jsonResponse({ error: "Unauthorized" }, 401));
397
-
398
- const err = await runCli([
399
- "integrations",
400
- "call",
401
- "get_board_info",
402
- "--provider",
403
- "linear",
404
- "--args",
405
- "{}",
406
- ]).then(
407
- () => {
408
- throw new Error("expected runCli to throw");
409
- },
410
- (e: unknown) => e,
411
- );
412
-
413
- expect(isAuthError(err)).toBe(true);
414
- expect(err).toBeInstanceOf(AuthError);
415
- expect((err as Error).message).toMatch(/hq login/);
416
- // The gateway request was actually attempted (connections + gateway call).
417
- expect(vaultApiFetchMock.mock.calls[1]![0].path).toBe("/v1/integrations/mcp");
418
- });
419
-
420
- it("throws an AuthError when the admin (fetchConnections) call 401s", async () => {
421
- vaultApiFetchMock.mockResolvedValueOnce(jsonResponse({ error: "Unauthorized" }, 401));
422
-
423
- const err = await runCli(["integrations", "list"]).then(
424
- () => {
425
- throw new Error("expected runCli to throw");
426
- },
427
- (e: unknown) => e,
428
- );
429
-
430
- expect(isAuthError(err)).toBe(true);
431
- expect((err as Error).message).toMatch(/hq login/);
432
- });
433
-
434
- it("throws an AuthError when an approve confirm call 401s", async () => {
435
- vaultApiFetchMock
436
- .mockResolvedValueOnce(connectionsResponse())
437
- .mockResolvedValueOnce(jsonResponse({ error: "Unauthorized" }, 401));
438
-
439
- const err = await runCli([
440
- "integrations",
441
- "approve",
442
- "cq_123",
443
- "--provider",
444
- "linear",
445
- ]).then(
446
- () => {
447
- throw new Error("expected runCli to throw");
448
- },
449
- (e: unknown) => e,
450
- );
451
-
452
- expect(isAuthError(err)).toBe(true);
453
- expect((err as Error).message).toMatch(/hq login/);
454
- });
455
-
456
- // Scope guards: only a 401 becomes an AuthError. A genuine server 500 still
457
- // reports (expected === false), and a provider-level JSON-RPC error (HTTP 200
458
- // with `message.error`) stays an IntegrationsCliError — so real faults and
459
- // upstream tool errors are never misclassified as an auth state.
460
- it("keeps a gateway 500 as a reporting IntegrationsCliError (not an AuthError)", async () => {
461
- vaultApiFetchMock
462
- .mockResolvedValueOnce(connectionsResponse())
463
- .mockResolvedValueOnce(jsonResponse({ error: "boom" }, 500));
464
-
465
- const err = await runCli([
466
- "integrations",
467
- "call",
468
- "get_board_info",
469
- "--provider",
470
- "linear",
471
- "--args",
472
- "{}",
473
- ]).then(
474
- () => {
475
- throw new Error("expected runCli to throw");
476
- },
477
- (e: unknown) => e,
478
- );
479
-
480
- expect(isAuthError(err)).toBe(false);
481
- expect(err).toBeInstanceOf(IntegrationsCliError);
482
- expect((err as IntegrationsCliError).expected).toBe(false);
483
- });
484
-
485
- it("keeps an upstream provider error (HTTP 200 message.error) as an IntegrationsCliError", async () => {
486
- vaultApiFetchMock
487
- .mockResolvedValueOnce(connectionsResponse())
488
- .mockResolvedValueOnce(
489
- jsonResponse({
490
- jsonrpc: "2.0",
491
- id: "x",
492
- error: { code: -32050, message: "monday rejected the board id" },
493
- }),
494
- );
495
-
496
- const err = await runCli([
497
- "integrations",
498
- "call",
499
- "get_board_info",
500
- "--provider",
501
- "linear",
502
- "--args",
503
- "{}",
504
- ]).then(
505
- () => {
506
- throw new Error("expected runCli to throw");
507
- },
508
- (e: unknown) => e,
509
- );
510
-
511
- expect(isAuthError(err)).toBe(false);
512
- expect(err).toBeInstanceOf(IntegrationsCliError);
513
- expect((err as Error).message).toMatch(/monday rejected the board id/);
514
- });
515
- });
516
-
517
- // HQ-CLI-B: `hq integrations tools|call … --provider …` against a connection the
518
- // caller has NOT been granted returned HTTP 200 with a JSON-RPC error
519
- // { code: -32003, message: "You do not have access to this integration. Ask its
520
- // owner to share it with you.", data: { code: "IntegrationAccessDenied" } }.
521
- // `callGateway` threw an IntegrationsCliError WITHOUT `expected`, so it defaulted
522
- // to `expected === false` and the top-level handler shipped a GOVERNED, correct
523
- // access denial to Sentry as an error-level fatal — 40 identical, unfixable
524
- // events for one user. A caller-side JSON-RPC code (UNAUTHORIZED / INVALID_PARAMS)
525
- // is the analog of a client 4xx and must be printed to the user, not captured;
526
- // genuine server/provider/protocol faults must still report.
527
- describe("gateway JSON-RPC error classification (HQ-CLI-B)", () => {
528
- function gatewayJsonRpcError(code: number | undefined, message: string, data?: unknown) {
529
- return jsonResponse({
530
- jsonrpc: "2.0",
531
- id: "x",
532
- error: { ...(code == null ? {} : { code }), message, ...(data ? { data } : {}) },
533
- });
534
- }
535
-
536
- async function runGatewayCall(): Promise<unknown> {
537
- return runCli([
538
- "integrations",
539
- "call",
540
- "get_board_info",
541
- "--provider",
542
- "linear",
543
- "--args",
544
- "{}",
545
- ]).then(
546
- () => {
547
- throw new Error("expected runCli to throw");
548
- },
549
- (e: unknown) => e,
550
- );
551
- }
552
-
553
- it("marks the access-denied gateway error (-32003) as expected and preserves its actionable message", async () => {
554
- vaultApiFetchMock
555
- .mockResolvedValueOnce(connectionsResponse())
556
- .mockResolvedValueOnce(
557
- gatewayJsonRpcError(
558
- -32003,
559
- "You do not have access to this integration. Ask its owner to share it with you.",
560
- { code: "IntegrationAccessDenied" },
561
- ),
562
- );
563
-
564
- const err = await runGatewayCall();
565
-
566
- expect(err).toBeInstanceOf(IntegrationsCliError);
567
- // The regression: this was `false` and flooded Sentry with a governed denial.
568
- expect((err as IntegrationsCliError).expected).toBe(true);
569
- // A connection-level denial is NOT an expired session — never an AuthError.
570
- expect(isAuthError(err)).toBe(false);
571
- expect((err as Error).message).toContain("do not have access to this integration");
572
- });
573
-
574
- it("also marks access-denied as expected on the `tools` path (same callGateway seam)", async () => {
575
- vaultApiFetchMock
576
- .mockResolvedValueOnce(connectionsResponse())
577
- .mockResolvedValueOnce(
578
- gatewayJsonRpcError(
579
- -32003,
580
- "You do not have access to this integration. Ask its owner to share it with you.",
581
- ),
582
- );
583
-
584
- const err = await runCli(["integrations", "tools", "--provider", "linear"]).then(
585
- () => {
586
- throw new Error("expected runCli to throw");
587
- },
588
- (e: unknown) => e,
589
- );
590
-
591
- expect(err).toBeInstanceOf(IntegrationsCliError);
592
- expect((err as IntegrationsCliError).expected).toBe(true);
593
- });
594
-
595
- it("marks an invalid-params gateway error (-32602) as expected", async () => {
596
- vaultApiFetchMock
597
- .mockResolvedValueOnce(connectionsResponse())
598
- .mockResolvedValueOnce(gatewayJsonRpcError(-32602, "Unknown integration tool: get_board_info"));
599
-
600
- const err = await runGatewayCall();
601
-
602
- expect(err).toBeInstanceOf(IntegrationsCliError);
603
- expect((err as IntegrationsCliError).expected).toBe(true);
604
- });
605
-
606
- it("still reports genuine provider/internal/conflict gateway faults (expected === false)", async () => {
607
- // -32050 PROVIDER_ERROR, -32603 INTERNAL_ERROR, and -32009 CONFLICT (which the
608
- // gateway raises for a confirm queue being unavailable or an owner
609
- // notification failing) are real faults that must keep reaching Sentry.
610
- for (const code of [-32050, -32603, -32009]) {
611
- vaultApiFetchMock
612
- .mockResolvedValueOnce(connectionsResponse())
613
- .mockResolvedValueOnce(gatewayJsonRpcError(code, "gateway fault"));
614
-
615
- const err = await runGatewayCall();
616
-
617
- expect(err).toBeInstanceOf(IntegrationsCliError);
618
- expect((err as IntegrationsCliError).expected).toBe(false);
619
- vaultApiFetchMock.mockClear();
620
- }
621
- });
622
-
623
- it("reports a gateway error with no JSON-RPC code (unclassified → expected === false)", async () => {
624
- vaultApiFetchMock
625
- .mockResolvedValueOnce(connectionsResponse())
626
- .mockResolvedValueOnce(gatewayJsonRpcError(undefined, "codeless failure"));
627
-
628
- const err = await runGatewayCall();
629
-
630
- expect(err).toBeInstanceOf(IntegrationsCliError);
631
- expect((err as IntegrationsCliError).expected).toBe(false);
632
- });
633
- });