@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,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Ensures the `maestro` CLI is available, since Bugmole's driver shells out to
|
|
3
|
+
// it for `bugmole run`. Runs as a postinstall step so installing bugmole installs
|
|
4
|
+
// maestro too. Never fails the parent `npm install` — this is best-effort.
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const fs = require("node:fs");
|
|
9
|
+
|
|
10
|
+
function log(message) {
|
|
11
|
+
console.log(`[bugmole postinstall] ${message}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function isMaestroOnPath() {
|
|
15
|
+
const result = spawnSync("maestro", ["--version"], { stdio: "ignore" });
|
|
16
|
+
return !result.error && result.status === 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function maestroHomeBin() {
|
|
20
|
+
return path.join(os.homedir(), ".maestro", "bin", "maestro");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isMaestroInstalledButNotOnPath() {
|
|
24
|
+
return fs.existsSync(maestroHomeBin());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function main() {
|
|
28
|
+
if (process.env.BUGMOLE_SKIP_MAESTRO_INSTALL === "true") {
|
|
29
|
+
log("Skipping maestro install (BUGMOLE_SKIP_MAESTRO_INSTALL=true).");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isMaestroOnPath()) {
|
|
34
|
+
log("maestro CLI already installed and on PATH.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (isMaestroInstalledButNotOnPath()) {
|
|
39
|
+
log(
|
|
40
|
+
`maestro is installed at ${maestroHomeBin()} but not on PATH. Add ` +
|
|
41
|
+
`"${path.join(os.homedir(), ".maestro", "bin")}" to your PATH.`,
|
|
42
|
+
);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.platform === "win32") {
|
|
47
|
+
log(
|
|
48
|
+
"maestro CLI not found and there is no supported auto-install on Windows. " +
|
|
49
|
+
"Install it manually: https://maestro.mobile.dev",
|
|
50
|
+
);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
log("maestro CLI not found — installing (curl -Ls https://get.maestro.mobile.dev | bash)...");
|
|
55
|
+
const install = spawnSync("bash", ["-c", 'curl -Ls "https://get.maestro.mobile.dev" | bash'], {
|
|
56
|
+
stdio: "inherit",
|
|
57
|
+
timeout: 5 * 60 * 1000,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (install.error || install.status !== 0) {
|
|
61
|
+
log(
|
|
62
|
+
"Automatic maestro install failed. `bugmole run` will report a blocked/driver_unavailable " +
|
|
63
|
+
'result until it is installed manually: curl -Ls "https://get.maestro.mobile.dev" | bash',
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
log(
|
|
69
|
+
`maestro installed to ${maestroHomeBin()}. Ensure ` +
|
|
70
|
+
`"${path.join(os.homedir(), ".maestro", "bin")}" is on your PATH (the installer usually ` +
|
|
71
|
+
"adds this to your shell profile — restart your shell if `maestro --version` still fails).",
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
main();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
log(`Skipping maestro auto-install after unexpected error: ${error.message}`);
|
|
79
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Keeps a usable iOS RemoteXPC tunnel up.
|
|
3
|
+
#
|
|
4
|
+
# Restarting only when the process exits is not enough: after the phone
|
|
5
|
+
# reboots or the cable is re-seated, the tunnel process happily stays alive
|
|
6
|
+
# holding a tunnel that no longer routes, and every developer command fails
|
|
7
|
+
# with "No route to host". So health is judged by whether the RSD endpoint
|
|
8
|
+
# actually answers, not by whether the process is running — a stale tunnel is
|
|
9
|
+
# killed and respawned like a dead one.
|
|
10
|
+
#
|
|
11
|
+
# `remote tunneld` would do this properly but needs root; the native macOS
|
|
12
|
+
# tunnel does not, so this supervises that instead.
|
|
13
|
+
# Stop with: pkill -f ios-tunnel-keeper
|
|
14
|
+
LOG=/tmp/ios-tunnel.log
|
|
15
|
+
STATE=/tmp/ios-tunnel-rsd
|
|
16
|
+
|
|
17
|
+
spawn() {
|
|
18
|
+
pkill -f "remote start-tunnel" 2>/dev/null
|
|
19
|
+
sleep 2
|
|
20
|
+
: > "$STATE"
|
|
21
|
+
caffeinate -s pymobiledevice3 remote start-tunnel >> "$LOG" 2>&1 &
|
|
22
|
+
# Wait for it to announce an address, rather than assuming a fixed delay.
|
|
23
|
+
for _ in $(seq 1 20); do
|
|
24
|
+
sleep 2
|
|
25
|
+
A=$(grep -oE "RSD Address: \S+" "$LOG" | tail -1 | awk '{print $3}')
|
|
26
|
+
P=$(grep -oE "RSD Port: [0-9]+" "$LOG" | tail -1 | awk '{print $3}')
|
|
27
|
+
if [ -n "$A" ] && [ -n "$P" ] && nc -z -G 3 -w 3 "$A" "$P" 2>/dev/null; then
|
|
28
|
+
printf '%s %s\n' "$A" "$P" > "$STATE"
|
|
29
|
+
echo "$(date -u +%H:%M:%S) tunnel up: $A $P" >> "$LOG"
|
|
30
|
+
return 0
|
|
31
|
+
fi
|
|
32
|
+
done
|
|
33
|
+
echo "$(date -u +%H:%M:%S) tunnel did not come up" >> "$LOG"
|
|
34
|
+
return 1
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
spawn
|
|
38
|
+
while true; do
|
|
39
|
+
sleep 20
|
|
40
|
+
read -r A P < "$STATE" 2>/dev/null
|
|
41
|
+
if [ -z "$A" ] || [ -z "$P" ] || ! nc -z -G 3 -w 3 "$A" "$P" 2>/dev/null; then
|
|
42
|
+
echo "$(date -u +%H:%M:%S) tunnel stale ($A $P) — respawning" >> "$LOG"
|
|
43
|
+
spawn
|
|
44
|
+
fi
|
|
45
|
+
done
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Keeps the WebDriverAgent runner alive on an attached iPhone.
|
|
3
|
+
#
|
|
4
|
+
# WDA is an xcodebuild test session, not a daemon: it ends when the process
|
|
5
|
+
# ends, and iOS input dies with it. Left unsupervised it simply stopped, while
|
|
6
|
+
# the dashboard went on advertising an Interact button that could not work.
|
|
7
|
+
#
|
|
8
|
+
# It also keeps the usbmux forward to WDA's port alive, because that is how
|
|
9
|
+
# the dashboard now sends input. Going through `pymobiledevice3 developer wda`
|
|
10
|
+
# meant paying a Python start-up per press — 3.8s for Home, and twice that for
|
|
11
|
+
# a tap, which read as "iPhone can't go home". Over this forward the same press
|
|
12
|
+
# is 0.06s. The forward is over the cable rather than the phone's Wi-Fi
|
|
13
|
+
# address, so it survives the phone changing network.
|
|
14
|
+
#
|
|
15
|
+
# Needs a built runner (see the WDA build in the repo history) and the device
|
|
16
|
+
# unlocked. Stop with: pkill -f ios-wda-keeper
|
|
17
|
+
UDID="${1:?usage: ios-wda-keeper.sh <udid> [wda-project-dir] [derived-data]}"
|
|
18
|
+
# Not /tmp: macOS clears it, and when it did the checkout and the build tree
|
|
19
|
+
# both vanished, so the keeper sat in a five-second restart loop against a
|
|
20
|
+
# project that no longer existed while iOS input and recording stayed down.
|
|
21
|
+
# Kept inside the repo (.local is gitignored) so it survives a reboot.
|
|
22
|
+
PROJECT="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/.local/WebDriverAgent}"
|
|
23
|
+
DERIVED="${3:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/.local/wda-build}"
|
|
24
|
+
PORT="${4:-8100}"
|
|
25
|
+
# WebDriverAgent's MJPEG server, which is how an iOS run is recorded.
|
|
26
|
+
MJPEG_PORT="${5:-9100}"
|
|
27
|
+
LOG=/tmp/wda-run.log
|
|
28
|
+
|
|
29
|
+
# Supervise the forward alongside the runner: it dies with an unplug, and
|
|
30
|
+
# without it every gesture fails while WDA itself is perfectly healthy.
|
|
31
|
+
forward_keeper() {
|
|
32
|
+
while true; do
|
|
33
|
+
if ! curl -sf -o /dev/null --max-time 2 "http://127.0.0.1:$PORT/status"; then
|
|
34
|
+
pkill -f "usbmux forward $PORT $PORT" 2>/dev/null
|
|
35
|
+
pymobiledevice3 usbmux forward "$PORT" "$PORT" --serial "$UDID" >> /tmp/wda-fwd.log 2>&1 &
|
|
36
|
+
fi
|
|
37
|
+
# Recording reads the MJPEG server, which is a separate port and dies
|
|
38
|
+
# independently of the one input goes through.
|
|
39
|
+
#
|
|
40
|
+
# Checked with a TCP connect, not a fetch: an MJPEG response never
|
|
41
|
+
# completes, so `curl --max-time` always exits 28 and the check can never
|
|
42
|
+
# pass. That killed and respawned this forward every five seconds — 253
|
|
43
|
+
# times before it was noticed — and would cut any recording in progress.
|
|
44
|
+
if ! nc -z 127.0.0.1 "$MJPEG_PORT" >/dev/null 2>&1; then
|
|
45
|
+
pkill -f "usbmux forward $MJPEG_PORT $MJPEG_PORT" 2>/dev/null
|
|
46
|
+
pymobiledevice3 usbmux forward "$MJPEG_PORT" "$MJPEG_PORT" --serial "$UDID" >> /tmp/wda-fwd.log 2>&1 &
|
|
47
|
+
fi
|
|
48
|
+
sleep 5
|
|
49
|
+
done
|
|
50
|
+
}
|
|
51
|
+
forward_keeper &
|
|
52
|
+
FORWARD_KEEPER=$!
|
|
53
|
+
# Take the forward supervisor down with this script, rather than leaving an
|
|
54
|
+
# orphan respawning a tunnel nothing is using.
|
|
55
|
+
trap 'kill "$FORWARD_KEEPER" 2>/dev/null; pkill -f "usbmux forward $PORT $PORT" 2>/dev/null; pkill -f "usbmux forward $MJPEG_PORT $MJPEG_PORT" 2>/dev/null' EXIT
|
|
56
|
+
|
|
57
|
+
while true; do
|
|
58
|
+
echo "$(date -u +%H:%M:%S) starting WDA runner" >> "$LOG"
|
|
59
|
+
xcodebuild test-without-building \
|
|
60
|
+
-project "$PROJECT/WebDriverAgent.xcodeproj" \
|
|
61
|
+
-scheme WebDriverAgentRunner \
|
|
62
|
+
-destination "id=$UDID" \
|
|
63
|
+
-derivedDataPath "$DERIVED" >> "$LOG" 2>&1
|
|
64
|
+
echo "$(date -u +%H:%M:%S) WDA runner exited; restarting in 5s" >> "$LOG"
|
|
65
|
+
sleep 5
|
|
66
|
+
done
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Copies the plan catalog into the dashboard, which deliberately builds
|
|
2
|
+
// without importing files from outside apps/qa. Run after editing
|
|
3
|
+
// src/billing/plan-catalog.ts; the root test suite fails if the copy drifts.
|
|
4
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
8
|
+
export const catalogSource = path.join(root, "src/billing/plan-catalog.ts");
|
|
9
|
+
export const catalogCopy = path.join(root, "apps/qa/src/lib/plan-catalog.ts");
|
|
10
|
+
export const COPY_HEADER = "// GENERATED by `npm run sync:catalog` from src/billing/plan-catalog.ts — edit that file instead.\n";
|
|
11
|
+
|
|
12
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
13
|
+
const source = await readFile(catalogSource, "utf8");
|
|
14
|
+
await writeFile(catalogCopy, COPY_HEADER + source);
|
|
15
|
+
console.log(`Wrote ${path.relative(root, catalogCopy)}`);
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Compare normalized reference and app screenshots with Pillow."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from PIL import Image, ImageChops, ImageEnhance
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
parser = argparse.ArgumentParser()
|
|
13
|
+
parser.add_argument("--reference", required=True, type=Path)
|
|
14
|
+
parser.add_argument("--actual", required=True, type=Path)
|
|
15
|
+
parser.add_argument("--output-dir", required=True, type=Path)
|
|
16
|
+
parser.add_argument("--width", type=int, default=390)
|
|
17
|
+
parser.add_argument("--height", type=int, default=844)
|
|
18
|
+
parser.add_argument("--threshold", type=int, default=16)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
if args.width <= 0 or args.height <= 0:
|
|
22
|
+
raise SystemExit("width and height must be positive")
|
|
23
|
+
if args.threshold < 0 or args.threshold > 255:
|
|
24
|
+
raise SystemExit("threshold must be between 0 and 255")
|
|
25
|
+
|
|
26
|
+
args.output_dir.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
size = (args.width, args.height)
|
|
28
|
+
reference = Image.open(args.reference).convert("RGB").resize(size)
|
|
29
|
+
actual = Image.open(args.actual).convert("RGB").resize(size)
|
|
30
|
+
reference_path = args.output_dir / "reference-normalized.png"
|
|
31
|
+
actual_path = args.output_dir / "actual-normalized.png"
|
|
32
|
+
reference.save(reference_path)
|
|
33
|
+
actual.save(actual_path)
|
|
34
|
+
|
|
35
|
+
diff = ImageChops.difference(reference, actual)
|
|
36
|
+
changed_pixels = sum(
|
|
37
|
+
1 for pixel in diff.getdata() if max(pixel) > args.threshold
|
|
38
|
+
)
|
|
39
|
+
total_pixels = args.width * args.height
|
|
40
|
+
changed_fraction = changed_pixels / total_pixels
|
|
41
|
+
heatmap_path = args.output_dir / "heatmap.png"
|
|
42
|
+
ImageEnhance.Brightness(diff).enhance(3).save(heatmap_path)
|
|
43
|
+
|
|
44
|
+
result = {
|
|
45
|
+
"schemaVersion": 1,
|
|
46
|
+
"width": args.width,
|
|
47
|
+
"height": args.height,
|
|
48
|
+
"threshold": args.threshold,
|
|
49
|
+
"changedPixels": changed_pixels,
|
|
50
|
+
"totalPixels": total_pixels,
|
|
51
|
+
"changedFraction": changed_fraction,
|
|
52
|
+
"differencePercent": changed_fraction * 100,
|
|
53
|
+
"scoreMeaning": "0% means identical pixels; higher percentages mean more difference",
|
|
54
|
+
"referenceNormalized": str(reference_path),
|
|
55
|
+
"actualNormalized": str(actual_path),
|
|
56
|
+
"heatmap": str(heatmap_path),
|
|
57
|
+
}
|
|
58
|
+
(args.output_dir / "diff.json").write_text(
|
|
59
|
+
json.dumps(result, indent=2) + "\n", encoding="utf-8"
|
|
60
|
+
)
|
|
61
|
+
print(json.dumps(result))
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Pillow>=10.0
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Runs the manage → plans flow against a live dashboard and records what it
|
|
4
|
+
* observed as test case verdicts.
|
|
5
|
+
*
|
|
6
|
+
* This replaces an earlier script that performed the same checks but wrote
|
|
7
|
+
* `{caseId, status, detail}` into a bespoke results.json that nothing read.
|
|
8
|
+
* Verdicts only count when they go through the test-case-results contract:
|
|
9
|
+
* that is what the dashboard reads to move a case off "planned", and what the
|
|
10
|
+
* pipeline's run stage requires before it calls a flow covered.
|
|
11
|
+
*
|
|
12
|
+
* Every result carries a detail describing what was actually seen, because
|
|
13
|
+
* bugmole_write_test_case_results rejects a verdict without one — a pass
|
|
14
|
+
* nobody can review is not evidence.
|
|
15
|
+
*
|
|
16
|
+
* npx tsx scripts/verify-manage-to-plans.mts [--base http://localhost:3020]
|
|
17
|
+
*/
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import fs from "node:fs/promises";
|
|
20
|
+
import { chromium, type Page } from "playwright";
|
|
21
|
+
import { callToolInternal } from "../src/mcp/server.js";
|
|
22
|
+
|
|
23
|
+
const CASE_SET_ID = "flow-manage-to-plans-64c3c61b";
|
|
24
|
+
const PROJECT = process.env.BUGMOLE_PROJECT_ID ?? "bugmole";
|
|
25
|
+
const BASE = argValue("--base") ?? process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3020";
|
|
26
|
+
const ARTIFACTS = process.env.BUGMOLE_ARTIFACTS_DIR ?? "./qa/bugmole-runs";
|
|
27
|
+
const SHOTS = path.join(ARTIFACTS, "projects", PROJECT, "verify", CASE_SET_ID);
|
|
28
|
+
|
|
29
|
+
type Status = "passed" | "failed" | "blocked" | "needs_setup";
|
|
30
|
+
type Result = { caseId: string; status: Status; detail: string; evidence?: string };
|
|
31
|
+
|
|
32
|
+
function argValue(flag: string): string | undefined {
|
|
33
|
+
const index = process.argv.indexOf(flag);
|
|
34
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const results: Result[] = [];
|
|
38
|
+
function record(caseId: string, status: Status, detail: string, evidence?: string) {
|
|
39
|
+
results.push({ caseId, status, detail, evidence });
|
|
40
|
+
const mark = status === "passed" ? "✓" : status === "failed" ? "✗" : "○";
|
|
41
|
+
console.log(`${mark} ${caseId.padEnd(24)} ${detail}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function visible(page: Page, selector: string): Promise<boolean> {
|
|
45
|
+
return page.locator(selector).first().isVisible().catch(() => false);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function shot(page: Page, name: string): Promise<string> {
|
|
49
|
+
const file = path.join(SHOTS, `${name}.png`);
|
|
50
|
+
await page.screenshot({ path: file }).catch(() => undefined);
|
|
51
|
+
return file;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main() {
|
|
55
|
+
await fs.mkdir(SHOTS, { recursive: true });
|
|
56
|
+
const browser = await chromium.launch({ headless: true });
|
|
57
|
+
const page = await browser.newPage();
|
|
58
|
+
page.setDefaultTimeout(15000);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// TC-MANAGE-ENTRANCE — the manage hub renders its four resource cards.
|
|
62
|
+
await page.goto(`${BASE}/manage?project=${PROJECT}`);
|
|
63
|
+
const hubParts: Array<[string, string]> = [
|
|
64
|
+
[".auth-title", "page title"],
|
|
65
|
+
["a.management-hub-card[href='/manage/workspaces']", "workspaces card"],
|
|
66
|
+
["a.management-hub-card[href='/manage/projects']", "projects card"],
|
|
67
|
+
["a.management-hub-card[href='/manage/apps']", "apps card"],
|
|
68
|
+
["a.management-hub-card[href='/manage/environments']", "environments card"],
|
|
69
|
+
["a.auth-back[href='/']", "back link"],
|
|
70
|
+
];
|
|
71
|
+
const missing: string[] = [];
|
|
72
|
+
for (const [selector, label] of hubParts) {
|
|
73
|
+
if (!(await visible(page, selector))) missing.push(label);
|
|
74
|
+
}
|
|
75
|
+
record(
|
|
76
|
+
"TC-MANAGE-ENTRANCE",
|
|
77
|
+
missing.length === 0 ? "passed" : "failed",
|
|
78
|
+
missing.length === 0
|
|
79
|
+
? "/manage rendered the title, all four resource cards and the back link"
|
|
80
|
+
: `/manage was missing: ${missing.join(", ")}`,
|
|
81
|
+
await shot(page, "01-manage"),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// TC-MANAGE-NAV-PLANS — the sidebar link reaches /plans carrying project context.
|
|
85
|
+
await page.getByRole("link", { name: "Test plans", exact: true }).click().catch(() => undefined);
|
|
86
|
+
await page.waitForLoadState("networkidle").catch(() => undefined);
|
|
87
|
+
const onPlans = /\/plans/.test(page.url());
|
|
88
|
+
const keptProject = page.url().includes(`project=${PROJECT}`);
|
|
89
|
+
record(
|
|
90
|
+
"TC-MANAGE-NAV-PLANS",
|
|
91
|
+
onPlans && keptProject ? "passed" : "failed",
|
|
92
|
+
onPlans && keptProject
|
|
93
|
+
? `sidebar navigation reached ${page.url()} with project context intact`
|
|
94
|
+
: onPlans
|
|
95
|
+
? `reached /plans but the sidebar dropped ?project=${PROJECT} (url was ${page.url()})`
|
|
96
|
+
: `sidebar navigation did not reach /plans (url was ${page.url()})`,
|
|
97
|
+
await shot(page, "02-plans"),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Continue from a known-good URL so one navigation defect does not cascade
|
|
101
|
+
// into failures for cases that are really about the plans workspace.
|
|
102
|
+
if (!onPlans || !keptProject) {
|
|
103
|
+
await page.goto(`${BASE}/plans?project=${PROJECT}`);
|
|
104
|
+
await page.waitForLoadState("networkidle").catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// TC-PLANS-GOAL — the plans workspace renders, both by navigation and directly.
|
|
108
|
+
await page.locator("section#plans .plan-list").waitFor({ state: "visible", timeout: 10000 }).catch(() => undefined);
|
|
109
|
+
const goalParts = [
|
|
110
|
+
await visible(page, "section#plans"),
|
|
111
|
+
await visible(page, "section#plans .plan-list"),
|
|
112
|
+
];
|
|
113
|
+
record(
|
|
114
|
+
"TC-PLANS-GOAL",
|
|
115
|
+
goalParts.every(Boolean) ? "passed" : "failed",
|
|
116
|
+
goalParts.every(Boolean)
|
|
117
|
+
? "the plans section and its plan list rendered on a direct load of /plans"
|
|
118
|
+
: `plans workspace incomplete: section=${goalParts[0]}, planList=${goalParts[1]}`,
|
|
119
|
+
await shot(page, "03-plans-workspace"),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// TC-PLANS-SELECT-PLAN — selecting a plan row enables the start button.
|
|
123
|
+
const rows = page.locator("button.plan-row");
|
|
124
|
+
const rowCount = await rows.count();
|
|
125
|
+
if (rowCount === 0) {
|
|
126
|
+
// Nothing to select, so nothing was proven either way. "blocked" says
|
|
127
|
+
// the check could not run, which is the honest verdict — not a failure
|
|
128
|
+
// of the behaviour under test.
|
|
129
|
+
record("TC-PLANS-SELECT-PLAN", "blocked", "no plan rows rendered, so selection could not be exercised");
|
|
130
|
+
record("TC-PLANS-START-BLOCKED", "blocked", "no plan could be selected, so starting one could not be exercised");
|
|
131
|
+
} else {
|
|
132
|
+
await rows.first().click();
|
|
133
|
+
const pressed = await rows.first().getAttribute("aria-pressed");
|
|
134
|
+
const startEnabled = await visible(page, "button.plan-start-button:not([disabled])");
|
|
135
|
+
record(
|
|
136
|
+
"TC-PLANS-SELECT-PLAN",
|
|
137
|
+
pressed === "true" && startEnabled ? "passed" : "failed",
|
|
138
|
+
pressed === "true" && startEnabled
|
|
139
|
+
? "clicking a plan row set aria-pressed=true and enabled the start button"
|
|
140
|
+
: `after clicking a plan row: aria-pressed=${pressed}, start button enabled=${startEnabled}`,
|
|
141
|
+
await shot(page, "04-plan-selected"),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// TC-PLANS-START-BLOCKED — starting posts a run, even with no worker attached.
|
|
145
|
+
const runPost = page.waitForResponse(
|
|
146
|
+
(response) => response.url().includes(`/api/projects/${PROJECT}/runs`) && response.request().method() === "POST",
|
|
147
|
+
{ timeout: 10000 },
|
|
148
|
+
).catch(() => null);
|
|
149
|
+
await page.locator("button.plan-start-button").click();
|
|
150
|
+
const response = await runPost;
|
|
151
|
+
const ok = response !== null && [200, 201].includes(response.status());
|
|
152
|
+
record(
|
|
153
|
+
"TC-PLANS-START-BLOCKED",
|
|
154
|
+
ok ? "passed" : "failed",
|
|
155
|
+
ok
|
|
156
|
+
? `starting the plan posted to /api/projects/${PROJECT}/runs and returned ${response!.status()}`
|
|
157
|
+
: response === null
|
|
158
|
+
? "starting the plan sent no POST to the runs endpoint within 10s"
|
|
159
|
+
: `the runs endpoint returned ${response.status()}`,
|
|
160
|
+
await shot(page, "05-start-plan"),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// TC-PLANS-AUTH-REDIRECT — unauthenticated /plans should redirect to login.
|
|
165
|
+
// The local dev bypass authenticates every request, so this cannot be
|
|
166
|
+
// exercised here at all; saying so is more useful than a pass or a fail.
|
|
167
|
+
record(
|
|
168
|
+
"TC-PLANS-AUTH-REDIRECT",
|
|
169
|
+
"blocked",
|
|
170
|
+
"BUGMOLE_LOCAL_DEV_BYPASS_AUTH authenticates every local request, so the unauthenticated redirect cannot be observed against this environment",
|
|
171
|
+
);
|
|
172
|
+
} finally {
|
|
173
|
+
await browser.close();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const cfg = {
|
|
177
|
+
project: { id: PROJECT },
|
|
178
|
+
runtime: { spec_dir: process.env.BUGMOLE_SPEC_DIR ?? "./spec", artifacts_dir: ARTIFACTS },
|
|
179
|
+
storage: { provider: "local", localDirectory: ARTIFACTS },
|
|
180
|
+
} as any;
|
|
181
|
+
|
|
182
|
+
const written = await callToolInternal("bugmole_write_test_case_results", {
|
|
183
|
+
resultSet: { schemaVersion: 1, caseSetId: CASE_SET_ID, results },
|
|
184
|
+
}, cfg);
|
|
185
|
+
|
|
186
|
+
console.log(`\nrecorded ${written.recorded}/${written.of} cases → ${written.objectKey}`);
|
|
187
|
+
console.log(JSON.stringify(written.counts));
|
|
188
|
+
process.exit(results.some((result) => result.status === "failed") ? 1 : 0);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
main().catch((error) => {
|
|
192
|
+
console.error(error);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://app.bugmole.com/schemas/app-ui-audit.schema.json",
|
|
4
|
+
"title": "Bugmole Application UI Audit",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schemaVersion",
|
|
9
|
+
"auditId",
|
|
10
|
+
"requestedFlow",
|
|
11
|
+
"journeyId",
|
|
12
|
+
"journeyRevision",
|
|
13
|
+
"flowStatus",
|
|
14
|
+
"exploredNodeIds",
|
|
15
|
+
"exploredEdgeIds",
|
|
16
|
+
"summary",
|
|
17
|
+
"screens",
|
|
18
|
+
"highestPriorityMissingImplementations",
|
|
19
|
+
"blockedVerification"
|
|
20
|
+
],
|
|
21
|
+
"properties": {
|
|
22
|
+
"schemaVersion": { "const": 1 },
|
|
23
|
+
"auditId": { "type": "string", "minLength": 1 },
|
|
24
|
+
"workspaceId": { "type": "string" },
|
|
25
|
+
"projectId": { "type": "string" },
|
|
26
|
+
"actor": { "type": "string" },
|
|
27
|
+
"baseUrl": { "type": "string", "format": "uri" },
|
|
28
|
+
"requestedFlow": { "type": "string", "minLength": 1 },
|
|
29
|
+
"journeyId": { "type": "string", "minLength": 1 },
|
|
30
|
+
"journeyRevision": { "type": "integer", "minimum": 1 },
|
|
31
|
+
"flowStatus": { "enum": ["exhausted", "blocked"] },
|
|
32
|
+
"exploredNodeIds": {
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "type": "string", "minLength": 1 }
|
|
35
|
+
},
|
|
36
|
+
"exploredEdgeIds": {
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": { "type": "string", "minLength": 1 }
|
|
39
|
+
},
|
|
40
|
+
"blockerResolutionId": { "type": "string", "minLength": 1 },
|
|
41
|
+
"clarification": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"required": ["fingerprint", "question", "reason"],
|
|
45
|
+
"properties": {
|
|
46
|
+
"fingerprint": { "type": "string", "minLength": 1 },
|
|
47
|
+
"question": { "type": "string", "minLength": 1 },
|
|
48
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
49
|
+
"status": { "enum": ["needed", "remembered", "resolved"] }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"startedAt": { "type": "string", "format": "date-time" },
|
|
53
|
+
"completedAt": { "type": "string", "format": "date-time" },
|
|
54
|
+
"summary": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"additionalProperties": false,
|
|
57
|
+
"required": ["total", "implemented", "partiallyImplemented", "notImplemented"],
|
|
58
|
+
"properties": {
|
|
59
|
+
"total": { "type": "integer", "minimum": 0 },
|
|
60
|
+
"implemented": { "type": "integer", "minimum": 0 },
|
|
61
|
+
"partiallyImplemented": { "type": "integer", "minimum": 0 },
|
|
62
|
+
"notImplemented": { "type": "integer", "minimum": 0 }
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"screens": {
|
|
66
|
+
"type": "array",
|
|
67
|
+
"items": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"additionalProperties": false,
|
|
70
|
+
"required": ["screenId", "controls"],
|
|
71
|
+
"properties": {
|
|
72
|
+
"screenId": { "type": "string", "minLength": 1 },
|
|
73
|
+
"route": { "type": "string" },
|
|
74
|
+
"title": { "type": "string" },
|
|
75
|
+
"controls": {
|
|
76
|
+
"type": "array",
|
|
77
|
+
"items": { "$ref": "#/$defs/control" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"highestPriorityMissingImplementations": {
|
|
83
|
+
"type": "array",
|
|
84
|
+
"items": { "$ref": "#/$defs/issue" }
|
|
85
|
+
},
|
|
86
|
+
"blockedVerification": {
|
|
87
|
+
"type": "array",
|
|
88
|
+
"items": {
|
|
89
|
+
"type": "object",
|
|
90
|
+
"additionalProperties": false,
|
|
91
|
+
"required": ["description", "reason"],
|
|
92
|
+
"properties": {
|
|
93
|
+
"description": { "type": "string" },
|
|
94
|
+
"reason": { "type": "string" },
|
|
95
|
+
"fingerprint": { "type": "string", "minLength": 1 },
|
|
96
|
+
"question": { "type": "string", "minLength": 1 },
|
|
97
|
+
"status": { "enum": ["needed", "remembered", "resolved"] },
|
|
98
|
+
"resolutionId": { "type": "string", "minLength": 1 }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"$defs": {
|
|
104
|
+
"evidence": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"additionalProperties": false,
|
|
107
|
+
"required": ["type", "description"],
|
|
108
|
+
"properties": {
|
|
109
|
+
"type": { "enum": ["runtime", "source"] },
|
|
110
|
+
"medium": { "enum": ["screenshot", "video", "log", "other"] },
|
|
111
|
+
"description": { "type": "string" },
|
|
112
|
+
"location": { "type": "string", "minLength": 1 }
|
|
113
|
+
},
|
|
114
|
+
"if": { "properties": { "type": { "const": "runtime" } } },
|
|
115
|
+
"then": {
|
|
116
|
+
"required": ["type", "description", "location"]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"issue": {
|
|
120
|
+
"type": "object",
|
|
121
|
+
"additionalProperties": false,
|
|
122
|
+
"required": ["description"],
|
|
123
|
+
"properties": {
|
|
124
|
+
"description": { "type": "string" },
|
|
125
|
+
"priority": { "enum": ["high", "medium", "low"] },
|
|
126
|
+
"screenId": { "type": "string" },
|
|
127
|
+
"controlId": { "type": "string" }
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"control": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"additionalProperties": false,
|
|
133
|
+
"required": [
|
|
134
|
+
"controlId",
|
|
135
|
+
"label",
|
|
136
|
+
"kind",
|
|
137
|
+
"status",
|
|
138
|
+
"destinationOrHandler",
|
|
139
|
+
"evidence",
|
|
140
|
+
"missingBehavior"
|
|
141
|
+
],
|
|
142
|
+
"properties": {
|
|
143
|
+
"controlId": { "type": "string", "minLength": 1 },
|
|
144
|
+
"label": { "type": "string" },
|
|
145
|
+
"kind": {
|
|
146
|
+
"enum": [
|
|
147
|
+
"button",
|
|
148
|
+
"link",
|
|
149
|
+
"row",
|
|
150
|
+
"card",
|
|
151
|
+
"tab",
|
|
152
|
+
"form",
|
|
153
|
+
"selectable",
|
|
154
|
+
"cta",
|
|
155
|
+
"menu",
|
|
156
|
+
"external",
|
|
157
|
+
"other"
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
"status": {
|
|
161
|
+
"enum": ["Implemented", "Partially implemented", "Not implemented"]
|
|
162
|
+
},
|
|
163
|
+
"destinationOrHandler": { "type": "string" },
|
|
164
|
+
"evidence": {
|
|
165
|
+
"type": "array",
|
|
166
|
+
"items": { "$ref": "#/$defs/evidence" }
|
|
167
|
+
},
|
|
168
|
+
"missingBehavior": {
|
|
169
|
+
"type": "array",
|
|
170
|
+
"items": { "type": "string" }
|
|
171
|
+
},
|
|
172
|
+
"priority": { "enum": ["high", "medium", "low"] }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|