@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.
- package/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** Where Google Chrome lives on macOS, kept first so existing Mac setups keep using the browser they already have. */
|
|
4
|
+
export const MAC_CHROME_PATH = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
5
|
+
|
|
6
|
+
export type BrowserResolutionDeps = {
|
|
7
|
+
env?: Record<string, string | undefined>;
|
|
8
|
+
exists?: (candidate: string) => boolean;
|
|
9
|
+
/** Playwright's bundled Chromium, which `npx playwright install chromium` provides on every platform. */
|
|
10
|
+
playwrightChromiumPath?: () => Promise<string | undefined>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
async function defaultPlaywrightChromiumPath(): Promise<string | undefined> {
|
|
14
|
+
try {
|
|
15
|
+
const { chromium } = await import("playwright");
|
|
16
|
+
return chromium.executablePath();
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Picks the browser binary used for headless page captures and DOM dumps.
|
|
24
|
+
*
|
|
25
|
+
* Order: an explicit BUGMOLE_BROWSER_PATH, then macOS Google Chrome, then
|
|
26
|
+
* Playwright's bundled Chromium. The last step is what lets a Linux worker
|
|
27
|
+
* (CI or a cloud container) capture pages without any extra configuration.
|
|
28
|
+
*/
|
|
29
|
+
export async function resolveHeadlessBrowserPath(deps: BrowserResolutionDeps = {}): Promise<string> {
|
|
30
|
+
const env = deps.env ?? process.env;
|
|
31
|
+
const exists = deps.exists ?? ((candidate: string) => fs.existsSync(candidate));
|
|
32
|
+
const explicit = env.BUGMOLE_BROWSER_PATH?.trim();
|
|
33
|
+
if (explicit) return explicit;
|
|
34
|
+
if (exists(MAC_CHROME_PATH)) return MAC_CHROME_PATH;
|
|
35
|
+
const bundled = await (deps.playwrightChromiumPath ?? defaultPlaywrightChromiumPath)();
|
|
36
|
+
if (bundled && exists(bundled)) return bundled;
|
|
37
|
+
throw new Error(
|
|
38
|
+
"No headless browser found. Set BUGMOLE_BROWSER_PATH, install Google Chrome, or run `npx playwright install chromium`.",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Chrome refuses to start as root without --no-sandbox, and containers
|
|
44
|
+
* (including the official Playwright image) run as root by default.
|
|
45
|
+
*/
|
|
46
|
+
export function headlessBrowserArgs(platform: NodeJS.Platform = process.platform, uid: number | undefined = process.getuid?.()): string[] {
|
|
47
|
+
return platform === "linux" && uid === 0 ? ["--no-sandbox"] : [];
|
|
48
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { startLocalRegistryStub } from "./local-registry-stub.js";
|
|
7
|
+
|
|
8
|
+
async function withStub(run: (origin: string) => Promise<void>) {
|
|
9
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "bugmole-local-registry-"));
|
|
10
|
+
const stub = await startLocalRegistryStub({ directory });
|
|
11
|
+
try {
|
|
12
|
+
await run(stub.origin);
|
|
13
|
+
} finally {
|
|
14
|
+
await stub.close();
|
|
15
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function postJson(origin: string, pathname: string, body: unknown) {
|
|
20
|
+
const response = await fetch(`${origin}${pathname}`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
});
|
|
25
|
+
return { status: response.status, body: await response.json() };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test("publishes a plan, then a run against it can be created, claimed, and updated to completion", async () => {
|
|
29
|
+
await withStub(async (origin) => {
|
|
30
|
+
const publish = await postJson(origin, "/api/projects/lumis/plans", {
|
|
31
|
+
planId: "member-login",
|
|
32
|
+
journeyId: "member-navigation",
|
|
33
|
+
objectKey: "projects/lumis/plans/member-login.flow.yaml",
|
|
34
|
+
});
|
|
35
|
+
assert.equal(publish.status, 201);
|
|
36
|
+
|
|
37
|
+
const created = await postJson(origin, "/api/projects/lumis/runs", {
|
|
38
|
+
planId: "member-login",
|
|
39
|
+
idempotencyKey: "idem-1",
|
|
40
|
+
});
|
|
41
|
+
assert.equal(created.status, 201);
|
|
42
|
+
const runId = (created.body as { run: { id: string } }).run.id;
|
|
43
|
+
assert.equal((created.body as { run: { journeyId: string } }).run.journeyId, "member-navigation");
|
|
44
|
+
assert.equal((created.body as { run: { status: string } }).run.status, "queued");
|
|
45
|
+
|
|
46
|
+
const claim = await postJson(origin, `/api/runs/${runId}/claim`, { workerId: "worker-1" });
|
|
47
|
+
assert.equal(claim.status, 200);
|
|
48
|
+
assert.equal((claim.body as { claimed: boolean }).claimed, true);
|
|
49
|
+
|
|
50
|
+
const doubleClaim = await postJson(origin, `/api/runs/${runId}/claim`, { workerId: "worker-2" });
|
|
51
|
+
assert.equal(doubleClaim.status, 409);
|
|
52
|
+
|
|
53
|
+
const update = await postJson(origin, `/api/runs/${runId}`, {
|
|
54
|
+
workerId: "worker-1",
|
|
55
|
+
status: "success",
|
|
56
|
+
stepsExecuted: 4,
|
|
57
|
+
stepsPassed: 4,
|
|
58
|
+
stepsFailed: 0,
|
|
59
|
+
});
|
|
60
|
+
assert.equal(update.status, 200);
|
|
61
|
+
|
|
62
|
+
const list = await fetch(`${origin}/api/projects/lumis/runs`);
|
|
63
|
+
const { runs } = await list.json() as { runs: Array<{ status: string; stepsPassed: number }> };
|
|
64
|
+
assert.equal(runs.length, 1);
|
|
65
|
+
assert.equal(runs[0].status, "success");
|
|
66
|
+
assert.equal(runs[0].stepsPassed, 4);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("creating a run against a plan that was never published is rejected", async () => {
|
|
71
|
+
await withStub(async (origin) => {
|
|
72
|
+
const created = await postJson(origin, "/api/projects/lumis/runs", {
|
|
73
|
+
planId: "never-published",
|
|
74
|
+
idempotencyKey: "idem-x",
|
|
75
|
+
});
|
|
76
|
+
assert.equal(created.status, 404);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("queuing the same task twice with the same idempotency key returns the same task, not a duplicate", async () => {
|
|
81
|
+
await withStub(async (origin) => {
|
|
82
|
+
const first = await postJson(origin, "/api/projects/lumis/tasks", {
|
|
83
|
+
title: "Explore the app",
|
|
84
|
+
description: "Run the exploration loop.",
|
|
85
|
+
taskType: "investigation",
|
|
86
|
+
idempotencyKey: "explore-1",
|
|
87
|
+
});
|
|
88
|
+
assert.equal(first.status, 201);
|
|
89
|
+
const second = await postJson(origin, "/api/projects/lumis/tasks", {
|
|
90
|
+
title: "Explore the app",
|
|
91
|
+
description: "Run the exploration loop.",
|
|
92
|
+
taskType: "investigation",
|
|
93
|
+
idempotencyKey: "explore-1",
|
|
94
|
+
});
|
|
95
|
+
assert.equal(second.status, 200);
|
|
96
|
+
assert.equal((first.body as { task: { id: string } }).task.id, (second.body as { task: { id: string } }).task.id);
|
|
97
|
+
|
|
98
|
+
const list = await fetch(`${origin}/api/projects/lumis/tasks`);
|
|
99
|
+
const { tasks } = await list.json() as { tasks: unknown[] };
|
|
100
|
+
assert.equal(tasks.length, 1);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a claimed task can be completed, and a UI task cannot complete without a parity score", async () => {
|
|
105
|
+
await withStub(async (origin) => {
|
|
106
|
+
const created = await postJson(origin, "/api/projects/lumis/tasks", {
|
|
107
|
+
title: "Fix the sign-in bug",
|
|
108
|
+
description: "UI parity fix.",
|
|
109
|
+
taskType: "ui",
|
|
110
|
+
idempotencyKey: "ui-task-1",
|
|
111
|
+
});
|
|
112
|
+
const taskId = (created.body as { task: { id: string } }).task.id;
|
|
113
|
+
|
|
114
|
+
const claim = await postJson(origin, `/api/tasks/${taskId}/claim`, { workerId: "worker-1" });
|
|
115
|
+
assert.equal(claim.status, 200);
|
|
116
|
+
|
|
117
|
+
const rejected = await postJson(origin, `/api/tasks/${taskId}`, { status: "completed" });
|
|
118
|
+
assert.equal(rejected.status, 422);
|
|
119
|
+
|
|
120
|
+
const completed = await postJson(origin, `/api/tasks/${taskId}`, {
|
|
121
|
+
status: "completed",
|
|
122
|
+
parityAuditId: "audit-1",
|
|
123
|
+
parityMatchScore: 0.98,
|
|
124
|
+
});
|
|
125
|
+
assert.equal(completed.status, 200);
|
|
126
|
+
assert.equal((completed.body as { task: { status: string } }).task.status, "completed");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a parked task can be resumed by a human, and only then becomes claimable again", async () => {
|
|
131
|
+
await withStub(async (origin) => {
|
|
132
|
+
const created = await postJson(origin, "/api/projects/lumis/tasks", {
|
|
133
|
+
title: "Verify authenticated routes",
|
|
134
|
+
description: "Needs an approved session before it can run.",
|
|
135
|
+
taskType: "investigation",
|
|
136
|
+
idempotencyKey: "parked-task-1",
|
|
137
|
+
});
|
|
138
|
+
const taskId = (created.body as { task: { id: string } }).task.id;
|
|
139
|
+
|
|
140
|
+
await postJson(origin, `/api/tasks/${taskId}/claim`, { workerId: "worker-1" });
|
|
141
|
+
const blocked = await postJson(origin, `/api/tasks/${taskId}`, {
|
|
142
|
+
status: "blocked",
|
|
143
|
+
errorMessage: "cursor-agent run timed out.",
|
|
144
|
+
});
|
|
145
|
+
assert.equal(blocked.status, 200);
|
|
146
|
+
|
|
147
|
+
// Parked: the backoff the block scheduled keeps it out of the queue.
|
|
148
|
+
const whileParked = await postJson(origin, `/api/tasks/${taskId}/claim`, { workerId: "worker-2" });
|
|
149
|
+
assert.equal(whileParked.status, 409);
|
|
150
|
+
|
|
151
|
+
const resumed = await postJson(origin, `/api/tasks/${taskId}/resume`, {});
|
|
152
|
+
assert.equal(resumed.status, 200);
|
|
153
|
+
const task = (resumed.body as { resumed: boolean; task: { status: string; attempts: number; errorMessage: string | null } }).task;
|
|
154
|
+
assert.equal(task.status, "queued");
|
|
155
|
+
assert.equal(task.attempts, 0, "the retry budget must be given back, or resuming changes nothing");
|
|
156
|
+
assert.equal(task.errorMessage, null, "the stale failure should not follow the task into its next run");
|
|
157
|
+
|
|
158
|
+
// The point of the whole exercise: a worker can pick it up again.
|
|
159
|
+
const afterResume = await postJson(origin, `/api/tasks/${taskId}/claim`, { workerId: "worker-2" });
|
|
160
|
+
assert.equal(afterResume.status, 200);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("resume refuses finished work and unknown tasks", async () => {
|
|
165
|
+
await withStub(async (origin) => {
|
|
166
|
+
const created = await postJson(origin, "/api/projects/lumis/tasks", {
|
|
167
|
+
title: "Already done",
|
|
168
|
+
description: "Completed work must not be reopened by resume.",
|
|
169
|
+
taskType: "investigation",
|
|
170
|
+
idempotencyKey: "finished-task-1",
|
|
171
|
+
});
|
|
172
|
+
const taskId = (created.body as { task: { id: string } }).task.id;
|
|
173
|
+
await postJson(origin, `/api/tasks/${taskId}/claim`, { workerId: "worker-1" });
|
|
174
|
+
await postJson(origin, `/api/tasks/${taskId}`, { status: "completed" });
|
|
175
|
+
|
|
176
|
+
const reopened = await postJson(origin, `/api/tasks/${taskId}/resume`, {});
|
|
177
|
+
assert.equal(reopened.status, 409);
|
|
178
|
+
|
|
179
|
+
const missing = await postJson(origin, "/api/tasks/task_does_not_exist/resume", {});
|
|
180
|
+
assert.equal(missing.status, 404);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("journey revisions increment per journeyId and reject a stale expectedRevision", async () => {
|
|
185
|
+
await withStub(async (origin) => {
|
|
186
|
+
const first = await postJson(origin, "/api/projects/lumis/journeys", {
|
|
187
|
+
journeyId: "member-navigation",
|
|
188
|
+
objectKey: "projects/lumis/journeys/member-navigation.json",
|
|
189
|
+
});
|
|
190
|
+
assert.equal(first.status, 201);
|
|
191
|
+
assert.equal((first.body as { revision: { revision: number } }).revision.revision, 1);
|
|
192
|
+
|
|
193
|
+
const stale = await postJson(origin, "/api/projects/lumis/journeys", {
|
|
194
|
+
journeyId: "member-navigation",
|
|
195
|
+
objectKey: "projects/lumis/journeys/member-navigation.json",
|
|
196
|
+
expectedRevision: 0,
|
|
197
|
+
});
|
|
198
|
+
assert.equal(stale.status, 409);
|
|
199
|
+
|
|
200
|
+
const second = await postJson(origin, "/api/projects/lumis/journeys", {
|
|
201
|
+
journeyId: "member-navigation",
|
|
202
|
+
objectKey: "projects/lumis/journeys/member-navigation.json",
|
|
203
|
+
expectedRevision: 1,
|
|
204
|
+
});
|
|
205
|
+
assert.equal(second.status, 201);
|
|
206
|
+
assert.equal((second.body as { revision: { revision: number } }).revision.revision, 2);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("collections are isolated per project", async () => {
|
|
211
|
+
await withStub(async (origin) => {
|
|
212
|
+
await postJson(origin, "/api/projects/project-a/tasks", {
|
|
213
|
+
title: "A task",
|
|
214
|
+
description: "For project A only.",
|
|
215
|
+
idempotencyKey: "a-1",
|
|
216
|
+
});
|
|
217
|
+
const listB = await fetch(`${origin}/api/projects/project-b/tasks`);
|
|
218
|
+
const { tasks } = await listB.json() as { tasks: unknown[] };
|
|
219
|
+
assert.equal(tasks.length, 0);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a run keeps its requested targets and environment URL through claim and completion", async () => {
|
|
224
|
+
await withStub(async (origin) => {
|
|
225
|
+
await postJson(origin, "/api/projects/lumis/plans", {
|
|
226
|
+
planId: "member-login",
|
|
227
|
+
journeyId: "member-navigation",
|
|
228
|
+
objectKey: "projects/lumis/plans/member-login.flow.yaml",
|
|
229
|
+
});
|
|
230
|
+
const rejected = await postJson(origin, "/api/projects/lumis/runs", {
|
|
231
|
+
planId: "member-login",
|
|
232
|
+
idempotencyKey: "idem-bad",
|
|
233
|
+
targets: [{ browser: "opera" }],
|
|
234
|
+
});
|
|
235
|
+
assert.equal(rejected.status, 422);
|
|
236
|
+
|
|
237
|
+
const created = await postJson(origin, "/api/projects/lumis/runs", {
|
|
238
|
+
planId: "member-login",
|
|
239
|
+
idempotencyKey: "idem-matrix",
|
|
240
|
+
environmentBaseUrl: "https://staging.example.com",
|
|
241
|
+
targets: [{ browser: "chromium" }, { browser: "webkit", device: "iPhone 15" }, { browser: "chromium" }],
|
|
242
|
+
});
|
|
243
|
+
assert.equal(created.status, 201);
|
|
244
|
+
const runId = (created.body as { run: { id: string } }).run.id;
|
|
245
|
+
|
|
246
|
+
const claim = await postJson(origin, `/api/runs/${runId}/claim`, { workerId: "worker-1" });
|
|
247
|
+
assert.deepEqual(claim.body, {
|
|
248
|
+
claimed: true,
|
|
249
|
+
leaseExpiresAt: (claim.body as { leaseExpiresAt: string }).leaseExpiresAt,
|
|
250
|
+
environmentBaseUrl: "https://staging.example.com",
|
|
251
|
+
targets: [{ browser: "chromium" }, { browser: "webkit", device: "iPhone 15" }],
|
|
252
|
+
fixFrom: null,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const targetResults = [{ targetId: "chromium", browser: "chromium", status: "passed", stepsPassed: 2, stepsFailed: 0, artifactKeys: [] }];
|
|
256
|
+
await postJson(origin, `/api/runs/${runId}`, { status: "running", targetResults });
|
|
257
|
+
await postJson(origin, `/api/runs/${runId}`, { status: "success" });
|
|
258
|
+
|
|
259
|
+
const run = await (await fetch(`${origin}/api/runs/${runId}`)).json() as {
|
|
260
|
+
run: { targetResults: unknown[]; durationMs: number; environmentBaseUrl: string };
|
|
261
|
+
};
|
|
262
|
+
// A later update without results keeps the last snapshot.
|
|
263
|
+
assert.deepEqual(run.run.targetResults, targetResults);
|
|
264
|
+
assert.equal(typeof run.run.durationMs, "number");
|
|
265
|
+
assert.equal(run.run.environmentBaseUrl, "https://staging.example.com");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("a late progress update cannot reopen a finished run", async () => {
|
|
270
|
+
await withStub(async (origin) => {
|
|
271
|
+
await postJson(origin, "/api/projects/lumis/plans", {
|
|
272
|
+
planId: "member-login",
|
|
273
|
+
journeyId: "member-navigation",
|
|
274
|
+
objectKey: "projects/lumis/plans/member-login.flow.yaml",
|
|
275
|
+
});
|
|
276
|
+
const created = await postJson(origin, "/api/projects/lumis/runs", { planId: "member-login", idempotencyKey: "idem-late" });
|
|
277
|
+
const runId = (created.body as { run: { id: string } }).run.id;
|
|
278
|
+
await postJson(origin, `/api/runs/${runId}/claim`, { workerId: "worker-1" });
|
|
279
|
+
await postJson(origin, `/api/runs/${runId}`, { status: "error" });
|
|
280
|
+
const late = await postJson(origin, `/api/runs/${runId}`, { status: "running" });
|
|
281
|
+
assert.equal(late.status, 409);
|
|
282
|
+
const run = await (await fetch(`${origin}/api/runs/${runId}`)).json() as { run: { status: string } };
|
|
283
|
+
assert.equal(run.run.status, "error");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("personas: the default Curious persona is created lazily and matches the real registry's schema", async () => {
|
|
288
|
+
await withStub(async (origin) => {
|
|
289
|
+
const first = await (await fetch(`${origin}/api/projects/lumis/personas`)).json() as { personas: Array<{ id: string; name: string; description: string; isDefault: boolean }> };
|
|
290
|
+
assert.equal(first.personas.length, 1);
|
|
291
|
+
assert.equal(first.personas[0].name, "Curious");
|
|
292
|
+
assert.equal(first.personas[0].isDefault, true);
|
|
293
|
+
assert.ok(first.personas[0].description.length > 0);
|
|
294
|
+
|
|
295
|
+
const second = await (await fetch(`${origin}/api/projects/lumis/personas`)).json() as { personas: unknown[] };
|
|
296
|
+
assert.equal(second.personas.length, 1, "the default is only ever created once");
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("personas: adding, renaming, and removing, with at least one always kept", async () => {
|
|
301
|
+
await withStub(async (origin) => {
|
|
302
|
+
const list0 = await (await fetch(`${origin}/api/projects/lumis/personas`)).json() as { personas: Array<{ id: string }> };
|
|
303
|
+
const defaultId = list0.personas[0].id;
|
|
304
|
+
|
|
305
|
+
const added = await postJson(origin, "/api/projects/lumis/personas", { name: "Impatient power user", description: "Skip straight to checkout." });
|
|
306
|
+
assert.equal(added.status, 201, JSON.stringify(added.body));
|
|
307
|
+
const personaId = (added.body as { persona: { id: string } }).persona.id;
|
|
308
|
+
|
|
309
|
+
const duplicate = await postJson(origin, "/api/projects/lumis/personas", { name: "Impatient power user", description: "Different text, same name." });
|
|
310
|
+
assert.equal(duplicate.status, 409);
|
|
311
|
+
|
|
312
|
+
const renamed = await fetch(`${origin}/api/projects/lumis/personas/${personaId}`, {
|
|
313
|
+
method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: "Very impatient", description: "Skip straight to checkout." }),
|
|
314
|
+
});
|
|
315
|
+
assert.equal(renamed.status, 200);
|
|
316
|
+
|
|
317
|
+
const list = await (await fetch(`${origin}/api/projects/lumis/personas`)).json() as { personas: Array<{ name: string }> };
|
|
318
|
+
assert.deepEqual(list.personas.map((persona) => persona.name).sort(), ["Curious", "Very impatient"]);
|
|
319
|
+
|
|
320
|
+
const removed = await fetch(`${origin}/api/projects/lumis/personas/${personaId}`, { method: "DELETE" });
|
|
321
|
+
assert.equal(removed.status, 200);
|
|
322
|
+
const removeLast = await fetch(`${origin}/api/projects/lumis/personas/${defaultId}`, { method: "DELETE" });
|
|
323
|
+
assert.equal(removeLast.status, 409, "the last persona in a project can't be removed");
|
|
324
|
+
});
|
|
325
|
+
});
|