@bugmole/cli 0.4.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 (308) hide show
  1. package/.bugmole.env.example +20 -0
  2. package/LICENSE +7 -0
  3. package/README.md +293 -0
  4. package/TESTING.md +117 -0
  5. package/bugmole.config.yaml +39 -0
  6. package/package.json +85 -0
  7. package/scripts/billing/paypal-setup.mjs +121 -0
  8. package/scripts/bugmole-continue.ts +318 -0
  9. package/scripts/bugmole-init.ts +188 -0
  10. package/scripts/bugmole.cjs +17 -0
  11. package/scripts/bugmole.test.ts +344 -0
  12. package/scripts/bugmole.ts +657 -0
  13. package/scripts/ensure-maestro.cjs +79 -0
  14. package/scripts/ios-tunnel-keeper.sh +45 -0
  15. package/scripts/ios-wda-keeper.sh +66 -0
  16. package/scripts/sync-plan-catalog.d.mts +3 -0
  17. package/scripts/sync-plan-catalog.mjs +16 -0
  18. package/scripts/ui-parity-diff.py +65 -0
  19. package/scripts/ui-parity-requirements.txt +1 -0
  20. package/scripts/verify-manage-to-plans.mts +194 -0
  21. package/spec/app-ui-audit.schema.json +176 -0
  22. package/spec/blockers.yaml +79 -0
  23. package/spec/bugs.index.json +42 -0
  24. package/spec/design-dna.schema.json +38 -0
  25. package/spec/domain_rules.yaml +24 -0
  26. package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
  27. package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
  28. package/spec/journey_graph.yaml +126 -0
  29. package/spec/journeys.graph.json +2618 -0
  30. package/spec/plans/dashboard-smoke.flow.yaml +20 -0
  31. package/spec/plans/example.flow.yaml +99 -0
  32. package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
  33. package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
  34. package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
  35. package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
  36. package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
  37. package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
  38. package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
  39. package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
  40. package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
  41. package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
  42. package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
  43. package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
  44. package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
  45. package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
  46. package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
  47. package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
  48. package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
  49. package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
  50. package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
  51. package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
  52. package/spec/plans/login.flow.yaml +22 -0
  53. package/spec/plans/owner-operations.flow.yaml +20 -0
  54. package/spec/project_config.yaml +55 -0
  55. package/spec/roles.yaml +30 -0
  56. package/spec/schema.md +394 -0
  57. package/spec/test-case-results.schema.json +62 -0
  58. package/spec/test-cases.schema.json +85 -0
  59. package/spec/ui-parity-audit.schema.json +194 -0
  60. package/spec/ui-reverse-engineering.schema.json +94 -0
  61. package/src/billing/plan-catalog.test.ts +46 -0
  62. package/src/billing/plan-catalog.ts +199 -0
  63. package/src/integrations/aws-sigv4.test.ts +42 -0
  64. package/src/integrations/aws-sigv4.ts +72 -0
  65. package/src/integrations/device-farm.ts +155 -0
  66. package/src/integrations/github-app.test.ts +57 -0
  67. package/src/integrations/github-app.ts +143 -0
  68. package/src/integrations/gitlab.ts +81 -0
  69. package/src/integrations/temp-email.test.ts +123 -0
  70. package/src/integrations/temp-email.ts +175 -0
  71. package/src/integrations/testflight-feedback.test.ts +51 -0
  72. package/src/integrations/testflight-feedback.ts +173 -0
  73. package/src/integrations/webdriver-client.ts +131 -0
  74. package/src/mcp/server.test.ts +1220 -0
  75. package/src/mcp/server.ts +3064 -0
  76. package/src/mcp/write-test-cases.test.ts +287 -0
  77. package/src/registry/api-key-client.ts +39 -0
  78. package/src/registry/control-plane-client.ts +212 -0
  79. package/src/registry/migrations/0001_registry.sql +47 -0
  80. package/src/registry/migrations/0002_device_authorizations.sql +23 -0
  81. package/src/registry/migrations/0003_project_environments.sql +25 -0
  82. package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
  83. package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
  84. package/src/registry/migrations/0006_workspaces.sql +36 -0
  85. package/src/registry/migrations/0007_project_apps.sql +26 -0
  86. package/src/registry/migrations/0008_agent_tasks.sql +30 -0
  87. package/src/registry/migrations/0009_journey_revisions.sql +17 -0
  88. package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
  89. package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
  90. package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
  91. package/src/registry/migrations/0013_device_sessions.sql +22 -0
  92. package/src/registry/migrations/0014_agent_task_step.sql +1 -0
  93. package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
  94. package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
  95. package/src/registry/migrations/0017_run_targets.sql +7 -0
  96. package/src/registry/migrations/0018_run_fix_from.sql +4 -0
  97. package/src/registry/migrations/0019_workspace_flags.sql +9 -0
  98. package/src/registry/migrations/0020_orgs.sql +40 -0
  99. package/src/registry/migrations/0021_billing_core.sql +58 -0
  100. package/src/registry/migrations/0022_cloud_runners.sql +19 -0
  101. package/src/registry/migrations/0023_signup.sql +4 -0
  102. package/src/registry/migrations/0024_billing.sql +67 -0
  103. package/src/registry/migrations/0025_notifications.sql +47 -0
  104. package/src/registry/migrations/0026_repo_bindings.sql +28 -0
  105. package/src/registry/migrations/0027_feedback.sql +29 -0
  106. package/src/registry/migrations/0028_devices.sql +48 -0
  107. package/src/registry/migrations/0029_sso.sql +31 -0
  108. package/src/registry/migrations/0030_workspace_domains.sql +18 -0
  109. package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
  110. package/src/registry/migrations/0032_personas.sql +15 -0
  111. package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
  112. package/src/registry/task-scheduling.test.ts +100 -0
  113. package/src/registry/task-scheduling.ts +80 -0
  114. package/src/registry-worker/ai/platform-model.ts +77 -0
  115. package/src/registry-worker/ai/routes.test.ts +88 -0
  116. package/src/registry-worker/ai/routes.ts +90 -0
  117. package/src/registry-worker/artifacts.test.ts +98 -0
  118. package/src/registry-worker/artifacts.ts +85 -0
  119. package/src/registry-worker/billing/billing-core.test.ts +175 -0
  120. package/src/registry-worker/billing/checkout-routes.ts +209 -0
  121. package/src/registry-worker/billing/enforcement.ts +69 -0
  122. package/src/registry-worker/billing/entitlements.ts +108 -0
  123. package/src/registry-worker/billing/ledger.ts +186 -0
  124. package/src/registry-worker/billing/paypal/api.ts +259 -0
  125. package/src/registry-worker/billing/paypal/client.ts +91 -0
  126. package/src/registry-worker/billing/paypal/provider.ts +143 -0
  127. package/src/registry-worker/billing/paypal.test.ts +466 -0
  128. package/src/registry-worker/billing/provider.ts +114 -0
  129. package/src/registry-worker/billing/routes.ts +66 -0
  130. package/src/registry-worker/billing/subscriptions.ts +780 -0
  131. package/src/registry-worker/billing/thresholds.ts +107 -0
  132. package/src/registry-worker/core.ts +308 -0
  133. package/src/registry-worker/devices/devices.test.ts +185 -0
  134. package/src/registry-worker/devices/policy.ts +71 -0
  135. package/src/registry-worker/devices/routes.ts +453 -0
  136. package/src/registry-worker/domains/domains.test.ts +210 -0
  137. package/src/registry-worker/domains/routes.ts +139 -0
  138. package/src/registry-worker/domains.ts +88 -0
  139. package/src/registry-worker/email/sender.ts +75 -0
  140. package/src/registry-worker/env.d.ts +14716 -0
  141. package/src/registry-worker/features.ts +20 -0
  142. package/src/registry-worker/feedback/feedback.test.ts +230 -0
  143. package/src/registry-worker/feedback/format.ts +148 -0
  144. package/src/registry-worker/feedback/routes.ts +386 -0
  145. package/src/registry-worker/flags.ts +39 -0
  146. package/src/registry-worker/github/checks.test.ts +177 -0
  147. package/src/registry-worker/github/checks.ts +374 -0
  148. package/src/registry-worker/gitlab/checks.test.ts +141 -0
  149. package/src/registry-worker/gitlab/checks.ts +349 -0
  150. package/src/registry-worker/hooks.ts +54 -0
  151. package/src/registry-worker/index.ts +2077 -0
  152. package/src/registry-worker/jobs/index.ts +29 -0
  153. package/src/registry-worker/jobs/retention.ts +68 -0
  154. package/src/registry-worker/mcp/mcp.test.ts +355 -0
  155. package/src/registry-worker/mcp/routes.ts +215 -0
  156. package/src/registry-worker/mcp/token.ts +126 -0
  157. package/src/registry-worker/mcp/tools.ts +563 -0
  158. package/src/registry-worker/notifications/alerts.ts +212 -0
  159. package/src/registry-worker/notifications/notifications.test.ts +298 -0
  160. package/src/registry-worker/notifications/outbox.ts +83 -0
  161. package/src/registry-worker/notifications/routes.ts +280 -0
  162. package/src/registry-worker/notifications/secrets.ts +49 -0
  163. package/src/registry-worker/notifications/slack.ts +96 -0
  164. package/src/registry-worker/notifications/teams.ts +46 -0
  165. package/src/registry-worker/org/audit.ts +116 -0
  166. package/src/registry-worker/org/routes.test.ts +163 -0
  167. package/src/registry-worker/org/routes.ts +302 -0
  168. package/src/registry-worker/personas/personas.test.ts +78 -0
  169. package/src/registry-worker/personas/routes.ts +100 -0
  170. package/src/registry-worker/repo-triggers.ts +20 -0
  171. package/src/registry-worker/routes/index.ts +74 -0
  172. package/src/registry-worker/run-events.ts +24 -0
  173. package/src/registry-worker/runner/dispatch.ts +219 -0
  174. package/src/registry-worker/runner/jobs.ts +43 -0
  175. package/src/registry-worker/runner/metering.ts +82 -0
  176. package/src/registry-worker/runner/policy.ts +59 -0
  177. package/src/registry-worker/runner/routes.ts +171 -0
  178. package/src/registry-worker/runner/runner.test.ts +358 -0
  179. package/src/registry-worker/runner/tokens.ts +93 -0
  180. package/src/registry-worker/runs.test.ts +60 -0
  181. package/src/registry-worker/signup/policy.ts +57 -0
  182. package/src/registry-worker/signup/routes.ts +106 -0
  183. package/src/registry-worker/signup/signup.test.ts +81 -0
  184. package/src/registry-worker/sso/aegis.ts +141 -0
  185. package/src/registry-worker/sso/membership.ts +157 -0
  186. package/src/registry-worker/sso/routes.ts +458 -0
  187. package/src/registry-worker/sso/sso.test.ts +344 -0
  188. package/src/registry-worker/testing/d1-shim.ts +180 -0
  189. package/src/registry-worker/testing/harness.ts +137 -0
  190. package/src/runner-worker/index.ts +108 -0
  191. package/src/runtime/ai-analysis.ts +97 -0
  192. package/src/runtime/ai-exploration.test.ts +32 -0
  193. package/src/runtime/ai-exploration.ts +69 -0
  194. package/src/runtime/ai-repair.ts +74 -0
  195. package/src/runtime/ai-work.test.ts +99 -0
  196. package/src/runtime/android-screen-record.test.ts +75 -0
  197. package/src/runtime/android-screen-record.ts +192 -0
  198. package/src/runtime/app-understanding.test.ts +123 -0
  199. package/src/runtime/app-understanding.ts +201 -0
  200. package/src/runtime/appium-driver.test.ts +179 -0
  201. package/src/runtime/appium-driver.ts +295 -0
  202. package/src/runtime/blocker-resolution.test.ts +113 -0
  203. package/src/runtime/blocker-resolution.ts +111 -0
  204. package/src/runtime/browser-matrix.integration.test.ts +212 -0
  205. package/src/runtime/browser-matrix.test.ts +143 -0
  206. package/src/runtime/browser-matrix.ts +200 -0
  207. package/src/runtime/canonical-flow.test.ts +52 -0
  208. package/src/runtime/config-validate.ts +185 -0
  209. package/src/runtime/continuous-execution.ts +291 -0
  210. package/src/runtime/cursor-applescript.ts +573 -0
  211. package/src/runtime/cursor-cli-driver.test.ts +78 -0
  212. package/src/runtime/cursor-cli-driver.ts +156 -0
  213. package/src/runtime/cursor-driver-example.ts +117 -0
  214. package/src/runtime/cursor-driver-index.ts +65 -0
  215. package/src/runtime/cursor-driver-init.ts +277 -0
  216. package/src/runtime/cursor-driver-run.test.ts +15 -0
  217. package/src/runtime/cursor-driver-run.ts +323 -0
  218. package/src/runtime/cursor-driver.ts +332 -0
  219. package/src/runtime/cursor-llm-example.ts +90 -0
  220. package/src/runtime/cursor-llm.ts +206 -0
  221. package/src/runtime/cursor-mcp-monitor.ts +386 -0
  222. package/src/runtime/device-clouds/browserstack.ts +73 -0
  223. package/src/runtime/device-clouds/device-farm.ts +52 -0
  224. package/src/runtime/device-clouds/index.ts +92 -0
  225. package/src/runtime/device-clouds/kobiton.ts +70 -0
  226. package/src/runtime/device-clouds/targets.ts +44 -0
  227. package/src/runtime/device-clouds/types.ts +62 -0
  228. package/src/runtime/diff-proposal.ts +84 -0
  229. package/src/runtime/discovery-task.test.ts +29 -0
  230. package/src/runtime/discovery-task.ts +284 -0
  231. package/src/runtime/driver-recovery.ts +69 -0
  232. package/src/runtime/driver.ts +79 -0
  233. package/src/runtime/environment.test.ts +104 -0
  234. package/src/runtime/environment.ts +137 -0
  235. package/src/runtime/executor.test.ts +509 -0
  236. package/src/runtime/executor.ts +921 -0
  237. package/src/runtime/explorer.test.ts +101 -0
  238. package/src/runtime/explorer.ts +1013 -0
  239. package/src/runtime/failure-analysis.test.ts +111 -0
  240. package/src/runtime/failure-analysis.ts +272 -0
  241. package/src/runtime/fixtures/fake-maestro.sh +36 -0
  242. package/src/runtime/flow-language.test.ts +268 -0
  243. package/src/runtime/flow-language.ts +414 -0
  244. package/src/runtime/init-wizard.ts +354 -0
  245. package/src/runtime/ios-screen-record.test.ts +68 -0
  246. package/src/runtime/ios-screen-record.ts +155 -0
  247. package/src/runtime/journey-editor.ts +452 -0
  248. package/src/runtime/journey-evidence.test.ts +161 -0
  249. package/src/runtime/journey-evidence.ts +180 -0
  250. package/src/runtime/journey-graph.test.ts +257 -0
  251. package/src/runtime/journey-graph.ts +170 -0
  252. package/src/runtime/legacy-names.ts +32 -0
  253. package/src/runtime/llm-example.ts +105 -0
  254. package/src/runtime/llm.ts +527 -0
  255. package/src/runtime/local-browser.test.ts +45 -0
  256. package/src/runtime/local-browser.ts +48 -0
  257. package/src/runtime/local-registry-stub.test.ts +325 -0
  258. package/src/runtime/local-registry-stub.ts +803 -0
  259. package/src/runtime/maestro-driver.test.ts +84 -0
  260. package/src/runtime/maestro-driver.ts +209 -0
  261. package/src/runtime/mole-voice.ts +21 -0
  262. package/src/runtime/nav-crawl.test.ts +100 -0
  263. package/src/runtime/nav-crawl.ts +153 -0
  264. package/src/runtime/pipeline.test.ts +405 -0
  265. package/src/runtime/pipeline.ts +833 -0
  266. package/src/runtime/planner.test.ts +37 -0
  267. package/src/runtime/planner.ts +274 -0
  268. package/src/runtime/platform-ai.ts +76 -0
  269. package/src/runtime/playwright-driver.test.ts +93 -0
  270. package/src/runtime/playwright-driver.ts +620 -0
  271. package/src/runtime/project-spec.ts +140 -0
  272. package/src/runtime/record-run-verdicts.ts +68 -0
  273. package/src/runtime/reporter.test.ts +56 -0
  274. package/src/runtime/reporter.ts +158 -0
  275. package/src/runtime/reset.test.ts +44 -0
  276. package/src/runtime/reset.ts +61 -0
  277. package/src/runtime/reviewer.test.ts +73 -0
  278. package/src/runtime/reviewer.ts +158 -0
  279. package/src/runtime/run-job.ts +136 -0
  280. package/src/runtime/run-once.test.ts +207 -0
  281. package/src/runtime/run-once.ts +168 -0
  282. package/src/runtime/run.ts +132 -0
  283. package/src/runtime/screen-recording.ts +34 -0
  284. package/src/runtime/serve-gateway.test.ts +74 -0
  285. package/src/runtime/serve-gateway.ts +164 -0
  286. package/src/runtime/serve-worker.test.ts +23 -0
  287. package/src/runtime/serve-worker.ts +278 -0
  288. package/src/runtime/site-discovery.test.ts +168 -0
  289. package/src/runtime/site-discovery.ts +308 -0
  290. package/src/runtime/target-runner.ts +144 -0
  291. package/src/runtime/test-case-verdicts.test.ts +94 -0
  292. package/src/runtime/test-case-verdicts.ts +120 -0
  293. package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
  294. package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
  295. package/src/runtime/ui-reverse-engineering/types.ts +197 -0
  296. package/src/runtime/web-suite.test.ts +97 -0
  297. package/src/runtime/web-suite.ts +176 -0
  298. package/src/storage/create-object-store.ts +144 -0
  299. package/src/storage/keys.ts +34 -0
  300. package/src/storage/local-artifact-server.test.ts +314 -0
  301. package/src/storage/local-artifact-server.ts +357 -0
  302. package/src/storage/object-store.test.ts +28 -0
  303. package/src/storage/object-store.ts +101 -0
  304. package/src/storage/registry-object-store.ts +88 -0
  305. package/src/storage/remote-object-store.ts +104 -0
  306. package/src/storage/storage-directory.test.ts +43 -0
  307. package/src/storage/storage-directory.ts +24 -0
  308. package/tsconfig.json +24 -0
@@ -0,0 +1,344 @@
1
+ import { strict as assert } from "node:assert";
2
+ import { fileURLToPath } from "node:url";
3
+ import { mkdtemp, readFile, rm } from "node:fs/promises";
4
+ import { describe, it } from "node:test";
5
+ import {
6
+ buildDashboardHandoffUrl,
7
+ parseArgs,
8
+ printHelp,
9
+ resolveDashboardOrigin,
10
+ resolveDashboardUrl,
11
+ resolveDeviceId,
12
+ resolveLocalRegistryEnabled,
13
+ resolveLocalRegistryPort,
14
+ resolveMode,
15
+ runModeExitCode,
16
+ shouldOpenServeBrowser,
17
+ } from "./bugmole.js";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+ import { runInitWizard, validateStorageProvider } from "../src/runtime/init-wizard.js";
21
+
22
+ describe("Bugmole CLI contract", () => {
23
+ it("starts MCP for the canonical --mcp flag", () => {
24
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "--mcp"])), "mcp");
25
+ });
26
+
27
+ it("keeps --mode mcp compatible", () => {
28
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "--mode", "mcp"])), "mcp");
29
+ });
30
+
31
+ it("accepts the init command and --init flag", () => {
32
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "--init"])), "init");
33
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "init"])), "init");
34
+ });
35
+
36
+ it("accepts the local artifacts serve command", () => {
37
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "artifacts", "serve"])), "artifacts");
38
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "serve"])), "serve");
39
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "gateway"])), "gateway");
40
+ assert.equal(resolveMode(parseArgs(["node", "bugmole", "reset"])), "reset");
41
+ });
42
+
43
+ it("accepts the serve no-open flag", () => {
44
+ const args = parseArgs(["node", "bugmole", "serve", "--no-open"]);
45
+ assert.equal(args.noopen, true);
46
+ assert.equal(shouldOpenServeBrowser(args), false);
47
+ assert.equal(shouldOpenServeBrowser(parseArgs(["node", "bugmole", "serve"]), {}, "darwin"), true);
48
+ });
49
+
50
+ it("does not open a browser for serve on a headless machine", () => {
51
+ const serve = parseArgs(["node", "bugmole", "serve"]);
52
+ assert.equal(shouldOpenServeBrowser(serve, {}, "linux"), false);
53
+ assert.equal(shouldOpenServeBrowser(serve, { DISPLAY: ":0" }, "linux"), true);
54
+ assert.equal(shouldOpenServeBrowser(serve, { CI: "true" }, "darwin"), false);
55
+ assert.equal(shouldOpenServeBrowser(serve, { BUGMOLE_HEADLESS: "true" }, "darwin"), false);
56
+ });
57
+
58
+ it("fails the process when --mode run reports anything but success", () => {
59
+ assert.equal(runModeExitCode("run", { status: "success" }), 0);
60
+ assert.equal(runModeExitCode("run", { status: "partial" }), 1);
61
+ assert.equal(runModeExitCode("run", { status: "error" }), 1);
62
+ assert.equal(runModeExitCode("run", undefined), 1);
63
+ });
64
+
65
+ it("leaves other modes' exit code alone regardless of their result", () => {
66
+ assert.equal(runModeExitCode("explore", { status: "partial" }), 0);
67
+ assert.equal(runModeExitCode("report", undefined), 0);
68
+ });
69
+
70
+ it("accepts a dashboard URL override for serve handoffs", () => {
71
+ const args = parseArgs([
72
+ "node",
73
+ "bugmole",
74
+ "serve",
75
+ "--dashboard-url",
76
+ "http://localhost:3020/dashboard?mode=dev",
77
+ ]);
78
+ const previousUrl = process.env.BUGMOLE_DASHBOARD_URL;
79
+ process.env.BUGMOLE_DASHBOARD_URL = "https://example.invalid";
80
+ try {
81
+ assert.equal(resolveDashboardUrl(args), "http://localhost:3020/dashboard?mode=dev");
82
+ const handoff = new URL(
83
+ buildDashboardHandoffUrl(
84
+ resolveDashboardUrl(args),
85
+ "project/demo",
86
+ new URLSearchParams({
87
+ "local-artifact-project": "project/demo",
88
+ "local-artifact-origin": "http://127.0.0.1:3190",
89
+ "local-artifact-token": "capability",
90
+ }),
91
+ ),
92
+ );
93
+ assert.equal(handoff.searchParams.get("mode"), "dev");
94
+ assert.equal(handoff.searchParams.get("project"), "project/demo");
95
+ assert.equal(
96
+ new URLSearchParams(handoff.hash.slice(1)).get("local-artifact-token"),
97
+ "capability",
98
+ );
99
+ } finally {
100
+ if (previousUrl === undefined) delete process.env.BUGMOLE_DASHBOARD_URL;
101
+ else process.env.BUGMOLE_DASHBOARD_URL = previousUrl;
102
+ }
103
+ });
104
+
105
+ it("defaults the bridge's CORS allowlist to the resolved dashboard URL's origin", () => {
106
+ // Reproduces a real bug caught by actually running `bugmole serve` against
107
+ // a local dashboard: --dashboard-url changed the printed handoff link,
108
+ // but the bridge's CORS allowlist silently stayed the production origin
109
+ // and every request from the local dashboard was rejected.
110
+ const args = parseArgs(["node", "bugmole", "serve", "--dashboard-url", "http://localhost:3020"]);
111
+ const dashboardUrl = resolveDashboardUrl(args);
112
+ assert.equal(resolveDashboardOrigin(args, dashboardUrl), "http://localhost:3020");
113
+ });
114
+
115
+ it("keeps an explicit --dashboard-origin as an override", () => {
116
+ const args = parseArgs([
117
+ "node",
118
+ "bugmole",
119
+ "serve",
120
+ "--dashboard-url",
121
+ "http://localhost:3020",
122
+ "--dashboard-origin",
123
+ "https://proxy.example.invalid",
124
+ ]);
125
+ const dashboardUrl = resolveDashboardUrl(args);
126
+ assert.equal(resolveDashboardOrigin(args, dashboardUrl), "https://proxy.example.invalid");
127
+ });
128
+
129
+ it("falls back to the production origin when neither is configured", () => {
130
+ const previousUrl = process.env.BUGMOLE_DASHBOARD_URL;
131
+ delete process.env.BUGMOLE_DASHBOARD_URL;
132
+ try {
133
+ const args = parseArgs(["node", "bugmole", "serve"]);
134
+ const dashboardUrl = resolveDashboardUrl(args);
135
+ assert.equal(resolveDashboardOrigin(args, dashboardUrl), "https://app.bugmole.com");
136
+ } finally {
137
+ if (previousUrl === undefined) delete process.env.BUGMOLE_DASHBOARD_URL;
138
+ else process.env.BUGMOLE_DASHBOARD_URL = previousUrl;
139
+ }
140
+ });
141
+
142
+ it("documents the dashboard URL option in help", () => {
143
+ const output: string[] = [];
144
+ const originalLog = console.log;
145
+ console.log = (message?: unknown) => output.push(String(message ?? ""));
146
+ try {
147
+ printHelp();
148
+ } finally {
149
+ console.log = originalLog;
150
+ }
151
+ assert.match(output.join("\n"), /--dashboard-url <url>/);
152
+ assert.match(output.join("\n"), /BUGMOLE_DASHBOARD_URL/);
153
+ assert.match(output.join("\n"), /gateway.*Multiplex project serve workers/s);
154
+ });
155
+
156
+ it("validates supported storage providers", () => {
157
+ assert.equal(validateStorageProvider("r2"), "r2");
158
+ assert.throws(() => validateStorageProvider("azure"), /Unsupported storage provider/);
159
+ });
160
+
161
+ it("writes safe defaults in non-interactive mode without stdin", async () => {
162
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "bugmole-init-"));
163
+ try {
164
+ const result = await runInitWizard({ cwd, nonInteractive: true });
165
+ const config = await readFile(path.join(cwd, result.configPath), "utf8");
166
+ const env = await readFile(path.join(cwd, result.envPath), "utf8");
167
+ assert.match(config, /provider: local/);
168
+ assert.match(config, /artifacts_dir: \.\/qa\/bugmole-runs/);
169
+ assert.match(env, /BUGMOLE_STORAGE_PROVIDER=local/);
170
+ assert.doesNotMatch(env, /ACCESS_KEY|SECRET_ACCESS_KEY|CREDENTIALS_JSON/);
171
+ } finally {
172
+ await rm(cwd, { recursive: true, force: true });
173
+ }
174
+ });
175
+
176
+ it("persists the remembered Aegis email without writing credentials", async () => {
177
+ const cwd = await mkdtemp(path.join(os.tmpdir(), "bugmole-init-email-"));
178
+ const previousEmail = process.env.BUGMOLE_AEGIS_EMAIL;
179
+ process.env.BUGMOLE_AEGIS_EMAIL = "tester@example.com";
180
+ try {
181
+ const result = await runInitWizard({ cwd, nonInteractive: true });
182
+ const env = await readFile(path.join(cwd, result.envPath), "utf8");
183
+ assert.match(env, /BUGMOLE_AEGIS_EMAIL="tester@example\.com"/);
184
+ assert.doesNotMatch(env, /BUGMOLE_API_KEY=/);
185
+ } finally {
186
+ if (previousEmail === undefined) delete process.env.BUGMOLE_AEGIS_EMAIL;
187
+ else process.env.BUGMOLE_AEGIS_EMAIL = previousEmail;
188
+ await rm(cwd, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ it("accepts config alongside a boolean MCP flag", () => {
193
+ assert.deepEqual(parseArgs(["node", "bugmole", "--mcp", "--config", "custom.yaml"]), {
194
+ mcp: true,
195
+ config: "custom.yaml",
196
+ });
197
+ });
198
+
199
+ it("keeps the local registry stub off by default, matching real-registry serve behavior", () => {
200
+ const previous = process.env.BUGMOLE_LOCAL_REGISTRY;
201
+ delete process.env.BUGMOLE_LOCAL_REGISTRY;
202
+ try {
203
+ assert.equal(resolveLocalRegistryEnabled(parseArgs(["node", "bugmole", "serve"])), false);
204
+ } finally {
205
+ if (previous === undefined) delete process.env.BUGMOLE_LOCAL_REGISTRY;
206
+ else process.env.BUGMOLE_LOCAL_REGISTRY = previous;
207
+ }
208
+ });
209
+
210
+ it("enables the local registry stub via --local-registry or BUGMOLE_LOCAL_REGISTRY", () => {
211
+ assert.equal(
212
+ resolveLocalRegistryEnabled(parseArgs(["node", "bugmole", "serve", "--local-registry"])),
213
+ true,
214
+ );
215
+ const previous = process.env.BUGMOLE_LOCAL_REGISTRY;
216
+ process.env.BUGMOLE_LOCAL_REGISTRY = "true";
217
+ try {
218
+ assert.equal(resolveLocalRegistryEnabled(parseArgs(["node", "bugmole", "serve"])), true);
219
+ } finally {
220
+ if (previous === undefined) delete process.env.BUGMOLE_LOCAL_REGISTRY;
221
+ else process.env.BUGMOLE_LOCAL_REGISTRY = previous;
222
+ }
223
+ });
224
+
225
+ it("an explicit --local-registry=false overrides BUGMOLE_LOCAL_REGISTRY=true", () => {
226
+ const previous = process.env.BUGMOLE_LOCAL_REGISTRY;
227
+ process.env.BUGMOLE_LOCAL_REGISTRY = "true";
228
+ try {
229
+ assert.equal(
230
+ resolveLocalRegistryEnabled(parseArgs(["node", "bugmole", "serve", "--local-registry", "false"])),
231
+ false,
232
+ );
233
+ } finally {
234
+ if (previous === undefined) delete process.env.BUGMOLE_LOCAL_REGISTRY;
235
+ else process.env.BUGMOLE_LOCAL_REGISTRY = previous;
236
+ }
237
+ });
238
+
239
+ it("resolves an explicit local registry port from either the flag or the env var", () => {
240
+ assert.equal(
241
+ resolveLocalRegistryPort(parseArgs(["node", "bugmole", "serve", "--local-registry-port", "4190"])),
242
+ 4190,
243
+ );
244
+ const previous = process.env.BUGMOLE_LOCAL_REGISTRY_PORT;
245
+ process.env.BUGMOLE_LOCAL_REGISTRY_PORT = "4191";
246
+ try {
247
+ assert.equal(resolveLocalRegistryPort(parseArgs(["node", "bugmole", "serve"])), 4191);
248
+ } finally {
249
+ if (previous === undefined) delete process.env.BUGMOLE_LOCAL_REGISTRY_PORT;
250
+ else process.env.BUGMOLE_LOCAL_REGISTRY_PORT = previous;
251
+ }
252
+ });
253
+
254
+ it("defaults the local registry port to a fixed, fetch-spec-safe port so .env.local never goes stale", () => {
255
+ // A fixed default (not a random ephemeral port, unlike the artifact
256
+ // bridge) so a dashboard .env.local pointing BUGMOLE_REGISTRY_URL here
257
+ // keeps working across `bugmole serve` restarts. Regression coverage for a
258
+ // real bug: an earlier default of 4190 is on the Fetch spec's forbidden
259
+ // port list (reserved for ManageSieve) — every fetch to it fails with
260
+ // "bad port" before even attempting a connection.
261
+ const previous = process.env.BUGMOLE_LOCAL_REGISTRY_PORT;
262
+ delete process.env.BUGMOLE_LOCAL_REGISTRY_PORT;
263
+ try {
264
+ const port = resolveLocalRegistryPort(parseArgs(["node", "bugmole", "serve", "--local-registry"]));
265
+ assert.equal(port, 4600);
266
+ const forbiddenPorts = new Set([
267
+ 1, 7, 9, 11, 13, 15, 17, 19, 20, 21, 22, 23, 25, 37, 42, 43, 53, 69, 77, 79, 87, 95, 101, 102, 103,
268
+ 104, 109, 110, 111, 113, 115, 117, 119, 123, 135, 137, 139, 143, 161, 179, 389, 427, 465, 512, 513,
269
+ 514, 515, 526, 530, 531, 532, 540, 548, 554, 556, 563, 587, 601, 636, 989, 990, 993, 995, 1719, 1720,
270
+ 1723, 2049, 3659, 4045, 4190, 5060, 5061, 6000, 6566, 6665, 6666, 6667, 6668, 6669, 6697, 10080,
271
+ ]);
272
+ assert.equal(forbiddenPorts.has(port), false);
273
+ } finally {
274
+ if (previous === undefined) delete process.env.BUGMOLE_LOCAL_REGISTRY_PORT;
275
+ else process.env.BUGMOLE_LOCAL_REGISTRY_PORT = previous;
276
+ }
277
+ });
278
+
279
+ it("publishes both bugmole and bm commands", async () => {
280
+ const packageJson = JSON.parse(
281
+ // A path string, not a URL: @types/node 26 types PathLike with
282
+ // node:url's URL while the global URL is the web-standard one, and the
283
+ // two are no longer interchangeable.
284
+ await readFile(fileURLToPath(new URL("../package.json", import.meta.url).href), "utf8"),
285
+ ) as { name: string; bin: Record<string, string> };
286
+
287
+ assert.equal(packageJson.name, "@bugmole/cli");
288
+ assert.equal(packageJson.bin.bugmole, packageJson.bin.bm);
289
+ assert.equal(packageJson.bin.bugmole, "scripts/bugmole.cjs");
290
+ });
291
+
292
+ it("generates a device id on first use and reuses the same one afterward", async () => {
293
+ const stateDir = await mkdtemp(path.join(os.tmpdir(), "bugmole-device-id-"));
294
+ try {
295
+ const first = resolveDeviceId({ stateDir });
296
+ const second = resolveDeviceId({ stateDir });
297
+ assert.equal(first, second);
298
+ assert.match(first, /^[a-z0-9][a-z0-9-]{0,62}$/);
299
+ } finally {
300
+ await rm(stateDir, { recursive: true, force: true });
301
+ }
302
+ });
303
+
304
+ it("gives two machines (two state dirs) two different device ids", async () => {
305
+ const stateDirA = await mkdtemp(path.join(os.tmpdir(), "bugmole-device-id-a-"));
306
+ const stateDirB = await mkdtemp(path.join(os.tmpdir(), "bugmole-device-id-b-"));
307
+ try {
308
+ assert.notEqual(resolveDeviceId({ stateDir: stateDirA }), resolveDeviceId({ stateDir: stateDirB }));
309
+ } finally {
310
+ await rm(stateDirA, { recursive: true, force: true });
311
+ await rm(stateDirB, { recursive: true, force: true });
312
+ }
313
+ });
314
+
315
+ it("never derives a device id from the project id, so two projects on one machine still share one binding", async () => {
316
+ const stateDir = await mkdtemp(path.join(os.tmpdir(), "bugmole-device-id-shared-"));
317
+ try {
318
+ // Regression test: deviceId used to be cfg.project?.id, so every
319
+ // checkout of this same repo (or any project named the same thing)
320
+ // produced the identical literal string. Aegis permanently binds a
321
+ // deviceId to one tenant on first claim, so that collided across
322
+ // teammates and even across a single person's second `bugmole serve` run.
323
+ const id = resolveDeviceId({ stateDir });
324
+ assert.notEqual(id, "bugmole");
325
+ assert.notEqual(id, "default");
326
+ } finally {
327
+ await rm(stateDir, { recursive: true, force: true });
328
+ }
329
+ });
330
+
331
+ it("respects an explicit BUGMOLE_DEVICE_ID override without touching the state dir", async () => {
332
+ const stateDir = await mkdtemp(path.join(os.tmpdir(), "bugmole-device-id-override-"));
333
+ const previous = process.env.BUGMOLE_DEVICE_ID;
334
+ process.env.BUGMOLE_DEVICE_ID = "operator-chosen-id";
335
+ try {
336
+ assert.equal(resolveDeviceId({ stateDir }), "operator-chosen-id");
337
+ await assert.rejects(readFile(path.join(stateDir, "device-id"), "utf8"));
338
+ } finally {
339
+ if (previous === undefined) delete process.env.BUGMOLE_DEVICE_ID;
340
+ else process.env.BUGMOLE_DEVICE_ID = previous;
341
+ await rm(stateDir, { recursive: true, force: true });
342
+ }
343
+ });
344
+ });