@jevitate/cli 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/ai-cli.js +40 -30
- package/dist/ai-cli.js.map +1 -1
- package/dist/author-journey-cli.d.ts +4 -0
- package/dist/author-journey-cli.js +109 -0
- package/dist/author-journey-cli.js.map +1 -0
- package/dist/bin.js +58077 -22454
- package/dist/bin.js.map +1 -1
- package/dist/browser-run-options.d.ts +58 -0
- package/dist/browser-run-options.js +106 -0
- package/dist/browser-run-options.js.map +1 -0
- package/dist/build-info.generated.d.ts +2 -0
- package/dist/build-info.generated.js +10 -0
- package/dist/build-info.generated.js.map +1 -0
- package/dist/check-api.d.ts +5 -0
- package/dist/check-api.js +233 -0
- package/dist/check-api.js.map +1 -0
- package/dist/check-budget.d.ts +26 -0
- package/dist/check-budget.js +89 -0
- package/dist/check-budget.js.map +1 -0
- package/dist/check-cli.d.ts +31 -0
- package/dist/check-cli.js +109 -0
- package/dist/check-cli.js.map +1 -0
- package/dist/check-execute.d.ts +25 -0
- package/dist/check-execute.js +385 -0
- package/dist/check-execute.js.map +1 -0
- package/dist/check-plan.d.ts +113 -0
- package/dist/check-plan.js +448 -0
- package/dist/check-plan.js.map +1 -0
- package/dist/check-suite.d.ts +171 -0
- package/dist/check-suite.js +515 -0
- package/dist/check-suite.js.map +1 -0
- package/dist/check-types.d.ts +181 -0
- package/dist/check-types.js +35 -0
- package/dist/check-types.js.map +1 -0
- package/dist/cli-args.d.ts +26 -0
- package/dist/cli-args.js +50 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-output.d.ts +102 -0
- package/dist/cli-output.js +516 -0
- package/dist/cli-output.js.map +1 -0
- package/dist/cli-reference.d.ts +3 -0
- package/dist/cli-reference.js +81 -0
- package/dist/cli-reference.js.map +1 -0
- package/dist/cli-refusal.d.ts +12 -0
- package/dist/cli-refusal.js +52 -0
- package/dist/cli-refusal.js.map +1 -0
- package/dist/cli-shared.d.ts +330 -0
- package/dist/cli-shared.js +492 -0
- package/dist/cli-shared.js.map +1 -0
- package/dist/cli-surface.d.ts +33 -0
- package/dist/cli-surface.js +48 -0
- package/dist/cli-surface.js.map +1 -0
- package/dist/conversation-options.d.ts +15 -0
- package/dist/conversation-options.js +10 -0
- package/dist/conversation-options.js.map +1 -0
- package/dist/credentials-file.d.ts +15 -0
- package/dist/credentials-file.js +56 -0
- package/dist/credentials-file.js.map +1 -0
- package/dist/data-dir.d.ts +1 -1
- package/dist/data-dir.js +1 -1
- package/dist/defect-evidence.d.ts +141 -0
- package/dist/defect-evidence.js +502 -0
- package/dist/defect-evidence.js.map +1 -0
- package/dist/demo-aspect-api.d.ts +209 -0
- package/dist/demo-aspect-api.js +397 -0
- package/dist/demo-aspect-api.js.map +1 -0
- package/dist/demo-aspect-cli.d.ts +3 -0
- package/dist/demo-aspect-cli.js +241 -0
- package/dist/demo-aspect-cli.js.map +1 -0
- package/dist/demo-capture.d.ts +108 -0
- package/dist/demo-capture.js +438 -0
- package/dist/demo-capture.js.map +1 -0
- package/dist/engine.d.ts +35 -0
- package/dist/engine.js +25 -0
- package/dist/engine.js.map +1 -0
- package/dist/environments.d.ts +155 -0
- package/dist/environments.js +364 -0
- package/dist/environments.js.map +1 -0
- package/dist/exit-codes.d.ts +46 -0
- package/dist/exit-codes.js +91 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/explore-adversarial.d.ts +137 -0
- package/dist/explore-adversarial.js +218 -0
- package/dist/explore-adversarial.js.map +1 -0
- package/dist/explore-api.d.ts +7 -187
- package/dist/explore-api.js +7 -301
- package/dist/explore-api.js.map +1 -1
- package/dist/explore-author.d.ts +79 -0
- package/dist/explore-author.js +86 -0
- package/dist/explore-author.js.map +1 -0
- package/dist/explore-cli.d.ts +7 -0
- package/dist/explore-cli.js +963 -0
- package/dist/explore-cli.js.map +1 -0
- package/dist/explore-coverage.d.ts +158 -0
- package/dist/explore-coverage.js +240 -0
- package/dist/explore-coverage.js.map +1 -0
- package/dist/explore-feature.d.ts +127 -0
- package/dist/explore-feature.js +220 -0
- package/dist/explore-feature.js.map +1 -0
- package/dist/explore-goal.d.ts +253 -0
- package/dist/explore-goal.js +374 -0
- package/dist/explore-goal.js.map +1 -0
- package/dist/explore-shared.d.ts +152 -0
- package/dist/explore-shared.js +129 -0
- package/dist/explore-shared.js.map +1 -0
- package/dist/explore-specs.d.ts +53 -0
- package/dist/explore-specs.js +333 -0
- package/dist/explore-specs.js.map +1 -0
- package/dist/findings-filing.d.ts +64 -0
- package/dist/findings-filing.js +108 -0
- package/dist/findings-filing.js.map +1 -0
- package/dist/fixture-auth.d.ts +52 -0
- package/dist/fixture-auth.js +82 -0
- package/dist/fixture-auth.js.map +1 -0
- package/dist/fixture-cli.d.ts +70 -0
- package/dist/fixture-cli.js +178 -0
- package/dist/fixture-cli.js.map +1 -0
- package/dist/github-issue-filer.d.ts +62 -0
- package/dist/github-issue-filer.js +188 -0
- package/dist/github-issue-filer.js.map +1 -0
- package/dist/host-health-run.d.ts +24 -0
- package/dist/host-health-run.js +27 -0
- package/dist/host-health-run.js.map +1 -0
- package/dist/http-5xx-outcome.d.ts +6 -0
- package/dist/http-5xx-outcome.js +39 -0
- package/dist/http-5xx-outcome.js.map +1 -0
- package/dist/inbox-cli.d.ts +7 -0
- package/dist/inbox-cli.js +197 -0
- package/dist/inbox-cli.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/init-cli.d.ts +4 -0
- package/dist/init-cli.js +127 -0
- package/dist/init-cli.js.map +1 -0
- package/dist/init-keys.d.ts +19 -2
- package/dist/init-keys.js +15 -4
- package/dist/init-keys.js.map +1 -1
- package/dist/init-next-steps.d.ts +35 -0
- package/dist/init-next-steps.js +79 -0
- package/dist/init-next-steps.js.map +1 -0
- package/dist/invariants-file.d.ts +34 -0
- package/dist/invariants-file.js +68 -0
- package/dist/invariants-file.js.map +1 -0
- package/dist/invariants-validate.d.ts +30 -0
- package/dist/invariants-validate.js +105 -0
- package/dist/invariants-validate.js.map +1 -0
- package/dist/journey-annotate-api.d.ts +76 -0
- package/dist/journey-annotate-api.js +256 -0
- package/dist/journey-annotate-api.js.map +1 -0
- package/dist/journey-annotate-cli.d.ts +9 -0
- package/dist/journey-annotate-cli.js +179 -0
- package/dist/journey-annotate-cli.js.map +1 -0
- package/dist/journey-api.d.ts +79 -2
- package/dist/journey-api.js +136 -19
- package/dist/journey-api.js.map +1 -1
- package/dist/journey-cli.d.ts +4 -0
- package/dist/journey-cli.js +370 -0
- package/dist/journey-cli.js.map +1 -0
- package/dist/journey-demo-api.d.ts +112 -0
- package/dist/journey-demo-api.js +392 -0
- package/dist/journey-demo-api.js.map +1 -0
- package/dist/journey-demo-cli.d.ts +10 -0
- package/dist/journey-demo-cli.js +155 -0
- package/dist/journey-demo-cli.js.map +1 -0
- package/dist/kill-signal.d.ts +193 -0
- package/dist/kill-signal.js +284 -0
- package/dist/kill-signal.js.map +1 -0
- package/dist/launch-armed.d.ts +37 -0
- package/dist/launch-armed.js +31 -0
- package/dist/launch-armed.js.map +1 -0
- package/dist/ledger-api.d.ts +106 -0
- package/dist/ledger-api.js +237 -0
- package/dist/ledger-api.js.map +1 -0
- package/dist/ledger-cli.d.ts +18 -0
- package/dist/ledger-cli.js +107 -0
- package/dist/ledger-cli.js.map +1 -0
- package/dist/load-api.d.ts +29 -11
- package/dist/load-api.js +23 -15
- package/dist/load-api.js.map +1 -1
- package/dist/load-cli.d.ts +4 -0
- package/dist/load-cli.js +97 -0
- package/dist/load-cli.js.map +1 -0
- package/dist/log-correlation.d.ts +147 -0
- package/dist/log-correlation.js +325 -0
- package/dist/log-correlation.js.map +1 -0
- package/dist/log-lines.d.ts +131 -0
- package/dist/log-lines.js +389 -0
- package/dist/log-lines.js.map +1 -0
- package/dist/log-sources.d.ts +71 -0
- package/dist/log-sources.js +235 -0
- package/dist/log-sources.js.map +1 -0
- package/dist/logs-cli.d.ts +3 -0
- package/dist/logs-cli.js +51 -0
- package/dist/logs-cli.js.map +1 -0
- package/dist/logs-retention.d.ts +28 -0
- package/dist/logs-retention.js +84 -0
- package/dist/logs-retention.js.map +1 -0
- package/dist/mcp-api.d.ts +89 -1
- package/dist/mcp-api.js +472 -18
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-args.d.ts +38 -0
- package/dist/mcp-args.js +131 -0
- package/dist/mcp-args.js.map +1 -0
- package/dist/mcp-cli-bridge.d.ts +34 -0
- package/dist/mcp-cli-bridge.js +62 -0
- package/dist/mcp-cli-bridge.js.map +1 -0
- package/dist/mcp-cli-runner.d.ts +29 -0
- package/dist/mcp-cli-runner.js +44 -0
- package/dist/mcp-cli-runner.js.map +1 -0
- package/dist/mcp-cli-tools.d.ts +125 -0
- package/dist/mcp-cli-tools.js +680 -0
- package/dist/mcp-cli-tools.js.map +1 -0
- package/dist/mcp-paths.d.ts +28 -0
- package/dist/mcp-paths.js +76 -0
- package/dist/mcp-paths.js.map +1 -0
- package/dist/mission-actors.d.ts +40 -0
- package/dist/mission-actors.js +72 -0
- package/dist/mission-actors.js.map +1 -0
- package/dist/mission-api.d.ts +35 -0
- package/dist/mission-api.js +58 -2
- package/dist/mission-api.js.map +1 -1
- package/dist/mission-cli.d.ts +4 -0
- package/dist/mission-cli.js +247 -0
- package/dist/mission-cli.js.map +1 -0
- package/dist/mission-exit.d.ts +22 -0
- package/dist/mission-exit.js +26 -0
- package/dist/mission-exit.js.map +1 -0
- package/dist/mission-fixtures.d.ts +225 -0
- package/dist/mission-fixtures.js +828 -0
- package/dist/mission-fixtures.js.map +1 -0
- package/dist/mission-journal.d.ts +60 -0
- package/dist/mission-journal.js +115 -0
- package/dist/mission-journal.js.map +1 -0
- package/dist/mission-queue-cli.d.ts +10 -0
- package/dist/mission-queue-cli.js +198 -0
- package/dist/mission-queue-cli.js.map +1 -0
- package/dist/mission-queue-runner.d.ts +126 -0
- package/dist/mission-queue-runner.js +397 -0
- package/dist/mission-queue-runner.js.map +1 -0
- package/dist/multi-run-cli.d.ts +37 -0
- package/dist/multi-run-cli.js +110 -0
- package/dist/multi-run-cli.js.map +1 -0
- package/dist/multi-run.d.ts +339 -0
- package/dist/multi-run.js +619 -0
- package/dist/multi-run.js.map +1 -0
- package/dist/openrouter-call.d.ts +22 -0
- package/dist/openrouter-call.js +63 -0
- package/dist/openrouter-call.js.map +1 -0
- package/dist/profile-cli.d.ts +4 -0
- package/dist/profile-cli.js +57 -0
- package/dist/profile-cli.js.map +1 -0
- package/dist/program.d.ts +3 -75
- package/dist/program.js +119 -1921
- package/dist/program.js.map +1 -1
- package/dist/project-dir.d.ts +96 -0
- package/dist/project-dir.js +220 -0
- package/dist/project-dir.js.map +1 -0
- package/dist/record-api.d.ts +10 -6
- package/dist/record-api.js +39 -28
- package/dist/record-api.js.map +1 -1
- package/dist/record-cli.d.ts +4 -0
- package/dist/record-cli.js +64 -0
- package/dist/record-cli.js.map +1 -0
- package/dist/recording-cli.d.ts +4 -0
- package/dist/recording-cli.js +248 -0
- package/dist/recording-cli.js.map +1 -0
- package/dist/regression-api.d.ts +101 -0
- package/dist/regression-api.js +384 -5
- package/dist/regression-api.js.map +1 -1
- package/dist/regression-cli.d.ts +4 -0
- package/dist/regression-cli.js +232 -0
- package/dist/regression-cli.js.map +1 -0
- package/dist/report-api.d.ts +146 -0
- package/dist/report-api.js +430 -0
- package/dist/report-api.js.map +1 -0
- package/dist/report-cli.d.ts +12 -0
- package/dist/report-cli.js +147 -0
- package/dist/report-cli.js.map +1 -0
- package/dist/result-schema.d.ts +22 -0
- package/dist/result-schema.js +28 -0
- package/dist/result-schema.js.map +1 -0
- package/dist/run-index.d.ts +29 -0
- package/dist/run-index.js +70 -0
- package/dist/run-index.js.map +1 -0
- package/dist/run-screenshots.d.ts +123 -0
- package/dist/run-screenshots.js +229 -0
- package/dist/run-screenshots.js.map +1 -0
- package/dist/runtime-env.d.ts +8 -0
- package/dist/runtime-env.js +22 -0
- package/dist/runtime-env.js.map +1 -0
- package/dist/secret-args.d.ts +23 -0
- package/dist/secret-args.js +45 -0
- package/dist/secret-args.js.map +1 -0
- package/dist/serve-cli.d.ts +4 -0
- package/dist/serve-cli.js +95 -0
- package/dist/serve-cli.js.map +1 -0
- package/dist/server-log-verify.d.ts +34 -0
- package/dist/server-log-verify.js +115 -0
- package/dist/server-log-verify.js.map +1 -0
- package/dist/session-check.d.ts +14 -0
- package/dist/session-check.js +44 -0
- package/dist/session-check.js.map +1 -0
- package/dist/site-cli.d.ts +4 -0
- package/dist/site-cli.js +127 -0
- package/dist/site-cli.js.map +1 -0
- package/dist/site-gate-cli.d.ts +28 -0
- package/dist/site-gate-cli.js +58 -0
- package/dist/site-gate-cli.js.map +1 -0
- package/dist/source-api.js +18 -3
- package/dist/source-api.js.map +1 -1
- package/dist/source-cli.d.ts +4 -0
- package/dist/source-cli.js +284 -0
- package/dist/source-cli.js.map +1 -0
- package/dist/source-run-api.d.ts +27 -1
- package/dist/source-run-api.js +22 -11
- package/dist/source-run-api.js.map +1 -1
- package/dist/storage-state-snapshot.d.ts +50 -0
- package/dist/storage-state-snapshot.js +87 -0
- package/dist/storage-state-snapshot.js.map +1 -0
- package/dist/suite-explore-options.d.ts +289 -0
- package/dist/suite-explore-options.js +140 -0
- package/dist/suite-explore-options.js.map +1 -0
- package/dist/target-config.d.ts +109 -0
- package/dist/target-config.js +216 -0
- package/dist/target-config.js.map +1 -0
- package/dist/transcript-file.d.ts +12 -0
- package/dist/transcript-file.js +23 -0
- package/dist/transcript-file.js.map +1 -0
- package/dist/ui-api.d.ts +3 -0
- package/dist/ui-api.js +7 -1
- package/dist/ui-api.js.map +1 -1
- package/dist/usability-capture.d.ts +43 -0
- package/dist/usability-capture.js +326 -0
- package/dist/usability-capture.js.map +1 -0
- package/dist/usage-config.d.ts +36 -0
- package/dist/usage-config.js +114 -0
- package/dist/usage-config.js.map +1 -0
- package/dist/ux-api.d.ts +3 -75
- package/dist/ux-api.js +3 -220
- package/dist/ux-api.js.map +1 -1
- package/dist/ux-cli.d.ts +4 -0
- package/dist/ux-cli.js +102 -0
- package/dist/ux-cli.js.map +1 -0
- package/dist/ux-config.d.ts +29 -0
- package/dist/ux-config.js +103 -0
- package/dist/ux-config.js.map +1 -0
- package/dist/ux-evidence.d.ts +54 -0
- package/dist/ux-evidence.js +197 -0
- package/dist/ux-evidence.js.map +1 -0
- package/dist/ux-review.d.ts +112 -0
- package/dist/ux-review.js +151 -0
- package/dist/ux-review.js.map +1 -0
- package/dist/ux-usability.d.ts +238 -0
- package/dist/ux-usability.js +494 -0
- package/dist/ux-usability.js.map +1 -0
- package/dist/verify-fix-api.d.ts +173 -0
- package/dist/verify-fix-api.js +454 -0
- package/dist/verify-fix-api.js.map +1 -0
- package/dist/verify-fix-cli.d.ts +4 -0
- package/dist/verify-fix-cli.js +110 -0
- package/dist/verify-fix-cli.js.map +1 -0
- package/package.json +19 -14
- package/skills/jevitate-ci-check/SKILL.md +77 -0
- package/skills/jevitate-demo/SKILL.md +61 -0
- package/skills/jevitate-explore/SKILL.md +88 -87
- package/skills/jevitate-getting-started/SKILL.md +88 -0
- package/skills/jevitate-load-test/SKILL.md +11 -2
- package/skills/jevitate-mission-scope/SKILL.md +19 -11
- package/skills/jevitate-record/SKILL.md +23 -5
- package/skills/jevitate-run-journey/SKILL.md +61 -17
- package/skills/jevitate-sources/SKILL.md +57 -14
- package/skills/jevitate-ux-review/SKILL.md +106 -16
- package/skills/jevitate-verify-fix/SKILL.md +76 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { type TargetConfig } from "./target-config.js";
|
|
2
|
+
import { type InvariantSpec, type Recording } from "@jevitate/recording";
|
|
3
|
+
import { type FixtureFlags } from "./fixture-cli.js";
|
|
4
|
+
import { type FixtureRecord } from "./mission-fixtures.js";
|
|
5
|
+
import { type BrowserPort, type EmulationSpec } from "@jevitate/playwright";
|
|
6
|
+
import { type BrowserRunOptions } from "./browser-run-options.js";
|
|
7
|
+
import { type HangSignal, type VerifyFixResult, type VerifyFixVerdict } from "@jevitate/explore";
|
|
8
|
+
import { type DefectEvidence } from "./defect-evidence.js";
|
|
9
|
+
import { type ScreenshotsResult, type ScreenshotsSpec } from "./run-screenshots.js";
|
|
10
|
+
/**
|
|
11
|
+
* The programmatic surface behind `jevitate verify-fix` and the MCP `verify_fix` tool: loads a
|
|
12
|
+
* finished mission's persisted typed result (`<stem>.result.json`), finds the defect by
|
|
13
|
+
* fingerprint, and replays its reproduction in a FRESH browser session (`@jevitate/explore`'s
|
|
14
|
+
* `verifyFix`, which reuses the Recording interpreter). The replay is authorized against the
|
|
15
|
+
* mission's own allowlist before any browser opens.
|
|
16
|
+
*
|
|
17
|
+
* Exit codes: 0 fixed · 1 still reproduces · 2 inconclusive (the replay could not reach the
|
|
18
|
+
* defect's step, or the input was unusable) · 4 intermittent (the signal fired on some but not
|
|
19
|
+
* all fresh-context replays — #74; mirrors the mission `intermittent` hang outcome) — a broken or
|
|
20
|
+
* flaky check never reads as "fixed".
|
|
21
|
+
*/
|
|
22
|
+
export declare const VERIFY_FIX_EXIT_CODES: Readonly<Record<VerifyFixVerdict, number>>;
|
|
23
|
+
export interface RunVerifyFixOptions {
|
|
24
|
+
/** Path of the mission's `<stem>.result.json`. */
|
|
25
|
+
readonly resultPath: string;
|
|
26
|
+
/** The defect (or hang) fingerprint to verify. */
|
|
27
|
+
readonly fingerprint: string;
|
|
28
|
+
/** Overrides the storageState recorded with the mission (CLI `--storage-state`). */
|
|
29
|
+
readonly storageState?: string;
|
|
30
|
+
readonly browserPortFactory?: () => BrowserPort;
|
|
31
|
+
/** How Chromium is launched and shown (#245: `--headed`/`--slow-mo`/`--record-video`). Default: headless. */
|
|
32
|
+
readonly browser?: BrowserRunOptions;
|
|
33
|
+
/** Settle ceiling after the replay (ms). */
|
|
34
|
+
readonly settleCeilingMs?: number;
|
|
35
|
+
/** Per-target settle/hang configuration, keyed by origin (`~/.jevitate/targets.json`). */
|
|
36
|
+
readonly targets?: Readonly<Record<string, TargetConfig>>;
|
|
37
|
+
/** Fresh-context replays for a non-hang defect signal (#74, CLI `--replays`). Default 3. */
|
|
38
|
+
readonly replays?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Invariant files (CLI `--invariants`, #86) to re-check a declared-invariant defect with, instead of
|
|
41
|
+
* the spec persisted with the mission. Validated against the MISSION's allowlist before any replay.
|
|
42
|
+
*/
|
|
43
|
+
readonly invariantFiles?: readonly string[];
|
|
44
|
+
/** Re-checking a `server-log` defect whose sources include a `cmd:` one needs this too (#142). */
|
|
45
|
+
readonly allowLogCmd?: boolean;
|
|
46
|
+
/** `--hang-replay-writes` (#153): a hang's replay may re-send a paid/destructive write. */
|
|
47
|
+
readonly hangReplayWrites?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Mission fixtures (#140/#144): every replay restores + re-runs the mission's own fixture (saved
|
|
50
|
+
* with its result) so it starts from the same state. `fixtures` overrides the saved spec; the
|
|
51
|
+
* mission's shell hooks are never replayed from the file — the operator re-supplies the SAME
|
|
52
|
+
* `before`/`after` (checked by hash) with `allowShellHooks`.
|
|
53
|
+
*/
|
|
54
|
+
readonly fixtureFlags?: FixtureFlags;
|
|
55
|
+
/** Values redacted from everything the fixture logs (CLI `--secret`). */
|
|
56
|
+
readonly secrets?: readonly string[];
|
|
57
|
+
/**
|
|
58
|
+
* Explicit `--viewport` / `--device` override (#149). Defaults to the finding's OWN recorded
|
|
59
|
+
* emulation (`recording.emulation`) — a 375px defect replays at 375px, not the caller's desktop
|
|
60
|
+
* default. An override that DIFFERS from the recorded emulation is refused (fails closed) unless
|
|
61
|
+
* `allowEmulationOverride` is set: replaying at the wrong device could report a false "fixed".
|
|
62
|
+
*/
|
|
63
|
+
readonly emulation?: EmulationSpec;
|
|
64
|
+
/** Replay at `emulation` even though it differs from the finding's recorded emulation. */
|
|
65
|
+
readonly allowEmulationOverride?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* #251 `--screenshots`: masked screenshots of the captioned "after" replay (one per distinct
|
|
68
|
+
* screen, or per step) + `index.md`.
|
|
69
|
+
*/
|
|
70
|
+
readonly screenshots?: ScreenshotsSpec;
|
|
71
|
+
/** #250: caption time per step in the "after" clip (ms). Default `EVIDENCE_PACE_MS`. */
|
|
72
|
+
readonly evidencePaceMs?: number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* #250 — `verify-fix --record-video`: a before/after pair for the PR. `before` is the run's own
|
|
76
|
+
* evidence clip of the defect (`--evidence-video` on the original run), `after` a captioned replay
|
|
77
|
+
* of the same steps now (still reproducing, or passing), its final card the verdict.
|
|
78
|
+
*/
|
|
79
|
+
export interface VerifyFixEvidence {
|
|
80
|
+
readonly before?: {
|
|
81
|
+
readonly videoPath?: string;
|
|
82
|
+
readonly screenshots: string[];
|
|
83
|
+
};
|
|
84
|
+
/** Why there is no `before` (the run recorded no evidence clip for this defect). */
|
|
85
|
+
readonly beforeMissing?: string;
|
|
86
|
+
readonly after: DefectEvidence;
|
|
87
|
+
}
|
|
88
|
+
export interface VerifyFixReport extends VerifyFixResult {
|
|
89
|
+
readonly exitCode: number;
|
|
90
|
+
readonly title?: string;
|
|
91
|
+
/** The fixture every replay started from (#140/#144): the mission's identity and this run's setup/restore log. */
|
|
92
|
+
readonly fixtures?: FixtureRecord & {
|
|
93
|
+
readonly missionIdentity: string;
|
|
94
|
+
};
|
|
95
|
+
/** #245: `--record-video` files of every replay session (each closed, so finalized). */
|
|
96
|
+
readonly videoPaths?: string[];
|
|
97
|
+
/** #250: with `--record-video` (or `--screenshots`), the before/after evidence pair. */
|
|
98
|
+
readonly evidence?: VerifyFixEvidence;
|
|
99
|
+
/** #251: `--screenshots` of the captioned after-replay. */
|
|
100
|
+
readonly screenshotPaths?: string[];
|
|
101
|
+
readonly screenshotIndex?: string;
|
|
102
|
+
readonly screenshotsSkipped?: ScreenshotsResult["screenshotsSkipped"];
|
|
103
|
+
}
|
|
104
|
+
export declare class VerifyFixInputError extends Error {
|
|
105
|
+
readonly code: "E_VERIFY_FIX_INPUT";
|
|
106
|
+
constructor(message: string);
|
|
107
|
+
}
|
|
108
|
+
export interface PersistedFinding {
|
|
109
|
+
readonly fingerprint: string;
|
|
110
|
+
readonly related?: readonly string[];
|
|
111
|
+
readonly kind: string;
|
|
112
|
+
readonly title?: string;
|
|
113
|
+
readonly repro: {
|
|
114
|
+
readonly recordingStepIndex: number;
|
|
115
|
+
};
|
|
116
|
+
/** For a hang finding: its signal (what the replay must no longer show). */
|
|
117
|
+
readonly hang?: HangSignal;
|
|
118
|
+
/** The finding's own Recording (found after a reset / on a coverage path), when it has one. */
|
|
119
|
+
readonly recording?: Recording;
|
|
120
|
+
/** How many times the mission's OWN run hit this same finding (its `occurrences`), when recorded. */
|
|
121
|
+
readonly occurrences?: number;
|
|
122
|
+
/** For a declared-invariant defect (#86): the invariant id to re-check. */
|
|
123
|
+
readonly invariantId?: string;
|
|
124
|
+
/** For a `server-log` defect (#142): what to re-tail and match, from the defect's own `serverLog`. */
|
|
125
|
+
readonly serverLog?: {
|
|
126
|
+
readonly sources: readonly string[];
|
|
127
|
+
readonly matcher: string;
|
|
128
|
+
readonly normalizedMessage: string;
|
|
129
|
+
readonly drainMs: number;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** #147: a persisted actor — its name, role and storageState PATH (never its contents). */
|
|
133
|
+
export interface PersistedActor {
|
|
134
|
+
readonly name: string;
|
|
135
|
+
readonly storageStatePath: string;
|
|
136
|
+
readonly role: "primary" | "observer";
|
|
137
|
+
}
|
|
138
|
+
export interface PersistedMission {
|
|
139
|
+
readonly recording: Recording | null;
|
|
140
|
+
readonly target: {
|
|
141
|
+
readonly seedUrl: string;
|
|
142
|
+
readonly allowlist: string[];
|
|
143
|
+
readonly storageStatePath?: string;
|
|
144
|
+
/** #147: the mission's actors (`--actor`), when it had any. */
|
|
145
|
+
readonly actors?: readonly PersistedActor[];
|
|
146
|
+
};
|
|
147
|
+
readonly findings: PersistedFinding[];
|
|
148
|
+
/** The declared-invariant spec the mission evaluated (#86), when it had one and it still validates. */
|
|
149
|
+
readonly invariantSpec?: InvariantSpec;
|
|
150
|
+
/** The mission's fixture (#140/#144): spec (re-validated before use), hook hashes and recorded outputs. */
|
|
151
|
+
readonly fixtures?: PersistedMissionFixtures;
|
|
152
|
+
}
|
|
153
|
+
interface PersistedMissionFixtures {
|
|
154
|
+
readonly identity: string;
|
|
155
|
+
readonly spec?: unknown;
|
|
156
|
+
readonly hooks: {
|
|
157
|
+
readonly before?: string;
|
|
158
|
+
readonly after?: string;
|
|
159
|
+
};
|
|
160
|
+
readonly outputs: Readonly<Record<string, string>>;
|
|
161
|
+
}
|
|
162
|
+
/** Parses a persisted mission result, failing closed on anything it cannot trust. */
|
|
163
|
+
export declare function parsePersistedMission(raw: unknown): PersistedMission;
|
|
164
|
+
/**
|
|
165
|
+
* The one place a mission's findings (declared-invariant defects, hangs, adversarial defects) are
|
|
166
|
+
* looked up by fingerprint — a fingerprint the CALLER supplied may equal the finding's own, or one
|
|
167
|
+
* of its `related` fingerprints (the same underlying defect observed at a different point). Shared
|
|
168
|
+
* by `verify-fix` and `regression capture` (#119/#129): the latter reuses this lookup rather than
|
|
169
|
+
* re-implementing "is this fingerprint a defect this mission found" itself.
|
|
170
|
+
*/
|
|
171
|
+
export declare function findFinding(mission: PersistedMission, fingerprint: string): PersistedFinding | undefined;
|
|
172
|
+
export declare function runVerifyFix(opts: RunVerifyFixOptions): Promise<VerifyFixReport>;
|
|
173
|
+
export {};
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolveTargetConfig } from "./target-config.js";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { InvariantSpecSchema, RecordingSchema, validateInvariantSpec } from "@jevitate/recording";
|
|
5
|
+
import { loadInvariantFiles, resolveInvariantAuthTokens } from "./invariants-file.js";
|
|
6
|
+
import { buildMissionFixtures } from "./fixture-cli.js";
|
|
7
|
+
import { observerSessions } from "./mission-actors.js";
|
|
8
|
+
import { FixtureSpecError, fixtureReplayOpener, hookHash, loadFixtureFile, parseFixtureSpec, secretFieldNames, } from "./mission-fixtures.js";
|
|
9
|
+
import { PlaywrightBrowserPort, resolveEmulation } from "@jevitate/playwright";
|
|
10
|
+
import { dirname as dirnameOf, join as joinPath, resolve as resolveFile } from "node:path";
|
|
11
|
+
import { listVideos, runVideoDir, sessionLaunchOptions } from "./browser-run-options.js";
|
|
12
|
+
import { artifactStamp } from "./mission-journal.js";
|
|
13
|
+
import { BrowseTheWeb, CastActor } from "@jevitate/screenplay";
|
|
14
|
+
import { assertAuthorizedExploreTarget, verifyFix, } from "@jevitate/explore";
|
|
15
|
+
import { verifyServerLogDefect } from "./server-log-verify.js";
|
|
16
|
+
import { defectSignalText, persistedDefect, replayWithEvidence, signalCheckable } from "./defect-evidence.js";
|
|
17
|
+
import { RunScreenshots, screenshotsDirFor } from "./run-screenshots.js";
|
|
18
|
+
/**
|
|
19
|
+
* The programmatic surface behind `jevitate verify-fix` and the MCP `verify_fix` tool: loads a
|
|
20
|
+
* finished mission's persisted typed result (`<stem>.result.json`), finds the defect by
|
|
21
|
+
* fingerprint, and replays its reproduction in a FRESH browser session (`@jevitate/explore`'s
|
|
22
|
+
* `verifyFix`, which reuses the Recording interpreter). The replay is authorized against the
|
|
23
|
+
* mission's own allowlist before any browser opens.
|
|
24
|
+
*
|
|
25
|
+
* Exit codes: 0 fixed · 1 still reproduces · 2 inconclusive (the replay could not reach the
|
|
26
|
+
* defect's step, or the input was unusable) · 4 intermittent (the signal fired on some but not
|
|
27
|
+
* all fresh-context replays — #74; mirrors the mission `intermittent` hang outcome) — a broken or
|
|
28
|
+
* flaky check never reads as "fixed".
|
|
29
|
+
*/
|
|
30
|
+
export const VERIFY_FIX_EXIT_CODES = {
|
|
31
|
+
fixed: 0,
|
|
32
|
+
"still-reproduces": 1,
|
|
33
|
+
inconclusive: 2,
|
|
34
|
+
intermittent: 4,
|
|
35
|
+
};
|
|
36
|
+
export class VerifyFixInputError extends Error {
|
|
37
|
+
code = "E_VERIFY_FIX_INPUT";
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "VerifyFixInputError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const HANG_KINDS = new Set(["main-thread-unresponsive", "request-pending", "never-settled", "ui-no-progress"]);
|
|
44
|
+
/** A persisted hang signal, validated just enough to re-check it (fail closed otherwise). */
|
|
45
|
+
function asHangSignal(v) {
|
|
46
|
+
if (!isRecord(v) || typeof v.kind !== "string" || !HANG_KINDS.has(v.kind))
|
|
47
|
+
return null;
|
|
48
|
+
const last = v.lastState;
|
|
49
|
+
if (!isRecord(last) || typeof last.signature !== "string" || !Array.isArray(last.controls))
|
|
50
|
+
return null;
|
|
51
|
+
if (typeof v.detail !== "string" || typeof v.route !== "string" || typeof v.url !== "string" || !Array.isArray(v.pending)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
function asPersistedFixtures(v) {
|
|
57
|
+
if (!isRecord(v) || typeof v.identity !== "string")
|
|
58
|
+
return undefined;
|
|
59
|
+
const hooks = isRecord(v.hooks) ? v.hooks : {};
|
|
60
|
+
const outputs = isRecord(v.outputs) ? v.outputs : {};
|
|
61
|
+
return {
|
|
62
|
+
identity: v.identity,
|
|
63
|
+
...(v.spec === undefined ? {} : { spec: v.spec }),
|
|
64
|
+
hooks: {
|
|
65
|
+
...(typeof hooks.before === "string" ? { before: hooks.before } : {}),
|
|
66
|
+
...(typeof hooks.after === "string" ? { after: hooks.after } : {}),
|
|
67
|
+
},
|
|
68
|
+
outputs: Object.fromEntries(Object.entries(outputs).filter((e) => typeof e[1] === "string")),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function isRecord(v) {
|
|
72
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
73
|
+
}
|
|
74
|
+
/** A persisted `server-log` defect's re-check spec (#142), validated just enough to replay it. */
|
|
75
|
+
function asServerLog(v) {
|
|
76
|
+
if (!isRecord(v))
|
|
77
|
+
return null;
|
|
78
|
+
if (!Array.isArray(v.sources) || !v.sources.every((s) => typeof s === "string"))
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof v.matcher !== "string" || typeof v.normalizedMessage !== "string" || typeof v.drainMs !== "number")
|
|
81
|
+
return null;
|
|
82
|
+
return { sources: v.sources, matcher: v.matcher, normalizedMessage: v.normalizedMessage, drainMs: v.drainMs };
|
|
83
|
+
}
|
|
84
|
+
function asFinding(v) {
|
|
85
|
+
if (!isRecord(v) || typeof v.fingerprint !== "string" || typeof v.kind !== "string")
|
|
86
|
+
return null;
|
|
87
|
+
const repro = v.repro;
|
|
88
|
+
if (!isRecord(repro) || typeof repro.recordingStepIndex !== "number")
|
|
89
|
+
return null;
|
|
90
|
+
const hang = v.kind === "hang" ? asHangSignal(v.signal) : null;
|
|
91
|
+
if (v.kind === "hang" && hang === null)
|
|
92
|
+
return null;
|
|
93
|
+
const serverLog = v.kind === "server-log" ? asServerLog(v.serverLog) : null;
|
|
94
|
+
if (v.kind === "server-log" && serverLog === null)
|
|
95
|
+
return null;
|
|
96
|
+
const own = repro.recording === undefined ? undefined : RecordingSchema.safeParse(repro.recording);
|
|
97
|
+
if (own !== undefined && !own.success)
|
|
98
|
+
return null; // a finding whose repro cannot be trusted is skipped
|
|
99
|
+
return {
|
|
100
|
+
...(own?.success === true ? { recording: own.data } : {}),
|
|
101
|
+
fingerprint: v.fingerprint,
|
|
102
|
+
kind: v.kind,
|
|
103
|
+
repro: { recordingStepIndex: repro.recordingStepIndex },
|
|
104
|
+
...(hang === null ? {} : { hang }),
|
|
105
|
+
...(serverLog === null ? {} : { serverLog }),
|
|
106
|
+
...(Array.isArray(v.related) ? { related: v.related.filter((r) => typeof r === "string") } : {}),
|
|
107
|
+
...(typeof v.title === "string" ? { title: v.title } : {}),
|
|
108
|
+
...(typeof v.occurrences === "number" ? { occurrences: v.occurrences } : {}),
|
|
109
|
+
...(v.kind === "invariant" && isRecord(v.invariant) && typeof v.invariant.id === "string" ? { invariantId: v.invariant.id } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Parses a persisted mission result, failing closed on anything it cannot trust. */
|
|
113
|
+
export function parsePersistedMission(raw) {
|
|
114
|
+
if (!isRecord(raw) || !isRecord(raw.result))
|
|
115
|
+
throw new VerifyFixInputError("not a mission result file");
|
|
116
|
+
const result = raw.result;
|
|
117
|
+
const target = result.target;
|
|
118
|
+
if (!isRecord(target) || typeof target.seedUrl !== "string" || !Array.isArray(target.allowlist)) {
|
|
119
|
+
throw new VerifyFixInputError("the mission result has no replay target (seedUrl/allowlist)");
|
|
120
|
+
}
|
|
121
|
+
// A run's own Recording (a coverage run has none; its findings carry their path).
|
|
122
|
+
const recording = result.recording === null || result.recording === undefined ? null : RecordingSchema.parse(result.recording);
|
|
123
|
+
const findings = [];
|
|
124
|
+
// `defects` holds every defect (#195); `serverLogDefects` is its deprecated server-log alias (and
|
|
125
|
+
// the only list a pre-#195 result has) — each fingerprint is taken once.
|
|
126
|
+
for (const list of [result.defects, result.hangs, result.serverLogDefects]) {
|
|
127
|
+
if (!Array.isArray(list))
|
|
128
|
+
continue;
|
|
129
|
+
for (const item of list) {
|
|
130
|
+
const f = asFinding(item);
|
|
131
|
+
if (f !== null && !findings.some((g) => g.fingerprint === f.fingerprint))
|
|
132
|
+
findings.push(f);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// A persisted spec that no longer validates is dropped: its defects are then inconclusive, never fixed.
|
|
136
|
+
const spec = result.invariantSpec === undefined ? undefined : InvariantSpecSchema.safeParse(result.invariantSpec);
|
|
137
|
+
const fixtures = asPersistedFixtures(result.fixtures);
|
|
138
|
+
const actors = Array.isArray(target.actors)
|
|
139
|
+
? target.actors.flatMap((a) => isRecord(a) && typeof a.name === "string" && typeof a.storageStatePath === "string" && (a.role === "primary" || a.role === "observer")
|
|
140
|
+
? [{ name: a.name, storageStatePath: a.storageStatePath, role: a.role }]
|
|
141
|
+
: [])
|
|
142
|
+
: [];
|
|
143
|
+
return {
|
|
144
|
+
...(spec?.success === true ? { invariantSpec: spec.data } : {}),
|
|
145
|
+
...(fixtures === undefined ? {} : { fixtures }),
|
|
146
|
+
recording,
|
|
147
|
+
target: {
|
|
148
|
+
seedUrl: target.seedUrl,
|
|
149
|
+
allowlist: target.allowlist.filter((a) => typeof a === "string"),
|
|
150
|
+
...(typeof target.storageStatePath === "string" ? { storageStatePath: target.storageStatePath } : {}),
|
|
151
|
+
...(actors.length === 0 ? {} : { actors }),
|
|
152
|
+
},
|
|
153
|
+
findings,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The one place a mission's findings (declared-invariant defects, hangs, adversarial defects) are
|
|
158
|
+
* looked up by fingerprint — a fingerprint the CALLER supplied may equal the finding's own, or one
|
|
159
|
+
* of its `related` fingerprints (the same underlying defect observed at a different point). Shared
|
|
160
|
+
* by `verify-fix` and `regression capture` (#119/#129): the latter reuses this lookup rather than
|
|
161
|
+
* re-implementing "is this fingerprint a defect this mission found" itself.
|
|
162
|
+
*/
|
|
163
|
+
export function findFinding(mission, fingerprint) {
|
|
164
|
+
return mission.findings.find((f) => f.fingerprint === fingerprint || (f.related ?? []).includes(fingerprint));
|
|
165
|
+
}
|
|
166
|
+
/** Whether an explicit `--viewport`/`--device` matches a finding's recorded emulation (#149). */
|
|
167
|
+
function emulationMatchesRecorded(explicit, recorded) {
|
|
168
|
+
if (explicit.device !== undefined || recorded.device !== undefined)
|
|
169
|
+
return explicit.device === recorded.device;
|
|
170
|
+
if (explicit.viewport === undefined)
|
|
171
|
+
return true;
|
|
172
|
+
return explicit.viewport.width === recorded.viewport.width && explicit.viewport.height === recorded.viewport.height;
|
|
173
|
+
}
|
|
174
|
+
/** The controls a persisted mission's side effects show firing a write (#181), for hang-replay withholding. */
|
|
175
|
+
function recordedWriters(raw) {
|
|
176
|
+
const r = raw;
|
|
177
|
+
const list = r?.sideEffects ?? r?.result?.sideEffects;
|
|
178
|
+
if (!Array.isArray(list))
|
|
179
|
+
return [];
|
|
180
|
+
return list.flatMap((e) => {
|
|
181
|
+
const control = e?.control;
|
|
182
|
+
return typeof control === "string" ? [control] : [];
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function describeEmulation(e) {
|
|
186
|
+
return e.device !== undefined ? `--device "${e.device}"` : `--viewport ${e.viewport.width}x${e.viewport.height}`;
|
|
187
|
+
}
|
|
188
|
+
export async function runVerifyFix(opts) {
|
|
189
|
+
let raw;
|
|
190
|
+
try {
|
|
191
|
+
raw = JSON.parse(await readFile(opts.resultPath, "utf8"));
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
throw new VerifyFixInputError(`cannot read mission result ${opts.resultPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
195
|
+
}
|
|
196
|
+
const mission = parsePersistedMission(raw);
|
|
197
|
+
const finding = findFinding(mission, opts.fingerprint);
|
|
198
|
+
if (finding === undefined) {
|
|
199
|
+
throw new VerifyFixInputError(`no finding with fingerprint ${opts.fingerprint} in ${opts.resultPath}`);
|
|
200
|
+
}
|
|
201
|
+
// Guardrail #1: the replay may only ever touch the mission's own authorized origins.
|
|
202
|
+
const origin = assertAuthorizedExploreTarget(mission.target.seedUrl, mission.target.allowlist);
|
|
203
|
+
const target = resolveTargetConfig(opts.targets ?? {}, origin);
|
|
204
|
+
// #175: the operator's targets.json session is the last fallback (verify_fix over MCP of a
|
|
205
|
+
// mission recorded without one); never an MCP argument.
|
|
206
|
+
const storageState = opts.storageState ?? mission.target.storageStatePath ?? target.storageState;
|
|
207
|
+
if (storageState !== undefined && !existsSync(storageState)) {
|
|
208
|
+
throw new VerifyFixInputError(`storage state not found: ${storageState}`);
|
|
209
|
+
}
|
|
210
|
+
const portFactory = opts.browserPortFactory ?? (() => new PlaywrightBrowserPort());
|
|
211
|
+
// #245: every replay (and observer) session is shown/recorded alike — videos in
|
|
212
|
+
// `verify-fix-<stamp>.videos/` beside the mission result (or under `--record-video <dir>`).
|
|
213
|
+
const vfArtifact = joinPath(dirnameOf(resolveFile(opts.resultPath)), `verify-fix-${artifactStamp(new Date().toISOString())}.json`);
|
|
214
|
+
const videoDir = runVideoDir(opts.browser, vfArtifact);
|
|
215
|
+
const shown = sessionLaunchOptions(opts.browser, videoDir);
|
|
216
|
+
const perceiveOpts = {
|
|
217
|
+
...(opts.settleCeilingMs === undefined ? {} : { renderWaitMs: opts.settleCeilingMs }),
|
|
218
|
+
...(target.settle === undefined ? {} : { settleConfig: target.settle }),
|
|
219
|
+
...(target.hangs === undefined ? {} : { hangConfig: target.hangs }),
|
|
220
|
+
};
|
|
221
|
+
const recording = finding.recording ?? mission.recording;
|
|
222
|
+
if (recording === null)
|
|
223
|
+
throw new VerifyFixInputError(`finding ${finding.fingerprint} has no Recording to replay`);
|
|
224
|
+
// #149: replay under the finding's OWN recorded emulation by default — a 375px defect reproduces
|
|
225
|
+
// at 375px, not the caller's desktop default. An explicit --viewport/--device that DIFFERS from
|
|
226
|
+
// it fails closed (never silently "verifies fixed" at the wrong device) unless overridden.
|
|
227
|
+
const recordedEmulation = recording.emulation;
|
|
228
|
+
let effectiveEmulation;
|
|
229
|
+
if (opts.emulation !== undefined) {
|
|
230
|
+
if (recordedEmulation !== undefined && !emulationMatchesRecorded(opts.emulation, recordedEmulation) && opts.allowEmulationOverride !== true) {
|
|
231
|
+
throw new VerifyFixInputError(`--viewport/--device differs from the finding's recorded emulation (${describeEmulation(recordedEmulation)}); ` +
|
|
232
|
+
"pass allowEmulationOverride (CLI --allow-emulation-override) to replay at a different emulation anyway");
|
|
233
|
+
}
|
|
234
|
+
effectiveEmulation = opts.emulation;
|
|
235
|
+
}
|
|
236
|
+
else if (recordedEmulation !== undefined) {
|
|
237
|
+
effectiveEmulation = recordedEmulation.device !== undefined ? { device: recordedEmulation.device } : { viewport: recordedEmulation.viewport };
|
|
238
|
+
}
|
|
239
|
+
resolveEmulation(effectiveEmulation); // refused BEFORE any browser opens (an unknown device, e.g.)
|
|
240
|
+
// A declared-invariant defect (#86) is re-checked with the same spec (or `--invariants`), its probes
|
|
241
|
+
// authorized against the MISSION's own origins before any browser opens.
|
|
242
|
+
let invariantSpec;
|
|
243
|
+
// #135: authFrom.secret refs (env:VAR), resolved from the environment HERE — the one place this
|
|
244
|
+
// package reads process.env for invariants — never inside @jevitate/explore or @jevitate/recording.
|
|
245
|
+
let invariantAuthTokens;
|
|
246
|
+
// #147: a cross-actor defect is re-checked from the SAME observers, each in a fresh context per replay.
|
|
247
|
+
const observers = (mission.target.actors ?? []).filter((a) => a.role === "observer");
|
|
248
|
+
for (const o of observers) {
|
|
249
|
+
if (!existsSync(o.storageStatePath))
|
|
250
|
+
throw new VerifyFixInputError(`actor ${o.name}: storage state not found: ${o.storageStatePath}`);
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const bounds = { allowlist: mission.target.allowlist, baseUrl: mission.target.seedUrl, observers: observers.map((o) => o.name) };
|
|
254
|
+
invariantSpec =
|
|
255
|
+
opts.invariantFiles !== undefined && opts.invariantFiles.length > 0
|
|
256
|
+
? loadInvariantFiles(opts.invariantFiles, bounds)
|
|
257
|
+
: mission.invariantSpec === undefined
|
|
258
|
+
? undefined
|
|
259
|
+
: validateInvariantSpec(mission.invariantSpec, bounds);
|
|
260
|
+
invariantAuthTokens = invariantSpec === undefined ? undefined : resolveInvariantAuthTokens(invariantSpec, process.env);
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
throw new VerifyFixInputError(e instanceof Error ? e.message : String(e));
|
|
264
|
+
}
|
|
265
|
+
const authTokenValues = [...(invariantAuthTokens?.values() ?? [])];
|
|
266
|
+
const primaryActor = mission.target.actors?.find((a) => a.role === "primary")?.name;
|
|
267
|
+
const fx = missionFixtures(mission, opts.fixtureFlags ?? {}, storageState, [...(opts.secrets ?? []), ...authTokenValues]);
|
|
268
|
+
const declared = finding.kind === "invariant" && finding.invariantId !== undefined && invariantSpec !== undefined
|
|
269
|
+
? {
|
|
270
|
+
spec: invariantSpec,
|
|
271
|
+
id: finding.invariantId,
|
|
272
|
+
allowlist: mission.target.allowlist,
|
|
273
|
+
baseUrl: mission.target.seedUrl,
|
|
274
|
+
...(invariantAuthTokens === undefined || invariantAuthTokens.size === 0 ? {} : { authTokens: invariantAuthTokens }),
|
|
275
|
+
...(observers.length === 0
|
|
276
|
+
? {}
|
|
277
|
+
: {
|
|
278
|
+
openObservers: () => observerSessions(portFactory, { ...shown, allowedOrigins: [...mission.target.allowlist], baseUrl: origin }, observers.map((o) => ({ name: o.name, storageState: o.storageStatePath }))),
|
|
279
|
+
}),
|
|
280
|
+
...(primaryActor === undefined ? {} : { primaryActor }),
|
|
281
|
+
}
|
|
282
|
+
: undefined;
|
|
283
|
+
const openSession = async () => {
|
|
284
|
+
const session = await portFactory().open({
|
|
285
|
+
...shown,
|
|
286
|
+
allowedOrigins: [...mission.target.allowlist],
|
|
287
|
+
baseUrl: origin,
|
|
288
|
+
...effectiveEmulation,
|
|
289
|
+
...(storageState !== undefined ? { storageState } : {}),
|
|
290
|
+
});
|
|
291
|
+
const actor = CastActor.named("verify-fix").whoCan(new BrowseTheWeb(session, [...mission.target.allowlist]));
|
|
292
|
+
return { page: session.page, actor, close: () => session.close() };
|
|
293
|
+
};
|
|
294
|
+
// Each replay restores the mission's fixture state first; a failed setup makes that replay
|
|
295
|
+
// "could not open a session" — no evidence, so never `fixed`.
|
|
296
|
+
const replaySession = fx === undefined ? openSession : fixtureReplayOpener(openSession, fx, recording.fixture?.outputs ?? mission.fixtures?.outputs ?? {});
|
|
297
|
+
let result;
|
|
298
|
+
try {
|
|
299
|
+
// A `server-log` defect (#142) is re-checked by REPLAYING and re-tailing the SAME log sources —
|
|
300
|
+
// never by `@jevitate/explore`'s `verifyFix`, which only looks at DOM/console/network signals.
|
|
301
|
+
if (finding.kind === "server-log") {
|
|
302
|
+
if (finding.serverLog === undefined) {
|
|
303
|
+
result = {
|
|
304
|
+
fingerprint: finding.fingerprint,
|
|
305
|
+
verdict: "inconclusive",
|
|
306
|
+
observedFingerprints: [],
|
|
307
|
+
replay: { outcome: "failed", at: -1, error: "not replayed" },
|
|
308
|
+
reason: "a server-log defect needs its log source(s) and matcher to be re-checked",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
result = await verifyServerLogDefect({
|
|
313
|
+
recording,
|
|
314
|
+
recordingStepIndex: finding.repro.recordingStepIndex,
|
|
315
|
+
fingerprint: finding.fingerprint,
|
|
316
|
+
sources: finding.serverLog.sources,
|
|
317
|
+
matcher: finding.serverLog.matcher,
|
|
318
|
+
normalizedMessage: finding.serverLog.normalizedMessage,
|
|
319
|
+
drainMs: finding.serverLog.drainMs,
|
|
320
|
+
// #142 follow-up: an explicit --allow-log-cmd wins; otherwise the operator's own
|
|
321
|
+
// ~/.jevitate/targets.json entry for this origin may opt in (never an MCP argument).
|
|
322
|
+
allowLogCmd: opts.allowLogCmd === true || target.allowLogCmd === true,
|
|
323
|
+
...(opts.settleCeilingMs === undefined ? {} : { settleCeilingMs: opts.settleCeilingMs }),
|
|
324
|
+
...(opts.replays === undefined ? {} : { replays: opts.replays }),
|
|
325
|
+
openSession: replaySession,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
result = await verifyFix({
|
|
331
|
+
perceive: perceiveOpts,
|
|
332
|
+
recording,
|
|
333
|
+
recordingStepIndex: finding.repro.recordingStepIndex,
|
|
334
|
+
fingerprint: finding.fingerprint,
|
|
335
|
+
defectKind: finding.kind,
|
|
336
|
+
...(declared === undefined ? {} : { invariant: declared }),
|
|
337
|
+
...(finding.hang === undefined ? {} : { hang: finding.hang }),
|
|
338
|
+
// #153: never re-send a paid/destructive write unless the operator opted in.
|
|
339
|
+
safety: { ...(target.safety ?? {}), ...(opts.hangReplayWrites === true ? { hangReplayWrites: true } : {}) },
|
|
340
|
+
writtenBy: recordedWriters(raw),
|
|
341
|
+
...(finding.occurrences === undefined ? {} : { occurrences: finding.occurrences }),
|
|
342
|
+
...(opts.settleCeilingMs === undefined ? {} : { settleCeilingMs: opts.settleCeilingMs }),
|
|
343
|
+
...(opts.replays === undefined ? {} : { replays: opts.replays }),
|
|
344
|
+
openSession: replaySession,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
await fx?.restore();
|
|
350
|
+
}
|
|
351
|
+
// #250/#251: the captioned "after" replay (video and/or screenshots), paired with the run's own clip.
|
|
352
|
+
let evidence;
|
|
353
|
+
let shotFields = {};
|
|
354
|
+
if (opts.browser?.recordVideo !== undefined || opts.screenshots !== undefined) {
|
|
355
|
+
const secrets = [...(opts.secrets ?? []), ...authTokenValues];
|
|
356
|
+
const stored = persistedDefect(raw, finding.fingerprint);
|
|
357
|
+
const storedEvidence = stored !== undefined && isRecord(stored.evidence) ? stored.evidence : undefined;
|
|
358
|
+
const beforeVideo = typeof storedEvidence?.videoPath === "string" ? storedEvidence.videoPath : undefined;
|
|
359
|
+
const beforeShots = Array.isArray(storedEvidence?.screenshots) ? storedEvidence.screenshots.filter((x) => typeof x === "string") : [];
|
|
360
|
+
const shots = opts.screenshots === undefined
|
|
361
|
+
? undefined
|
|
362
|
+
: new RunScreenshots({ spec: opts.screenshots, dir: screenshotsDirFor(opts.screenshots, vfArtifact), secrets, title: `verify-fix ${finding.fingerprint}` });
|
|
363
|
+
const verdictCard = () => result.verdict === "fixed"
|
|
364
|
+
? { text: "After the fix: the defect no longer reproduces", ok: true }
|
|
365
|
+
: { text: `After: ${result.verdict === "still-reproduces" ? "still reproduces" : result.verdict}`, ok: false };
|
|
366
|
+
const after = fx !== undefined || observers.length > 0
|
|
367
|
+
? { screenshots: [], skipped: fx !== undefined ? "the run used mission fixtures: the after-clip replay does not restore them" : "a cross-actor defect: no after-clip replay" }
|
|
368
|
+
: await replayWithEvidence({
|
|
369
|
+
recording,
|
|
370
|
+
stepIndex: finding.repro.recordingStepIndex,
|
|
371
|
+
fingerprints: [finding.fingerprint, ...(finding.related ?? [])],
|
|
372
|
+
signalCheckable: signalCheckable(finding.kind),
|
|
373
|
+
signal: defectSignalText(stored ?? { kind: finding.kind, ...(finding.title === undefined ? {} : { title: finding.title }) }, secrets),
|
|
374
|
+
seedUrl: mission.target.seedUrl,
|
|
375
|
+
allowlist: mission.target.allowlist,
|
|
376
|
+
...(storageState === undefined ? {} : { storageState }),
|
|
377
|
+
...(effectiveEmulation === undefined ? {} : { emulation: effectiveEmulation }),
|
|
378
|
+
outDir: joinPath(dirnameOf(videoDir ?? vfArtifact), `${vfArtifact.replace(/^.*[\\/]/, "").replace(/\.json$/, "")}.evidence`),
|
|
379
|
+
video: opts.browser?.recordVideo !== undefined,
|
|
380
|
+
...(shots === undefined ? {} : { screenshots: shots }),
|
|
381
|
+
secrets,
|
|
382
|
+
...(opts.browser === undefined ? {} : { browser: opts.browser }),
|
|
383
|
+
browserPortFactory: portFactory,
|
|
384
|
+
...(opts.evidencePaceMs === undefined ? {} : { paceMs: opts.evidencePaceMs }),
|
|
385
|
+
...(opts.settleCeilingMs === undefined ? {} : { settleCeilingMs: opts.settleCeilingMs }),
|
|
386
|
+
finalCard: verdictCard,
|
|
387
|
+
});
|
|
388
|
+
evidence = {
|
|
389
|
+
...(beforeVideo === undefined && beforeShots.length === 0 ? {} : { before: { ...(beforeVideo === undefined ? {} : { videoPath: beforeVideo }), screenshots: beforeShots } }),
|
|
390
|
+
...(beforeVideo === undefined ? { beforeMissing: "the run recorded no evidence clip for this defect (run it with --evidence-video)" } : {}),
|
|
391
|
+
after,
|
|
392
|
+
};
|
|
393
|
+
shotFields = shots === undefined ? {} : await shots.finish();
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
...result,
|
|
397
|
+
exitCode: VERIFY_FIX_EXIT_CODES[result.verdict],
|
|
398
|
+
...(evidence === undefined ? {} : { evidence }),
|
|
399
|
+
...shotFields,
|
|
400
|
+
...(finding.title === undefined ? {} : { title: finding.title }),
|
|
401
|
+
...(fx === undefined ? {} : { fixtures: { ...fx.record(), missionIdentity: mission.fixtures?.identity ?? "none" } }),
|
|
402
|
+
// Every replay closed its own session (finalizing its video) before verifyFix returned.
|
|
403
|
+
...(videoDir === undefined ? {} : { videoPaths: listVideos(videoDir) }),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* The replay fixture: the mission's saved spec (re-validated against the MISSION's allowlist, never
|
|
408
|
+
* trusted as-is) or `--fixtures`, plus the operator's re-supplied shell hooks, which must match the
|
|
409
|
+
* mission's by hash. A mission that ran with a fixture never replays without one.
|
|
410
|
+
*/
|
|
411
|
+
function missionFixtures(mission, flags, storageState, secrets) {
|
|
412
|
+
const saved = mission.fixtures;
|
|
413
|
+
if (saved === undefined && flags.fixtures === undefined && flags.before === undefined && flags.after === undefined)
|
|
414
|
+
return undefined;
|
|
415
|
+
const bounds = { allowlist: mission.target.allowlist, baseUrl: mission.target.seedUrl };
|
|
416
|
+
const hooks = saved?.hooks ?? {};
|
|
417
|
+
const given = {
|
|
418
|
+
...(flags.before === undefined ? {} : { before: hookHash(flags.before) }),
|
|
419
|
+
...(flags.after === undefined ? {} : { after: hookHash(flags.after) }),
|
|
420
|
+
};
|
|
421
|
+
if (given.before !== hooks.before || given.after !== hooks.after) {
|
|
422
|
+
throw new VerifyFixInputError(hooks.before === undefined && hooks.after === undefined
|
|
423
|
+
? "the mission ran without shell hooks; do not pass --before/--after"
|
|
424
|
+
: "the mission ran with --before/--after shell hooks: re-supply the SAME commands with --allow-shell-hooks (they are never replayed from the result file)");
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const openRefs = { openRefs: hooks.before !== undefined };
|
|
428
|
+
const spec = flags.fixtures !== undefined
|
|
429
|
+
? loadFixtureFile(flags.fixtures, bounds, openRefs)
|
|
430
|
+
: saved?.spec !== undefined
|
|
431
|
+
? parseFixtureSpec(saved.spec, bounds, openRefs)
|
|
432
|
+
: undefined;
|
|
433
|
+
// `--secret-field` bindings a spec authenticates with come from the same environment variables.
|
|
434
|
+
const names = secretFieldNames(spec); // `auth: {from: "secretField"}` and `${secretField.VAR}` (#166)
|
|
435
|
+
const secretFields = names.flatMap((name) => {
|
|
436
|
+
const secret = process.env[name];
|
|
437
|
+
return secret === undefined ? [] : [{ descriptor: name, matcher: { key: "name", value: name }, name, kind: "value", secret }];
|
|
438
|
+
});
|
|
439
|
+
const { fixtures: _file, ...hookFlags } = flags;
|
|
440
|
+
return buildMissionFixtures(hookFlags, {
|
|
441
|
+
...bounds,
|
|
442
|
+
...(storageState === undefined ? {} : { storageState }),
|
|
443
|
+
secretFields,
|
|
444
|
+
secrets,
|
|
445
|
+
...(spec === undefined ? {} : { spec }),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
if (e instanceof FixtureSpecError)
|
|
450
|
+
throw new VerifyFixInputError(`mission fixtures: ${e.message}`);
|
|
451
|
+
throw e;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
//# sourceMappingURL=verify-fix-api.js.map
|