tinybeeai 0.0.0-stage → 0.16.1
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/.railway/railway.ts +83 -0
- package/.railway/tsconfig.json +13 -0
- package/AGENTS.md +102 -0
- package/Dockerfile +108 -0
- package/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/localdost.js +29 -0
- package/bin/tinybeeai.js +28 -0
- package/compose.yml +63 -0
- package/demo/payloads/ci_failure.json +46 -0
- package/demo/payloads/outage.json +18 -0
- package/demo/payloads/pr_comment_edited.json +50 -0
- package/demo/payloads/pr_merged.json +80 -0
- package/demo/payloads/pr_opened.json +69 -0
- package/demo/payloads/pr_synchronize.json +69 -0
- package/demo/payloads/ticket_created.json +138 -0
- package/demo/sample-app/playwright.config.ts +20 -0
- package/demo/sample-app/server.ts +163 -0
- package/demo/sample-app/tests/checkout.spec.ts +13 -0
- package/demo/sample-app/tests/smoke.spec.ts +8 -0
- package/dist/agent/approval-retries.js +59 -0
- package/dist/agent/approval-retries.js.map +1 -0
- package/dist/agent/approvals.js +390 -0
- package/dist/agent/approvals.js.map +1 -0
- package/dist/agent/chat-bridge.js +154 -0
- package/dist/agent/chat-bridge.js.map +1 -0
- package/dist/agent/chat-lead.js +475 -0
- package/dist/agent/chat-lead.js.map +1 -0
- package/dist/agent/chat-loop.js +106 -0
- package/dist/agent/chat-loop.js.map +1 -0
- package/dist/agent/chat-mcp.js +49 -0
- package/dist/agent/chat-mcp.js.map +1 -0
- package/dist/agent/chat-tool-blocks.js +453 -0
- package/dist/agent/chat-tool-blocks.js.map +1 -0
- package/dist/agent/chat-view.js +94 -0
- package/dist/agent/chat-view.js.map +1 -0
- package/dist/agent/chat-views.js +781 -0
- package/dist/agent/chat-views.js.map +1 -0
- package/dist/agent/chat.js +1703 -0
- package/dist/agent/chat.js.map +1 -0
- package/dist/agent/destinations.js +150 -0
- package/dist/agent/destinations.js.map +1 -0
- package/dist/agent/llm-cli.js +451 -0
- package/dist/agent/llm-cli.js.map +1 -0
- package/dist/agent/llm-codex.js +493 -0
- package/dist/agent/llm-codex.js.map +1 -0
- package/dist/agent/llm-openai.js +211 -0
- package/dist/agent/llm-openai.js.map +1 -0
- package/dist/agent/llm.js +1464 -0
- package/dist/agent/llm.js.map +1 -0
- package/dist/agent/memory.js +33 -0
- package/dist/agent/memory.js.map +1 -0
- package/dist/agent/mock-views.js +93 -0
- package/dist/agent/mock-views.js.map +1 -0
- package/dist/agent/model-availability.js +207 -0
- package/dist/agent/model-availability.js.map +1 -0
- package/dist/agent/model-errors.js +188 -0
- package/dist/agent/model-errors.js.map +1 -0
- package/dist/agent/model-guard.js +311 -0
- package/dist/agent/model-guard.js.map +1 -0
- package/dist/agent/prompts.js +374 -0
- package/dist/agent/prompts.js.map +1 -0
- package/dist/agent/qa-lenses/qa-auth.md +32 -0
- package/dist/agent/qa-lenses/qa-check-then-act.md +33 -0
- package/dist/agent/qa-lenses/qa-control-has-no-effect.md +32 -0
- package/dist/agent/qa-lenses/qa-dates.md +31 -0
- package/dist/agent/qa-lenses/qa-double-submit.md +34 -0
- package/dist/agent/qa-lenses/qa-feature-flag.md +29 -0
- package/dist/agent/qa-lenses/qa-migration.md +35 -0
- package/dist/agent/qa-lenses/qa-money.md +38 -0
- package/dist/agent/qa-lenses/qa-response-shape-break.md +33 -0
- package/dist/agent/qa-lenses/qa-validation.md +29 -0
- package/dist/agent/qa-prompts.js +660 -0
- package/dist/agent/qa-prompts.js.map +1 -0
- package/dist/agent/reasoning.js +55 -0
- package/dist/agent/reasoning.js.map +1 -0
- package/dist/agent/redact.js +48 -0
- package/dist/agent/redact.js.map +1 -0
- package/dist/agent/rules.js +927 -0
- package/dist/agent/rules.js.map +1 -0
- package/dist/agent/runs.js +440 -0
- package/dist/agent/runs.js.map +1 -0
- package/dist/agent/send-retry.js +62 -0
- package/dist/agent/send-retry.js.map +1 -0
- package/dist/agent/team-notes.js +288 -0
- package/dist/agent/team-notes.js.map +1 -0
- package/dist/agent/token-meter.js +31 -0
- package/dist/agent/token-meter.js.map +1 -0
- package/dist/agent/triage.js +506 -0
- package/dist/agent/triage.js.map +1 -0
- package/dist/api.js +1047 -0
- package/dist/api.js.map +1 -0
- package/dist/approval-targets.js +107 -0
- package/dist/approval-targets.js.map +1 -0
- package/dist/bee/cloudflared.js +108 -0
- package/dist/bee/cloudflared.js.map +1 -0
- package/dist/bee/health.js +99 -0
- package/dist/bee/health.js.map +1 -0
- package/dist/bee/hive-client.js +75 -0
- package/dist/bee/hive-client.js.map +1 -0
- package/dist/bee/identity.js +39 -0
- package/dist/bee/identity.js.map +1 -0
- package/dist/bee/models.js +510 -0
- package/dist/bee/models.js.map +1 -0
- package/dist/bee/pairing.js +351 -0
- package/dist/bee/pairing.js.map +1 -0
- package/dist/bee/relay-client.js +50 -0
- package/dist/bee/relay-client.js.map +1 -0
- package/dist/bee/routes.js +214 -0
- package/dist/bee/routes.js.map +1 -0
- package/dist/bee/setup-page.js +223 -0
- package/dist/bee/setup-page.js.map +1 -0
- package/dist/bee/state.js +41 -0
- package/dist/bee/state.js.map +1 -0
- package/dist/bee/status.js +117 -0
- package/dist/bee/status.js.map +1 -0
- package/dist/bee/tunnel-manager.js +245 -0
- package/dist/bee/tunnel-manager.js.map +1 -0
- package/dist/bee/updater.js +268 -0
- package/dist/bee/updater.js.map +1 -0
- package/dist/boot.js +41 -0
- package/dist/boot.js.map +1 -0
- package/dist/call/index.js +145 -0
- package/dist/call/index.js.map +1 -0
- package/dist/case-folders.js +294 -0
- package/dist/case-folders.js.map +1 -0
- package/dist/cases.js +554 -0
- package/dist/cases.js.map +1 -0
- package/dist/chat-blocks.js +709 -0
- package/dist/chat-blocks.js.map +1 -0
- package/dist/chat-parse-util.js +38 -0
- package/dist/chat-parse-util.js.map +1 -0
- package/dist/chat-results.js +96 -0
- package/dist/chat-results.js.map +1 -0
- package/dist/chat-stats.js +193 -0
- package/dist/chat-stats.js.map +1 -0
- package/dist/chat-view-specs.js +186 -0
- package/dist/chat-view-specs.js.map +1 -0
- package/dist/cli/doctor-ops.js +361 -0
- package/dist/cli/doctor-ops.js.map +1 -0
- package/dist/cli/doctor.js +289 -0
- package/dist/cli/doctor.js.map +1 -0
- package/dist/cli/dogfood.js +70 -0
- package/dist/cli/dogfood.js.map +1 -0
- package/dist/cli/env-file.js +34 -0
- package/dist/cli/env-file.js.map +1 -0
- package/dist/cli/eval-chat-cases.js +359 -0
- package/dist/cli/eval-chat-cases.js.map +1 -0
- package/dist/cli/eval-chat-results.js +85 -0
- package/dist/cli/eval-chat-results.js.map +1 -0
- package/dist/cli/eval-chat.js +508 -0
- package/dist/cli/eval-chat.js.map +1 -0
- package/dist/cli/eval-designer.js +67 -0
- package/dist/cli/eval-designer.js.map +1 -0
- package/dist/cli/eval-qa-review.js +284 -0
- package/dist/cli/eval-qa-review.js.map +1 -0
- package/dist/cli/golden-export.js +35 -0
- package/dist/cli/golden-export.js.map +1 -0
- package/dist/cli/index.js +90 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.js +746 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/scan-repo.js +199 -0
- package/dist/cli/scan-repo.js.map +1 -0
- package/dist/cli/seed.js +108 -0
- package/dist/cli/seed.js.map +1 -0
- package/dist/cli/tinybee.js +247 -0
- package/dist/cli/tinybee.js.map +1 -0
- package/dist/config.js +138 -0
- package/dist/config.js.map +1 -0
- package/dist/connect/aivision-local.js +833 -0
- package/dist/connect/aivision-local.js.map +1 -0
- package/dist/connect/aivision-token.js +84 -0
- package/dist/connect/aivision-token.js.map +1 -0
- package/dist/connect/crypto.js +65 -0
- package/dist/connect/crypto.js.map +1 -0
- package/dist/connect/deps.js +110 -0
- package/dist/connect/deps.js.map +1 -0
- package/dist/connect/engines.js +144 -0
- package/dist/connect/engines.js.map +1 -0
- package/dist/connect/github-access.js +74 -0
- package/dist/connect/github-access.js.map +1 -0
- package/dist/connect/github-app.js +129 -0
- package/dist/connect/github-app.js.map +1 -0
- package/dist/connect/index.js +9 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/connect/model-choices.js +201 -0
- package/dist/connect/model-choices.js.map +1 -0
- package/dist/connect/providers.js +900 -0
- package/dist/connect/providers.js.map +1 -0
- package/dist/connect/readiness.js +232 -0
- package/dist/connect/readiness.js.map +1 -0
- package/dist/connect/redact.js +16 -0
- package/dist/connect/redact.js.map +1 -0
- package/dist/connect/registry.js +406 -0
- package/dist/connect/registry.js.map +1 -0
- package/dist/connect/store.js +283 -0
- package/dist/connect/store.js.map +1 -0
- package/dist/connect/tunnel.js +134 -0
- package/dist/connect/tunnel.js.map +1 -0
- package/dist/connect/types.js +8 -0
- package/dist/connect/types.js.map +1 -0
- package/dist/connections/index.js +5 -0
- package/dist/connections/index.js.map +1 -0
- package/dist/connections/manager.js +223 -0
- package/dist/connections/manager.js.map +1 -0
- package/dist/connections/presets.js +264 -0
- package/dist/connections/presets.js.map +1 -0
- package/dist/connections/store.js +132 -0
- package/dist/connections/store.js.map +1 -0
- package/dist/connections/tools.js +206 -0
- package/dist/connections/tools.js.map +1 -0
- package/dist/db-schema.js +355 -0
- package/dist/db-schema.js.map +1 -0
- package/dist/db.js +379 -0
- package/dist/db.js.map +1 -0
- package/dist/delivery.js +49 -0
- package/dist/delivery.js.map +1 -0
- package/dist/deploy-wait.js +701 -0
- package/dist/deploy-wait.js.map +1 -0
- package/dist/disk.js +243 -0
- package/dist/disk.js.map +1 -0
- package/dist/dot-health.js +966 -0
- package/dist/dot-health.js.map +1 -0
- package/dist/dot-remove.js +246 -0
- package/dist/dot-remove.js.map +1 -0
- package/dist/events.js +38 -0
- package/dist/events.js.map +1 -0
- package/dist/evidence-pack.js +423 -0
- package/dist/evidence-pack.js.map +1 -0
- package/dist/flows/answers.js +289 -0
- package/dist/flows/answers.js.map +1 -0
- package/dist/flows/change.js +811 -0
- package/dist/flows/change.js.map +1 -0
- package/dist/flows/checkStatus.js +524 -0
- package/dist/flows/checkStatus.js.map +1 -0
- package/dist/flows/ci.js +236 -0
- package/dist/flows/ci.js.map +1 -0
- package/dist/flows/ciFailure.js +76 -0
- package/dist/flows/ciFailure.js.map +1 -0
- package/dist/flows/common.js +522 -0
- package/dist/flows/common.js.map +1 -0
- package/dist/flows/default-branch.js +29 -0
- package/dist/flows/default-branch.js.map +1 -0
- package/dist/flows/defectDraft.js +447 -0
- package/dist/flows/defectDraft.js.map +1 -0
- package/dist/flows/defects.js +623 -0
- package/dist/flows/defects.js.map +1 -0
- package/dist/flows/digest.js +295 -0
- package/dist/flows/digest.js.map +1 -0
- package/dist/flows/impact.js +82 -0
- package/dist/flows/impact.js.map +1 -0
- package/dist/flows/index.js +23 -0
- package/dist/flows/index.js.map +1 -0
- package/dist/flows/intent-sources.js +72 -0
- package/dist/flows/intent-sources.js.map +1 -0
- package/dist/flows/mergeCases.js +169 -0
- package/dist/flows/mergeCases.js.map +1 -0
- package/dist/flows/nightlyRun.js +82 -0
- package/dist/flows/nightlyRun.js.map +1 -0
- package/dist/flows/outage.js +64 -0
- package/dist/flows/outage.js.map +1 -0
- package/dist/flows/postMerge.js +5 -0
- package/dist/flows/postMerge.js.map +1 -0
- package/dist/flows/prCheck.js +5 -0
- package/dist/flows/prCheck.js.map +1 -0
- package/dist/flows/prTarget.js +66 -0
- package/dist/flows/prTarget.js.map +1 -0
- package/dist/flows/qa.js +363 -0
- package/dist/flows/qa.js.map +1 -0
- package/dist/flows/questions.js +160 -0
- package/dist/flows/questions.js.map +1 -0
- package/dist/flows/recovery.js +64 -0
- package/dist/flows/recovery.js.map +1 -0
- package/dist/flows/rerun.js +50 -0
- package/dist/flows/rerun.js.map +1 -0
- package/dist/flows/testDesign.js +294 -0
- package/dist/flows/testDesign.js.map +1 -0
- package/dist/flows/testDesignJob.js +46 -0
- package/dist/flows/testDesignJob.js.map +1 -0
- package/dist/flows/testRepo.js +228 -0
- package/dist/flows/testRepo.js.map +1 -0
- package/dist/flows/testSavings.js +381 -0
- package/dist/flows/testSavings.js.map +1 -0
- package/dist/flows/testStatus.js +343 -0
- package/dist/flows/testStatus.js.map +1 -0
- package/dist/flows/ticketContext.js +425 -0
- package/dist/flows/ticketContext.js.map +1 -0
- package/dist/flows/ticketReview.js +249 -0
- package/dist/flows/ticketReview.js.map +1 -0
- package/dist/flows/ticks.js +73 -0
- package/dist/flows/ticks.js.map +1 -0
- package/dist/hive/admin.js +122 -0
- package/dist/hive/admin.js.map +1 -0
- package/dist/hive/auth.js +130 -0
- package/dist/hive/auth.js.map +1 -0
- package/dist/hive/billing.js +278 -0
- package/dist/hive/billing.js.map +1 -0
- package/dist/hive/config.js +63 -0
- package/dist/hive/config.js.map +1 -0
- package/dist/hive/db.js +186 -0
- package/dist/hive/db.js.map +1 -0
- package/dist/hive/embed.js +76 -0
- package/dist/hive/embed.js.map +1 -0
- package/dist/hive/errors.js +17 -0
- package/dist/hive/errors.js.map +1 -0
- package/dist/hive/google.js +75 -0
- package/dist/hive/google.js.map +1 -0
- package/dist/hive/index.js +25 -0
- package/dist/hive/index.js.map +1 -0
- package/dist/hive/jwt.js +57 -0
- package/dist/hive/jwt.js.map +1 -0
- package/dist/hive/keys.js +61 -0
- package/dist/hive/keys.js.map +1 -0
- package/dist/hive/mail.js +27 -0
- package/dist/hive/mail.js.map +1 -0
- package/dist/hive/native-push.js +255 -0
- package/dist/hive/native-push.js.map +1 -0
- package/dist/hive/pairing.js +342 -0
- package/dist/hive/pairing.js.map +1 -0
- package/dist/hive/presence.js +121 -0
- package/dist/hive/presence.js.map +1 -0
- package/dist/hive/push.js +185 -0
- package/dist/hive/push.js.map +1 -0
- package/dist/hive/relay.js +205 -0
- package/dist/hive/relay.js.map +1 -0
- package/dist/hive/routes.js +228 -0
- package/dist/hive/routes.js.map +1 -0
- package/dist/hive/server.js +12 -0
- package/dist/hive/server.js.map +1 -0
- package/dist/hive/session.js +28 -0
- package/dist/hive/session.js.map +1 -0
- package/dist/hive/tenants.js +287 -0
- package/dist/hive/tenants.js.map +1 -0
- package/dist/hive/tunnel/cloudflare.js +160 -0
- package/dist/hive/tunnel/cloudflare.js.map +1 -0
- package/dist/hive/tunnel/mock.js +52 -0
- package/dist/hive/tunnel/mock.js.map +1 -0
- package/dist/hive/tunnel/provider.js +19 -0
- package/dist/hive/tunnel/provider.js.map +1 -0
- package/dist/hive/updates.js +61 -0
- package/dist/hive/updates.js.map +1 -0
- package/dist/http-error.js +11 -0
- package/dist/http-error.js.map +1 -0
- package/dist/impact/classify.js +226 -0
- package/dist/impact/classify.js.map +1 -0
- package/dist/impact/find.js +259 -0
- package/dist/impact/find.js.map +1 -0
- package/dist/impact/pr.js +54 -0
- package/dist/impact/pr.js.map +1 -0
- package/dist/impact/store.js +365 -0
- package/dist/impact/store.js.map +1 -0
- package/dist/impact/text.js +158 -0
- package/dist/impact/text.js.map +1 -0
- package/dist/index.js +288 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/adf.js +397 -0
- package/dist/integrations/adf.js.map +1 -0
- package/dist/integrations/bot-marker.js +103 -0
- package/dist/integrations/bot-marker.js.map +1 -0
- package/dist/integrations/github-issues.js +705 -0
- package/dist/integrations/github-issues.js.map +1 -0
- package/dist/integrations/github.js +1284 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/issue-create.js +60 -0
- package/dist/integrations/issue-create.js.map +1 -0
- package/dist/integrations/jira.js +279 -0
- package/dist/integrations/jira.js.map +1 -0
- package/dist/integrations/linear.js +273 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/own-identity-warm.js +47 -0
- package/dist/integrations/own-identity-warm.js.map +1 -0
- package/dist/integrations/own-identity.js +39 -0
- package/dist/integrations/own-identity.js.map +1 -0
- package/dist/integrations/status-change.js +42 -0
- package/dist/integrations/status-change.js.map +1 -0
- package/dist/lead-defects.js +202 -0
- package/dist/lead-defects.js.map +1 -0
- package/dist/lead-digest.js +333 -0
- package/dist/lead-digest.js.map +1 -0
- package/dist/lead-release.js +286 -0
- package/dist/lead-release.js.map +1 -0
- package/dist/lead.js +328 -0
- package/dist/lead.js.map +1 -0
- package/dist/legacy-names.js +51 -0
- package/dist/legacy-names.js.map +1 -0
- package/dist/limits.js +44 -0
- package/dist/limits.js.map +1 -0
- package/dist/link-trust.js +224 -0
- package/dist/link-trust.js.map +1 -0
- package/dist/links.js +219 -0
- package/dist/links.js.map +1 -0
- package/dist/loopback.js +35 -0
- package/dist/loopback.js.map +1 -0
- package/dist/model-cap.js +60 -0
- package/dist/model-cap.js.map +1 -0
- package/dist/model-usage.js +173 -0
- package/dist/model-usage.js.map +1 -0
- package/dist/monitor/backfill.js +39 -0
- package/dist/monitor/backfill.js.map +1 -0
- package/dist/monitor/browser-probe.js +198 -0
- package/dist/monitor/browser-probe.js.map +1 -0
- package/dist/monitor/catch-up.js +31 -0
- package/dist/monitor/catch-up.js.map +1 -0
- package/dist/monitor/pollers.js +1606 -0
- package/dist/monitor/pollers.js.map +1 -0
- package/dist/monitor/probes.js +178 -0
- package/dist/monitor/probes.js.map +1 -0
- package/dist/monitor/self-health.js +228 -0
- package/dist/monitor/self-health.js.map +1 -0
- package/dist/monitor/statuspage.js +105 -0
- package/dist/monitor/statuspage.js.map +1 -0
- package/dist/nightly-scheduler.js +127 -0
- package/dist/nightly-scheduler.js.map +1 -0
- package/dist/nightly.js +141 -0
- package/dist/nightly.js.map +1 -0
- package/dist/notify/headline.js +114 -0
- package/dist/notify/headline.js.map +1 -0
- package/dist/notify/push.js +174 -0
- package/dist/notify/push.js.map +1 -0
- package/dist/object-cards.js +585 -0
- package/dist/object-cards.js.map +1 -0
- package/dist/object-links.js +47 -0
- package/dist/object-links.js.map +1 -0
- package/dist/onboarding-checks.js +141 -0
- package/dist/onboarding-checks.js.map +1 -0
- package/dist/onboarding.js +401 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/path-filters.js +62 -0
- package/dist/path-filters.js.map +1 -0
- package/dist/phone/alerts.js +67 -0
- package/dist/phone/alerts.js.map +1 -0
- package/dist/phone/auth.js +240 -0
- package/dist/phone/auth.js.map +1 -0
- package/dist/phone/pairing.js +442 -0
- package/dist/phone/pairing.js.map +1 -0
- package/dist/phone/prefs.js +27 -0
- package/dist/phone/prefs.js.map +1 -0
- package/dist/phone/tailscale.js +161 -0
- package/dist/phone/tailscale.js.map +1 -0
- package/dist/pr-scope.js +441 -0
- package/dist/pr-scope.js.map +1 -0
- package/dist/proc.js +156 -0
- package/dist/proc.js.map +1 -0
- package/dist/profile/cache.js +108 -0
- package/dist/profile/cache.js.map +1 -0
- package/dist/profile/ci.js +216 -0
- package/dist/profile/ci.js.map +1 -0
- package/dist/profile/index.js +8 -0
- package/dist/profile/index.js.map +1 -0
- package/dist/profile/job.js +135 -0
- package/dist/profile/job.js.map +1 -0
- package/dist/profile/queries/go-tags.scm +47 -0
- package/dist/profile/queries/java-tags.scm +25 -0
- package/dist/profile/queries/javascript-tags.scm +104 -0
- package/dist/profile/queries/python-tags.scm +19 -0
- package/dist/profile/queries/typescript-tags.scm +28 -0
- package/dist/profile/scan-thread.js +42 -0
- package/dist/profile/scan-thread.js.map +1 -0
- package/dist/profile/scan-worker.js +16 -0
- package/dist/profile/scan-worker.js.map +1 -0
- package/dist/profile/scan.js +351 -0
- package/dist/profile/scan.js.map +1 -0
- package/dist/profile/stack.js +528 -0
- package/dist/profile/stack.js.map +1 -0
- package/dist/profile/store.js +35 -0
- package/dist/profile/store.js.map +1 -0
- package/dist/profile/tests.js +399 -0
- package/dist/profile/tests.js.map +1 -0
- package/dist/queue.js +350 -0
- package/dist/queue.js.map +1 -0
- package/dist/refs.js +756 -0
- package/dist/refs.js.map +1 -0
- package/dist/release-notes.js +272 -0
- package/dist/release-notes.js.map +1 -0
- package/dist/reports/build.js +243 -0
- package/dist/reports/build.js.map +1 -0
- package/dist/reports/index.js +211 -0
- package/dist/reports/index.js.map +1 -0
- package/dist/reports/store.js +150 -0
- package/dist/reports/store.js.map +1 -0
- package/dist/reports/ticket-titles.js +124 -0
- package/dist/reports/ticket-titles.js.map +1 -0
- package/dist/reports/trace-map.js +1278 -0
- package/dist/reports/trace-map.js.map +1 -0
- package/dist/reports/traceability.js +422 -0
- package/dist/reports/traceability.js.map +1 -0
- package/dist/repos/mirror.js +356 -0
- package/dist/repos/mirror.js.map +1 -0
- package/dist/repos-github.js +108 -0
- package/dist/repos-github.js.map +1 -0
- package/dist/repos.js +891 -0
- package/dist/repos.js.map +1 -0
- package/dist/review/backend-anthropic.js +90 -0
- package/dist/review/backend-anthropic.js.map +1 -0
- package/dist/review/backend-cli.js +248 -0
- package/dist/review/backend-cli.js.map +1 -0
- package/dist/review/backend-codex.js +81 -0
- package/dist/review/backend-codex.js.map +1 -0
- package/dist/review/backend-mock.js +686 -0
- package/dist/review/backend-mock.js.map +1 -0
- package/dist/review/backend-only.js +168 -0
- package/dist/review/backend-only.js.map +1 -0
- package/dist/review/backend.js +105 -0
- package/dist/review/backend.js.map +1 -0
- package/dist/review/baseline/render-v1.js +483 -0
- package/dist/review/baseline/render-v1.js.map +1 -0
- package/dist/review/change-summary.js +197 -0
- package/dist/review/change-summary.js.map +1 -0
- package/dist/review/changelog.js +40 -0
- package/dist/review/changelog.js.map +1 -0
- package/dist/review/check.js +494 -0
- package/dist/review/check.js.map +1 -0
- package/dist/review/checkout.js +185 -0
- package/dist/review/checkout.js.map +1 -0
- package/dist/review/classify.js +178 -0
- package/dist/review/classify.js.map +1 -0
- package/dist/review/coverage.js +347 -0
- package/dist/review/coverage.js.map +1 -0
- package/dist/review/decision-eval.js +121 -0
- package/dist/review/decision-eval.js.map +1 -0
- package/dist/review/deliver.js +104 -0
- package/dist/review/deliver.js.map +1 -0
- package/dist/review/designer-eval.js +280 -0
- package/dist/review/designer-eval.js.map +1 -0
- package/dist/review/dogfood-digest.js +136 -0
- package/dist/review/dogfood-digest.js.map +1 -0
- package/dist/review/dogfood-eval.js +486 -0
- package/dist/review/dogfood-eval.js.map +1 -0
- package/dist/review/environment.js +109 -0
- package/dist/review/environment.js.map +1 -0
- package/dist/review/eval.js +405 -0
- package/dist/review/eval.js.map +1 -0
- package/dist/review/glob.js +110 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/golden-export.js +219 -0
- package/dist/review/golden-export.js.map +1 -0
- package/dist/review/impact.js +406 -0
- package/dist/review/impact.js.map +1 -0
- package/dist/review/imports.js +542 -0
- package/dist/review/imports.js.map +1 -0
- package/dist/review/index.js +512 -0
- package/dist/review/index.js.map +1 -0
- package/dist/review/intent.js +128 -0
- package/dist/review/intent.js.map +1 -0
- package/dist/review/lenses.js +167 -0
- package/dist/review/lenses.js.map +1 -0
- package/dist/review/linked-tickets.js +293 -0
- package/dist/review/linked-tickets.js.map +1 -0
- package/dist/review/merge.js +370 -0
- package/dist/review/merge.js.map +1 -0
- package/dist/review/on-demand.js +218 -0
- package/dist/review/on-demand.js.map +1 -0
- package/dist/review/prompt-eval.js +299 -0
- package/dist/review/prompt-eval.js.map +1 -0
- package/dist/review/prompt-variants.js +37 -0
- package/dist/review/prompt-variants.js.map +1 -0
- package/dist/review/readability-eval.js +217 -0
- package/dist/review/readability-eval.js.map +1 -0
- package/dist/review/readability.js +295 -0
- package/dist/review/readability.js.map +1 -0
- package/dist/review/render.js +2409 -0
- package/dist/review/render.js.map +1 -0
- package/dist/review/repo.js +194 -0
- package/dist/review/repo.js.map +1 -0
- package/dist/review/report-plan.js +90 -0
- package/dist/review/report-plan.js.map +1 -0
- package/dist/review/routes.js +195 -0
- package/dist/review/routes.js.map +1 -0
- package/dist/review/run-summary.js +34 -0
- package/dist/review/run-summary.js.map +1 -0
- package/dist/review/run.js +245 -0
- package/dist/review/run.js.map +1 -0
- package/dist/review/scope.js +454 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/review/security.js +432 -0
- package/dist/review/security.js.map +1 -0
- package/dist/review/signals.js +550 -0
- package/dist/review/signals.js.map +1 -0
- package/dist/review/specs.js +150 -0
- package/dist/review/specs.js.map +1 -0
- package/dist/review/ste.js +133 -0
- package/dist/review/ste.js.map +1 -0
- package/dist/review/test-repo-file.js +18 -0
- package/dist/review/test-repo-file.js.map +1 -0
- package/dist/review/test-repo-lines.js +53 -0
- package/dist/review/test-repo-lines.js.map +1 -0
- package/dist/review/ticket-on-demand.js +232 -0
- package/dist/review/ticket-on-demand.js.map +1 -0
- package/dist/review/yaml.js +298 -0
- package/dist/review/yaml.js.map +1 -0
- package/dist/room.js +361 -0
- package/dist/room.js.map +1 -0
- package/dist/routes/activity.js +219 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/call.js +29 -0
- package/dist/routes/call.js.map +1 -0
- package/dist/routes/cards.js +40 -0
- package/dist/routes/cards.js.map +1 -0
- package/dist/routes/cases.js +133 -0
- package/dist/routes/cases.js.map +1 -0
- package/dist/routes/connect.js +268 -0
- package/dist/routes/connect.js.map +1 -0
- package/dist/routes/connection-presets.js +32 -0
- package/dist/routes/connection-presets.js.map +1 -0
- package/dist/routes/connections.js +177 -0
- package/dist/routes/connections.js.map +1 -0
- package/dist/routes/disk.js +19 -0
- package/dist/routes/disk.js.map +1 -0
- package/dist/routes/docs.js +71 -0
- package/dist/routes/docs.js.map +1 -0
- package/dist/routes/dot-suggest.js +1002 -0
- package/dist/routes/dot-suggest.js.map +1 -0
- package/dist/routes/evidence-pack.js +16 -0
- package/dist/routes/evidence-pack.js.map +1 -0
- package/dist/routes/health.js +121 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/impact.js +48 -0
- package/dist/routes/impact.js.map +1 -0
- package/dist/routes/lead.js +50 -0
- package/dist/routes/lead.js.map +1 -0
- package/dist/routes/links.js +13 -0
- package/dist/routes/links.js.map +1 -0
- package/dist/routes/markdown.js +62 -0
- package/dist/routes/markdown.js.map +1 -0
- package/dist/routes/onboarding.js +47 -0
- package/dist/routes/onboarding.js.map +1 -0
- package/dist/routes/phone.js +231 -0
- package/dist/routes/phone.js.map +1 -0
- package/dist/routes/release-notes.js +30 -0
- package/dist/routes/release-notes.js.map +1 -0
- package/dist/routes/reports.js +102 -0
- package/dist/routes/reports.js.map +1 -0
- package/dist/routes/repos.js +91 -0
- package/dist/routes/repos.js.map +1 -0
- package/dist/routes/review.js +283 -0
- package/dist/routes/review.js.map +1 -0
- package/dist/routes/roles.js +114 -0
- package/dist/routes/roles.js.map +1 -0
- package/dist/routes/room.js +21 -0
- package/dist/routes/room.js.map +1 -0
- package/dist/routes/runner.js +156 -0
- package/dist/routes/runner.js.map +1 -0
- package/dist/routes/runs.js +84 -0
- package/dist/routes/runs.js.map +1 -0
- package/dist/routes/setup.js +18 -0
- package/dist/routes/setup.js.map +1 -0
- package/dist/routes/test-runs.js +220 -0
- package/dist/routes/test-runs.js.map +1 -0
- package/dist/routes/ticket-status.js +17 -0
- package/dist/routes/ticket-status.js.map +1 -0
- package/dist/routes/traceability.js +143 -0
- package/dist/routes/traceability.js.map +1 -0
- package/dist/routes/true.js +142 -0
- package/dist/routes/true.js.map +1 -0
- package/dist/routes/updates.js +70 -0
- package/dist/routes/updates.js.map +1 -0
- package/dist/runner/adapters/index.js +12 -0
- package/dist/runner/adapters/index.js.map +1 -0
- package/dist/runner/adapters/playwright.js +68 -0
- package/dist/runner/adapters/playwright.js.map +1 -0
- package/dist/runner/aivision/cases.js +172 -0
- package/dist/runner/aivision/cases.js.map +1 -0
- package/dist/runner/aivision/cli.js +201 -0
- package/dist/runner/aivision/cli.js.map +1 -0
- package/dist/runner/aivision/index.js +281 -0
- package/dist/runner/aivision/index.js.map +1 -0
- package/dist/runner/aivision/server.js +394 -0
- package/dist/runner/aivision/server.js.map +1 -0
- package/dist/runner/aivision/shared.js +140 -0
- package/dist/runner/aivision/shared.js.map +1 -0
- package/dist/runner/artifact-cap.js +189 -0
- package/dist/runner/artifact-cap.js.map +1 -0
- package/dist/runner/artifacts.js +433 -0
- package/dist/runner/artifacts.js.map +1 -0
- package/dist/runner/command.js +564 -0
- package/dist/runner/command.js.map +1 -0
- package/dist/runner/copyfiles.js +221 -0
- package/dist/runner/copyfiles.js.map +1 -0
- package/dist/runner/engines.js +32 -0
- package/dist/runner/engines.js.map +1 -0
- package/dist/runner/evidence.js +114 -0
- package/dist/runner/evidence.js.map +1 -0
- package/dist/runner/host-env.js +160 -0
- package/dist/runner/host-env.js.map +1 -0
- package/dist/runner/index.js +1221 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/observe.js +603 -0
- package/dist/runner/observe.js.map +1 -0
- package/dist/runner/playwright-json.js +108 -0
- package/dist/runner/playwright-json.js.map +1 -0
- package/dist/runner/pwgen/exec.js +192 -0
- package/dist/runner/pwgen/exec.js.map +1 -0
- package/dist/runner/pwgen/explore.js +111 -0
- package/dist/runner/pwgen/explore.js.map +1 -0
- package/dist/runner/pwgen/index.js +423 -0
- package/dist/runner/pwgen/index.js.map +1 -0
- package/dist/runner/pwgen/propose.js +69 -0
- package/dist/runner/pwgen/propose.js.map +1 -0
- package/dist/runner/pwgen/source.js +632 -0
- package/dist/runner/pwgen/source.js.map +1 -0
- package/dist/runner/setup.js +222 -0
- package/dist/runner/setup.js.map +1 -0
- package/dist/runner/step-reporter.js +107 -0
- package/dist/runner/step-reporter.js.map +1 -0
- package/dist/runner/test-repo.js +326 -0
- package/dist/runner/test-repo.js.map +1 -0
- package/dist/runner/typed-value.js +46 -0
- package/dist/runner/typed-value.js.map +1 -0
- package/dist/runner/verify.js +133 -0
- package/dist/runner/verify.js.map +1 -0
- package/dist/runner/worktree.js +349 -0
- package/dist/runner/worktree.js.map +1 -0
- package/dist/sandbox.js +61 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/server.js +262 -0
- package/dist/server.js.map +1 -0
- package/dist/shadow-week.js +126 -0
- package/dist/shadow-week.js.map +1 -0
- package/dist/shutdown.js +92 -0
- package/dist/shutdown.js.map +1 -0
- package/dist/slack/index.js +645 -0
- package/dist/slack/index.js.map +1 -0
- package/dist/stages/config.js +147 -0
- package/dist/stages/config.js.map +1 -0
- package/dist/stages/index.js +41 -0
- package/dist/stages/index.js.map +1 -0
- package/dist/suites.js +293 -0
- package/dist/suites.js.map +1 -0
- package/dist/target-url.js +116 -0
- package/dist/target-url.js.map +1 -0
- package/dist/test-runs.js +662 -0
- package/dist/test-runs.js.map +1 -0
- package/dist/ticket-keys.js +97 -0
- package/dist/ticket-keys.js.map +1 -0
- package/dist/ticket-supersede.js +105 -0
- package/dist/ticket-supersede.js.map +1 -0
- package/dist/tinybee/ed25519.js +99 -0
- package/dist/tinybee/ed25519.js.map +1 -0
- package/dist/tinybee/models-routes.js +19 -0
- package/dist/tinybee/models-routes.js.map +1 -0
- package/dist/tinybee/pair-payload.js +41 -0
- package/dist/tinybee/pair-payload.js.map +1 -0
- package/dist/tinybee/update-manifest.js +77 -0
- package/dist/tinybee/update-manifest.js.map +1 -0
- package/dist/tinybee/well-known.js +64 -0
- package/dist/tinybee/well-known.js.map +1 -0
- package/dist/tinybee/workflows/activity.js +23 -0
- package/dist/tinybee/workflows/activity.js.map +1 -0
- package/dist/tinybee/workflows/cron.js +210 -0
- package/dist/tinybee/workflows/cron.js.map +1 -0
- package/dist/tinybee/workflows/engine.js +371 -0
- package/dist/tinybee/workflows/engine.js.map +1 -0
- package/dist/tinybee/workflows/from-text.js +203 -0
- package/dist/tinybee/workflows/from-text.js.map +1 -0
- package/dist/tinybee/workflows/routes.js +189 -0
- package/dist/tinybee/workflows/routes.js.map +1 -0
- package/dist/tinybee/workflows/scheduler.js +100 -0
- package/dist/tinybee/workflows/scheduler.js.map +1 -0
- package/dist/tinybee/workflows/skills.js +227 -0
- package/dist/tinybee/workflows/skills.js.map +1 -0
- package/dist/tinybee/workflows/store.js +205 -0
- package/dist/tinybee/workflows/store.js.map +1 -0
- package/dist/tinybee/workflows/types.js +180 -0
- package/dist/tinybee/workflows/types.js.map +1 -0
- package/dist/tinybee/workflows/yaml.js +323 -0
- package/dist/tinybee/workflows/yaml.js.map +1 -0
- package/dist/true/api.js +495 -0
- package/dist/true/api.js.map +1 -0
- package/dist/true/client.js +292 -0
- package/dist/true/client.js.map +1 -0
- package/dist/true/clock.js +36 -0
- package/dist/true/clock.js.map +1 -0
- package/dist/true/events.js +125 -0
- package/dist/true/events.js.map +1 -0
- package/dist/true/hosts.js +98 -0
- package/dist/true/hosts.js.map +1 -0
- package/dist/true/index.js +29 -0
- package/dist/true/index.js.map +1 -0
- package/dist/true/key.js +54 -0
- package/dist/true/key.js.map +1 -0
- package/dist/true/line.js +69 -0
- package/dist/true/line.js.map +1 -0
- package/dist/true/mask.js +31 -0
- package/dist/true/mask.js.map +1 -0
- package/dist/true/names.js +114 -0
- package/dist/true/names.js.map +1 -0
- package/dist/true/outbox.js +233 -0
- package/dist/true/outbox.js.map +1 -0
- package/dist/true/replay.js +164 -0
- package/dist/true/replay.js.map +1 -0
- package/dist/true/settings.js +123 -0
- package/dist/true/settings.js.map +1 -0
- package/dist/true/status.js +189 -0
- package/dist/true/status.js.map +1 -0
- package/dist/true/sync-cases.js +222 -0
- package/dist/true/sync-cases.js.map +1 -0
- package/dist/true/sync-manual.js +295 -0
- package/dist/true/sync-manual.js.map +1 -0
- package/dist/true/sync-runs.js +109 -0
- package/dist/true/sync-runs.js.map +1 -0
- package/dist/true/worker.js +414 -0
- package/dist/true/worker.js.map +1 -0
- package/dist/types.js +32 -0
- package/dist/types.js.map +1 -0
- package/dist/updates/asks.js +38 -0
- package/dist/updates/asks.js.map +1 -0
- package/dist/updates/index.js +835 -0
- package/dist/updates/index.js.map +1 -0
- package/dist/updates/owner.js +24 -0
- package/dist/updates/owner.js.map +1 -0
- package/dist/updates/prefs.js +118 -0
- package/dist/updates/prefs.js.map +1 -0
- package/dist/updates/route.js +78 -0
- package/dist/updates/route.js.map +1 -0
- package/dist/updates/sources.js +345 -0
- package/dist/updates/sources.js.map +1 -0
- package/dist/updates/words.js +452 -0
- package/dist/updates/words.js.map +1 -0
- package/dist/version.js +53 -0
- package/dist/version.js.map +1 -0
- package/dist/view-compute.js +382 -0
- package/dist/view-compute.js.map +1 -0
- package/dist/view-spec.js +894 -0
- package/dist/view-spec.js.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/ApprovalCard-Br_z5z7G.css +1 -0
- package/dist/web/assets/ApprovalCard-CfGhGFo_.js +3 -0
- package/dist/web/assets/CardFromRef-BaTGmZ9c.js +2 -0
- package/dist/web/assets/CardFromRef-DO7i2uDd.css +1 -0
- package/dist/web/assets/Cases-8l_6UcLh.js +2 -0
- package/dist/web/assets/Cases-CpfED0uB.css +1 -0
- package/dist/web/assets/Chat-BKDiR0qt.js +3 -0
- package/dist/web/assets/Chat-DxjdyYUu.css +1 -0
- package/dist/web/assets/ConfirmDialog-CIMveD-b.js +2 -0
- package/dist/web/assets/ConnectSheet-Cm5vbZdR.js +4 -0
- package/dist/web/assets/Docs-HKGuY0WE.js +2 -0
- package/dist/web/assets/Docs-fodKx8S7.css +1 -0
- package/dist/web/assets/DotDetail-DskS3NWe.js +2 -0
- package/dist/web/assets/DotDetail-OWRSD_1x.css +1 -0
- package/dist/web/assets/DotStage-BZDclfzV.css +1 -0
- package/dist/web/assets/DotStage-CtMNzCN3.js +2 -0
- package/dist/web/assets/Dots-BqCsS34m.js +2 -0
- package/dist/web/assets/EvidencePack-DQ3kCJX4.js +2 -0
- package/dist/web/assets/EvidencePack-DV_9ETsA.css +1 -0
- package/dist/web/assets/FailedDeliveries-1F2kFcs9.js +2 -0
- package/dist/web/assets/FailedDeliveries-DXa2_VvI.css +1 -0
- package/dist/web/assets/Feed-5wiHRTFK.js +2 -0
- package/dist/web/assets/GfmView-C-NYaduC.js +4 -0
- package/dist/web/assets/ImpactList-C-RNuDB0.js +2 -0
- package/dist/web/assets/ImpactList-C612dFdw.css +1 -0
- package/dist/web/assets/Inbox-kc1bKVL_.js +2 -0
- package/dist/web/assets/JoinScreen-C3L4wmP3.js +2 -0
- package/dist/web/assets/JoinScreen-COeAUv5v.css +1 -0
- package/dist/web/assets/LeadDialog-N3Zul9Rw.js +8 -0
- package/dist/web/assets/LeadDialog-xyWjAuAS.css +1 -0
- package/dist/web/assets/ListeningLine-BRFNaczY.js +2 -0
- package/dist/web/assets/ListeningLine-pN8CgyiK.css +1 -0
- package/dist/web/assets/Mascots-pIyGpxtD.js +2 -0
- package/dist/web/assets/ModelChoices-BxO1zyyr.js +2 -0
- package/dist/web/assets/ModelChoices-DQJnVxfs.css +1 -0
- package/dist/web/assets/NewDotDialog-CDPUWmgN.js +2 -0
- package/dist/web/assets/NewDotDialog-DrPPqkSZ.css +1 -0
- package/dist/web/assets/NewTestRunDialog-BQBthVHE.js +2 -0
- package/dist/web/assets/NoBreakNames-Csxdqdt6.js +2 -0
- package/dist/web/assets/Pair-omex_K7H.js +2 -0
- package/dist/web/assets/PushSection-CmqHlE_2.js +2 -0
- package/dist/web/assets/ReleaseNotes-B6VqFewP.js +2 -0
- package/dist/web/assets/ReleaseNotes-DBJUL7Vm.css +1 -0
- package/dist/web/assets/ReportPage-BHS1xWEt.js +162 -0
- package/dist/web/assets/Reports-BEHnDIb1.js +2 -0
- package/dist/web/assets/ReviewChangeDialog-BVSt-cw-.js +2 -0
- package/dist/web/assets/ReviewChangeDialog-BbIlHRx0.css +1 -0
- package/dist/web/assets/Room-BoG_uge4.js +2 -0
- package/dist/web/assets/Room-evAfiW_e.css +1 -0
- package/dist/web/assets/RunActions-ZY7yf1AF.js +2 -0
- package/dist/web/assets/RunDetail-C8St20GQ.css +1 -0
- package/dist/web/assets/RunDetail-qADXjBvd.js +9 -0
- package/dist/web/assets/RunRow-BKEJv8ib.js +2 -0
- package/dist/web/assets/RunnerTry-Dgpp67-s.js +6 -0
- package/dist/web/assets/RunnerTry-DsweWOfO.css +1 -0
- package/dist/web/assets/Settings-fs-QJwNM.js +2 -0
- package/dist/web/assets/Setup-DI6hBEP-.js +2 -0
- package/dist/web/assets/TestRows-wh03T5Li.js +4 -0
- package/dist/web/assets/TestRun-DTOyF-Pv.js +2 -0
- package/dist/web/assets/TestRuns-ggDjm1Q8.js +2 -0
- package/dist/web/assets/TinyBeeAddLaptop-bc3pmh3T.js +2 -0
- package/dist/web/assets/TinyBeeAi-CkyOzhm5.js +2 -0
- package/dist/web/assets/TinyBeeConnections-B2K_P38u.js +2 -0
- package/dist/web/assets/TinyBeeHome-DSpxX5KS.js +3 -0
- package/dist/web/assets/TinyBeePlan-BamiLziu.js +2 -0
- package/dist/web/assets/TinyBeeSignup-vHcz0Wen.js +2 -0
- package/dist/web/assets/TinyBeeSkills-By9RLUse.js +2 -0
- package/dist/web/assets/TinyBeeWorkflow-DeNO7Jsh.js +2 -0
- package/dist/web/assets/TinyBeeWorkflowEditor-B6OGC2OT.js +2 -0
- package/dist/web/assets/TinyBeeWorkflows-BN4-h6lt.js +2 -0
- package/dist/web/assets/Trace-BBCI1aIB.css +1 -0
- package/dist/web/assets/Trace-aF92UWaH.js +2 -0
- package/dist/web/assets/UpdatesSection-CCWEbkue.js +2 -0
- package/dist/web/assets/Welcome-CCcy558Q.js +2 -0
- package/dist/web/assets/activity-C3qilrDm.css +1 -0
- package/dist/web/assets/activity-CBfTSxqw.js +2 -0
- package/dist/web/assets/authedMedia-BTYt_tyu.js +2 -0
- package/dist/web/assets/beeApi-BHW8Tx1s.js +2 -0
- package/dist/web/assets/data-CmXvx3Mj.js +2 -0
- package/dist/web/assets/engines-CfdJODFj.css +1 -0
- package/dist/web/assets/format-BBZ8vFyT.js +2 -0
- package/dist/web/assets/forms-Coc7p2UF.css +1 -0
- package/dist/web/assets/hivePush-DZJDokj7.js +2 -0
- package/dist/web/assets/impactModel-MY1OFuiP.js +2 -0
- package/dist/web/assets/index-B6qKm1NW.js +13 -0
- package/dist/web/assets/index-BvKyT1X7.css +1 -0
- package/dist/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/web/assets/lead-BL_P_kri.css +1 -0
- package/dist/web/assets/model-BIQy5IVG.js +2 -0
- package/dist/web/assets/modelsApi-PUSySRQw.js +2 -0
- package/dist/web/assets/newDotModel-BaLLyw57.js +2 -0
- package/dist/web/assets/onboarding-bfKqnM4Z.css +1 -0
- package/dist/web/assets/phone-Rs7FoyG0.css +1 -0
- package/dist/web/assets/phoneApi-BG_1-M-D.js +3 -0
- package/dist/web/assets/releaseModel-EynYMVOC.js +2 -0
- package/dist/web/assets/releaseNotesModel-A1Rtv3cV.js +2 -0
- package/dist/web/assets/reports-BWaA1HhM.js +37 -0
- package/dist/web/assets/reports-DXwdWQsB.css +1 -0
- package/dist/web/assets/router-W6nZsQ33.js +2 -0
- package/dist/web/assets/runs-BY0Owv93.css +1 -0
- package/dist/web/assets/shadowWeekModel-BUUiww09.js +2 -0
- package/dist/web/assets/storage-yvL36AWt.js +2 -0
- package/dist/web/assets/store-IqrqHuFR.js +2 -0
- package/dist/web/assets/testruns-BzIZ0cR0.css +1 -0
- package/dist/web/assets/testruns-DyaqX4ji.js +2 -0
- package/dist/web/assets/true-9Q68cs5b.js +2 -0
- package/dist/web/assets/true-DPGBpoir.css +1 -0
- package/dist/web/assets/ui-DcmhYyYp.js +13 -0
- package/dist/web/assets/ui-DxEDgbIx.css +1 -0
- package/dist/web/assets/usePauseDot-BV60ZVy1.js +2 -0
- package/dist/web/assets/usePauseDot-NrSzMI1C.css +1 -0
- package/dist/web/assets/usePush-Dsm-TjlG.js +2 -0
- package/dist/web/assets/useRefChips-CbafNx59.js +68 -0
- package/dist/web/assets/useRestoreDot-D0B6W82H.js +2 -0
- package/dist/web/assets/view-worker-pyeVAhUT.js +2 -0
- package/dist/web/assets/workflowUi-QYsd3n9J.js +2 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/icon-192.png +0 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/icon-maskable-512.png +0 -0
- package/dist/web/index.html +45 -0
- package/dist/web/manifest.webmanifest +23 -0
- package/dist/web/mascots/argus-attention.svg +7 -0
- package/dist/web/mascots/argus-ok.svg +7 -0
- package/dist/web/mascots/argus-outage.svg +7 -0
- package/dist/web/mascots/argus-running.svg +7 -0
- package/dist/web/mascots/bolt-attention.svg +7 -0
- package/dist/web/mascots/bolt-ok.svg +7 -0
- package/dist/web/mascots/bolt-outage.svg +7 -0
- package/dist/web/mascots/bolt-running.svg +7 -0
- package/dist/web/mascots/echo-attention.svg +7 -0
- package/dist/web/mascots/echo-ok.svg +7 -0
- package/dist/web/mascots/echo-outage.svg +7 -0
- package/dist/web/mascots/echo-running.svg +7 -0
- package/dist/web/mascots/lens-attention.svg +7 -0
- package/dist/web/mascots/lens-ok.svg +7 -0
- package/dist/web/mascots/lens-outage.svg +7 -0
- package/dist/web/mascots/lens-running.svg +7 -0
- package/dist/web/mascots/moss-attention.svg +7 -0
- package/dist/web/mascots/moss-ok.svg +7 -0
- package/dist/web/mascots/moss-outage.svg +7 -0
- package/dist/web/mascots/moss-running.svg +7 -0
- package/dist/web/mascots/nit-attention.svg +7 -0
- package/dist/web/mascots/nit-ok.svg +7 -0
- package/dist/web/mascots/nit-outage.svg +7 -0
- package/dist/web/mascots/nit-running.svg +7 -0
- package/dist/web/mascots/nova-attention.svg +7 -0
- package/dist/web/mascots/nova-ok.svg +7 -0
- package/dist/web/mascots/nova-outage.svg +7 -0
- package/dist/web/mascots/nova-running.svg +7 -0
- package/dist/web/mascots/penn-attention.svg +7 -0
- package/dist/web/mascots/penn-ok.svg +7 -0
- package/dist/web/mascots/penn-outage.svg +7 -0
- package/dist/web/mascots/penn-running.svg +7 -0
- package/dist/web/mascots/pip-attention.svg +7 -0
- package/dist/web/mascots/pip-ok.svg +7 -0
- package/dist/web/mascots/pip-outage.svg +7 -0
- package/dist/web/mascots/pip-running.svg +7 -0
- package/dist/web/mascots/ward-attention.svg +7 -0
- package/dist/web/mascots/ward-ok.svg +7 -0
- package/dist/web/mascots/ward-outage.svg +7 -0
- package/dist/web/mascots/ward-running.svg +7 -0
- package/dist/web/sw.js +98 -0
- package/dist/webhook-seen.js +53 -0
- package/dist/webhook-seen.js.map +1 -0
- package/dist/webhooks.js +625 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/zip.js +63 -0
- package/dist/zip.js.map +1 -0
- package/docs/API-V2.md +532 -0
- package/docs/CHAT-EVAL.md +130 -0
- package/docs/CHAT-UI.md +416 -0
- package/docs/CONNECTIONS.md +89 -0
- package/docs/DATA-MODEL.md +518 -0
- package/docs/DEPLOY.md +577 -0
- package/docs/DOGFOOD-LEDGER.md +81 -0
- package/docs/GITHUB.md +148 -0
- package/docs/INTEGRATION-MODEL.md +200 -0
- package/docs/LEAD-DOT.md +553 -0
- package/docs/LIST-PATTERNS.md +52 -0
- package/docs/MULTI-REPO.md +766 -0
- package/docs/PARITY.md +276 -0
- package/docs/PR-REVIEW-QA.md +1266 -0
- package/docs/PR-REVIEW.md +357 -0
- package/docs/SANDBOX.md +320 -0
- package/docs/SETUP.md +776 -0
- package/docs/SLACK.md +80 -0
- package/docs/STAGES.md +619 -0
- package/docs/TRACE-MAP.md +89 -0
- package/docs/TRACKERS.md +225 -0
- package/docs/VOICE.md +36 -0
- package/docs/tinybee/00-README.md +45 -0
- package/docs/tinybee/01-PRD.md +216 -0
- package/docs/tinybee/02-architecture-pairing.md +199 -0
- package/docs/tinybee/03-local-agent.md +222 -0
- package/docs/tinybee/04-local-setup-ui.md +128 -0
- package/docs/tinybee/05-mobile-app-ux.md +199 -0
- package/docs/tinybee/06-environments-and-delivery.md +148 -0
- package/docs/tinybee/07-coding-agent-handoff.md +116 -0
- package/docs/tinybee/OPEN-QUESTIONS.md +26 -0
- package/docs/tinybee/PROGRESS.md +134 -0
- package/package.json +111 -4
|
@@ -0,0 +1,1221 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { config } from '../config.js';
|
|
7
|
+
import { publish } from '../events.js';
|
|
8
|
+
import { githubFor } from '../integrations/github.js';
|
|
9
|
+
import { defaultBranchOf, pickMergeBranch, readPullHead, unlistedBranchNote, verifyCommit } from './verify.js';
|
|
10
|
+
import { targetRepo } from '../repos.js';
|
|
11
|
+
import { currentRunId } from '../queue.js';
|
|
12
|
+
import { killTrackedFor, runTracked, spawnTrackedPiped, terminate } from '../proc.js';
|
|
13
|
+
import { isRunCancelled } from '../agent/runs.js';
|
|
14
|
+
import { redact } from '../agent/redact.js';
|
|
15
|
+
import { CdpGrabber, FrameThrottle, appendArtifact, artifactUrl, collectOutputFiles, copyArtifact, freePort, outputArtifactName, leasePorts, releasePorts, safeName, writeArtifact, } from './artifacts.js';
|
|
16
|
+
import { reportTests, stripAnsi } from './playwright-json.js';
|
|
17
|
+
import { adapterFor } from './adapters/index.js';
|
|
18
|
+
import { playwrightVersion } from './adapters/playwright.js';
|
|
19
|
+
import { MARKER, parseMarkerLine } from './step-reporter.js';
|
|
20
|
+
import { cancelAiVision, runAiVision } from './aivision/index.js';
|
|
21
|
+
import { cancelPwgen } from './pwgen/index.js';
|
|
22
|
+
import { DEFAULT_INSTALL, forwardingArgv, parseCommand, scriptRun, shQuote, splitSetupSteps } from './command.js';
|
|
23
|
+
import { runSetup } from './setup.js';
|
|
24
|
+
import { writePlaywrightObservations } from './observe.js';
|
|
25
|
+
import { collectEvidence, normTitle } from './evidence.js';
|
|
26
|
+
import { copyFilesHint, copyFilesHintFiles, copyFilesIntoWorktree, untrackedDotfiles } from './copyfiles.js';
|
|
27
|
+
import { prepareWorktree, scrubReason } from './worktree.js';
|
|
28
|
+
import { hostVars, passEnvVars, valueMasker } from './host-env.js';
|
|
29
|
+
import { hideTypedValue } from './typed-value.js';
|
|
30
|
+
import { checkoutTestRepo, effectiveRunner, testRepoRefusal } from './test-repo.js';
|
|
31
|
+
// hostVars() is the one filter of the host environment; it lives in host-env.ts with the list of what it holds back.
|
|
32
|
+
export { hostVars };
|
|
33
|
+
const DEFAULT_TIMEOUT_MS = 10 * 60_000;
|
|
34
|
+
/** Setup steps (install, browsers, build) get their own budget on top of the test timeout. */
|
|
35
|
+
const SETUP_TIMEOUT_MS = Number(process.env.LOCALDOST_SETUP_TIMEOUT_MS) || 15 * 60_000;
|
|
36
|
+
const DEMO_DIR = path.join(config.demoDir, 'sample-app');
|
|
37
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const PW_CONFIGS = ['playwright.config.ts', 'playwright.config.mts', 'playwright.config.js', 'playwright.config.mjs', 'playwright.config.cjs'];
|
|
39
|
+
/** Upper bounds per attempt so a long suite cannot flood the bus or the disk. */
|
|
40
|
+
const MAX_FRAMES = 600;
|
|
41
|
+
const MAX_LOG_EVENTS = 2000;
|
|
42
|
+
/** Files never copied into the Docker test container. A private-registry .npmrc must come from the image. */
|
|
43
|
+
export const SECRET_FILES = [
|
|
44
|
+
'.env',
|
|
45
|
+
'.env.*',
|
|
46
|
+
'*.env',
|
|
47
|
+
'.npmrc',
|
|
48
|
+
'.yarnrc',
|
|
49
|
+
'.yarnrc.yml',
|
|
50
|
+
'.netrc',
|
|
51
|
+
'.pgpass',
|
|
52
|
+
'.git-credentials',
|
|
53
|
+
'.aws',
|
|
54
|
+
'.ssh',
|
|
55
|
+
'.gnupg',
|
|
56
|
+
'.docker',
|
|
57
|
+
'.kube',
|
|
58
|
+
'.config/gcloud',
|
|
59
|
+
'.terraform',
|
|
60
|
+
'*.pem',
|
|
61
|
+
'*.key',
|
|
62
|
+
'*.p12',
|
|
63
|
+
'*.pfx',
|
|
64
|
+
'*.jks',
|
|
65
|
+
'*.keystore',
|
|
66
|
+
'id_rsa*',
|
|
67
|
+
'id_ed25519*',
|
|
68
|
+
'id_ecdsa*',
|
|
69
|
+
'credentials.json',
|
|
70
|
+
'service-account*.json',
|
|
71
|
+
'*.tfstate',
|
|
72
|
+
'*.tfvars',
|
|
73
|
+
];
|
|
74
|
+
/** Thrown by run() when the run was cancelled before or while the tests ran. */
|
|
75
|
+
export class RunCancelledError extends Error {
|
|
76
|
+
runId;
|
|
77
|
+
constructor(runId) {
|
|
78
|
+
super('Run cancelled');
|
|
79
|
+
this.runId = runId;
|
|
80
|
+
this.name = 'RunCancelledError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ---------- process registry (cancel) ----------
|
|
84
|
+
const activeProcs = new Map();
|
|
85
|
+
const cancelRequested = new Set();
|
|
86
|
+
const attempts = new Map();
|
|
87
|
+
/** Kills the test process of a run (the whole process group, browsers included). Returns true when one was running. */
|
|
88
|
+
export function cancelRunProcess(runId) {
|
|
89
|
+
cancelRequested.add(runId);
|
|
90
|
+
const pw = cancelPwgen(runId);
|
|
91
|
+
if (cancelAiVision(runId))
|
|
92
|
+
return true;
|
|
93
|
+
// Git clones and other tracked children of the run die with its test process.
|
|
94
|
+
const others = killTrackedFor(runId);
|
|
95
|
+
if (pw)
|
|
96
|
+
return true;
|
|
97
|
+
const kill = activeProcs.get(runId);
|
|
98
|
+
if (!kill)
|
|
99
|
+
return others > 0;
|
|
100
|
+
kill();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
export function hasActiveProcess(runId) {
|
|
104
|
+
return activeProcs.has(runId);
|
|
105
|
+
}
|
|
106
|
+
function publishLive(e) {
|
|
107
|
+
publish(e);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Turns the step reporter's stdout markers into live events and artifacts for one attempt.
|
|
111
|
+
* Exported for tests: feed it lines with `line()` and read what it published.
|
|
112
|
+
*/
|
|
113
|
+
export class LiveSession {
|
|
114
|
+
runId;
|
|
115
|
+
attempt;
|
|
116
|
+
grabber;
|
|
117
|
+
emit;
|
|
118
|
+
titles = new Map();
|
|
119
|
+
projects = new Map();
|
|
120
|
+
retries = new Map();
|
|
121
|
+
frames;
|
|
122
|
+
frameCount = 0;
|
|
123
|
+
logCount = 0;
|
|
124
|
+
copied = new Map();
|
|
125
|
+
saved = [];
|
|
126
|
+
/** The `test.step` names of each test attempt, top level only (KAT-377). Key: retry and normalized title. */
|
|
127
|
+
named = new Map();
|
|
128
|
+
openSteps = new Map();
|
|
129
|
+
prefix;
|
|
130
|
+
constructor(runId, attempt, grabber, emit = publishLive, minFrameGapMs = 500) {
|
|
131
|
+
this.runId = runId;
|
|
132
|
+
this.attempt = attempt;
|
|
133
|
+
this.grabber = grabber;
|
|
134
|
+
this.emit = emit;
|
|
135
|
+
this.prefix = `a${attempt}`;
|
|
136
|
+
this.frames = new FrameThrottle((f) => this.sendFrame(f), minFrameGapMs);
|
|
137
|
+
}
|
|
138
|
+
/** Feeds one stdout or stderr line. Returns true when it was a reporter marker. */
|
|
139
|
+
line(raw) {
|
|
140
|
+
const ev = parseMarkerLine(raw);
|
|
141
|
+
if (ev) {
|
|
142
|
+
appendArtifact(this.runId, `${this.prefix}-steps.jsonl`, `${JSON.stringify({ at: new Date().toISOString(), ...ev })}\n`);
|
|
143
|
+
this.onEvent(ev);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const text = stripAnsi(raw).replace(/\s+$/, '');
|
|
147
|
+
if (!text)
|
|
148
|
+
return false;
|
|
149
|
+
appendArtifact(this.runId, `${this.prefix}-output.log`, `${text}\n`);
|
|
150
|
+
this.log(text);
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
log(line) {
|
|
154
|
+
if (this.logCount++ >= MAX_LOG_EVENTS)
|
|
155
|
+
return;
|
|
156
|
+
this.emit({ type: 'run_log', runId: this.runId, line: line.slice(0, 2000) });
|
|
157
|
+
}
|
|
158
|
+
step(label, status, detail, test, project) {
|
|
159
|
+
const step = { at: new Date().toISOString(), label: label.slice(0, 200), status };
|
|
160
|
+
if (detail)
|
|
161
|
+
step.detail = detail.slice(0, 1000);
|
|
162
|
+
if (test)
|
|
163
|
+
step.test = test.slice(0, 300);
|
|
164
|
+
if (test && project)
|
|
165
|
+
step.project = project.slice(0, 100);
|
|
166
|
+
this.emit({ type: 'run_step', runId: this.runId, step });
|
|
167
|
+
}
|
|
168
|
+
onEvent(ev) {
|
|
169
|
+
switch (ev.t) {
|
|
170
|
+
case 'begin':
|
|
171
|
+
this.log(`Running ${ev.total} test${ev.total === 1 ? '' : 's'}`);
|
|
172
|
+
break;
|
|
173
|
+
case 'test_begin':
|
|
174
|
+
this.titles.set(ev.test, ev.title);
|
|
175
|
+
this.retries.set(ev.test, ev.retry);
|
|
176
|
+
if (ev.project)
|
|
177
|
+
this.projects.set(ev.test, ev.project);
|
|
178
|
+
this.step(`Test started: ${ev.title}`, 'info', `${path.basename(ev.file)}${ev.retry ? `, retry ${ev.retry}` : ''}`, ev.title, ev.project);
|
|
179
|
+
break;
|
|
180
|
+
case 'step_begin':
|
|
181
|
+
this.log(`> ${this.titles.get(ev.test) ?? ''}: ${hideTypedValue(ev.step)}`);
|
|
182
|
+
if (ev.category === 'test.step')
|
|
183
|
+
this.openSteps.set(ev.test, (this.openSteps.get(ev.test) ?? 0) + 1);
|
|
184
|
+
break;
|
|
185
|
+
case 'step_end': {
|
|
186
|
+
const test = this.titles.get(ev.test) ?? '';
|
|
187
|
+
// A fill step is titled with the value it typed, a password included. The title goes into the step list, the live frame and the report as written
|
|
188
|
+
// below, so the value is hidden here, once, and the label of a frame still equals the name of the step it was taken after.
|
|
189
|
+
const title = hideTypedValue(ev.step);
|
|
190
|
+
if (ev.category === 'test.step') {
|
|
191
|
+
const open = Math.max(0, (this.openSteps.get(ev.test) ?? 1) - 1);
|
|
192
|
+
this.openSteps.set(ev.test, open);
|
|
193
|
+
// Steps end last in, first out: a step that ends with none left open was a top-level one.
|
|
194
|
+
if (open === 0)
|
|
195
|
+
this.nameStep(ev.test, title, !!ev.error);
|
|
196
|
+
}
|
|
197
|
+
this.step(title, ev.error ? 'error' : 'ok', ev.error ? `${test}\n${hideTypedValue(ev.error)}` : test, test, this.projects.get(ev.test));
|
|
198
|
+
if (this.grabber && (ev.category === 'pw:api' || ev.category === 'test.step' || ev.category === 'expect')) {
|
|
199
|
+
this.frames.offer({ test, ...this.where(ev.test), step: title });
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case 'test_end': {
|
|
204
|
+
const passed = ev.status === 'passed';
|
|
205
|
+
const status = passed ? 'ok' : ev.status === 'skipped' ? 'warn' : 'error';
|
|
206
|
+
const label = passed ? 'Test passed' : ev.status === 'skipped' ? 'Test skipped' : ev.status === 'timedOut' ? 'Test timed out' : 'Test failed';
|
|
207
|
+
this.step(`${label}: ${ev.title}`, status, ev.error ?? `${(ev.ms / 1000).toFixed(1)} s`, ev.title, ev.project ?? this.projects.get(ev.test));
|
|
208
|
+
const shot = ev.attachments.filter((a) => a.contentType.startsWith('image/')).pop();
|
|
209
|
+
if (shot)
|
|
210
|
+
this.frames.offer({ test: ev.title, ...this.where(ev.test, ev.project), step: label, file: shot.path });
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
case 'end':
|
|
214
|
+
this.log(`Playwright finished: ${ev.status}`);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** One key per attempt of one test in one project: two projects run the same title, and their steps must stay apart. */
|
|
219
|
+
nameKey(retry, title, project = '') {
|
|
220
|
+
return `${retry}\u0000${project}\u0000${normTitle(title)}`;
|
|
221
|
+
}
|
|
222
|
+
nameStep(id, step, failed = false) {
|
|
223
|
+
const key = this.nameKey(this.retries.get(id) ?? 0, this.titles.get(id) ?? '', this.projects.get(id));
|
|
224
|
+
const list = this.named.get(key) ?? [];
|
|
225
|
+
const label = step.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
226
|
+
const last = list[list.length - 1];
|
|
227
|
+
if (label && last?.name === label) {
|
|
228
|
+
if (failed)
|
|
229
|
+
last.failed = true;
|
|
230
|
+
}
|
|
231
|
+
else if (label && list.length < 12) {
|
|
232
|
+
list.push(failed ? { name: label, failed: true } : { name: label });
|
|
233
|
+
}
|
|
234
|
+
this.named.set(key, list);
|
|
235
|
+
}
|
|
236
|
+
/** The `test.step` blocks of the newest attempt of one test (in one project), by its report title, with the one that threw marked. */
|
|
237
|
+
namedSteps(title, project = '') {
|
|
238
|
+
for (let retry = 5; retry >= 0; retry--) {
|
|
239
|
+
const found = this.named.get(this.nameKey(retry, title, project));
|
|
240
|
+
if (found?.length)
|
|
241
|
+
return found.map((s) => ({ ...s }));
|
|
242
|
+
}
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
/** The `test.step` names of the newest attempt of one test (in one project), by its report title. Undefined when the test named none. */
|
|
246
|
+
stepNames(title, project = '') {
|
|
247
|
+
return this.namedSteps(title, project)?.map((s) => s.name);
|
|
248
|
+
}
|
|
249
|
+
/** The project and retry a test is running in, for the frames taken in it. */
|
|
250
|
+
where(id, project) {
|
|
251
|
+
return { project: project ?? this.projects.get(id), retry: this.retries.get(id) ?? 0 };
|
|
252
|
+
}
|
|
253
|
+
async sendFrame(f) {
|
|
254
|
+
if (this.frameCount >= MAX_FRAMES)
|
|
255
|
+
return true;
|
|
256
|
+
const at = new Date().toISOString();
|
|
257
|
+
const n = String(this.frameCount + 1).padStart(4, '0');
|
|
258
|
+
let name = null;
|
|
259
|
+
if (f.file) {
|
|
260
|
+
name = copyArtifact(this.runId, f.file, `${this.prefix}-frame-${n}-${safeName(f.test, 40)}${path.extname(f.file) || '.png'}`);
|
|
261
|
+
if (name)
|
|
262
|
+
this.copied.set(path.resolve(f.file), name);
|
|
263
|
+
}
|
|
264
|
+
else if (this.grabber) {
|
|
265
|
+
const buf = await this.grabber.capture();
|
|
266
|
+
if (buf)
|
|
267
|
+
name = writeArtifact(this.runId, `${this.prefix}-frame-${n}.jpg`, buf);
|
|
268
|
+
}
|
|
269
|
+
if (!name)
|
|
270
|
+
return false;
|
|
271
|
+
this.frameCount++;
|
|
272
|
+
this.saved.push({ title: f.test, project: f.project, retry: f.retry ?? 0, name, step: f.step });
|
|
273
|
+
this.emit({
|
|
274
|
+
type: 'run_frame',
|
|
275
|
+
runId: this.runId,
|
|
276
|
+
frame: { at, test: f.test, step: f.step, image: artifactUrl(this.runId, name), ...(f.project ? { project: f.project } : {}) },
|
|
277
|
+
});
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
/** Source files already stored as frames, so the end-of-run copy skips them. */
|
|
281
|
+
alreadyCopied(file) {
|
|
282
|
+
return this.copied.has(path.resolve(file));
|
|
283
|
+
}
|
|
284
|
+
/** The artifact name a copied source file was stored under. */
|
|
285
|
+
storedName(file) {
|
|
286
|
+
return this.copied.get(path.resolve(file));
|
|
287
|
+
}
|
|
288
|
+
/** The step screenshots saved so far, with the test each was taken in. */
|
|
289
|
+
shots() {
|
|
290
|
+
return this.saved;
|
|
291
|
+
}
|
|
292
|
+
async close() {
|
|
293
|
+
await this.frames.flush();
|
|
294
|
+
this.frames.stop();
|
|
295
|
+
this.grabber?.close?.();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** Splits a byte stream into lines and hands each complete line to `fn`. */
|
|
299
|
+
function lineSplitter(fn) {
|
|
300
|
+
let buf = '';
|
|
301
|
+
return {
|
|
302
|
+
push(chunk) {
|
|
303
|
+
buf += chunk;
|
|
304
|
+
let i;
|
|
305
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
306
|
+
fn(buf.slice(0, i).replace(/\r$/, ''));
|
|
307
|
+
buf = buf.slice(i + 1);
|
|
308
|
+
}
|
|
309
|
+
// A JSON report on stdout can be one very long line; do not hold megabytes waiting for a newline.
|
|
310
|
+
if (buf.length > 1_000_000) {
|
|
311
|
+
fn(buf);
|
|
312
|
+
buf = '';
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
end() {
|
|
316
|
+
if (buf)
|
|
317
|
+
fn(buf);
|
|
318
|
+
buf = '';
|
|
319
|
+
},
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
// ---------- Playwright wiring ----------
|
|
323
|
+
/** Absolute path of the step reporter, .ts under tsx and .js in dist/. */
|
|
324
|
+
export function stepReporterPath() {
|
|
325
|
+
const own = path.extname(fileURLToPath(import.meta.url)) || '.js';
|
|
326
|
+
for (const ext of [own, '.js', '.ts']) {
|
|
327
|
+
const p = path.join(HERE, `step-reporter${ext}`);
|
|
328
|
+
if (existsSync(p))
|
|
329
|
+
return p;
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
function readScripts(workdir) {
|
|
334
|
+
try {
|
|
335
|
+
const pkg = JSON.parse(readFileSync(path.join(workdir, 'package.json'), 'utf8'));
|
|
336
|
+
return Object.fromEntries(Object.entries(pkg.scripts ?? {}).filter((e) => typeof e[1] === 'string'));
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
return {};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/** The package.json script an `npm run x` style argv calls, or undefined. */
|
|
343
|
+
function scriptBody(argv, scripts) {
|
|
344
|
+
const call = scriptRun(argv, scripts);
|
|
345
|
+
return call ? scripts[call.name] : undefined;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Wrapper config that turns on screenshots, traces, video, and (optionally) the DevTools port. The video mode
|
|
349
|
+
* comes from LOCALDOST_VIDEO: retain-on-failure (the default) keeps the videos of tests that did not pass, on keeps
|
|
350
|
+
* all of them. A mode the repo's config sets itself (on, on-first-retry) stays unless the dot asks for on.
|
|
351
|
+
*/
|
|
352
|
+
export function wrapperConfigSource(baseFile) {
|
|
353
|
+
const spec = /\.m?ts$/.test(baseFile) ? `./${baseFile.replace(/\.m?ts$/, '')}` : `./${baseFile}`;
|
|
354
|
+
return `// Generated by localdost for one run and deleted when the run ends.
|
|
355
|
+
import base from ${JSON.stringify(spec)};
|
|
356
|
+
|
|
357
|
+
const port = process.env.LOCALDOST_CDP_PORT;
|
|
358
|
+
const cfg = base || {};
|
|
359
|
+
const chromium = (u) => !u.browserName && (!u.defaultBrowserType || u.defaultBrowserType === 'chromium') || u.browserName === 'chromium';
|
|
360
|
+
const keep = (v, fallback) => (v && v !== 'off' ? v : fallback);
|
|
361
|
+
const always = process.env.LOCALDOST_VIDEO === 'on';
|
|
362
|
+
const live = (u) => {
|
|
363
|
+
const out = { ...u, screenshot: 'on', trace: keep(u.trace, 'retain-on-failure'), video: always ? 'on' : keep(u.video, 'retain-on-failure') };
|
|
364
|
+
if (port && chromium(u)) {
|
|
365
|
+
const lo = u.launchOptions || {};
|
|
366
|
+
out.launchOptions = { ...lo, args: [...(lo.args || []), '--remote-debugging-port=' + port] };
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
export default {
|
|
372
|
+
...cfg,
|
|
373
|
+
...(port ? { workers: 1 } : {}),
|
|
374
|
+
use: live(cfg.use || {}),
|
|
375
|
+
...(Array.isArray(cfg.projects) ? { projects: cfg.projects.map((p) => ({ ...p, use: live({ ...(cfg.use || {}), ...(p.use || {}) }) })) } : {}),
|
|
376
|
+
};
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
/** The config file a command line names with `--config x`, `--config=x`, `-c x`, or `-c=x`. The last one wins, as in Playwright. */
|
|
380
|
+
export function configArgOf(words) {
|
|
381
|
+
let named;
|
|
382
|
+
words.forEach((w, i) => {
|
|
383
|
+
const eq = /^(?:--config|-c)=(.+)$/.exec(w);
|
|
384
|
+
const value = eq ? eq[1] : w === '--config' || w === '-c' ? words[i + 1] : undefined;
|
|
385
|
+
if (value && !value.startsWith('-'))
|
|
386
|
+
named = value.replace(/^(['"])(.*)\1$/, '$2');
|
|
387
|
+
});
|
|
388
|
+
return named;
|
|
389
|
+
}
|
|
390
|
+
function findPlaywrightConfig(workdir) {
|
|
391
|
+
return PW_CONFIGS.find((f) => existsSync(path.join(workdir, f)));
|
|
392
|
+
}
|
|
393
|
+
function liveFramesEnabled(mode) {
|
|
394
|
+
const flag = (process.env.LOCALDOST_LIVE_FRAMES ?? '').trim();
|
|
395
|
+
if (flag === '0' || flag === 'false')
|
|
396
|
+
return false;
|
|
397
|
+
if (flag === '1' || flag === 'true')
|
|
398
|
+
return mode !== 'docker';
|
|
399
|
+
return mode === 'demo';
|
|
400
|
+
}
|
|
401
|
+
/** Copies screenshots, traces, videos, and the JSON report into data/artifacts/<runId>/. */
|
|
402
|
+
function saveOutputs(runId, prefix, outDir, live) {
|
|
403
|
+
const results = path.join(outDir, 'test-results');
|
|
404
|
+
for (const file of collectOutputFiles(results)) {
|
|
405
|
+
if (live?.alreadyCopied(file))
|
|
406
|
+
continue;
|
|
407
|
+
copyArtifact(runId, file, outputArtifactName(prefix, results, file));
|
|
408
|
+
}
|
|
409
|
+
const report = path.join(outDir, 'report.json');
|
|
410
|
+
if (existsSync(report))
|
|
411
|
+
copyArtifact(runId, report, `${prefix}-report.json`);
|
|
412
|
+
}
|
|
413
|
+
// ---------- run ----------
|
|
414
|
+
/**
|
|
415
|
+
* Why the checkout at `dir` cannot give results for commit `sha`, or null when it can or when that cannot be
|
|
416
|
+
* told (a folder that is not a git checkout). The checkout's HEAD must be `sha`; short shas of 7 or more match.
|
|
417
|
+
*/
|
|
418
|
+
export async function staleCheckout(dir, sha) {
|
|
419
|
+
const r = await runTracked('git', ['rev-parse', '--verify', 'HEAD'], { cwd: dir, timeoutMs: 10_000, maxBuffer: 1000 });
|
|
420
|
+
const head = r.code === 0 ? r.stdout.trim().toLowerCase() : '';
|
|
421
|
+
if (!/^[0-9a-f]{40,64}$/.test(head))
|
|
422
|
+
return null;
|
|
423
|
+
const want = sha.trim().toLowerCase();
|
|
424
|
+
if (want.length >= 7 && (head === want || head.startsWith(want)))
|
|
425
|
+
return null;
|
|
426
|
+
return `The tests did not run for ${want.slice(0, 7)}: the checkout in the working folder is on ${head.slice(0, 7)}, so its tests would count results for other code. No test count is shown for this commit.`;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Renames the cases of a Playwright report from their path under the testDir (`config.rootDir` of the report) to their path under `root`,
|
|
430
|
+
* the checkout. A case whose file is not under `root` keeps its name.
|
|
431
|
+
*/
|
|
432
|
+
export function repoRelativeFiles(cases, json, root) {
|
|
433
|
+
const rootDir = json?.config?.rootDir;
|
|
434
|
+
if (typeof rootDir !== 'string' || !rootDir)
|
|
435
|
+
return;
|
|
436
|
+
const real = (p) => {
|
|
437
|
+
try {
|
|
438
|
+
return realpathSync(p);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
return path.resolve(p);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const base = real(root);
|
|
445
|
+
const tests = real(rootDir);
|
|
446
|
+
for (const c of cases) {
|
|
447
|
+
if (!c.file)
|
|
448
|
+
continue;
|
|
449
|
+
const rel = path.relative(base, path.join(tests, c.file));
|
|
450
|
+
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel))
|
|
451
|
+
c.file = rel.split(path.sep).join('/');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Hides what a person must not read in one case of the report. The test login values are masked in the error text (Playwright's call log of a
|
|
456
|
+
* failed fill prints the value it typed), in the step names and in the label of each screenshot. The label of a screenshot is the title of the step
|
|
457
|
+
* it was taken after, so it can also name any value a test typed (`Fill "<value>"`), which is hidden whatever it is.
|
|
458
|
+
*/
|
|
459
|
+
export function maskCase(c, mask) {
|
|
460
|
+
if (c.error)
|
|
461
|
+
c.error = mask(c.error);
|
|
462
|
+
if (c.steps)
|
|
463
|
+
c.steps = c.steps.map(mask);
|
|
464
|
+
if (c.namedSteps)
|
|
465
|
+
c.namedSteps = c.namedSteps.map((b) => ({ ...b, name: mask(b.name) }));
|
|
466
|
+
if (c.evidence)
|
|
467
|
+
c.evidence.screenshots = c.evidence.screenshots.map((s) => (s.step ? { ...s, step: mask(hideTypedValue(s.step)) } : s));
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Runs the tests of one spec. `onLine` gets every setup and command output line (reporter markers
|
|
471
|
+
* excluded), for callers without a run id such as Try it in the dot settings.
|
|
472
|
+
*/
|
|
473
|
+
export async function run(spec, opts = {}) {
|
|
474
|
+
const mode = spec.watch.runner.mode;
|
|
475
|
+
const runId = opts.runId ?? currentRunId();
|
|
476
|
+
if (runId && isRunCancelled(runId))
|
|
477
|
+
throw new RunCancelledError(runId);
|
|
478
|
+
const attempt = runId ? (attempts.get(runId) ?? 0) + 1 : 0;
|
|
479
|
+
if (runId)
|
|
480
|
+
attempts.set(runId, attempt);
|
|
481
|
+
// AI-Vision cases run on their own backend (src/runner/aivision), never through the Playwright command.
|
|
482
|
+
if (spec.aivision) {
|
|
483
|
+
try {
|
|
484
|
+
const report = await runAiVision(spec, { ...(runId ? { runId, isCancelled: () => isRunCancelled(runId) } : {}), attempt: attempt || 1 });
|
|
485
|
+
if (runId && (cancelRequested.has(runId) || isRunCancelled(runId)))
|
|
486
|
+
throw new RunCancelledError(runId);
|
|
487
|
+
return report;
|
|
488
|
+
}
|
|
489
|
+
finally {
|
|
490
|
+
if (runId)
|
|
491
|
+
cancelRequested.delete(runId);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const started = Date.now();
|
|
495
|
+
const startedAt = new Date(started).toISOString();
|
|
496
|
+
const baseUrl = spec.baseUrl ?? spec.watch.baseUrl;
|
|
497
|
+
const timeoutMs = spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
498
|
+
const adapter = adapterFor(spec.testRepo?.config.framework);
|
|
499
|
+
const baseUrlEnv = spec.testRepo?.config.baseUrlEnv;
|
|
500
|
+
// A Tests repo run takes its command, setup, folder and test login from the Tests repo; every other run uses the dot's runner.
|
|
501
|
+
const runner = effectiveRunner(spec);
|
|
502
|
+
const rawCommand = runner.command?.trim() || adapter.defaultCommand;
|
|
503
|
+
const setupSteps = splitSetupSteps(runner.setup);
|
|
504
|
+
// The test login, set after the credential filter, and a mask for its values in everything a person reads.
|
|
505
|
+
const passVars = passEnvVars(runner.passEnv);
|
|
506
|
+
const mask = valueMasker(passVars);
|
|
507
|
+
const notRunReport = (notRun, tail, commandLine = rawCommand) => {
|
|
508
|
+
const r = {
|
|
509
|
+
mode,
|
|
510
|
+
command: commandLine,
|
|
511
|
+
startedAt,
|
|
512
|
+
durationMs: Date.now() - started,
|
|
513
|
+
passed: 0,
|
|
514
|
+
failed: 0,
|
|
515
|
+
flaky: 0,
|
|
516
|
+
skipped: 0,
|
|
517
|
+
cases: [],
|
|
518
|
+
stdoutTail: mask(redact(stripAnsi(tail).slice(-4000))),
|
|
519
|
+
notRun,
|
|
520
|
+
};
|
|
521
|
+
if (baseUrl)
|
|
522
|
+
r.baseUrl = baseUrl;
|
|
523
|
+
return r;
|
|
524
|
+
};
|
|
525
|
+
// The test command runs without a shell. A shell operator in it is a configuration error, not a test result.
|
|
526
|
+
const parsedCommand = parseCommand(rawCommand);
|
|
527
|
+
if (!parsedCommand.ok)
|
|
528
|
+
return notRunReport({ stage: 'command', reason: parsedCommand.error }, '');
|
|
529
|
+
const cmdVars = parsedCommand.env;
|
|
530
|
+
// A Tests repo run that must not start (no adapter for its framework, a fork PR outside Docker) ends here, before anything is fetched.
|
|
531
|
+
const refusal = spec.testRepo ? testRepoRefusal(spec.testRepo, mode, spec.watch.runner.passEnv) : null;
|
|
532
|
+
if (refusal)
|
|
533
|
+
return notRunReport({ stage: 'checkout', reason: refusal }, '');
|
|
534
|
+
const outDir = await mkdtemp(path.join(os.tmpdir(), 'localdost-out-'));
|
|
535
|
+
let cloneDir;
|
|
536
|
+
let worktree;
|
|
537
|
+
/** The working folder a worktree was made from: where runner.copyFiles are read, and where the failure hint looks (KAT-293). */
|
|
538
|
+
let worktreeSource;
|
|
539
|
+
let wrapperFile;
|
|
540
|
+
let live;
|
|
541
|
+
try {
|
|
542
|
+
// A Tests repo run starts from a worktree of the Tests repo at the commit that was resolved for it, checked before the tests run.
|
|
543
|
+
// It never clones or verifies the app repo: only the app's URL crosses over.
|
|
544
|
+
if (spec.testRepo) {
|
|
545
|
+
const tr = spec.testRepo;
|
|
546
|
+
const stopped = () => !!runId && (cancelRequested.has(runId) || isRunCancelled(runId));
|
|
547
|
+
const checkout = await checkoutTestRepo({
|
|
548
|
+
repo: tr.repo,
|
|
549
|
+
sha: tr.sha,
|
|
550
|
+
id: runId ? `${runId}-a${attempt}-tests` : `adhoc-tests-${process.pid}-${Date.now().toString(36)}`,
|
|
551
|
+
gh: githubFor(spec.watch, { repo: tr.repo }),
|
|
552
|
+
...(runId ? { runId, cancelled: stopped } : {}),
|
|
553
|
+
});
|
|
554
|
+
if (stopped()) {
|
|
555
|
+
if (checkout.ok)
|
|
556
|
+
await checkout.worktree.cleanup();
|
|
557
|
+
throw new RunCancelledError(runId);
|
|
558
|
+
}
|
|
559
|
+
if (!checkout.ok)
|
|
560
|
+
return notRunReport({ stage: 'checkout', reason: checkout.reason }, '');
|
|
561
|
+
worktree = checkout.worktree;
|
|
562
|
+
}
|
|
563
|
+
let workdir = worktree ? worktree.dir : mode === 'demo' ? (runner.workdir ?? DEMO_DIR) : runner.workdir;
|
|
564
|
+
const cloneEvent = spec.repo ? { repo: spec.repo } : undefined;
|
|
565
|
+
const cloneRepo = workdir ? null : targetRepo(spec.watch, cloneEvent);
|
|
566
|
+
if (!workdir && cloneRepo) {
|
|
567
|
+
// No local checkout configured: clone the commit of the event's repo on the host, then mount it.
|
|
568
|
+
const gh = githubFor(spec.watch, cloneEvent);
|
|
569
|
+
if (spec.sha) {
|
|
570
|
+
// Nothing is fetched or run until GitHub confirms the commit (KAT-291). The event's own fork flag and branch name are not trusted.
|
|
571
|
+
const target = spec.pr
|
|
572
|
+
? { kind: 'pr', number: spec.pr.number }
|
|
573
|
+
: { kind: 'default_branch', ...(spec.merge?.branch ? { named: spec.merge.branch } : {}), testBranches: spec.watch.testBranches };
|
|
574
|
+
const check = await verifyCommit(gh, cloneRepo, spec.sha, target);
|
|
575
|
+
if (!check.ok)
|
|
576
|
+
return notRunReport({ stage: 'verify', reason: check.reason }, '');
|
|
577
|
+
}
|
|
578
|
+
cloneDir = await mkdtemp(path.join(os.tmpdir(), 'localdost-src-'));
|
|
579
|
+
await gh.cloneTo(cloneDir, spec.sha ?? '');
|
|
580
|
+
workdir = cloneDir;
|
|
581
|
+
}
|
|
582
|
+
if (!workdir)
|
|
583
|
+
throw new Error(`Watch ${spec.watch.name} has no runner.workdir and no repo to clone`);
|
|
584
|
+
workdir = path.resolve(workdir);
|
|
585
|
+
// A configured checkout runs whatever commit it is on, so a run for a commit (a PR head or a merge) must
|
|
586
|
+
// be on that commit or its results belong to other code (KAT-284).
|
|
587
|
+
if (!spec.testRepo && !cloneDir && mode !== 'demo' && spec.sha && (spec.pr || spec.merge)) {
|
|
588
|
+
// A pull request of a fork never runs here: fork code stays off this machine (KAT-201) unless the dot trusts forks.
|
|
589
|
+
if (spec.pr?.fork && !runner.trustForks) {
|
|
590
|
+
return notRunReport({
|
|
591
|
+
stage: 'checkout',
|
|
592
|
+
reason: `The tests did not run for ${spec.sha.slice(0, 7)}: PR #${spec.pr.number} comes from a fork, and localdost does not run fork code on this machine. No test count is shown for this commit.`,
|
|
593
|
+
fork: true,
|
|
594
|
+
}, '');
|
|
595
|
+
}
|
|
596
|
+
// A pull request of the repo itself, or a merge commit of its base branch, runs in a temporary worktree of the
|
|
597
|
+
// working folder at that commit. The folder's own checkout and branch stay as they are; the worktree is removed in the finally below.
|
|
598
|
+
const repo = targetRepo(spec.watch, cloneEvent);
|
|
599
|
+
if (repo && !spec.pr?.fork) {
|
|
600
|
+
// The commit of the event is checked against the base repository and GitHub before it is checked out (see worktree.ts).
|
|
601
|
+
const github = githubFor(spec.watch, cloneEvent);
|
|
602
|
+
// Same rules as the clone above (verify.ts): the PR head is read from GitHub and judged by judgePullHead in worktree.ts.
|
|
603
|
+
const pullHead = async (n) => {
|
|
604
|
+
const pull = await readPullHead(github, repo, n);
|
|
605
|
+
return pull.ok ? pull.head : null;
|
|
606
|
+
};
|
|
607
|
+
// A merge commit must be on the default branch of the repository as GitHub reports it, or on a branch the event
|
|
608
|
+
// names that the dot lists in testBranches (KAT-292). An unlisted branch the event names decides nothing. With no
|
|
609
|
+
// GitHub token the default branch recorded for the repo stands in: an event cannot change it.
|
|
610
|
+
let defaultBranch;
|
|
611
|
+
let mergeBranch;
|
|
612
|
+
if (spec.merge) {
|
|
613
|
+
if (github.mode === 'demo')
|
|
614
|
+
defaultBranch = repo.defaultBranch;
|
|
615
|
+
else {
|
|
616
|
+
const def = await defaultBranchOf(github, repo, spec.sha);
|
|
617
|
+
if (!def.ok) {
|
|
618
|
+
return notRunReport({
|
|
619
|
+
stage: 'checkout',
|
|
620
|
+
reason: scrubReason(`The tests did not run for ${spec.sha.slice(0, 7)}: ${def.reason} No test count is shown for this commit.`),
|
|
621
|
+
}, '');
|
|
622
|
+
}
|
|
623
|
+
defaultBranch = def.branch;
|
|
624
|
+
}
|
|
625
|
+
mergeBranch = pickMergeBranch(defaultBranch, spec.merge?.branch, spec.watch.testBranches);
|
|
626
|
+
}
|
|
627
|
+
const plan = await prepareWorktree({
|
|
628
|
+
workdir,
|
|
629
|
+
repo,
|
|
630
|
+
sha: spec.sha,
|
|
631
|
+
...(spec.pr ? { prNumber: spec.pr.number, pullHead } : {}),
|
|
632
|
+
...(mergeBranch
|
|
633
|
+
? {
|
|
634
|
+
branch: mergeBranch.branch,
|
|
635
|
+
branchLabel: mergeBranch.listed ? 'test branch' : 'base branch',
|
|
636
|
+
...(mergeBranch.unlisted ? { branchNote: unlistedBranchNote(mergeBranch.unlisted, mergeBranch.branch) } : {}),
|
|
637
|
+
}
|
|
638
|
+
: {}),
|
|
639
|
+
id: runId ? `${runId}-a${attempt}` : `adhoc-${process.pid}-${Date.now().toString(36)}`,
|
|
640
|
+
...(runId ? { runId, cancelled: () => cancelRequested.has(runId) || isRunCancelled(runId) } : {}),
|
|
641
|
+
});
|
|
642
|
+
if (plan.kind === 'ready')
|
|
643
|
+
worktree = plan.worktree;
|
|
644
|
+
if (runId && (cancelRequested.has(runId) || isRunCancelled(runId)))
|
|
645
|
+
throw new RunCancelledError(runId);
|
|
646
|
+
if (plan.kind === 'failed')
|
|
647
|
+
return notRunReport({ stage: 'checkout', reason: scrubReason(plan.reason) }, '');
|
|
648
|
+
if (worktree) {
|
|
649
|
+
worktreeSource = workdir;
|
|
650
|
+
workdir = worktree.dir;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// The guard stays on every path: the count is reported only if the checkout the tests ran in is on the commit.
|
|
655
|
+
// A Tests repo checkout was checked against its own commit when it was made.
|
|
656
|
+
if (!spec.testRepo && !cloneDir && mode !== 'demo' && spec.sha) {
|
|
657
|
+
const stale = await staleCheckout(workdir, spec.sha);
|
|
658
|
+
if (stale)
|
|
659
|
+
return notRunReport({ stage: 'checkout', reason: stale }, '');
|
|
660
|
+
}
|
|
661
|
+
// runner.workingDirectory: setup steps, the Playwright config, and the test command all run in this
|
|
662
|
+
// folder of the checkout (a CI job's working-directory). Docker still mounts the whole checkout.
|
|
663
|
+
const checkoutDir = workdir;
|
|
664
|
+
const subdir = runner.workingDirectory?.trim() || undefined;
|
|
665
|
+
if (subdir) {
|
|
666
|
+
workdir = path.join(checkoutDir, subdir);
|
|
667
|
+
if (!existsSync(workdir))
|
|
668
|
+
throw new Error(`runner.workingDirectory "${subdir}" does not exist in the checkout of ${spec.watch.name}`);
|
|
669
|
+
}
|
|
670
|
+
// `npm run <script>` forwards the runner's options to the script's last command only after a `--`,
|
|
671
|
+
// and only when that command is Playwright. The same helper decides what the suggestion proposes (KAT-135).
|
|
672
|
+
const scripts = readScripts(workdir);
|
|
673
|
+
const forwarded = forwardingArgv(parsedCommand.argv, scripts);
|
|
674
|
+
const runArgv = forwarded.argv;
|
|
675
|
+
const command = runArgv === parsedCommand.argv ? rawCommand : `${rawCommand} --`;
|
|
676
|
+
const playwright = forwarded.playwright;
|
|
677
|
+
// A VAR=value word of the test command wins over the test login.
|
|
678
|
+
const extraVars = { ...passVars, ...cmdVars };
|
|
679
|
+
Object.assign(extraVars, await runPortVars(cmdVars, mode, outDir));
|
|
680
|
+
Object.assign(extraVars, appServerVars(cmdVars, mode, baseUrl));
|
|
681
|
+
let reporter;
|
|
682
|
+
if (runId && mode !== 'docker')
|
|
683
|
+
live = new LiveSession(runId, attempt);
|
|
684
|
+
// Setup steps run before the test command, in the workdir, through the shell. Their output
|
|
685
|
+
// goes to the live terminal and the report tail. Docker runs them inside the container.
|
|
686
|
+
const local = localSetupSteps({ mode, cloned: !!cloneDir || !!worktree, setupSteps, hasPackageJson: existsSync(path.join(workdir, 'package.json')) });
|
|
687
|
+
const localSteps = local.steps;
|
|
688
|
+
// Dev dependencies (@playwright/test) are always installed: the image sets NODE_ENV=production, which would skip them.
|
|
689
|
+
const installVars = local.defaultInstall
|
|
690
|
+
? { ...extraVars, ...DEFAULT_INSTALL_ENV }
|
|
691
|
+
: { ...extraVars, NPM_CONFIG_INCLUDE: DEFAULT_INSTALL_ENV.NPM_CONFIG_INCLUDE };
|
|
692
|
+
let setupOut = '';
|
|
693
|
+
let setup;
|
|
694
|
+
const log = (line) => {
|
|
695
|
+
setupOut = `${setupOut}${line}\n`.slice(-64_000);
|
|
696
|
+
live?.line(line);
|
|
697
|
+
opts.onLine?.(line);
|
|
698
|
+
};
|
|
699
|
+
// Files the worktree lacks (an .env, not tracked by git) come from the working folder, before any setup step (KAT-293).
|
|
700
|
+
if (worktree && worktreeSource && runner.copyFiles?.length) {
|
|
701
|
+
for (const note of copyFilesIntoWorktree({ from: worktreeSource, to: worktree.dir, root: worktree.root, files: runner.copyFiles }).notes)
|
|
702
|
+
log(note);
|
|
703
|
+
}
|
|
704
|
+
// When a run in a worktree did not pass, names runner.copyFiles if the working folder has untracked dotfiles that were not copied.
|
|
705
|
+
const withHint = async (r) => {
|
|
706
|
+
if (!worktree || !worktreeSource || !(r.notRun || r.failed > 0))
|
|
707
|
+
return r;
|
|
708
|
+
const found = await untrackedDotfiles(worktreeSource, runner.copyFiles ?? []);
|
|
709
|
+
const hint = copyFilesHint(found);
|
|
710
|
+
if (hint)
|
|
711
|
+
r.hint = hint;
|
|
712
|
+
const files = copyFilesHintFiles(found);
|
|
713
|
+
if (files)
|
|
714
|
+
r.hintFiles = files;
|
|
715
|
+
return r;
|
|
716
|
+
};
|
|
717
|
+
if (localSteps.length) {
|
|
718
|
+
setup = await runSetup({
|
|
719
|
+
workdir,
|
|
720
|
+
steps: localSteps,
|
|
721
|
+
log,
|
|
722
|
+
exec: (step) => runStep({ workdir: workdir, step, timeoutMs: SETUP_TIMEOUT_MS, runId, vars: installVars, mask, onLine: log }),
|
|
723
|
+
});
|
|
724
|
+
if (runId && (cancelRequested.has(runId) || isRunCancelled(runId)))
|
|
725
|
+
throw new RunCancelledError(runId);
|
|
726
|
+
if (!setup.ok) {
|
|
727
|
+
const r = notRunReport({
|
|
728
|
+
stage: 'setup',
|
|
729
|
+
reason: `Setup step "${setup.failedStep}" ${setup.reason || 'failed'}, so the tests did not run.${setup.detail ? `\n\n${setup.detail}` : ''}`,
|
|
730
|
+
exitCode: setup.code ?? null,
|
|
731
|
+
}, setupOut);
|
|
732
|
+
r.setup = { steps: localSteps.length, cache: setup.cache, durationMs: setup.durationMs };
|
|
733
|
+
return withHint(r);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Live view: local and demo stream reporter markers; docker logs arrive only at the end.
|
|
737
|
+
if (runId && mode !== 'docker' && playwright) {
|
|
738
|
+
reporter = stepReporterPath();
|
|
739
|
+
const words = [...runArgv, ...(scriptBody(runArgv, scripts)?.split(/\s+/) ?? [])];
|
|
740
|
+
// The config the run uses: the one the command names, else the repo's default. The wrapper sits next to it,
|
|
741
|
+
// so the paths inside the config keep their meaning, and the video, trace, and screenshot settings apply to it.
|
|
742
|
+
const named = configArgOf(words);
|
|
743
|
+
const hasConfigArg = words.some((a) => a === '-c' || a === '--config' || a.startsWith('--config=') || a.startsWith('-c='));
|
|
744
|
+
let baseConfig;
|
|
745
|
+
if (named) {
|
|
746
|
+
const abs = path.resolve(workdir, named);
|
|
747
|
+
if (existsSync(abs) && statSync(abs).isFile())
|
|
748
|
+
baseConfig = abs;
|
|
749
|
+
}
|
|
750
|
+
else if (!hasConfigArg) {
|
|
751
|
+
const found = findPlaywrightConfig(workdir);
|
|
752
|
+
if (found)
|
|
753
|
+
baseConfig = path.join(workdir, found);
|
|
754
|
+
}
|
|
755
|
+
if (baseConfig) {
|
|
756
|
+
wrapperFile = path.join(path.dirname(baseConfig), `.localdost-run-${safeName(runId, 40)}-${attempt}.config.ts`);
|
|
757
|
+
writeFileSync(wrapperFile, wrapperConfigSource(path.basename(baseConfig)));
|
|
758
|
+
if (runner.video === 'on')
|
|
759
|
+
extraVars.LOCALDOST_VIDEO = 'on';
|
|
760
|
+
if (liveFramesEnabled(mode)) {
|
|
761
|
+
const port = await freePort();
|
|
762
|
+
extraVars.LOCALDOST_CDP_PORT = String(port);
|
|
763
|
+
await live?.close();
|
|
764
|
+
live = new LiveSession(runId, attempt, new CdpGrabber(port));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
const args = adapter.args(spec, { reporter });
|
|
769
|
+
if (wrapperFile)
|
|
770
|
+
args.push(`--config=${path.relative(workdir, wrapperFile)}`);
|
|
771
|
+
// Per-run artifact dir: Playwright empties its output dir at start, so two concurrent runs
|
|
772
|
+
// sharing the default test-results/ delete each other's traces mid-run.
|
|
773
|
+
const playwrightArgs = playwright || (!scriptRun(parsedCommand.argv, scripts) && /playwright/.test(command));
|
|
774
|
+
if (playwrightArgs) {
|
|
775
|
+
args.push(`--output=${mode === 'docker' ? '/out/test-results' : path.join(outDir, 'test-results')}`);
|
|
776
|
+
// A trace of every test is what the Traceability map reads the visited pages from (src/runner/observe.ts).
|
|
777
|
+
// Traces of tests that passed are deleted once read, so the run keeps what it kept before.
|
|
778
|
+
if (runId)
|
|
779
|
+
args.push('--trace=on');
|
|
780
|
+
}
|
|
781
|
+
let exec;
|
|
782
|
+
let commandLine;
|
|
783
|
+
if (mode === 'docker') {
|
|
784
|
+
const image = adapter.image(workdir, runner.image);
|
|
785
|
+
commandLine = `docker ${image} ${command} ${args.join(' ')}`;
|
|
786
|
+
exec = await runDocker({
|
|
787
|
+
workdir: checkoutDir,
|
|
788
|
+
...(subdir ? { subdir } : {}),
|
|
789
|
+
outDir,
|
|
790
|
+
image,
|
|
791
|
+
argv: runArgv,
|
|
792
|
+
cmdVars,
|
|
793
|
+
passVars,
|
|
794
|
+
setupSteps,
|
|
795
|
+
args,
|
|
796
|
+
baseUrl,
|
|
797
|
+
timeoutMs,
|
|
798
|
+
runId,
|
|
799
|
+
adapter,
|
|
800
|
+
baseUrlEnv,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
commandLine = `${command} ${args.join(' ')}`;
|
|
805
|
+
exec = await runLocal({
|
|
806
|
+
workdir,
|
|
807
|
+
outDir,
|
|
808
|
+
argv: runArgv,
|
|
809
|
+
args,
|
|
810
|
+
baseUrl,
|
|
811
|
+
timeoutMs,
|
|
812
|
+
runId,
|
|
813
|
+
extraVars,
|
|
814
|
+
live,
|
|
815
|
+
onLine: opts.onLine,
|
|
816
|
+
mask,
|
|
817
|
+
adapter,
|
|
818
|
+
baseUrlEnv,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
await live?.close();
|
|
822
|
+
if (runId && (cancelRequested.has(runId) || isRunCancelled(runId)))
|
|
823
|
+
throw new RunCancelledError(runId);
|
|
824
|
+
// Docker: setup output comes back in the container log; a failed setup step exits 3 with a marker line.
|
|
825
|
+
// Without runner.setup the container runs its default install, which reports as step 1.
|
|
826
|
+
if (mode === 'docker') {
|
|
827
|
+
const m = /LOCALDOST_SETUP_FAILED (\d+) (\d+)/.exec(exec.output);
|
|
828
|
+
if (m) {
|
|
829
|
+
const step = setupSteps[Number(m[1]) - 1] ?? DEFAULT_INSTALL;
|
|
830
|
+
return withHint(notRunReport({ stage: 'setup', reason: `Setup step "${step}" exited with code ${m[2]}, so the tests did not run.`, exitCode: Number(m[2]) }, exec.output, commandLine));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
let parsed = null;
|
|
834
|
+
const reportFile = path.join(outDir, 'report.json');
|
|
835
|
+
const read = await adapter.readReport({ reportFile, stdout: exec.stdout });
|
|
836
|
+
const json = read?.json ?? null;
|
|
837
|
+
if (read)
|
|
838
|
+
parsed = read.parsed;
|
|
839
|
+
if (json && runId) {
|
|
840
|
+
try {
|
|
841
|
+
writePlaywrightObservations(runId, attempt, json, outDir);
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
/* observations are best effort */
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Name each test's screenshots, video, and trace. After the observations, which delete the traces of tests that passed.
|
|
848
|
+
if (json && parsed && runId) {
|
|
849
|
+
try {
|
|
850
|
+
const evidence = collectEvidence(json, {
|
|
851
|
+
outDir,
|
|
852
|
+
prefix: `a${attempt}`,
|
|
853
|
+
...(live ? { shots: live.shots(), storedName: (file) => live?.storedName(file) } : {}),
|
|
854
|
+
});
|
|
855
|
+
const tests = reportTests(json);
|
|
856
|
+
parsed.cases.forEach((c, i) => {
|
|
857
|
+
const e = evidence[i];
|
|
858
|
+
if (e)
|
|
859
|
+
c.evidence = e;
|
|
860
|
+
// The names the test gave its own steps become the steps to reproduce in a defect draft (KAT-377).
|
|
861
|
+
const named = c.status === 'failed' || c.status === 'timedOut' ? live?.stepNames(c.title, tests[i]?.project) : undefined;
|
|
862
|
+
if (named)
|
|
863
|
+
c.steps = named;
|
|
864
|
+
// Every test that ran keeps its step blocks, with the one that threw marked: the evidence pack pairs them with the screenshots.
|
|
865
|
+
const blocks = c.status === 'skipped' ? undefined : live?.namedSteps(c.title, tests[i]?.project);
|
|
866
|
+
if (blocks)
|
|
867
|
+
c.namedSteps = blocks;
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
/* evidence is best effort */
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// Playwright names a spec by its path under the testDir. A case of a Tests repo is named by its path in the repo, so a link to the spec
|
|
875
|
+
// and the file name a retry filters on point at the real file, whatever the testDir is.
|
|
876
|
+
if (spec.testRepo && parsed && worktree)
|
|
877
|
+
repoRelativeFiles(parsed.cases, json, worktree.root);
|
|
878
|
+
// A failed fill() prints the value it typed in Playwright's call log, so the login values are masked in the error text too.
|
|
879
|
+
if (parsed) {
|
|
880
|
+
for (const c of parsed.cases)
|
|
881
|
+
maskCase(c, mask);
|
|
882
|
+
parsed.errors = parsed.errors.map(mask);
|
|
883
|
+
}
|
|
884
|
+
const cmdTail = mask(stripAnsi(exec.output).slice(-4000));
|
|
885
|
+
const report = {
|
|
886
|
+
mode,
|
|
887
|
+
command: commandLine,
|
|
888
|
+
startedAt,
|
|
889
|
+
durationMs: Date.now() - started,
|
|
890
|
+
passed: parsed?.passed ?? 0,
|
|
891
|
+
failed: parsed?.failed ?? 0,
|
|
892
|
+
flaky: parsed?.flaky ?? 0,
|
|
893
|
+
skipped: parsed?.skipped ?? 0,
|
|
894
|
+
cases: parsed?.cases ?? [],
|
|
895
|
+
// The tail is shown on the run page and quoted in comments, so credentials in it are masked.
|
|
896
|
+
stdoutTail: mask(redact(setupOut && cmdTail.trim()
|
|
897
|
+
? `${stripAnsi(setupOut).slice(-1500)}\n--- test command ---\n${cmdTail}`
|
|
898
|
+
: setupOut
|
|
899
|
+
? stripAnsi(setupOut).slice(-4000)
|
|
900
|
+
: cmdTail)),
|
|
901
|
+
};
|
|
902
|
+
if (baseUrl)
|
|
903
|
+
report.baseUrl = baseUrl;
|
|
904
|
+
if (exec.timedOut)
|
|
905
|
+
report.interrupted = true;
|
|
906
|
+
if (setup)
|
|
907
|
+
report.setup = { steps: localSteps.length, cache: setup.cache, durationMs: setup.durationMs };
|
|
908
|
+
// No parsable report: the tests did not run, or ran without the JSON reporter. That says
|
|
909
|
+
// nothing about the product, so it is an environment problem, never a failed test.
|
|
910
|
+
if (!parsed) {
|
|
911
|
+
const missing = exec.code === -1 || /ENOENT|command not found/i.test(exec.output.slice(-1000));
|
|
912
|
+
report.notRun = {
|
|
913
|
+
stage: 'report',
|
|
914
|
+
reason: exec.timedOut
|
|
915
|
+
? `The test command timed out after ${Math.round(timeoutMs / 60_000)} min before it wrote a Playwright JSON report.`
|
|
916
|
+
: missing
|
|
917
|
+
? `The test command could not start (exit code ${exec.code}). Check that "${parsedCommand.argv[0]}" exists in the workdir, or add its install to Setup steps.`
|
|
918
|
+
: `The test command produced no Playwright JSON report (exit code ${exec.code}). Check that it runs Playwright and honours --reporter=json.`,
|
|
919
|
+
exitCode: exec.code,
|
|
920
|
+
};
|
|
921
|
+
return withHint(report);
|
|
922
|
+
}
|
|
923
|
+
// A crashed run with a report but no failed test must not read as a pass.
|
|
924
|
+
if (parsed && !exec.timedOut && report.failed === 0 && exec.code !== 0) {
|
|
925
|
+
const why = parsed.errors.join('\n') || cmdTail.slice(-2000) || `exit code ${exec.code}`;
|
|
926
|
+
report.cases.push({ title: 'Test run did not complete', file: '', status: 'failed', durationMs: 0, error: why, retries: 0 });
|
|
927
|
+
report.failed++;
|
|
928
|
+
}
|
|
929
|
+
return withHint(report);
|
|
930
|
+
}
|
|
931
|
+
finally {
|
|
932
|
+
try {
|
|
933
|
+
releaseRunPorts(outDir);
|
|
934
|
+
await live?.close().catch(() => { });
|
|
935
|
+
if (runId) {
|
|
936
|
+
try {
|
|
937
|
+
saveOutputs(runId, `a${attempt}`, outDir, live);
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
/* artifacts are best effort */
|
|
941
|
+
}
|
|
942
|
+
activeProcs.delete(runId);
|
|
943
|
+
cancelRequested.delete(runId);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
finally {
|
|
947
|
+
// The temp folders go even when a step above throws.
|
|
948
|
+
if (wrapperFile)
|
|
949
|
+
rmSync(wrapperFile, { force: true });
|
|
950
|
+
await rm(outDir, { recursive: true, force: true }).catch(() => { });
|
|
951
|
+
if (cloneDir)
|
|
952
|
+
await rm(cloneDir, { recursive: true, force: true }).catch(() => { });
|
|
953
|
+
await worktree?.cleanup();
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
export { playwrightVersion };
|
|
958
|
+
/**
|
|
959
|
+
* PORT and HOST for a test project that starts its own app (`process.env.PORT ?? 4400`). The localdost server's
|
|
960
|
+
* own values are not passed on, so they come from the app's address instead: a base URL on this machine
|
|
961
|
+
* with a port gives that port and host. A value the test command sets itself wins. Docker runs get none.
|
|
962
|
+
*/
|
|
963
|
+
export function appServerVars(cmdVars, mode, baseUrl) {
|
|
964
|
+
if (mode === 'docker' || !baseUrl)
|
|
965
|
+
return {};
|
|
966
|
+
let u;
|
|
967
|
+
try {
|
|
968
|
+
u = new URL(baseUrl);
|
|
969
|
+
}
|
|
970
|
+
catch {
|
|
971
|
+
return {};
|
|
972
|
+
}
|
|
973
|
+
const loopback = u.hostname === 'localhost' || u.hostname === '127.0.0.1' || u.hostname === '[::1]';
|
|
974
|
+
if (!loopback || !u.port)
|
|
975
|
+
return {};
|
|
976
|
+
const out = {};
|
|
977
|
+
if (cmdVars.PORT === undefined)
|
|
978
|
+
out.PORT = u.port;
|
|
979
|
+
if (cmdVars.HOST === undefined)
|
|
980
|
+
out.HOST = u.hostname;
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
/** Spawns one process in its own group, streams lines, and registers it for cancel. */
|
|
984
|
+
function spawnStreaming(o) {
|
|
985
|
+
return new Promise((resolve) => {
|
|
986
|
+
const child = spawnTrackedPiped(o.cmd, o.args, { cwd: o.cwd, env: o.vars, ...(o.runId ? { runId: o.runId } : {}) }); // own group, tracked for shutdown
|
|
987
|
+
child.stdin.on('error', () => undefined);
|
|
988
|
+
child.stdin.end();
|
|
989
|
+
let stdout = '';
|
|
990
|
+
let output = '';
|
|
991
|
+
let timedOut = false;
|
|
992
|
+
const keep = (s, d, max) => (s + d).slice(-max);
|
|
993
|
+
const outLines = lineSplitter((raw) => {
|
|
994
|
+
const line = o.mask ? o.mask(raw) : raw;
|
|
995
|
+
if (o.onOut(line))
|
|
996
|
+
return;
|
|
997
|
+
stdout = keep(stdout, `${line}\n`, 20_000_000);
|
|
998
|
+
output = keep(output, `${line}\n`, 64_000);
|
|
999
|
+
});
|
|
1000
|
+
const errLines = lineSplitter((raw) => {
|
|
1001
|
+
const line = o.mask ? o.mask(raw) : raw;
|
|
1002
|
+
if (o.onErr(line))
|
|
1003
|
+
return;
|
|
1004
|
+
output = keep(output, `${line}\n`, 64_000);
|
|
1005
|
+
});
|
|
1006
|
+
child.stdout.setEncoding('utf8');
|
|
1007
|
+
child.stderr.setEncoding('utf8');
|
|
1008
|
+
child.stdout.on('data', (d) => outLines.push(d));
|
|
1009
|
+
child.stderr.on('data', (d) => errLines.push(d));
|
|
1010
|
+
// Whole group: playwright plus browsers plus webServer.
|
|
1011
|
+
const stop = () => terminate(child, 5000);
|
|
1012
|
+
if (o.runId)
|
|
1013
|
+
activeProcs.set(o.runId, stop);
|
|
1014
|
+
const timer = setTimeout(() => {
|
|
1015
|
+
timedOut = true;
|
|
1016
|
+
stop();
|
|
1017
|
+
}, o.timeoutMs);
|
|
1018
|
+
child.on('error', (err) => {
|
|
1019
|
+
clearTimeout(timer);
|
|
1020
|
+
if (o.runId)
|
|
1021
|
+
activeProcs.delete(o.runId);
|
|
1022
|
+
resolve({ code: -1, timedOut, stdout, output: `${output}\n${err.message}`.trim(), error: err.message });
|
|
1023
|
+
});
|
|
1024
|
+
child.on('close', (code, signal) => {
|
|
1025
|
+
clearTimeout(timer);
|
|
1026
|
+
if (o.runId)
|
|
1027
|
+
activeProcs.delete(o.runId);
|
|
1028
|
+
outLines.end();
|
|
1029
|
+
errLines.end();
|
|
1030
|
+
resolve({ code, timedOut, stdout, output, signal });
|
|
1031
|
+
});
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
function runLocal(o) {
|
|
1035
|
+
// Local runs inherit the host env minus anything that looks like a credential.
|
|
1036
|
+
const vars = { ...hostVars(), ...o.adapter.env({ outDir: o.outDir, baseUrl: o.baseUrl, baseUrlEnv: o.baseUrlEnv }), ...(o.extraVars ?? {}) };
|
|
1037
|
+
const [cmd = 'npx', ...cmdArgs] = o.argv;
|
|
1038
|
+
// Markers go to the live session only; the JSON fallback and the stdout tail never see them.
|
|
1039
|
+
const onLine = (line) => {
|
|
1040
|
+
if (line.startsWith(MARKER)) {
|
|
1041
|
+
o.live?.line(line);
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
o.onLine?.(stripAnsi(line));
|
|
1045
|
+
return !!o.live?.line(line);
|
|
1046
|
+
};
|
|
1047
|
+
return spawnStreaming({
|
|
1048
|
+
cwd: o.workdir,
|
|
1049
|
+
cmd,
|
|
1050
|
+
args: [...cmdArgs, ...o.args],
|
|
1051
|
+
vars,
|
|
1052
|
+
timeoutMs: o.timeoutMs,
|
|
1053
|
+
runId: o.runId,
|
|
1054
|
+
onOut: onLine,
|
|
1055
|
+
onErr: onLine,
|
|
1056
|
+
mask: o.mask,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
/** One setup step through `sh -c`, with the same filtered environment as the test command. */
|
|
1060
|
+
/**
|
|
1061
|
+
* Two distinct free ports per local run (LOCALDOST_RUN_PORT, LOCALDOST_RUN_PORT_2), so suites that start
|
|
1062
|
+
* their own servers can run side by side: the queue runs two jobs at once. A value the test command
|
|
1063
|
+
* sets itself wins. Docker runs get none: each container has its own network namespace.
|
|
1064
|
+
*/
|
|
1065
|
+
const runPorts = new Map();
|
|
1066
|
+
export async function runPortVars(cmdVars, mode, runKey) {
|
|
1067
|
+
if (mode === 'docker')
|
|
1068
|
+
return {};
|
|
1069
|
+
const ports = await leasePorts(2);
|
|
1070
|
+
runPorts.set(runKey, ports);
|
|
1071
|
+
const [a, b] = ports;
|
|
1072
|
+
return { LOCALDOST_RUN_PORT: cmdVars.LOCALDOST_RUN_PORT ?? String(a), LOCALDOST_RUN_PORT_2: cmdVars.LOCALDOST_RUN_PORT_2 ?? String(b) };
|
|
1073
|
+
}
|
|
1074
|
+
/** Frees the ports a run leased; called when the run ends, whatever the outcome. */
|
|
1075
|
+
export function releaseRunPorts(runKey) {
|
|
1076
|
+
const ports = runPorts.get(runKey);
|
|
1077
|
+
if (ports)
|
|
1078
|
+
releasePorts(ports);
|
|
1079
|
+
runPorts.delete(runKey);
|
|
1080
|
+
}
|
|
1081
|
+
export async function runStep(o) {
|
|
1082
|
+
const vars = { ...hostVars(), CI: '1', FORCE_COLOR: '0', ...(o.vars ?? {}) };
|
|
1083
|
+
let printed = '';
|
|
1084
|
+
const onLine = (line) => {
|
|
1085
|
+
const text = stripAnsi(line).replace(/\s+$/, '');
|
|
1086
|
+
if (text) {
|
|
1087
|
+
printed = `${printed}${text}\n`.slice(-8000);
|
|
1088
|
+
o.onLine(text);
|
|
1089
|
+
}
|
|
1090
|
+
return true;
|
|
1091
|
+
};
|
|
1092
|
+
const r = await spawnStreaming({
|
|
1093
|
+
cwd: o.workdir,
|
|
1094
|
+
cmd: 'sh',
|
|
1095
|
+
args: ['-c', o.step],
|
|
1096
|
+
vars,
|
|
1097
|
+
timeoutMs: o.timeoutMs,
|
|
1098
|
+
runId: o.runId,
|
|
1099
|
+
onOut: onLine,
|
|
1100
|
+
onErr: onLine,
|
|
1101
|
+
mask: o.mask,
|
|
1102
|
+
});
|
|
1103
|
+
if (o.runId && (cancelRequested.has(o.runId) || isRunCancelled(o.runId)))
|
|
1104
|
+
return { code: r.code, timedOut: false, error: 'cancelled', output: printed };
|
|
1105
|
+
// A spawn error carries the real message (ENOENT for a missing workdir, EACCES, ...), never a bare "could not start".
|
|
1106
|
+
const error = r.error ? `could not start sh: ${r.error}` : undefined;
|
|
1107
|
+
return { code: r.code, timedOut: r.timedOut, signal: r.signal ?? null, output: printed, ...(error ? { error } : {}) };
|
|
1108
|
+
}
|
|
1109
|
+
export { DEFAULT_INSTALL };
|
|
1110
|
+
/**
|
|
1111
|
+
* Environment for the default install only. The image has the browsers, so the install must not download
|
|
1112
|
+
* them again. The image sets NODE_ENV=production, which would make npm skip devDependencies such as
|
|
1113
|
+
* @playwright/test, so the install asks for them.
|
|
1114
|
+
*/
|
|
1115
|
+
export const DEFAULT_INSTALL_ENV = { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1', NPM_CONFIG_INCLUDE: 'dev' };
|
|
1116
|
+
/**
|
|
1117
|
+
* The setup steps a local or demo run executes. A fresh clone (no runner.workdir) has no node_modules,
|
|
1118
|
+
* so without runner.setup it gets the default install, as long as it has a package.json. A configured
|
|
1119
|
+
* runner.workdir is the person's own checkout and already has its dependencies. Docker mode installs
|
|
1120
|
+
* inside the container (dockerScript), so it never gets a host-side step.
|
|
1121
|
+
*/
|
|
1122
|
+
export function localSetupSteps(o) {
|
|
1123
|
+
if (o.mode === 'docker')
|
|
1124
|
+
return { steps: [], defaultInstall: false };
|
|
1125
|
+
if (o.setupSteps.length)
|
|
1126
|
+
return { steps: o.setupSteps, defaultInstall: false };
|
|
1127
|
+
if (o.cloned && o.hasPackageJson)
|
|
1128
|
+
return { steps: [DEFAULT_INSTALL], defaultInstall: true };
|
|
1129
|
+
return { steps: [], defaultInstall: false };
|
|
1130
|
+
}
|
|
1131
|
+
const shq = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1132
|
+
/** The bash script the Docker container runs: copy the checkout, run setup (or the default install), run the test command. */
|
|
1133
|
+
export function dockerScript(o) {
|
|
1134
|
+
// The checkout is mounted read-only and copied into the container; only BASE_URL crosses over.
|
|
1135
|
+
// Repo-local secret and credential files stay behind, so test code cannot read them.
|
|
1136
|
+
const excludes = SECRET_FILES.map((p) => `--exclude=${shq(p)}`).join(' ');
|
|
1137
|
+
const setup = o.setupSteps.length
|
|
1138
|
+
? o.setupSteps.map((step, i) => `echo ${shq(`Setup ${i + 1}/${o.setupSteps.length}: $ ${step}`)}\nbash -c ${shq(step)} || { rc=$?; echo "LOCALDOST_SETUP_FAILED ${i + 1} $rc"; exit 3; }`)
|
|
1139
|
+
: [`(${DEFAULT_INSTALL}) > /out/install.log 2>&1 || { rc=$?; tail -50 /out/install.log; echo "LOCALDOST_SETUP_FAILED 1 $rc"; exit 3; }`];
|
|
1140
|
+
const assigns = Object.entries(o.cmdVars).map(([k, v]) => shq(`${k}=${v}`));
|
|
1141
|
+
const test = [...(assigns.length ? ['env', ...assigns] : []), ...o.argv.map(shQuote), ...o.args.map(shq)].join(' ');
|
|
1142
|
+
return [
|
|
1143
|
+
'set -e',
|
|
1144
|
+
'mkdir -p /work',
|
|
1145
|
+
`tar -C /src --exclude=./node_modules --exclude=./.git ${excludes} -cf - . | tar -C /work -xf -`,
|
|
1146
|
+
`cd ${shq(o.subdir ? `/work/${o.subdir}` : '/work')}`,
|
|
1147
|
+
...setup,
|
|
1148
|
+
test,
|
|
1149
|
+
].join('\n');
|
|
1150
|
+
}
|
|
1151
|
+
/** The whole environment of a test container: the report paths, BASE_URL, and the test login. No host variable crosses over. */
|
|
1152
|
+
export function dockerEnv(baseUrl, passVars = {}, adapter = adapterFor(), baseUrlEnv) {
|
|
1153
|
+
return {
|
|
1154
|
+
...passVars,
|
|
1155
|
+
...adapter.env({ outDir: '/out', baseUrl, baseUrlEnv }),
|
|
1156
|
+
PLAYWRIGHT_JSON_OUTPUT_FILE: '/out/report.json',
|
|
1157
|
+
PLAYWRIGHT_JSON_OUTPUT_NAME: '/out/report.json',
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
async function runDocker(o) {
|
|
1161
|
+
const { default: Docker } = await import('dockerode');
|
|
1162
|
+
const docker = new Docker();
|
|
1163
|
+
await ensureImage(docker, o.image);
|
|
1164
|
+
const script = dockerScript(o);
|
|
1165
|
+
const container = await docker.createContainer({
|
|
1166
|
+
Image: o.image,
|
|
1167
|
+
Cmd: ['bash', '-lc', script],
|
|
1168
|
+
Tty: true,
|
|
1169
|
+
Env: Object.entries(dockerEnv(o.baseUrl, o.passVars, o.adapter, o.baseUrlEnv)).map(([k, v]) => `${k}=${v}`),
|
|
1170
|
+
HostConfig: {
|
|
1171
|
+
Binds: [`${o.workdir}:/src:ro`, `${o.outDir}:/out`],
|
|
1172
|
+
IpcMode: 'host',
|
|
1173
|
+
Init: true,
|
|
1174
|
+
Memory: 4 * 1024 ** 3,
|
|
1175
|
+
NanoCpus: 2_000_000_000,
|
|
1176
|
+
ExtraHosts: ['host.docker.internal:host-gateway'],
|
|
1177
|
+
},
|
|
1178
|
+
});
|
|
1179
|
+
let timedOut = false;
|
|
1180
|
+
let code = null;
|
|
1181
|
+
try {
|
|
1182
|
+
await container.start();
|
|
1183
|
+
if (o.runId)
|
|
1184
|
+
activeProcs.set(o.runId, () => void container.kill().catch(() => { }));
|
|
1185
|
+
let timer;
|
|
1186
|
+
const timeout = new Promise((r) => {
|
|
1187
|
+
timer = setTimeout(() => r('timeout'), o.timeoutMs + (o.setupSteps.length ? SETUP_TIMEOUT_MS : 0));
|
|
1188
|
+
});
|
|
1189
|
+
const res = await Promise.race([container.wait(), timeout]);
|
|
1190
|
+
clearTimeout(timer);
|
|
1191
|
+
if (res === 'timeout') {
|
|
1192
|
+
timedOut = true;
|
|
1193
|
+
await container.kill().catch(() => { });
|
|
1194
|
+
}
|
|
1195
|
+
else {
|
|
1196
|
+
code = res.StatusCode;
|
|
1197
|
+
}
|
|
1198
|
+
const logs = (await container.logs({ stdout: true, stderr: true, follow: false }));
|
|
1199
|
+
const text = logs.toString('utf8');
|
|
1200
|
+
return { code, timedOut, stdout: text, output: text.slice(-64_000) };
|
|
1201
|
+
}
|
|
1202
|
+
finally {
|
|
1203
|
+
if (o.runId)
|
|
1204
|
+
activeProcs.delete(o.runId);
|
|
1205
|
+
await container.remove({ force: true }).catch(() => { });
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
async function ensureImage(docker, image) {
|
|
1209
|
+
try {
|
|
1210
|
+
await docker.getImage(image).inspect();
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
catch {
|
|
1214
|
+
/* not present locally */
|
|
1215
|
+
}
|
|
1216
|
+
const stream = await docker.pull(image);
|
|
1217
|
+
await new Promise((resolve, reject) => {
|
|
1218
|
+
docker.modem.followProgress(stream, (err) => (err ? reject(err) : resolve()));
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
//# sourceMappingURL=index.js.map
|