@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,509 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import type { AppDriver, DriverContext, DriverResult } from "./driver.js";
|
|
7
|
+
|
|
8
|
+
function makeTestConfig(testDir: string) {
|
|
9
|
+
const specDir = path.join(testDir, "spec");
|
|
10
|
+
const artifactsDir = path.join(testDir, "artifacts");
|
|
11
|
+
fs.mkdirSync(path.join(specDir, "plans"), { recursive: true });
|
|
12
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
13
|
+
return {
|
|
14
|
+
project: { id: "demo" },
|
|
15
|
+
runtime: { spec_dir: specDir, artifacts_dir: artifactsDir },
|
|
16
|
+
// "false" always exits 1 without touching --test-output-dir, reproducing
|
|
17
|
+
// a driver that fails before any step executes (e.g. "device offline")
|
|
18
|
+
// without depending on a real Maestro install or device.
|
|
19
|
+
execution: { maestro: { command: "false" } },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Reproduces run_1786624327747 from a real Bugmole project: Maestro failed
|
|
24
|
+
// to connect to a device before any step ran, yet the old executor reported
|
|
25
|
+
// every declared step as independently "failed" with an identical message.
|
|
26
|
+
// The fix must stop claiming per-step verification it never performed.
|
|
27
|
+
test("does not fabricate per-step verification when the driver fails before any step runs", async () => {
|
|
28
|
+
const { execute } = await import("./executor.js");
|
|
29
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-test-"));
|
|
30
|
+
const cfg = makeTestConfig(testDir);
|
|
31
|
+
const planYaml = `
|
|
32
|
+
suites:
|
|
33
|
+
- journeys:
|
|
34
|
+
- id: checkout
|
|
35
|
+
driver: maestro
|
|
36
|
+
platform: android
|
|
37
|
+
flow: flows/checkout.yaml
|
|
38
|
+
steps:
|
|
39
|
+
- node_id: step-1
|
|
40
|
+
action: launch
|
|
41
|
+
- node_id: step-2
|
|
42
|
+
action: tap-sign-in
|
|
43
|
+
- node_id: step-3
|
|
44
|
+
action: enter-email
|
|
45
|
+
`;
|
|
46
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "checkout.yaml"), planYaml, "utf8");
|
|
47
|
+
|
|
48
|
+
const result = await execute(cfg, { planId: "checkout", journeyId: "checkout" });
|
|
49
|
+
|
|
50
|
+
assert.equal(result.status, "partial");
|
|
51
|
+
assert.equal(result.stepsExecuted, 3);
|
|
52
|
+
assert.equal(result.stepsFailed, 3);
|
|
53
|
+
assert.ok(result.results);
|
|
54
|
+
for (const step of result.results!) {
|
|
55
|
+
assert.equal(step.outcome, "FAIL");
|
|
56
|
+
// The old behavior copied a single opaque "The journey action failed."
|
|
57
|
+
// onto every step, presenting it as independently verified. The fix
|
|
58
|
+
// must say plainly that only the aggregate run outcome is known.
|
|
59
|
+
assert.equal(step.reason.code, "aggregate_only");
|
|
60
|
+
assert.match(step.reason.message, /not independently verified/);
|
|
61
|
+
}
|
|
62
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// The companion, positive case: when the driver's per-step count matches the
|
|
66
|
+
// plan's declared steps, each step must carry its own real observed status
|
|
67
|
+
// instead of the aggregate one.
|
|
68
|
+
test("uses real per-step status when the driver's step count matches the plan", async () => {
|
|
69
|
+
const { execute } = await import("./executor.js");
|
|
70
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-test-"));
|
|
71
|
+
const cfg = makeTestConfig(testDir);
|
|
72
|
+
cfg.execution.maestro.command = path.join(import.meta.dirname, "fixtures", "fake-maestro.sh");
|
|
73
|
+
const planYaml = `
|
|
74
|
+
suites:
|
|
75
|
+
- journeys:
|
|
76
|
+
- id: checkout
|
|
77
|
+
driver: maestro
|
|
78
|
+
platform: android
|
|
79
|
+
flow: flows/checkout.yaml
|
|
80
|
+
steps:
|
|
81
|
+
- node_id: step-1
|
|
82
|
+
action: launch
|
|
83
|
+
- node_id: step-2
|
|
84
|
+
action: tap-sign-in
|
|
85
|
+
- node_id: step-3
|
|
86
|
+
action: assert-home
|
|
87
|
+
`;
|
|
88
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "checkout.yaml"), planYaml, "utf8");
|
|
89
|
+
|
|
90
|
+
const result = await execute(cfg, { planId: "checkout", journeyId: "checkout" });
|
|
91
|
+
|
|
92
|
+
assert.equal(result.stepsExecuted, 3);
|
|
93
|
+
assert.equal(result.stepsPassed, 2);
|
|
94
|
+
assert.equal(result.stepsFailed, 1);
|
|
95
|
+
assert.ok(result.results);
|
|
96
|
+
const [first, second, third] = result.results!;
|
|
97
|
+
assert.equal(first.outcome, "PASS");
|
|
98
|
+
assert.equal(first.reason.code, "verified");
|
|
99
|
+
assert.equal(second.outcome, "PASS");
|
|
100
|
+
assert.equal(third.outcome, "FAIL");
|
|
101
|
+
assert.equal(third.reason.code, "execution_failed");
|
|
102
|
+
// Real, distinct per-step timing from Maestro's own commands.json, not a
|
|
103
|
+
// single timestamp copied onto every step.
|
|
104
|
+
assert.notEqual(first.startedAt, third.startedAt);
|
|
105
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// A flow edited after its plan was written has a different step count. The
|
|
109
|
+
// steps then carry only the aggregate verdict, but the run's counts must still
|
|
110
|
+
// be the driver's real ones: one failed command is "1 failed", not "4 failed".
|
|
111
|
+
test("counts the driver's own steps when they do not line up with the plan", async () => {
|
|
112
|
+
const { execute } = await import("./executor.js");
|
|
113
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-test-"));
|
|
114
|
+
const cfg = makeTestConfig(testDir);
|
|
115
|
+
cfg.execution.maestro.command = path.join(import.meta.dirname, "fixtures", "fake-maestro.sh");
|
|
116
|
+
const planYaml = `
|
|
117
|
+
suites:
|
|
118
|
+
- journeys:
|
|
119
|
+
- id: checkout
|
|
120
|
+
driver: maestro
|
|
121
|
+
platform: android
|
|
122
|
+
flow: flows/checkout.yaml
|
|
123
|
+
steps:
|
|
124
|
+
- node_id: step-1
|
|
125
|
+
action: launch
|
|
126
|
+
- node_id: step-2
|
|
127
|
+
action: tap-sign-in
|
|
128
|
+
- node_id: step-3
|
|
129
|
+
action: enter-email
|
|
130
|
+
- node_id: step-4
|
|
131
|
+
action: assert-home
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "checkout.yaml"), planYaml, "utf8");
|
|
134
|
+
|
|
135
|
+
const result = await execute(cfg, { planId: "checkout", journeyId: "checkout" });
|
|
136
|
+
|
|
137
|
+
assert.equal(result.status, "partial");
|
|
138
|
+
assert.equal(result.stepsExecuted, 4);
|
|
139
|
+
assert.equal(result.stepsPassed, 2);
|
|
140
|
+
assert.equal(result.stepsFailed, 1);
|
|
141
|
+
assert.ok(result.results!.every((step) => step.reason.code === "aggregate_only"));
|
|
142
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("loads the plan and its flow from the object store when they are not on the worker's disk", async () => {
|
|
146
|
+
const { execute } = await import("./executor.js");
|
|
147
|
+
const { LocalObjectStore } = await import("../storage/object-store.js");
|
|
148
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-store-"));
|
|
149
|
+
const cfg = makeTestConfig(testDir);
|
|
150
|
+
const store = new LocalObjectStore(cfg.runtime.artifacts_dir);
|
|
151
|
+
await store.put("projects/demo/plans/cloud-checkout.flow.yaml", `
|
|
152
|
+
suites:
|
|
153
|
+
- journeys:
|
|
154
|
+
- id: cloud-checkout
|
|
155
|
+
driver: maestro
|
|
156
|
+
platform: android
|
|
157
|
+
flow: flows/cloud-checkout.yaml
|
|
158
|
+
steps:
|
|
159
|
+
- node_id: step-1
|
|
160
|
+
action: launch
|
|
161
|
+
`, "text/yaml");
|
|
162
|
+
await store.put("projects/demo/spec/flows/cloud-checkout.yaml", "appId: demo\n---\n- launchApp\n", "text/yaml");
|
|
163
|
+
|
|
164
|
+
const result = await execute(cfg, { planId: "cloud-checkout", journeyId: "cloud-checkout", runId: "run-store" });
|
|
165
|
+
|
|
166
|
+
assert.doesNotMatch(result.error ?? "", /Plan not found/);
|
|
167
|
+
assert.equal(result.stepsExecuted, 1);
|
|
168
|
+
assert.ok(fs.existsSync(path.join(cfg.runtime.artifacts_dir, "demo", "run-store", "spec", "flows", "cloud-checkout.yaml")));
|
|
169
|
+
assert.equal(result.manifestKey, "projects/demo/runs/run-store/manifest.json");
|
|
170
|
+
assert.equal(result.evidencePath, "projects/demo/runs/run-store");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
test("fans a web run out across browser/device targets in parallel against the selected environment", async () => {
|
|
175
|
+
const { execute } = await import("./executor.js");
|
|
176
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-matrix-"));
|
|
177
|
+
const cfg = makeTestConfig(testDir);
|
|
178
|
+
const planYaml = `
|
|
179
|
+
suites:
|
|
180
|
+
- journeys:
|
|
181
|
+
- id: sign-in
|
|
182
|
+
driver: playwright
|
|
183
|
+
platform: web
|
|
184
|
+
flow: flows/sign-in.yaml
|
|
185
|
+
target:
|
|
186
|
+
base_url: http://localhost:3000
|
|
187
|
+
steps:
|
|
188
|
+
- node_id: open
|
|
189
|
+
action: open
|
|
190
|
+
- node_id: submit
|
|
191
|
+
action: submit
|
|
192
|
+
`;
|
|
193
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "sign-in.yaml"), planYaml, "utf8");
|
|
194
|
+
|
|
195
|
+
const contexts: DriverContext[] = [];
|
|
196
|
+
let inFlight = 0;
|
|
197
|
+
let peak = 0;
|
|
198
|
+
const driver: AppDriver = {
|
|
199
|
+
id: "playwright",
|
|
200
|
+
async run(context): Promise<DriverResult> {
|
|
201
|
+
contexts.push(context);
|
|
202
|
+
inFlight += 1;
|
|
203
|
+
peak = Math.max(peak, inFlight);
|
|
204
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
205
|
+
inFlight -= 1;
|
|
206
|
+
const target = context.runTarget!;
|
|
207
|
+
if (target.browser === "msedge") {
|
|
208
|
+
return { status: "blocked", message: "Edge is not installed", failureCode: "browser_not_installed", artifacts: [] };
|
|
209
|
+
}
|
|
210
|
+
const screenshot = path.join(testDir, `${target.id}-final.png`);
|
|
211
|
+
fs.writeFileSync(screenshot, "png");
|
|
212
|
+
const failed = target.browser === "firefox";
|
|
213
|
+
return {
|
|
214
|
+
status: failed ? "failed" : "passed",
|
|
215
|
+
message: failed ? "Submit button not found" : "ok",
|
|
216
|
+
artifacts: [{ name: "playwright-final.png", path: screenshot, contentType: "image/png" }],
|
|
217
|
+
steps: [
|
|
218
|
+
{ sequenceNumber: 0, status: "passed" },
|
|
219
|
+
{ sequenceNumber: 1, status: failed ? "failed" : "passed" },
|
|
220
|
+
],
|
|
221
|
+
durationMs: 40,
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
} as AppDriver;
|
|
225
|
+
|
|
226
|
+
const snapshots: string[][] = [];
|
|
227
|
+
const result = await execute(cfg, {
|
|
228
|
+
planId: "sign-in",
|
|
229
|
+
journeyId: "sign-in",
|
|
230
|
+
runId: "run-matrix",
|
|
231
|
+
baseUrl: "https://staging.example.com",
|
|
232
|
+
targets: [
|
|
233
|
+
{ id: "chromium", browser: "chromium" },
|
|
234
|
+
{ id: "firefox", browser: "firefox" },
|
|
235
|
+
{ id: "msedge", browser: "msedge" },
|
|
236
|
+
{ id: "webkit-iphone-15", browser: "webkit", device: "iPhone 15" },
|
|
237
|
+
],
|
|
238
|
+
driverFactory: () => driver,
|
|
239
|
+
onProgress: (progress) => {
|
|
240
|
+
if (progress.targetResults) snapshots.push(progress.targetResults.map((entry) => entry.status));
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
assert.equal(contexts.length, 4);
|
|
245
|
+
assert.ok(peak > 1, "targets should run concurrently");
|
|
246
|
+
for (const context of contexts) {
|
|
247
|
+
assert.equal(context.target.baseUrl, "https://staging.example.com");
|
|
248
|
+
assert.equal(context.rebaseOrigin, true);
|
|
249
|
+
}
|
|
250
|
+
assert.equal(result.status, "partial");
|
|
251
|
+
assert.deepEqual(result.targetResults?.map((entry) => [entry.targetId, entry.status]), [
|
|
252
|
+
["chromium", "passed"],
|
|
253
|
+
["firefox", "failed"],
|
|
254
|
+
["msedge", "blocked"],
|
|
255
|
+
["webkit-iphone-15", "passed"],
|
|
256
|
+
]);
|
|
257
|
+
const chromium = result.targetResults![0];
|
|
258
|
+
assert.equal(chromium.durationMs, 40);
|
|
259
|
+
assert.equal(chromium.stepsPassed, 2);
|
|
260
|
+
assert.equal(chromium.finalScreenshotKey, "projects/demo/runs/run-matrix/artifacts/chromium/playwright-final.png");
|
|
261
|
+
assert.deepEqual(result.targetResults![2].artifactKeys, []);
|
|
262
|
+
// The worst status per step across targets that ran.
|
|
263
|
+
assert.equal(result.stepsFailed, 1);
|
|
264
|
+
assert.deepEqual(snapshots[0], ["queued", "queued", "queued", "queued"]);
|
|
265
|
+
assert.ok(snapshots.some((snapshot) => snapshot.includes("running")));
|
|
266
|
+
assert.equal(typeof result.durationMs, "number");
|
|
267
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(cfg.runtime.artifacts_dir, "projects", "demo", "runs", "run-matrix", "manifest.json"), "utf8"));
|
|
268
|
+
assert.equal(manifest.targets.length, 4);
|
|
269
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("keeps the single-driver path when no matrix is requested", async () => {
|
|
273
|
+
const { execute } = await import("./executor.js");
|
|
274
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-single-"));
|
|
275
|
+
const cfg = makeTestConfig(testDir);
|
|
276
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "home.yaml"), `
|
|
277
|
+
suites:
|
|
278
|
+
- journeys:
|
|
279
|
+
- id: home
|
|
280
|
+
driver: playwright
|
|
281
|
+
platform: web
|
|
282
|
+
flow: flows/home.yaml
|
|
283
|
+
steps:
|
|
284
|
+
- node_id: open
|
|
285
|
+
action: open
|
|
286
|
+
`, "utf8");
|
|
287
|
+
const contexts: DriverContext[] = [];
|
|
288
|
+
const driver = {
|
|
289
|
+
id: "playwright",
|
|
290
|
+
async run(context: DriverContext): Promise<DriverResult> {
|
|
291
|
+
contexts.push(context);
|
|
292
|
+
return { status: "passed", message: "ok", artifacts: [], steps: [{ sequenceNumber: 0, status: "passed" }] };
|
|
293
|
+
},
|
|
294
|
+
} as unknown as AppDriver;
|
|
295
|
+
const result = await execute(cfg, { planId: "home", journeyId: "home", driverFactory: () => driver });
|
|
296
|
+
assert.equal(contexts.length, 1);
|
|
297
|
+
assert.equal(contexts[0].runTarget, undefined);
|
|
298
|
+
assert.equal(contexts[0].rebaseOrigin, false);
|
|
299
|
+
assert.equal(result.targetResults, undefined);
|
|
300
|
+
assert.equal(result.status, "success");
|
|
301
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("a failing progress update never stops a browser target from running", async () => {
|
|
305
|
+
const { execute } = await import("./executor.js");
|
|
306
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-progress-"));
|
|
307
|
+
const cfg = makeTestConfig(testDir);
|
|
308
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "home.yaml"), `
|
|
309
|
+
suites:
|
|
310
|
+
- journeys:
|
|
311
|
+
- id: home
|
|
312
|
+
driver: playwright
|
|
313
|
+
platform: web
|
|
314
|
+
flow: flows/home.yaml
|
|
315
|
+
steps:
|
|
316
|
+
- node_id: open
|
|
317
|
+
action: open
|
|
318
|
+
`, "utf8");
|
|
319
|
+
const ran: string[] = [];
|
|
320
|
+
const driver = {
|
|
321
|
+
id: "playwright",
|
|
322
|
+
async run(context: DriverContext): Promise<DriverResult> {
|
|
323
|
+
ran.push(context.runTarget!.id);
|
|
324
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
325
|
+
return { status: "passed", message: "ok", artifacts: [], steps: [{ sequenceNumber: 0, status: "passed" }] };
|
|
326
|
+
},
|
|
327
|
+
} as unknown as AppDriver;
|
|
328
|
+
// Reproduces concurrent registry updates failing (HTTP 500) while lanes report.
|
|
329
|
+
let calls = 0;
|
|
330
|
+
const result = await execute(cfg, {
|
|
331
|
+
planId: "home",
|
|
332
|
+
journeyId: "home",
|
|
333
|
+
targets: [
|
|
334
|
+
{ id: "chromium", browser: "chromium" },
|
|
335
|
+
{ id: "firefox", browser: "firefox" },
|
|
336
|
+
{ id: "webkit", browser: "webkit" },
|
|
337
|
+
],
|
|
338
|
+
driverFactory: () => driver,
|
|
339
|
+
onProgress: async () => {
|
|
340
|
+
calls += 1;
|
|
341
|
+
if (calls > 1) throw new Error("Registry update failed (500)");
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
assert.deepEqual([...ran].sort(), ["chromium", "firefox", "webkit"]);
|
|
345
|
+
assert.equal(result.status, "success");
|
|
346
|
+
assert.deepEqual(result.targetResults?.map((entry) => entry.status), ["passed", "passed", "passed"]);
|
|
347
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("apply fix patches the flow before running, and refuses a stale or empty fix", async () => {
|
|
351
|
+
const { execute } = await import("./executor.js");
|
|
352
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-fix-"));
|
|
353
|
+
const cfg = makeTestConfig(testDir);
|
|
354
|
+
fs.mkdirSync(path.join(cfg.runtime.spec_dir, "flows"), { recursive: true });
|
|
355
|
+
const flowFile = path.join(cfg.runtime.spec_dir, "flows", "home.yaml");
|
|
356
|
+
fs.writeFileSync(flowFile, 'url: http://localhost\n---\n- launchApp\n- tapOn: "Sign in"\n', "utf8");
|
|
357
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "home.yaml"), `
|
|
358
|
+
suites:
|
|
359
|
+
- journeys:
|
|
360
|
+
- id: home
|
|
361
|
+
driver: playwright
|
|
362
|
+
platform: web
|
|
363
|
+
flow: flows/home.yaml
|
|
364
|
+
steps:
|
|
365
|
+
- node_id: open
|
|
366
|
+
action: open
|
|
367
|
+
`, "utf8");
|
|
368
|
+
const seen: string[] = [];
|
|
369
|
+
const driver = {
|
|
370
|
+
id: "playwright",
|
|
371
|
+
async run(context: DriverContext): Promise<DriverResult> {
|
|
372
|
+
seen.push(fs.readFileSync(context.flowPath, "utf8"));
|
|
373
|
+
return { status: "passed", message: "ok", artifacts: [], steps: [{ sequenceNumber: 0, status: "passed" }] };
|
|
374
|
+
},
|
|
375
|
+
} as unknown as AppDriver;
|
|
376
|
+
const change = { op: "replace" as const, index: 1, expect: '{"type":"tapOn","text":"Sign in","index":0,"optional":false}', step: { tapOn: "Log in" } };
|
|
377
|
+
const fixed = await execute(cfg, {
|
|
378
|
+
planId: "home",
|
|
379
|
+
journeyId: "home",
|
|
380
|
+
targets: [{ id: "chromium", browser: "chromium" }],
|
|
381
|
+
applyFix: { changes: [change], sourceRunId: "run-old", targetId: "chromium" },
|
|
382
|
+
driverFactory: () => driver,
|
|
383
|
+
});
|
|
384
|
+
assert.match(seen[0], /Log in/);
|
|
385
|
+
assert.match(fs.readFileSync(flowFile, "utf8"), /Log in/);
|
|
386
|
+
assert.deepEqual(fixed.fixApplied, { sourceRunId: "run-old", targetId: "chromium", flow: "flows/home.yaml" });
|
|
387
|
+
|
|
388
|
+
// Same fix again: the step no longer says "Sign in", so it is stale.
|
|
389
|
+
const stale = await execute(cfg, { planId: "home", journeyId: "home", targets: [{ id: "chromium", browser: "chromium" }], applyFix: { changes: [change] }, driverFactory: () => driver });
|
|
390
|
+
assert.equal(stale.failureCode, "fix_not_applicable");
|
|
391
|
+
assert.equal(seen.length, 1);
|
|
392
|
+
const empty = await execute(cfg, { planId: "home", journeyId: "home", targets: [{ id: "chromium", browser: "chromium" }], applyFix: { changes: [] }, driverFactory: () => driver });
|
|
393
|
+
assert.equal(empty.failureCode, "fix_not_applicable");
|
|
394
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("an inserted fix adds matching nodes to a plan that lists one node per flow step", async () => {
|
|
398
|
+
const { execute } = await import("./executor.js");
|
|
399
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-fix-plan-"));
|
|
400
|
+
const cfg = makeTestConfig(testDir);
|
|
401
|
+
fs.mkdirSync(path.join(cfg.runtime.spec_dir, "flows"), { recursive: true });
|
|
402
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "flows", "form.yaml"), 'url: http://localhost\n---\n- launchApp\n- tapOn: "Submit"\n', "utf8");
|
|
403
|
+
const planFile = path.join(cfg.runtime.spec_dir, "plans", "form.yaml");
|
|
404
|
+
fs.writeFileSync(planFile, `
|
|
405
|
+
suites:
|
|
406
|
+
- journeys:
|
|
407
|
+
- id: form
|
|
408
|
+
driver: playwright
|
|
409
|
+
platform: web
|
|
410
|
+
flow: flows/form.yaml
|
|
411
|
+
steps:
|
|
412
|
+
- { node_id: open, action: Open }
|
|
413
|
+
- { node_id: submit, action: Submit }
|
|
414
|
+
`, "utf8");
|
|
415
|
+
const driver = {
|
|
416
|
+
id: "playwright",
|
|
417
|
+
async run(): Promise<DriverResult> {
|
|
418
|
+
return {
|
|
419
|
+
status: "passed",
|
|
420
|
+
message: "ok",
|
|
421
|
+
artifacts: [],
|
|
422
|
+
steps: [0, 1, 2, 3].map((sequenceNumber) => ({ sequenceNumber, status: "passed" as const })),
|
|
423
|
+
};
|
|
424
|
+
},
|
|
425
|
+
} as unknown as AppDriver;
|
|
426
|
+
const result = await execute(cfg, {
|
|
427
|
+
planId: "form",
|
|
428
|
+
journeyId: "form",
|
|
429
|
+
targets: [{ id: "chromium", browser: "chromium" }],
|
|
430
|
+
applyFix: { changes: [{ op: "insert", index: 1, steps: [{ tapOn: "Email" }, { inputText: "qa@example.com" }] }] },
|
|
431
|
+
driverFactory: () => driver,
|
|
432
|
+
});
|
|
433
|
+
const plan = fs.readFileSync(planFile, "utf8");
|
|
434
|
+
assert.match(plan, /node_id: fix-1[\s\S]*Tap “Email”[\s\S]*node_id: fix-2[\s\S]*node_id: submit/);
|
|
435
|
+
// Four plan nodes for four flow steps: every step is reported on its own.
|
|
436
|
+
assert.equal(result.stepsExecuted, 4);
|
|
437
|
+
assert.equal(result.results?.[0].reason.code, "verified");
|
|
438
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("AI repair asks the model for a checked edit, then runs the fixed flow", async () => {
|
|
442
|
+
const { execute } = await import("./executor.js");
|
|
443
|
+
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), "bugmole-executor-ai-fix-"));
|
|
444
|
+
const cfg = makeTestConfig(testDir);
|
|
445
|
+
fs.mkdirSync(path.join(cfg.runtime.spec_dir, "flows"), { recursive: true });
|
|
446
|
+
const flowFile = path.join(cfg.runtime.spec_dir, "flows", "home.yaml");
|
|
447
|
+
fs.writeFileSync(flowFile, 'url: http://localhost\n---\n- launchApp\n- tapOn: "Continue"\n- assertVisible: "Payment"\n', "utf8");
|
|
448
|
+
fs.writeFileSync(path.join(cfg.runtime.spec_dir, "plans", "home.yaml"), `
|
|
449
|
+
suites:
|
|
450
|
+
- journeys:
|
|
451
|
+
- id: home
|
|
452
|
+
driver: playwright
|
|
453
|
+
platform: web
|
|
454
|
+
flow: flows/home.yaml
|
|
455
|
+
`, "utf8");
|
|
456
|
+
const seen: string[] = [];
|
|
457
|
+
const driver = {
|
|
458
|
+
id: "playwright",
|
|
459
|
+
async run(context: DriverContext): Promise<DriverResult> {
|
|
460
|
+
seen.push(fs.readFileSync(context.flowPath, "utf8"));
|
|
461
|
+
return { status: "passed", message: "ok", artifacts: [], steps: [] };
|
|
462
|
+
},
|
|
463
|
+
} as unknown as AppDriver;
|
|
464
|
+
const failureContext = {
|
|
465
|
+
failedStepIndex: 2, error: "Timed out", pageErrors: [], consoleErrors: [],
|
|
466
|
+
candidates: [{ role: "textbox", text: "Email" }], alerts: ["Enter your email"], invalidFields: [],
|
|
467
|
+
};
|
|
468
|
+
const requests: any[] = [];
|
|
469
|
+
const originalFetch = globalThis.fetch;
|
|
470
|
+
const saved = { url: process.env.BUGMOLE_REGISTRY_URL, key: process.env.BUGMOLE_API_KEY };
|
|
471
|
+
process.env.BUGMOLE_REGISTRY_URL = "https://registry.test";
|
|
472
|
+
process.env.BUGMOLE_API_KEY = "vk_test";
|
|
473
|
+
const reply = { summary: "Enter the email first.", changes: [{ op: "insert", index: 1, steps: [{ tapOn: "Email" }, { inputText: "qa@example.com" }] }] };
|
|
474
|
+
globalThis.fetch = (async (url: string, init: RequestInit) => {
|
|
475
|
+
requests.push({ url, body: JSON.parse(String(init.body)) });
|
|
476
|
+
return Response.json({ text: JSON.stringify(reply), model: "claude-sonnet-5" });
|
|
477
|
+
}) as unknown as typeof fetch;
|
|
478
|
+
try {
|
|
479
|
+
const result = await execute(cfg, {
|
|
480
|
+
planId: "home",
|
|
481
|
+
journeyId: "home",
|
|
482
|
+
runId: "run-new",
|
|
483
|
+
targets: [{ id: "chromium", browser: "chromium" }],
|
|
484
|
+
applyFix: { changes: [], mode: "ai", sourceRunId: "run-old", targetId: "chromium", failureContext },
|
|
485
|
+
driverFactory: () => driver,
|
|
486
|
+
});
|
|
487
|
+
assert.equal(result.fixApplied?.sourceRunId, "run-old");
|
|
488
|
+
assert.match(seen[0], /inputText: qa@example.com/);
|
|
489
|
+
assert.equal(requests.length, 1);
|
|
490
|
+
assert.equal(requests[0].url, "https://registry.test/api/ai/complete");
|
|
491
|
+
assert.equal(requests[0].body.purpose, "repair");
|
|
492
|
+
assert.equal(requests[0].body.runId, "run-new");
|
|
493
|
+
assert.match(requests[0].body.prompt, /Enter your email/);
|
|
494
|
+
|
|
495
|
+
// A model answer the flow checker rejects never reaches the test.
|
|
496
|
+
reply.changes = [{ op: "insert", index: 42, steps: [{ tapOn: "x" }] }];
|
|
497
|
+
const rejected = await execute(cfg, {
|
|
498
|
+
planId: "home", journeyId: "home", runId: "run-3", targets: [{ id: "chromium", browser: "chromium" }],
|
|
499
|
+
applyFix: { changes: [], mode: "ai", failureContext }, driverFactory: () => driver,
|
|
500
|
+
});
|
|
501
|
+
assert.equal(rejected.failureCode, "fix_not_applicable");
|
|
502
|
+
assert.equal(seen.length, 1);
|
|
503
|
+
} finally {
|
|
504
|
+
globalThis.fetch = originalFetch;
|
|
505
|
+
if (saved.url === undefined) delete process.env.BUGMOLE_REGISTRY_URL; else process.env.BUGMOLE_REGISTRY_URL = saved.url;
|
|
506
|
+
if (saved.key === undefined) delete process.env.BUGMOLE_API_KEY; else process.env.BUGMOLE_API_KEY = saved.key;
|
|
507
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
508
|
+
}
|
|
509
|
+
});
|