@jevitate/cli 0.11.0 → 0.13.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/dist/approval-provenance.d.ts +6 -0
- package/dist/approval-provenance.js +9 -1
- package/dist/approval-provenance.js.map +1 -1
- package/dist/author-journey-cli.js +2 -1
- package/dist/author-journey-cli.js.map +1 -1
- package/dist/bin.js +25396 -23850
- package/dist/build-info.generated.js +2 -2
- package/dist/demo-aspect-cli.js +2 -1
- package/dist/demo-aspect-cli.js.map +1 -1
- package/dist/explore-cli/adversarial-strategy.d.ts +3 -0
- package/dist/explore-cli/adversarial-strategy.js +99 -0
- package/dist/explore-cli/adversarial-strategy.js.map +1 -0
- package/dist/explore-cli/coverage-strategy.d.ts +3 -0
- package/dist/explore-cli/coverage-strategy.js +95 -0
- package/dist/explore-cli/coverage-strategy.js.map +1 -0
- package/dist/explore-cli/feature-strategy.d.ts +3 -0
- package/dist/explore-cli/feature-strategy.js +59 -0
- package/dist/explore-cli/feature-strategy.js.map +1 -0
- package/dist/explore-cli/goal-strategy.d.ts +3 -0
- package/dist/explore-cli/goal-strategy.js +184 -0
- package/dist/explore-cli/goal-strategy.js.map +1 -0
- package/dist/explore-cli/options.d.ts +3 -0
- package/dist/explore-cli/options.js +155 -0
- package/dist/explore-cli/options.js.map +1 -0
- package/dist/explore-cli/run.d.ts +7 -0
- package/dist/explore-cli/run.js +682 -0
- package/dist/explore-cli/run.js.map +1 -0
- package/dist/explore-cli/shared.d.ts +158 -0
- package/dist/explore-cli/shared.js +20 -0
- package/dist/explore-cli/shared.js.map +1 -0
- package/dist/explore-cli/usability-strategy.d.ts +3 -0
- package/dist/explore-cli/usability-strategy.js +136 -0
- package/dist/explore-cli/usability-strategy.js.map +1 -0
- package/dist/explore-cli.d.ts +2 -2
- package/dist/explore-cli.js +2 -1346
- package/dist/explore-cli.js.map +1 -1
- package/dist/explore-goal.js +4 -3
- package/dist/explore-goal.js.map +1 -1
- package/dist/journey-anchor-api.d.ts +10 -9
- package/dist/journey-anchor-api.js +12 -22
- package/dist/journey-anchor-api.js.map +1 -1
- package/dist/journey-anchor-cli.d.ts +1 -1
- package/dist/journey-anchor-cli.js +10 -6
- package/dist/journey-anchor-cli.js.map +1 -1
- package/dist/journey-api.d.ts +12 -3
- package/dist/journey-api.js +34 -12
- package/dist/journey-api.js.map +1 -1
- package/dist/journey-cli/anchors.d.ts +4 -0
- package/dist/journey-cli/anchors.js +48 -0
- package/dist/journey-cli/anchors.js.map +1 -0
- package/dist/journey-cli/find.d.ts +4 -0
- package/dist/journey-cli/find.js +44 -0
- package/dist/journey-cli/find.js.map +1 -0
- package/dist/journey-cli/lint.d.ts +4 -0
- package/dist/journey-cli/lint.js +76 -0
- package/dist/journey-cli/lint.js.map +1 -0
- package/dist/journey-cli/list.d.ts +4 -0
- package/dist/journey-cli/list.js +48 -0
- package/dist/journey-cli/list.js.map +1 -0
- package/dist/journey-cli/migrate.d.ts +4 -0
- package/dist/journey-cli/migrate.js +101 -0
- package/dist/journey-cli/migrate.js.map +1 -0
- package/dist/journey-cli/promote.d.ts +4 -0
- package/dist/journey-cli/promote.js +181 -0
- package/dist/journey-cli/promote.js.map +1 -0
- package/dist/journey-cli/publish.d.ts +4 -0
- package/dist/journey-cli/publish.js +70 -0
- package/dist/journey-cli/publish.js.map +1 -0
- package/dist/journey-cli/review.d.ts +4 -0
- package/dist/journey-cli/review.js +103 -0
- package/dist/journey-cli/review.js.map +1 -0
- package/dist/journey-cli/run.d.ts +4 -0
- package/dist/journey-cli/run.js +244 -0
- package/dist/journey-cli/run.js.map +1 -0
- package/dist/journey-cli/shared.d.ts +9 -0
- package/dist/journey-cli/shared.js +12 -0
- package/dist/journey-cli/shared.js.map +1 -0
- package/dist/journey-cli/verify.d.ts +4 -0
- package/dist/journey-cli/verify.js +125 -0
- package/dist/journey-cli/verify.js.map +1 -0
- package/dist/journey-cli.d.ts +2 -2
- package/dist/journey-cli.js +21 -878
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-labels-migrate-api.d.ts +100 -0
- package/dist/journey-labels-migrate-api.js +194 -0
- package/dist/journey-labels-migrate-api.js.map +1 -0
- package/dist/journey-lint-sarif.js +4 -0
- package/dist/journey-lint-sarif.js.map +1 -1
- package/dist/journey-review/render.d.ts +43 -0
- package/dist/journey-review/render.js +461 -0
- package/dist/journey-review/render.js.map +1 -0
- package/dist/journey-review.d.ts +9 -2
- package/dist/journey-review.js +183 -144
- package/dist/journey-review.js.map +1 -1
- package/dist/log-sources.js +17 -1
- package/dist/log-sources.js.map +1 -1
- package/dist/mcp-api.js +16 -821
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-cli-tools.js +14 -5
- package/dist/mcp-cli-tools.js.map +1 -1
- package/dist/mcp-tools/cli.d.ts +3 -0
- package/dist/mcp-tools/cli.js +20 -0
- package/dist/mcp-tools/cli.js.map +1 -0
- package/dist/mcp-tools/explore.d.ts +3 -0
- package/dist/mcp-tools/explore.js +141 -0
- package/dist/mcp-tools/explore.js.map +1 -0
- package/dist/mcp-tools/findings.d.ts +3 -0
- package/dist/mcp-tools/findings.js +277 -0
- package/dist/mcp-tools/findings.js.map +1 -0
- package/dist/mcp-tools/inbox.d.ts +3 -0
- package/dist/mcp-tools/inbox.js +106 -0
- package/dist/mcp-tools/inbox.js.map +1 -0
- package/dist/mcp-tools/journey.d.ts +3 -0
- package/dist/mcp-tools/journey.js +260 -0
- package/dist/mcp-tools/journey.js.map +1 -0
- package/dist/mcp-tools/shared.d.ts +39 -0
- package/dist/mcp-tools/shared.js +69 -0
- package/dist/mcp-tools/shared.js.map +1 -0
- package/dist/regression-api.js +6 -4
- package/dist/regression-api.js.map +1 -1
- package/dist/review-render-options.d.ts +17 -0
- package/dist/review-render-options.js +17 -0
- package/dist/review-render-options.js.map +1 -0
- package/dist/sweep-api.d.ts +9 -0
- package/dist/sweep-api.js +70 -2
- package/dist/sweep-api.js.map +1 -1
- package/dist/sweep-cli.d.ts +2 -0
- package/dist/sweep-cli.js +22 -5
- package/dist/sweep-cli.js.map +1 -1
- package/dist/ux-usability/analysis.d.ts +14 -0
- package/dist/ux-usability/analysis.js +106 -0
- package/dist/ux-usability/analysis.js.map +1 -0
- package/dist/ux-usability/done-adjudication.d.ts +15 -0
- package/dist/ux-usability/done-adjudication.js +56 -0
- package/dist/ux-usability/done-adjudication.js.map +1 -0
- package/dist/ux-usability/hang.d.ts +5 -0
- package/dist/ux-usability/hang.js +41 -0
- package/dist/ux-usability/hang.js.map +1 -0
- package/dist/ux-usability/loop.d.ts +15 -0
- package/dist/ux-usability/loop.js +104 -0
- package/dist/ux-usability/loop.js.map +1 -0
- package/dist/ux-usability/observe.d.ts +7 -0
- package/dist/ux-usability/observe.js +89 -0
- package/dist/ux-usability/observe.js.map +1 -0
- package/dist/ux-usability/result.d.ts +11 -0
- package/dist/ux-usability/result.js +113 -0
- package/dist/ux-usability/result.js.map +1 -0
- package/dist/ux-usability/setup.d.ts +75 -0
- package/dist/ux-usability/setup.js +203 -0
- package/dist/ux-usability/setup.js.map +1 -0
- package/dist/ux-usability/teardown.d.ts +3 -0
- package/dist/ux-usability/teardown.js +13 -0
- package/dist/ux-usability/teardown.js.map +1 -0
- package/dist/ux-usability.d.ts +14 -17
- package/dist/ux-usability.js +15 -586
- package/dist/ux-usability.js.map +1 -1
- package/dist/verify-fix-api.js +2 -0
- package/dist/verify-fix-api.js.map +1 -1
- package/package.json +11 -11
package/dist/explore-cli.js
CHANGED
|
@@ -1,1353 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { TAG_FLAG, TAG_HELP, TARGET_FLAG, TARGET_HELP, collectTag, taggedAction } from "./run-tags-cli.js";
|
|
3
|
-
import { existsSync } from "node:fs";
|
|
4
|
-
import { envCredentialStore, MissingCredentialError } from "@jevitate/ai-core";
|
|
5
|
-
import { loadLocalCredentials } from "./credentials-file.js";
|
|
6
|
-
import { FixtureNotFoundError, ScopeUnderivableError, UnauthorizedExploreTargetError, resolveCoverageThresholds, TypeFixtureSpecError, validateAllowControlPatterns, validateDenyPatterns, } from "@jevitate/explore";
|
|
7
|
-
import { loadTypeFixtures } from "./type-fixture-file.js";
|
|
8
|
-
import { secretCommandRunner } from "./secret-command.js";
|
|
9
|
-
import { ok, fail } from "./envelope.js";
|
|
10
|
-
import { SessionFileInProjectError, assertSessionFileOutsideProject } from "./project-dir.js";
|
|
11
|
-
import { InvariantsFileError, loadInvariantFiles, resolveInvariantAuthTokens } from "./invariants-file.js";
|
|
12
|
-
import { FilingConfigError, loadFilingFileConfig, resolveFilingConfig } from "./findings-filing.js";
|
|
13
|
-
import { fileDraftsWithEvidence } from "./defect-evidence.js";
|
|
14
|
-
import { parseScreenshotsArg } from "./run-screenshots.js";
|
|
15
|
-
import { GitHubIssueFiler } from "./github-issue-filer.js";
|
|
16
|
-
import { TargetConfigError, loadTargetsFile, resolveTargetConfig } from "./target-config.js";
|
|
17
|
-
import { buildMissionFixtures, checkSetupRefs, checkUrlRefOrigin, substituteUrlSetupRefs, setupRefFreeUrl, invariantSetupTexts, substituteSpecSetupRefs, fixtureSetupFailedResult, withFixtureFlags, } from "./fixture-cli.js";
|
|
18
|
-
import { FixtureSetupError, FixtureSpecError, UnboundSetupRefError, substituteSetupRefs, } from "./mission-fixtures.js";
|
|
19
|
-
import { LITERAL_SECRET_WARNING, SecretArgError, resolveSecretArgs } from "./secret-args.js";
|
|
20
|
-
import { withEngine } from "./engine.js";
|
|
21
|
-
import { setKillSwitchOutput } from "./kill-signal.js";
|
|
22
|
-
import { EXIT_CODES } from "./exit-codes.js";
|
|
23
|
-
import { positiveNumberArg, nonNegativeIntArg, positiveIntArg, ratioArg } from "./cli-args.js";
|
|
24
|
-
import { formatMissionHuman, formatMultiRunHuman } from "./cli-output.js";
|
|
25
|
-
import { runExploration, runCoverageMission, runAdversarialCliMission, CLI_ADVERSARIAL_STRATEGIES, runFeatureCliMission, parseSuccessSpec, resolveExploreAllowlist, } from "./explore-api.js";
|
|
26
|
-
import { parseLogSourceSpecs, LogSourceSpecError } from "./log-sources.js";
|
|
27
|
-
import { triagedServerLog } from "./explore-shared.js";
|
|
28
|
-
import { parseLogDefectSpecs, parseLogIgnoreSpecs, parseLogScopeSpecs } from "./log-correlation.js";
|
|
29
|
-
import { LogClassesError, loadLogClassRules } from "./log-classes.js";
|
|
30
|
-
import { parseCorrelationHeaders, parseLogIdPatterns } from "./log-trace.js";
|
|
31
|
-
import { LogSpecError } from "./log-lines.js";
|
|
32
|
-
import { MultiRunArgsError, projectPersonaFor, resolveMultiRunPlan, wantsMultiRun } from "./multi-run.js";
|
|
33
|
-
import { authExpiredResult, ensurePersonaSession, LoginArgsError, parseAuthCheck } from "./persona-login.js";
|
|
34
|
-
import { MultiRunAbortedError, runExploreMultiRun } from "./multi-run-cli.js";
|
|
35
|
-
import { checkActorsAgainstSpec, resolveMissionActors } from "./mission-actors.js";
|
|
36
|
-
import { runUsabilityMission, UsabilityInvariantsUnsupportedError } from "./ux-api.js";
|
|
37
|
-
import { UxConfigError } from "./ux-config.js";
|
|
38
|
-
import { MinConfidenceError, QualityPolicyError, MaxFindingsPerRouteError, ProductFactsError } from "@jevitate/ux";
|
|
39
|
-
import { PlaywrightBrowserPort } from "@jevitate/playwright";
|
|
40
|
-
import { withBrowserLaunchFlags, withDemoFlags, withScreenshotsFlag, browserRunFromFlags, withEmulationFlags, emulationFromFlags, emitCommandResult, collectParam, environmentSeams, resolveDbPath, resolveJourneysDir, resolveMissionTargetsDir, stallTimeoutMs, EXPLORE_STRATEGIES, EXPLORE_OUTCOME_HELP, GatewaySelectionError, buildExploreGateways, } from "./cli-shared.js";
|
|
41
|
-
import { allowWithExtensions, assertExtensionTargetLoaded, multiWindowWarning } from "./browser-run-options.js";
|
|
42
|
-
import { ParamValidationError } from "@jevitate/journey";
|
|
43
|
-
import { SiteGateRefusedError } from "@jevitate/runtime";
|
|
44
|
-
import { JourneyRequiresAuthError, UnknownJourneyError } from "./journey-api.js";
|
|
45
|
-
import { environmentFromFlags, isEnvironmentError, withEnvironmentFlags } from "./environments.js";
|
|
46
|
-
import { resolve as resolvePath } from "node:path";
|
|
47
|
-
import { CAMPAIGN_LIMITS, isSweepMode } from "@jevitate/journey";
|
|
48
|
-
import { CampaignSpecError, runCampaign, validateCampaignSpec } from "./campaign-api.js";
|
|
49
|
-
import { formatCampaignHuman } from "./campaign-cli.js";
|
|
50
|
-
import { forwardedArgv } from "./multi-run-cli.js";
|
|
51
|
-
import { ANCHORED_STRATEGIES, JourneyPrefixArgsError, JourneyPrefixStaleError, journeyStaleResult, resolveJourneyPrefix, } from "./journey-prefix.js";
|
|
52
|
-
import { clock } from "@jevitate/domain";
|
|
53
|
-
import { GOAL_RUN_OPTIONS, GoalRunFlagError, conversationFromFlags, dialogsFromFlags, secretFieldsFromFlags, successWhenFromFlags, targetFlagsFromFlags, } from "./goal-run-flags.js";
|
|
54
|
-
/** #427: the app root of a start URL bound from --fixtures (`<origin>${setup.x}`), or undefined without an origin. */
|
|
55
|
-
function fixtureBoundUrlRoot(url) {
|
|
56
|
-
try {
|
|
57
|
-
return `${new URL(url.slice(0, url.indexOf("${"))).origin}/`;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/** #427: how long the pre-flight auth check (and a refresh's sign-in) may take to load a page. */
|
|
64
|
-
const AUTH_PREFLIGHT_TIMEOUT_MS = 30_000;
|
|
65
|
-
/**
|
|
66
|
-
* #293: the flags a sweep sets on each of its missions itself (the rest of the command line is
|
|
67
|
-
* forwarded to every mission as given).
|
|
68
|
-
*/
|
|
69
|
-
const SWEEP_OWNED = new Set([
|
|
70
|
-
"fromJourney", "atStep", "strategy", "journeysDir", "param", "env", "baseUrl", "storageState", "maxActions", "maxDecisions",
|
|
71
|
-
"goal", "success", "appClass", "real", "fakeAi", "fixtures", "before", "after", "allowShellHooks", "hookTimeoutMs",
|
|
72
|
-
]);
|
|
1
|
+
import { registerExploreRun } from "./explore-cli/run.js";
|
|
73
2
|
/**
|
|
74
3
|
* Registers `jevitate explore` (every strategy: goal, coverage, exploratory, adversarial, usability, feature, multi-run).
|
|
75
4
|
* `buildProgram` builds a fresh program for each run of a multi-run (#141/#143).
|
|
76
5
|
*/
|
|
77
6
|
export function registerExploreCommands(program, deps, buildProgram) {
|
|
78
|
-
|
|
79
|
-
.command("explore")
|
|
80
|
-
.description("goal-directed exploration -> a deterministic Recording (authoring/test plane)")), { recordVideo: true, overlay: true })))))
|
|
81
|
-
.option("--url <url>", "target URL (must be an authorized origin)")
|
|
82
|
-
.option(TARGET_FLAG, TARGET_HELP)
|
|
83
|
-
.option("--from-journey <id>", "journey-anchored exploration (#293): start from a PROMOTED Journey instead of --url — its first --at-step steps are replayed " +
|
|
84
|
-
"in the mission's own browser context (page, form contents and session kept; fail-closed, never self-healed; --env/--base-url apply), " +
|
|
85
|
-
"then the mission starts on the live page. A replay that stops before the anchor ends the run inconclusive (failure.kind journey-stale, exit 2). " +
|
|
86
|
-
"Strategies: goal, coverage, exploratory, adversarial, usability")
|
|
87
|
-
.option("--at-step <n|name|all|anchors>", "with --from-journey: the step to branch off — a 1-based top-level step number or an anchor name (`jevitate journey anchors <id>`); " +
|
|
88
|
-
"`all` sweeps every step and `anchors` every anchor: each a fresh session (restored by --fixtures), --max-actions/--max-decisions split evenly per stop, one deduped report")
|
|
89
|
-
.option("--param <kv>", "with --from-journey: a Journey param as key=value (repeatable); only the prefix's own params are required", collectParam, {})
|
|
90
|
-
.option("--journeys-dir <path>", "with --from-journey: the journeys directory (default: the repo's .jevitate/journeys; outside a repo ~/.jevitate/journeys)")
|
|
91
|
-
.option("--strategy <name>", "exploration strategy: goal (default) | coverage | exploratory | adversarial | usability (UX review: ranked, cited findings)", "goal")
|
|
92
|
-
.option("--goal <text>", "natural-language goal / job (required for --strategy goal and usability)")
|
|
93
|
-
.option("--app-class <class>", "app class for UX calibration (required for --strategy usability), e.g. consumer|admin|internal")
|
|
94
|
-
.option("--show <labels>", "opt-in filter on the quality grade (comma list of actionable,relevant-minor,generic,wrong); others are suppressed and counted; default JEVITATE_UX_SHOW, then ~/.jevitate/config.json ux.show, then ALL grades — the grader is uncalibrated (#133), so by default every finding is shown with its grade")
|
|
95
|
-
.option("--min-confidence <n>", "(--strategy usability) findings below this FINDING confidence (0..1, a finding's own violation/applicability/grounding score — NOT its quality-grade confidence, a separate independent-grader number shown as finding.quality.confidence) are suppressed and counted in report.suppressed; default JEVITATE_UX_MIN_CONFIDENCE, then ~/.jevitate/config.json ux.minConfidence, then 0.3")
|
|
96
|
-
.option("--max-findings-per-page <n>", "(--strategy usability) cap on UX findings per route/page, highest-confidence first; the rest are counted in report.suppressed as per-page-cap, never dropped silently; default JEVITATE_UX_MAX_FINDINGS_PER_PAGE, then ~/.jevitate/config.json ux.maxFindingsPerPage, then 5")
|
|
97
|
-
.option("--product <file>", "(--strategy usability) product facts JSON (plans/prices, key journeys, each page's intended next step) the review checks screens against in code; default .jevitate/product.json in the project when present (docs/ux-findings.md)")
|
|
98
|
-
.option("--probe-guards", "(--strategy usability) opt in to clicking each destructive control once to check for a confirmation step — fail-safe: every write and destructive-looking request is aborted, and a page with an open WebSocket/EventSource or a service worker is not probed; without it those claims are reported unverifiable (docs/ux-findings.md)")
|
|
99
|
-
.option("--polish", "(--strategy usability) polish each verified UX finding's recommendation with one generation call (opt-in; the default prose is built from templates)")
|
|
100
|
-
.option("--success <spec>", [
|
|
101
|
-
"independent success check (repeatable; every one must hold; --strategy goal and usability). Kinds:",
|
|
102
|
-
"urlIncludes:<text> | visible:<d> | textIncludes:<d>|<text> (case-insensitive) | count:<d>|min=<n>,max=<n>",
|
|
103
|
-
"| valueEquals:<d>|<value> (a form control's value) | reloadThen:<check> (reload first: proves it persisted)",
|
|
104
|
-
"| visual state (#148, read and decided by code): style:<d>|<prop><op><value> (computed style of every match;",
|
|
105
|
-
"<prop> an allowlisted CSS property or a channel of one, e.g. alpha(background-color)>0, color=rgb(255, 0, 0); op = != > >= < <=)",
|
|
106
|
-
"| inViewport:<d>[|min=<ratio>] (visible fraction, default 0.5) | box:<d>|minWidth=<n>,maxWidth=<n>,minHeight=<n>,maxHeight=<n>",
|
|
107
|
-
"| overlaps:<d>|<d2> | noOverlap:<d>|<d2> | attr:<d>|<name>=<value> (or <name> present, !<name> absent)",
|
|
108
|
-
"| flashed:<d>|class=<cls> (or attr=<name>, animation)[|withinMs=<n>] (a transient state gained after the last user input)",
|
|
109
|
-
"| requestMade:<METHOD> <path-glob> | responseStatus:<METHOD> <path-glob>=<2xx|4xx|code>.",
|
|
110
|
-
"<d> is testId=..;role=..;name=..;label=..;text=..;css=.. or a CSS selector such as [data-testid=x].",
|
|
111
|
-
"<path-glob> must start with \"/\" (it matches the request's path, e.g. /api/profile/* or /api/**); * as METHOD matches any method.",
|
|
112
|
-
"e.g. --success 'requestMade:PUT /api/profile' --success 'reloadThen:valueEquals:[data-testid=last-name]|Litmus'.",
|
|
113
|
-
"Omit it for a find-out goal (e.g. \"find out how many contacts... report the answer\"): the run must then end",
|
|
114
|
-
"with the model's own `report` op, and the grounded answer (#101) is the verdict — no page/network check needed.",
|
|
115
|
-
].join(" "), (v, prev) => [...prev, v], [])
|
|
116
|
-
.addOption(GOAL_RUN_OPTIONS.successWhen())
|
|
117
|
-
.addOption(GOAL_RUN_OPTIONS.allowVacuousChecks())
|
|
118
|
-
.addOption(GOAL_RUN_OPTIONS.actionDeltas())
|
|
119
|
-
.option("--feature <name>", "run the capability-scoped feature-testing mission (instead of --goal/--success)")
|
|
120
|
-
.option("--route <glob>", "in-scope route glob (repeatable), e.g. /thread/** — for --feature it replaces the default scope (the start URL's route and everything under it); it widens --strategy adversarial/coverage/exploratory beyond the start URL's route", (v, prev) => [...prev, v], [])
|
|
121
|
-
.option("--scope <mode>", "--strategy coverage/exploratory: 'app' widens containment to the whole app (same as --route '/**'); default: the start URL's route plus --route globs")
|
|
122
|
-
.option("--allow <origin>", "authorized origin (repeatable); REPLACES the default allowlist when given (the URL's own origin is used only when --allow is omitted entirely) -- include the URL's own origin explicitly if you still need it", (v, prev) => [...prev, v], [])
|
|
123
|
-
.addOption(GOAL_RUN_OPTIONS.secret())
|
|
124
|
-
.addOption(GOAL_RUN_OPTIONS.secretField())
|
|
125
|
-
.addOption(GOAL_RUN_OPTIONS.totp())
|
|
126
|
-
.addOption(GOAL_RUN_OPTIONS.typeFixture())
|
|
127
|
-
.addOption(GOAL_RUN_OPTIONS.fixture())
|
|
128
|
-
.option("--storage-state <file>", "Playwright storageState JSON to start the session authenticated (deterministic login pre-step); must exist")
|
|
129
|
-
.option("--auth-check <mode>", "#427 pre-flight auth check before a run that starts from a session (--storage-state, --persona/--personas, --actor's primary): " +
|
|
130
|
-
"load the session, open --url and end the run fast (inconclusive, failure.kind auth-expired, exit 2) when it lands on a sign-in page — the login page is never explored. " +
|
|
131
|
-
"auto (default): a login-like URL (/login, /signin, /sign-in, /auth, …) or a visible password field, unless --url is itself such a route; " +
|
|
132
|
-
"urlExcludes:<text>: expired when the landed URL includes <text>; selector:<css>: alive only when this signed-in marker is visible; off. " +
|
|
133
|
-
"A persona with login parameters (a personas file entry's `login`, or .jevitate/personas.json) is signed in again once instead")
|
|
134
|
-
.option("--actor <name=storageState>", "multi-actor mission (#147, goal only; repeatable): the FIRST actor is the primary (the only one the model drives, " +
|
|
135
|
-
"from its own storageState); every other actor is an observer in its OWN fresh context that only runs the " +
|
|
136
|
-
"--invariants' cross-actor checks (capture + probe as:/deniedAs) — never clicks or types. Replaces --storage-state", (v, prev) => [...prev, v], [])
|
|
137
|
-
.addOption(GOAL_RUN_OPTIONS.saveStorageState())
|
|
138
|
-
.option("--max-actions <n>", "hard cap on executed actions", positiveIntArg)
|
|
139
|
-
.option("--max-decisions <n>", "hard cap on model decisions", positiveIntArg)
|
|
140
|
-
.option("--min-actions <n>", "--strategy goal (#424): the minimum actions before the model may conclude — until then an early report, blocked or answerless done is deferred and the run steered to breadth " +
|
|
141
|
-
"(unvisited tabs, detail views, primary forms). Default: 12 (at most half the budget) for an open-ended find-out goal (no --success; \"the main features\", \"what works\", \"every error\", \"explore\"…), none otherwise. Capped by --max-actions (with a warning)", positiveIntArg)
|
|
142
|
-
.option("--min-distinct-states <n>", "--strategy goal (#424): the minimum distinct page states (URL + visible controls) observed before the model may conclude. Default: 5 (scaled to the budget) for an open-ended find-out goal, none otherwise. Capped by the budget (with a warning)", positiveIntArg)
|
|
143
|
-
.option("--stall-timeout <seconds>", "--strategy coverage/exploratory and --feature: end the run inconclusive (stalled) when no step completes within this many seconds (default 120)", positiveNumberArg)
|
|
144
|
-
.addOption(GOAL_RUN_OPTIONS.replyWaitMs())
|
|
145
|
-
.addOption(GOAL_RUN_OPTIONS.replyQuietMs())
|
|
146
|
-
.addOption(GOAL_RUN_OPTIONS.replyCeilingMs())
|
|
147
|
-
.addOption(GOAL_RUN_OPTIONS.replyMaxChars())
|
|
148
|
-
.addOption(GOAL_RUN_OPTIONS.jobWaitMs())
|
|
149
|
-
.addOption(GOAL_RUN_OPTIONS.deny())
|
|
150
|
-
.addOption(GOAL_RUN_OPTIONS.paid())
|
|
151
|
-
.addOption(GOAL_RUN_OPTIONS.allowControl())
|
|
152
|
-
.addOption(GOAL_RUN_OPTIONS.allowDestructive())
|
|
153
|
-
.addOption(GOAL_RUN_OPTIONS.dialogs())
|
|
154
|
-
.option("--allow-writes", "let a find-out goal (no --success check, ended by report) change the app. By default it is read-only: controls that start a write flow " +
|
|
155
|
-
"(checkout, upgrade, create, save, submit…) are refused and the write requests an action fires are blocked, unless the goal itself asks for a change")
|
|
156
|
-
.option("--allow-write <glob>", "a write-request path a read-only find-out goal never blocks (repeatable; ** spans segments; a glob starting with https:// matches " +
|
|
157
|
-
"origin + path, e.g. https://abc.supabase.co/rest/v1/**), beyond the built-in auth-refresh ones " +
|
|
158
|
-
"(**/refresh*, **/token*, **/oauth/**, **/auth/**/refresh*). The app's background writes outside an action always pass", (v, prev) => [...prev, v], [])
|
|
159
|
-
.addOption(GOAL_RUN_OPTIONS.readRpc())
|
|
160
|
-
.option("--real", "use live Jev + OpenRouter gateways (requires keys)", false)
|
|
161
|
-
.option("--fake-ai", "use deterministic fake gateways (pipeline smoke only)", false)
|
|
162
|
-
.option("--jev-provider <provider>", JEV_PROVIDER_FLAG_HELP, jevProviderArg)
|
|
163
|
-
.option("--out <dir>", "directory to write the emitted Recording")
|
|
164
|
-
.option("--file-issues", "file findings as issues (needs a repo: --issue-repo or ~/.jevitate/filing.json); default: drafts only")
|
|
165
|
-
.option("--issue-repo <owner/name>", "the system-under-test repo findings for THIS target are filed to")
|
|
166
|
-
.addOption(GOAL_RUN_OPTIONS.hangReplays())
|
|
167
|
-
.option("--hang-replay-writes", "let hang replays re-send a paid/destructive write the run sent (default: such a hang is reported inconclusive, never replayed)")
|
|
168
|
-
.addOption(GOAL_RUN_OPTIONS.settleIgnore())
|
|
169
|
-
.addOption(GOAL_RUN_OPTIONS.longPollMs())
|
|
170
|
-
.addOption(GOAL_RUN_OPTIONS.apiPrefix())
|
|
171
|
-
.addOption(GOAL_RUN_OPTIONS.ignoreNoProgress())
|
|
172
|
-
.option("--jevitate-repo <owner/name>", "where jevitate engine findings are filed (default matt-cochran/jevitate)")
|
|
173
|
-
.option("--min-control-coverage <ratio>", "adversarial: share of the target's controls (0..1) a run must exercise before 'found nothing' is clean (default 0.25); below it the run is inconclusive", ratioArg)
|
|
174
|
-
.option("--no-require-form-submit", "adversarial: do not require a submitted form for a clean result (default: required when the target has a form)")
|
|
175
|
-
.option("--invariants <file>", "app-declared invariants JSON (repeatable; goal, coverage, exploratory, adversarial, --feature): checked around every action, a violation is a defect (exit 1). Validated before any browser opens; probes are GET/HEAD on an --allow origin only", (v, prev) => [...prev, v], [])
|
|
176
|
-
.option("--log-source <spec>", "backend log source (repeatable; every strategy, incl. usability): file:<path> (tailed from its current end) | docker:<container> (docker logs -f --since 0s) | cmd:<command> (needs --allow-log-cmd). Read-only, operator-declared, never the model's choice. Error/warning lines are correlated to the step they landed during and attached to its transcript evidence, redacted", (v, prev) => [...prev, v], [])
|
|
177
|
-
.addOption(GOAL_RUN_OPTIONS.allowSecretCmd())
|
|
178
|
-
.addOption(GOAL_RUN_OPTIONS.secretCmdAttempts())
|
|
179
|
-
.option("--allow-log-cmd", "opt-in: a --log-source cmd:<command> may run as a subprocess (operator-declared only; refused otherwise)", false)
|
|
180
|
-
.option("--log-defect <level|/regex/>", "backend log lines matching this (repeatable) become a server-log defect: a level (error|warn|info|debug, matched as level>=this) or a /regex/flags/ over the raw line. Its fingerprint is the normalized message (ids/numbers/uuids/timestamps stripped) plus the correlated route; verify-fix re-checks it by re-tailing the same --log-source(s)", (v, prev) => [...prev, v], [])
|
|
181
|
-
.option("--log-quiet-ok <spec>", "compatibility only since 0.8.0 (#420): a --log-source that opens and reads zero lines is now always a healthy oracle (the silence is recorded as serverLogs.quietSources), so this flag no longer changes any outcome. Kept so existing invocations keep working", (v, prev) => [...prev, v], [])
|
|
182
|
-
.option("--log-ignore <regex|substring>", "excludes known-noise backend log lines (repeatable, /regex/flags/ over the raw line or a plain substring) from BOTH correlation and the --log-defect oracle (#169 item 3) — e.g. a periodic background job's own expected error. Counted separately as serverLogs.ignoredLines; never makes --log-quiet-ok unnecessary, since an ignored line still proves the source is being tailed", (v, prev) => [...prev, v], [])
|
|
183
|
-
.option("--log-scope <regex|substring>", "attributes only backend log lines matching this (repeatable, /regex/flags/ or a plain substring, e.g. a tenant id) to the run (#282); the rest count as serverLogs.ignoredLines. For concurrent runs tailing one log. A line carrying one of the run's own correlation ids is in scope", (v, prev) => [...prev, v], [])
|
|
184
|
-
.option("--log-correlation-header <name>", "another request/response header that carries a correlation id (repeatable; built in: traceparent, x-request-id, x-correlation-id, request-id, x-amzn-trace-id, x-b3-traceid, x-cloud-trace-context). A log line carrying a request's id is attached to that exact request and the step that sent it, not by time (#204)", (v, prev) => [...prev, v], [])
|
|
185
|
-
.option("--log-id-pattern </regex/>", "how a correlation id is written in your log lines, when not as trace_id=/request_id=/correlation_id= or a traceparent (repeatable; the first capture group is the id). Once ids correlate, a line with another request's id is never attributed to the run (#204)", (v, prev) => [...prev, v], [])
|
|
186
|
-
.option("--log-triage", "#313: record the run's whole signal timeline (backend lines at every level, the browser's console, page errors, failed requests) to <run>.signals.jsonl, " +
|
|
187
|
-
"and attach to each defect only the lines that relate to it (defects[].relatedLogs): code keeps the lines correlated to its request and prefilters its step's window, " +
|
|
188
|
-
"then, with --real, Jev scores each remaining line's relevance (log text goes to the judgment model, redacted — operator opt-in, never an MCP argument). Needs --log-source")
|
|
189
|
-
.option("--server-log-drain-ms <ms>", "how long to keep tailing --log-source after the run's last action, to catch async backend work that settles after the browser gave up (default 3000)", nonNegativeIntArg)
|
|
190
|
-
.option("--repeat <n>", "run the mission N times, one after another, each in a fresh browser context, and vote (#141): findings seen in fewer than --min-agreement runs are reported as flaky, not counted")
|
|
191
|
-
.option("--min-agreement <k>", "with --repeat: runs a finding (and the outcome) must recur in to count (default: a majority of N)")
|
|
192
|
-
.option("--persona <name=storageState>", "run the same mission once per persona (repeatable), serially, each from its own storageState, and diff them (#143): requests, statuses (a 403 vs 200 is a candidate RBAC finding), controls, outcome", (v, prev) => [...prev, v], [])
|
|
193
|
-
.option("--personas <file>", "personas JSON: {\"<name>\": \"<storageState>\"} or {\"personas\": [{\"name\", \"storageState\", \"login\"?}]} — #427: `login` " +
|
|
194
|
-
"({url, userEnv, passwordEnv, userField?, passwordField?, submit?, success?}, or #449 {api, userEnv, passwordEnv} for an HTTP sign-in endpoint; environment variable NAMES only) re-mints an expired session once. " +
|
|
195
|
-
"A bare --persona <name> is the project's persona of that name (.jevitate/personas.json, same format)")
|
|
196
|
-
.option("--check-overflow", "check the horizontal-overflow (#149) and vertical-clipping (#302: text cut off by a fixed-height box or above the page top) hard signals even at a desktop (>=1024px) viewport — --strategy coverage/exploratory " +
|
|
197
|
-
"(a defect), adversarial (a defect) or usability (a signal finding). " +
|
|
198
|
-
"On by default whenever --viewport/--device emulates a viewport narrower than 1024px")
|
|
199
|
-
.option("--ignore-overflow <selector>", "a CSS selector (repeatable) whose overflow or clipping is intentional — excluded from the horizontal-overflow and vertical-clipping signals, like --ignore-no-progress", (v, prev) => [...prev, v], [])
|
|
200
|
-
.option("--evidence-video", "per defect: replay its minimal repro with captions + the failing step marked, record a masked clip and before/at screenshots (defects[].evidence; linked from drafts)")
|
|
201
|
-
.option("--json", "emit the JSON envelope (default: a human summary)")
|
|
202
|
-
.addHelpText("after", [
|
|
203
|
-
"",
|
|
204
|
-
"Authenticated missions:",
|
|
205
|
-
" --secret only REDACTS a value; it is never typed. Prefer starting logged in: save a Playwright",
|
|
206
|
-
" storageState once (e.g. `npx playwright codegen --save-storage=auth.json <url>`) and pass",
|
|
207
|
-
" --storage-state auth.json. To drive a login/signup form, bind fields to environment variables:",
|
|
208
|
-
" --secret-field 'label=Password=env:APP_PASSWORD' and, for MFA, --totp 'label=Code=env:APP_TOTP_SEED'.",
|
|
209
|
-
" See 'Authenticated missions' in the README.",
|
|
210
|
-
].join("\n"))
|
|
211
|
-
.addHelpText("after", [
|
|
212
|
-
"",
|
|
213
|
-
"Viewport/device emulation (#149):",
|
|
214
|
-
" Default: Playwright's own default viewport (1280x720, desktop, no touch) — nothing narrower",
|
|
215
|
-
" unless --viewport or --device is given (mutually exclusive). --device validates against",
|
|
216
|
-
" Playwright's built-in devices registry (viewport + scale + mobile/touch + UA); an unknown",
|
|
217
|
-
" name is refused before any browser opens. The emulation is recorded on the Recording, so",
|
|
218
|
-
" verify-fix/regression replay reproduce under the SAME device by default.",
|
|
219
|
-
].join("\n"))
|
|
220
|
-
.addHelpText("after", EXPLORE_OUTCOME_HELP)
|
|
221
|
-
.option(TAG_FLAG, TAG_HELP, collectTag, [])
|
|
222
|
-
.action(taggedAction(program, "explore", async function () {
|
|
223
|
-
const o = this.opts();
|
|
224
|
-
// #210: one output rule for every strategy — the envelope with --json, a human summary without.
|
|
225
|
-
const emitExplore = (envelope, exitCode, human = formatMissionHuman) => emitCommandResult(program, envelope, { json: o.json === true, command: "explore", human, ...(exitCode === undefined ? {} : { exitCode }) });
|
|
226
|
-
// #230: an unknown --strategy must be refused before any other required-option message — it
|
|
227
|
-
// would otherwise fall through to the default goal-strategy path and silently run a goal
|
|
228
|
-
// mission. Checked first, ahead of every other validation below.
|
|
229
|
-
if (o.strategy !== undefined && !EXPLORE_STRATEGIES.includes(o.strategy)) {
|
|
230
|
-
emitExplore(fail("E_EXPLORE_ARGS", `unknown strategy ${JSON.stringify(o.strategy)} (one of ${EXPLORE_STRATEGIES.join(", ")})`));
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
// #334: a mistyped --dialogs never silently means "dismiss".
|
|
234
|
-
let dialogs;
|
|
235
|
-
try {
|
|
236
|
-
dialogs = dialogsFromFlags(o);
|
|
237
|
-
}
|
|
238
|
-
catch (err) {
|
|
239
|
-
if (!(err instanceof GoalRunFlagError))
|
|
240
|
-
throw err;
|
|
241
|
-
emitExplore(fail(err.code, err.message));
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
// #427: an unusable --auth-check is a usage error, refused before anything runs.
|
|
245
|
-
let authCheck;
|
|
246
|
-
try {
|
|
247
|
-
authCheck = parseAuthCheck(o.authCheck);
|
|
248
|
-
}
|
|
249
|
-
catch (err) {
|
|
250
|
-
if (!(err instanceof LoginArgsError))
|
|
251
|
-
throw err;
|
|
252
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
// #195: a session file never lands in the repo's .jevitate/ (refused before any run, multi-runs included).
|
|
256
|
-
if (o.saveStorageState !== undefined) {
|
|
257
|
-
try {
|
|
258
|
-
assertSessionFileOutsideProject(o.saveStorageState, "--save-storage-state");
|
|
259
|
-
}
|
|
260
|
-
catch (err) {
|
|
261
|
-
if (!(err instanceof SessionFileInProjectError))
|
|
262
|
-
throw err;
|
|
263
|
-
emitExplore(fail(err.code, err.message));
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
const strategy = o.strategy ?? "goal";
|
|
268
|
-
// #293 journey-anchored exploration: the Journey, step, params and environment are resolved (and
|
|
269
|
-
// refused, exit 64) before anything else — the start URL every later check uses is where the
|
|
270
|
-
// Journey's prefix lands.
|
|
271
|
-
let journeyPrefix;
|
|
272
|
-
/** #399: how the prefix was resolved — re-resolved once `${setup.x}` in a --param is bound (goal + --fixtures). */
|
|
273
|
-
let prefixRequest;
|
|
274
|
-
const anchoredFlags = [o.fromJourney, o.atStep, o.journeysDir, o.env, o.baseUrl].some((v) => v !== undefined);
|
|
275
|
-
if (anchoredFlags || Object.keys(o.param).length > 0) {
|
|
276
|
-
const refuse = (message) => emitExplore(fail("E_EXPLORE_ARGS", message));
|
|
277
|
-
if (o.fromJourney === undefined || o.atStep === undefined) {
|
|
278
|
-
refuse("--from-journey and --at-step go together (and --param, --env, --base-url and --journeys-dir need them)");
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
|
-
if (o.url !== undefined) {
|
|
282
|
-
refuse("--url cannot be combined with --from-journey: the mission starts where the Journey's prefix leaves the page");
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (o.feature !== undefined || !ANCHORED_STRATEGIES.includes(strategy)) {
|
|
286
|
-
refuse(`--from-journey supports --strategy ${ANCHORED_STRATEGIES.join(", ")} (not --feature)`);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (wantsMultiRun(o) || o.actor.length > 0) {
|
|
290
|
-
refuse("--from-journey runs one anchored mission: --repeat, --persona, --personas and --actor are not supported with it (a campaign runs several)");
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
// #293 sweep: `--at-step all|anchors` runs the strategy from EVERY step (or anchor), each in a
|
|
294
|
-
// fresh session with --fixtures restored around it, --max-actions/--max-decisions split evenly
|
|
295
|
-
// over the stop points, and reads them as one deduped report (a one-job campaign).
|
|
296
|
-
// #312: one anchored step with a state restore (--fixtures/--before/--after) on a non-goal
|
|
297
|
-
// strategy runs the same way — as a one-stop campaign, whose runner restores around the run.
|
|
298
|
-
const restoredSingle = !isSweepMode(o.atStep) && strategy !== "goal" && (o.fixtures !== undefined || o.before !== undefined || o.after !== undefined);
|
|
299
|
-
if ((isSweepMode(o.atStep) || restoredSingle) && Object.values(o.param).some((v) => v.includes("${setup."))) {
|
|
300
|
-
refuse("a --param holding ${setup.x} is bound from --fixtures only by a single --strategy goal run; pass the value itself here");
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
if (isSweepMode(o.atStep) || restoredSingle) {
|
|
304
|
-
if (o.real !== true && o.fakeAi !== true) {
|
|
305
|
-
emitExplore(fail("E_AI_SETUP_REQUIRED", "a sweep's missions are model-driven: pass --real or --fake-ai"));
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
const journeysDir = resolveJourneysDir(deps, o.journeysDir);
|
|
309
|
-
const abs = (p) => resolvePath(p);
|
|
310
|
-
const spec = {
|
|
311
|
-
version: 1,
|
|
312
|
-
name: restoredSingle ? `${o.fromJourney} at step ${o.atStep.trim()} (${strategy}, restored)` : `sweep of ${o.fromJourney} (${o.atStep}, ${strategy})`,
|
|
313
|
-
...(o.env === undefined ? {} : { env: o.env }),
|
|
314
|
-
...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }),
|
|
315
|
-
...(o.storageState === undefined ? {} : { storageState: abs(o.storageState) }),
|
|
316
|
-
...(o.fixtures === undefined ? {} : { fixtures: abs(o.fixtures) }),
|
|
317
|
-
...(o.before === undefined ? {} : { before: o.before }),
|
|
318
|
-
...(o.after === undefined ? {} : { after: o.after }),
|
|
319
|
-
discovery: false,
|
|
320
|
-
maxRuns: CAMPAIGN_LIMITS.maxRuns,
|
|
321
|
-
jobs: [
|
|
322
|
-
{
|
|
323
|
-
id: "sweep",
|
|
324
|
-
journey: o.fromJourney,
|
|
325
|
-
params: o.param,
|
|
326
|
-
anchors: restoredSingle ? [o.atStep.trim()] : o.atStep.trim(),
|
|
327
|
-
strategies: [strategy],
|
|
328
|
-
...(o.goal === undefined ? {} : { goal: o.goal }),
|
|
329
|
-
...(o.appClass === undefined ? {} : { appClass: o.appClass }),
|
|
330
|
-
...(o.success.length === 0 ? {} : { success: o.success }),
|
|
331
|
-
...(o.maxActions === undefined ? {} : { maxActions: Number(o.maxActions) }),
|
|
332
|
-
...(o.maxDecisions === undefined ? {} : { maxDecisions: Number(o.maxDecisions) }),
|
|
333
|
-
},
|
|
334
|
-
],
|
|
335
|
-
};
|
|
336
|
-
try {
|
|
337
|
-
const plan = await validateCampaignSpec(spec, resolvePath("explore-sweep.json"), {
|
|
338
|
-
journeysDir,
|
|
339
|
-
allowShellHooks: o.allowShellHooks === true,
|
|
340
|
-
...(o.hookTimeoutMs === undefined ? {} : { hookTimeoutMs: Number(o.hookTimeoutMs) }),
|
|
341
|
-
environmentSeams: environmentSeams(deps),
|
|
342
|
-
});
|
|
343
|
-
const result = await runCampaign(plan, {
|
|
344
|
-
newProgram: () => buildProgram(deps),
|
|
345
|
-
journeysDir,
|
|
346
|
-
missionTargetsDir: resolveMissionTargetsDir(deps),
|
|
347
|
-
...(o.out === undefined ? {} : { outDir: o.out }),
|
|
348
|
-
...(o.real === true ? { real: true } : {}),
|
|
349
|
-
...(o.fakeAi === true ? { fakeAi: true } : {}),
|
|
350
|
-
missionArgs: forwardedArgv(this, SWEEP_OWNED),
|
|
351
|
-
});
|
|
352
|
-
emitExplore(ok(withEngine({ sweep: { journeyId: o.fromJourney, mode: restoredSingle ? "step" : o.atStep.trim(), ...(restoredSingle ? { atStep: o.atStep.trim() } : {}), strategy, stops: plan.totalRuns, budgetPerStop: { maxActions: plan.jobs[0]?.maxActions, ...(plan.jobs[0]?.maxDecisions === undefined ? {} : { maxDecisions: plan.jobs[0].maxDecisions }) } }, ...result })), result.exitCode, formatCampaignHuman);
|
|
353
|
-
}
|
|
354
|
-
catch (err) {
|
|
355
|
-
if (err instanceof CampaignSpecError)
|
|
356
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
357
|
-
else
|
|
358
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
359
|
-
}
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
try {
|
|
363
|
-
const environment = environmentFromFlags({ ...(o.env === undefined ? {} : { env: o.env }), ...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }) }, environmentSeams(deps));
|
|
364
|
-
// The environment's own session applies when --storage-state names none (as `journey run`).
|
|
365
|
-
if (o.storageState === undefined && environment?.storageState !== undefined)
|
|
366
|
-
o.storageState = environment.storageState;
|
|
367
|
-
prefixRequest = {
|
|
368
|
-
dir: resolveJourneysDir(deps, o.journeysDir),
|
|
369
|
-
id: o.fromJourney,
|
|
370
|
-
atStep: o.atStep,
|
|
371
|
-
params: o.param,
|
|
372
|
-
...(environment === undefined ? {} : { environment }),
|
|
373
|
-
...(o.storageState === undefined ? {} : { storageState: o.storageState }),
|
|
374
|
-
dbPath: resolveDbPath(deps),
|
|
375
|
-
environmentFlags: { ...(o.env === undefined ? {} : { env: o.env }), ...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }) },
|
|
376
|
-
};
|
|
377
|
-
journeyPrefix = await resolveJourneyPrefix(prefixRequest);
|
|
378
|
-
}
|
|
379
|
-
catch (err) {
|
|
380
|
-
if (isEnvironmentError(err) || err instanceof JourneyPrefixArgsError)
|
|
381
|
-
emitExplore(fail(err.code, err.message));
|
|
382
|
-
else if (err instanceof UnknownJourneyError)
|
|
383
|
-
emitExplore(fail("E_UNKNOWN_JOURNEY", err.message));
|
|
384
|
-
else if (err instanceof ParamValidationError)
|
|
385
|
-
emitExplore(fail("E_INVALID_PARAMS", err.message));
|
|
386
|
-
else if (err instanceof JourneyRequiresAuthError)
|
|
387
|
-
emitExplore(fail("E_JOURNEY_REQUIRES_AUTH", err.message));
|
|
388
|
-
else
|
|
389
|
-
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
// The prefix types its secret params into the page the mission perceives: they are redacted like
|
|
393
|
-
// --secret. Coverage/exploratory carry no redaction set, so a prefix with secrets refuses them.
|
|
394
|
-
if (journeyPrefix.secrets.length > 0 && (strategy === "coverage" || strategy === "exploratory")) {
|
|
395
|
-
refuse(`journey '${o.fromJourney}' types a secret param before step ${journeyPrefix.branch.step}: --strategy ${strategy} cannot redact it — use goal, adversarial or usability`);
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
o.url = journeyPrefix.startUrl;
|
|
399
|
-
// Default allowlist: the origins the Journey's steps may be on (the environment's), not just the landing's.
|
|
400
|
-
if (o.allow.length === 0)
|
|
401
|
-
o.allow = [...journeyPrefix.allowedOrigins];
|
|
402
|
-
}
|
|
403
|
-
const withPrefix = journeyPrefix === undefined ? {} : { journeyPrefix };
|
|
404
|
-
/** #293: a prefix that no longer replays, or a site policy that refused it — typed, handled once for every strategy. */
|
|
405
|
-
const emitJourneyFailure = (err) => {
|
|
406
|
-
if (err instanceof JourneyPrefixStaleError) {
|
|
407
|
-
emitExplore(ok(withEngine(journeyStaleResult(err, strategy))), EXIT_CODES.inconclusive);
|
|
408
|
-
return true;
|
|
409
|
-
}
|
|
410
|
-
if (err instanceof SiteGateRefusedError) {
|
|
411
|
-
emitExplore(fail(err.code, err.message));
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
return false;
|
|
415
|
-
};
|
|
416
|
-
// #195: `--secret env:VAR` is resolved from the environment before anything runs (fail closed).
|
|
417
|
-
try {
|
|
418
|
-
const resolved = resolveSecretArgs(o.secret, process.env, "--secret");
|
|
419
|
-
if (resolved.literals > 0)
|
|
420
|
-
program.configureOutput().writeErr?.(LITERAL_SECRET_WARNING);
|
|
421
|
-
o.secret = [...resolved.secrets, ...(journeyPrefix?.secrets ?? [])];
|
|
422
|
-
}
|
|
423
|
-
catch (err) {
|
|
424
|
-
if (!(err instanceof SecretArgError))
|
|
425
|
-
throw err;
|
|
426
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
// #245 demo mode: resolved (and a headed run without a display refused, exit 64) before any
|
|
430
|
-
// browser opens — a multi-run's every run re-resolves the same flags.
|
|
431
|
-
let browser;
|
|
432
|
-
try {
|
|
433
|
-
browser = browserRunFromFlags(o, deps.explore?.env ?? process.env);
|
|
434
|
-
// #256: a chrome-extension:// --url must be a loaded extension's; loaded extensions' origins are allowed.
|
|
435
|
-
assertExtensionTargetLoaded(o.url, browser);
|
|
436
|
-
o.allow = allowWithExtensions(o.url, o.allow, browser);
|
|
437
|
-
}
|
|
438
|
-
catch (err) {
|
|
439
|
-
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
// #251: an unusable --screenshots value is a usage error (64), refused before any browser opens.
|
|
443
|
-
let screenshots;
|
|
444
|
-
try {
|
|
445
|
-
screenshots = parseScreenshotsArg(o.screenshots);
|
|
446
|
-
}
|
|
447
|
-
catch (err) {
|
|
448
|
-
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
// #251/#250: the capture options every strategy's runner takes.
|
|
452
|
-
const screenshotsOpt = { ...(screenshots === undefined ? {} : { screenshots }), ...(o.evidenceVideo === true ? { evidenceVideo: true } : {}) };
|
|
453
|
-
if (browser?.headed === true && (wantsMultiRun(o) || o.actor.length > 1)) {
|
|
454
|
-
program.configureOutput().writeErr?.(multiWindowWarning(o.actor.length > 1 ? "several --actor sessions" : "--repeat/--persona"));
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* #427: the pre-flight auth check of one session — undefined when it is alive (or refreshed, or
|
|
458
|
-
* not checked: --auth-check off, no --url, a --from-journey run whose prefix replay is its own
|
|
459
|
-
* check), or the `auth-expired` result when it is not. Throws `UnauthorizedExploreTargetError`.
|
|
460
|
-
*/
|
|
461
|
-
const sessionPreflight = async (storageState, persona) => {
|
|
462
|
-
if (authCheck.mode === "off" || o.url === undefined || journeyPrefix !== undefined)
|
|
463
|
-
return undefined;
|
|
464
|
-
const allowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
465
|
-
// A start URL bound from --fixtures (`${setup.x}`) does not exist before the setup runs: the
|
|
466
|
-
// check opens the app's root instead (the mission itself starts after the setup, as before).
|
|
467
|
-
const checkUrl = o.url.includes("${") ? fixtureBoundUrlRoot(o.url) : o.url;
|
|
468
|
-
if (checkUrl === undefined)
|
|
469
|
-
return undefined; // no origin before the placeholder: the mission's own URL check refuses it
|
|
470
|
-
const r = await ensurePersonaSession({
|
|
471
|
-
...(persona === undefined ? {} : { persona: persona.name }),
|
|
472
|
-
...(persona?.login === undefined ? {} : { login: persona.login }),
|
|
473
|
-
storageState,
|
|
474
|
-
url: checkUrl,
|
|
475
|
-
allowlist,
|
|
476
|
-
check: authCheck,
|
|
477
|
-
port: (deps.explore?.browserPortFactory ?? (() => new PlaywrightBrowserPort()))(),
|
|
478
|
-
...(browser === undefined ? {} : { browser }),
|
|
479
|
-
env: deps.explore?.env ?? process.env,
|
|
480
|
-
timeoutMs: AUTH_PREFLIGHT_TIMEOUT_MS,
|
|
481
|
-
note: (line) => program.configureOutput().writeErr?.(line),
|
|
482
|
-
});
|
|
483
|
-
return r.ok ? undefined : authExpiredResult(strategy, o.url, r, persona?.name);
|
|
484
|
-
};
|
|
485
|
-
// Repeat-and-vote (#141) / persona matrix (#143): the same command, run sequentially and aggregated.
|
|
486
|
-
if (wantsMultiRun(o)) {
|
|
487
|
-
try {
|
|
488
|
-
const plan = resolveMultiRunPlan(o);
|
|
489
|
-
const result = await runExploreMultiRun({
|
|
490
|
-
cmd: this,
|
|
491
|
-
newProgram: () => buildProgram(deps),
|
|
492
|
-
plan,
|
|
493
|
-
strategy,
|
|
494
|
-
...(o.out === undefined ? {} : { out: o.out }),
|
|
495
|
-
// #220: a killed multi-run prints ITS partial summary, by this command's own output rule.
|
|
496
|
-
killOutput: (partial) => (o.json === true ? `${JSON.stringify(ok(withEngine(partial)))}\n` : formatMultiRunHuman(partial)),
|
|
497
|
-
// #427: each run's session (its persona's, or the mission's own) is checked before it starts.
|
|
498
|
-
...(authCheck.mode === "off"
|
|
499
|
-
? {}
|
|
500
|
-
: {
|
|
501
|
-
authPreflight: async (p) => {
|
|
502
|
-
const state = p?.storageState ?? o.storageState;
|
|
503
|
-
if (state === undefined)
|
|
504
|
-
return undefined;
|
|
505
|
-
const r = await sessionPreflight(state, p ?? projectPersonaFor(state));
|
|
506
|
-
return r === undefined ? undefined : withEngine(r);
|
|
507
|
-
},
|
|
508
|
-
}),
|
|
509
|
-
});
|
|
510
|
-
emitExplore(ok(withEngine(result)), result.exitCode, formatMultiRunHuman);
|
|
511
|
-
}
|
|
512
|
-
catch (err) {
|
|
513
|
-
if (err instanceof MultiRunArgsError)
|
|
514
|
-
emitExplore(fail(err.code, err.message));
|
|
515
|
-
else if (err instanceof UnauthorizedExploreTargetError)
|
|
516
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
517
|
-
else if (err instanceof MultiRunAbortedError)
|
|
518
|
-
emitExplore(fail(err.envelope.error.code, err.envelope.error.message));
|
|
519
|
-
else
|
|
520
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
521
|
-
}
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
// #120: a killed run prints what this command would have printed — the envelope with --json,
|
|
525
|
-
// else the human summary (#210) — before it exits.
|
|
526
|
-
setKillSwitchOutput(o.json === true ? "envelope" : "human");
|
|
527
|
-
let conversation;
|
|
528
|
-
try {
|
|
529
|
-
conversation = conversationFromFlags(o);
|
|
530
|
-
}
|
|
531
|
-
catch (err) {
|
|
532
|
-
if (!(err instanceof GoalRunFlagError))
|
|
533
|
-
throw err;
|
|
534
|
-
emitExplore(fail(err.code, err.message));
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
// #154: refused BEFORE any browser opens — `nonNegativeIntArg` already rejects a bad value at
|
|
538
|
-
// parse time (#275: the value is a number here). 0 is valid: "don't replay" — a hang is then
|
|
539
|
-
// reported unconfirmed (inconclusive), never replayed and never a crash.
|
|
540
|
-
try {
|
|
541
|
-
validateDenyPatterns(o.deny);
|
|
542
|
-
validateDenyPatterns(o.paid, "--paid");
|
|
543
|
-
validateAllowControlPatterns(o.allowControl ?? []);
|
|
544
|
-
}
|
|
545
|
-
catch (err) {
|
|
546
|
-
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
547
|
-
return;
|
|
548
|
-
}
|
|
549
|
-
// #149: refused BEFORE any browser opens (an unknown --device, or --viewport + --device together).
|
|
550
|
-
let emulation;
|
|
551
|
-
try {
|
|
552
|
-
emulation = emulationFromFlags(o);
|
|
553
|
-
}
|
|
554
|
-
catch (err) {
|
|
555
|
-
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
const overflow = { checkOverflow: o.checkOverflow ?? false, ignoreSelectors: o.ignoreOverflow };
|
|
559
|
-
// Issue filing: drafts are always written; filing needs --file-issues (or config) AND a repo.
|
|
560
|
-
let filing;
|
|
561
|
-
if (o.url !== undefined) {
|
|
562
|
-
try {
|
|
563
|
-
filing = resolveFilingConfig(loadFilingFileConfig(deps.explore?.filingConfigPath), {
|
|
564
|
-
...(o.fileIssues === undefined ? {} : { fileIssues: o.fileIssues }),
|
|
565
|
-
...(o.issueRepo === undefined ? {} : { issueRepo: o.issueRepo }),
|
|
566
|
-
...(o.jevitateRepo === undefined ? {} : { jevitateRepo: o.jevitateRepo }),
|
|
567
|
-
}, new URL(setupRefFreeUrl(o.url)).origin);
|
|
568
|
-
}
|
|
569
|
-
catch (err) {
|
|
570
|
-
if (err instanceof FilingConfigError) {
|
|
571
|
-
emitExplore(fail(err.code, err.message));
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
if (!(err instanceof TypeError))
|
|
575
|
-
throw err;
|
|
576
|
-
// An unparseable --url is refused by the authorized-target guard below.
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
// Per-target settle/hang configuration: ~/.jevitate/targets.json by origin, plus flags.
|
|
580
|
-
let target;
|
|
581
|
-
if (o.url !== undefined) {
|
|
582
|
-
try {
|
|
583
|
-
target = resolveTargetConfig(loadTargetsFile(deps.explore?.targetsConfigPath), new URL(setupRefFreeUrl(o.url)).origin, targetFlagsFromFlags(o, dialogs));
|
|
584
|
-
}
|
|
585
|
-
catch (err) {
|
|
586
|
-
if (err instanceof TargetConfigError) {
|
|
587
|
-
emitExplore(fail(err.code, err.message));
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
if (!(err instanceof TypeError))
|
|
591
|
-
throw err;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
const issueFiler = deps.explore?.issueFiler ??
|
|
595
|
-
(() => new GitHubIssueFiler({ store: envCredentialStore(process.env, loadLocalCredentials()) }));
|
|
596
|
-
// #250 --evidence-video: the runner attaches each defect's captioned repro clip + key
|
|
597
|
-
// screenshots; drafts are filed only AFTER their media is linked (the run writes drafts only).
|
|
598
|
-
const evidenceOn = o.evidenceVideo === true;
|
|
599
|
-
const runFiling = filing === undefined ? undefined : evidenceOn ? { ...filing, enabled: false } : filing;
|
|
600
|
-
const withEvidence = async (result) => {
|
|
601
|
-
if (!evidenceOn)
|
|
602
|
-
return result;
|
|
603
|
-
let out = result;
|
|
604
|
-
if (filing?.enabled === true)
|
|
605
|
-
out = await fileDraftsWithEvidence(out, filing, issueFiler, clock.nowIso());
|
|
606
|
-
return out;
|
|
607
|
-
};
|
|
608
|
-
// `--fixture` feeds the upload op, which only the explore loop (goal and
|
|
609
|
-
// usability strategies) can issue. Refuse it elsewhere rather than
|
|
610
|
-
// silently ignoring a file the user expected to be uploaded.
|
|
611
|
-
// #424: the minimum effort gates the goal loop's own endings (report / blocked / done) — a goal run's.
|
|
612
|
-
if ((o.minActions !== undefined || o.minDistinctStates !== undefined) && (o.feature !== undefined || strategy !== "goal")) {
|
|
613
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--min-actions and --min-distinct-states are supported only with --strategy goal (a goal or find-out run)"));
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (o.fixture !== undefined && (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability"))) {
|
|
617
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--fixture is supported only with --strategy goal or usability"));
|
|
618
|
-
return;
|
|
619
|
-
}
|
|
620
|
-
// #198: product facts and polish shape the UX review's findings only.
|
|
621
|
-
if ((o.product !== undefined || o.polish === true || o.probeGuards === true) && (o.feature !== undefined || strategy !== "usability")) {
|
|
622
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--product, --polish and --probe-guards are supported only with --strategy usability"));
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
// #225: success checks judge a goal / a usability job — every other strategy (and --feature) would
|
|
626
|
-
// silently ignore them, so they are refused up front, never dropped.
|
|
627
|
-
if ((o.success.length > 0 || o.successWhen !== undefined || o.allowVacuousChecks === true) &&
|
|
628
|
-
(o.feature !== undefined || (strategy !== "goal" && strategy !== "usability"))) {
|
|
629
|
-
emitExplore(fail("E_EXPLORE_ARGS", `--success, --success-when and --allow-vacuous-checks are supported only with --strategy goal or usability (not ${o.feature !== undefined ? "--feature" : `--strategy ${strategy}`})`));
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
// #303: action deltas are recorded by the goal loop (goal and usability runs) only — refused
|
|
633
|
-
// elsewhere, never silently ignored.
|
|
634
|
-
if (o.actionDeltas === true && o.feature !== undefined) {
|
|
635
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--action-deltas is not supported with --feature (goal, usability, coverage, exploratory and adversarial runs record deltas)"));
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
// #427: the project persona (.jevitate/personas.json) this --storage-state is, when it is one — its
|
|
639
|
-
// session may not exist yet: the pre-flight mints it from its login parameters.
|
|
640
|
-
let projectPersona;
|
|
641
|
-
try {
|
|
642
|
-
projectPersona = o.storageState === undefined ? undefined : projectPersonaFor(o.storageState);
|
|
643
|
-
}
|
|
644
|
-
catch (err) {
|
|
645
|
-
if (!(err instanceof MultiRunArgsError))
|
|
646
|
-
throw err;
|
|
647
|
-
emitExplore(fail(err.code, err.message));
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
if (o.storageState !== undefined && !existsSync(o.storageState) && (authCheck.mode === "off" || projectPersona?.login === undefined)) {
|
|
651
|
-
emitExplore(fail("E_EXPLORE_ARGS", `storage state not found: ${o.storageState}`));
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
// Multi-actor missions (#147): the first --actor is the primary, the rest are observers.
|
|
655
|
-
let actors;
|
|
656
|
-
try {
|
|
657
|
-
actors = resolveMissionActors(o.actor);
|
|
658
|
-
}
|
|
659
|
-
catch (err) {
|
|
660
|
-
if (!(err instanceof MultiRunArgsError))
|
|
661
|
-
throw err;
|
|
662
|
-
emitExplore(fail(err.code, err.message));
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
if (actors !== null) {
|
|
666
|
-
if (o.feature !== undefined || strategy !== "goal") {
|
|
667
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--actor is supported only with --strategy goal"));
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
if (o.storageState !== undefined) {
|
|
671
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--storage-state cannot be combined with --actor (the first --actor is the primary's session)"));
|
|
672
|
-
return;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
// The primary's session: its --actor state, else --storage-state.
|
|
676
|
-
const primaryStorageState = actors?.primary.storageState ?? o.storageState;
|
|
677
|
-
// App-declared invariants (#86): validated (schema, observables, probe origins) BEFORE any browser.
|
|
678
|
-
let invariants;
|
|
679
|
-
// #135: authFrom.secret refs (env:VAR), resolved from the environment HERE — the one place this
|
|
680
|
-
// package reads process.env for invariants — never inside @jevitate/explore or @jevitate/recording.
|
|
681
|
-
let invariantAuthTokens;
|
|
682
|
-
if (o.invariants.length > 0) {
|
|
683
|
-
if (o.url !== undefined) {
|
|
684
|
-
try {
|
|
685
|
-
const loaded = loadInvariantFiles(o.invariants, {
|
|
686
|
-
allowlist: resolveExploreAllowlist(o.url, o.allow),
|
|
687
|
-
baseUrl: o.url,
|
|
688
|
-
observers: actors?.observers.map((a) => a.name) ?? [],
|
|
689
|
-
});
|
|
690
|
-
invariants = loaded;
|
|
691
|
-
invariantAuthTokens = loaded === undefined ? undefined : resolveInvariantAuthTokens(loaded, process.env);
|
|
692
|
-
checkActorsAgainstSpec(actors, loaded);
|
|
693
|
-
}
|
|
694
|
-
catch (err) {
|
|
695
|
-
if (err instanceof MultiRunArgsError) {
|
|
696
|
-
emitExplore(fail(err.code, err.message));
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
if (!(err instanceof InvariantsFileError))
|
|
700
|
-
throw err;
|
|
701
|
-
emitExplore(fail(err.code, err.message));
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
// #147: captures and cross-actor checks run in the goal loop only — never silently skipped elsewhere.
|
|
706
|
-
if (invariants?.capture !== undefined && (o.feature !== undefined || strategy !== "goal")) {
|
|
707
|
-
emitExplore(fail("E_EXPLORE_ARGS", "invariants with capture (cross-actor checks) are supported only with --strategy goal"));
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
const withInvariants = {
|
|
712
|
-
...(invariants === undefined ? {} : { invariants }),
|
|
713
|
-
...(invariantAuthTokens === undefined || invariantAuthTokens.size === 0 ? {} : { invariantAuthTokens }),
|
|
714
|
-
};
|
|
715
|
-
// Backend log sources (#142): validated (spec shape, --allow-log-cmd gate, matcher regexes)
|
|
716
|
-
// BEFORE any browser opens — the same fail-closed discipline as --invariants above. Supported
|
|
717
|
-
// on every strategy, INCLUDING usability (#142 follow-up): lines attach to usability steps the
|
|
718
|
-
// same way, though a UX run's own outcome stays advisory (a server-log defect is still reported,
|
|
719
|
-
// never gates the exit code — the same rule as every other UX finding).
|
|
720
|
-
let serverLog;
|
|
721
|
-
if (o.logSource.length > 0 || o.logDefect.length > 0) {
|
|
722
|
-
try {
|
|
723
|
-
const sources = parseLogSourceSpecs(o.logSource, o.allowLogCmd ?? false);
|
|
724
|
-
const logDefect = parseLogDefectSpecs(o.logDefect);
|
|
725
|
-
const logIgnore = parseLogIgnoreSpecs(o.logIgnore);
|
|
726
|
-
const logScope = parseLogScopeSpecs(o.logScope);
|
|
727
|
-
const correlationHeaders = parseCorrelationHeaders(o.logCorrelationHeader);
|
|
728
|
-
const idPatterns = parseLogIdPatterns(o.logIdPattern);
|
|
729
|
-
// #422: the project's .jevitate/log-classes.json, validated before any browser opens.
|
|
730
|
-
const logClasses = loadLogClassRules();
|
|
731
|
-
serverLog = {
|
|
732
|
-
sources,
|
|
733
|
-
logDefect,
|
|
734
|
-
allowLogCmd: o.allowLogCmd ?? false,
|
|
735
|
-
quietOk: o.logQuietOk,
|
|
736
|
-
logIgnore,
|
|
737
|
-
logScope,
|
|
738
|
-
correlationHeaders,
|
|
739
|
-
idPatterns,
|
|
740
|
-
logClasses,
|
|
741
|
-
...(o.serverLogDrainMs === undefined ? {} : { drainMs: Number(o.serverLogDrainMs) }),
|
|
742
|
-
};
|
|
743
|
-
}
|
|
744
|
-
catch (err) {
|
|
745
|
-
if (err instanceof LogSourceSpecError || err instanceof LogSpecError || err instanceof LogClassesError) {
|
|
746
|
-
emitExplore(fail(err.code, err.message));
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
throw err;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
if (o.logTriage === true) {
|
|
753
|
-
if (serverLog === undefined) {
|
|
754
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--log-triage triages the run's backend and browser signals: it needs at least one --log-source"));
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
serverLog = { ...serverLog, triage: {} };
|
|
758
|
-
}
|
|
759
|
-
const withServerLog = serverLog === undefined ? {} : { serverLog };
|
|
760
|
-
// Secret field bindings (#72): resolved from the environment here, typed by code in the goal loop.
|
|
761
|
-
let secretFields = [];
|
|
762
|
-
if (o.secretField.length > 0 || o.totp.length > 0) {
|
|
763
|
-
if (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability")) {
|
|
764
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--secret-field and --totp are supported only with --strategy goal or usability"));
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
try {
|
|
768
|
-
secretFields = secretFieldsFromFlags(o);
|
|
769
|
-
}
|
|
770
|
-
catch (err) {
|
|
771
|
-
if (!(err instanceof GoalRunFlagError))
|
|
772
|
-
throw err;
|
|
773
|
-
emitExplore(fail(err.code, err.message));
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
// #324: a cmd: binding's command runs at type time; its output becomes a run secret at once.
|
|
778
|
-
const secretCommand = secretFields.some((f) => f.kind === "cmd") ? secretCommandRunner() : undefined;
|
|
779
|
-
// #281: fields typed with a file's exact text — read (and checked) before any browser opens.
|
|
780
|
-
let typeFixtures = [];
|
|
781
|
-
if (o.typeFixture.length > 0) {
|
|
782
|
-
if (o.feature !== undefined || strategy !== "goal") {
|
|
783
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--type-fixture is supported only with --strategy goal"));
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
try {
|
|
787
|
-
typeFixtures = loadTypeFixtures(o.typeFixture);
|
|
788
|
-
}
|
|
789
|
-
catch (err) {
|
|
790
|
-
if (!(err instanceof TypeFixtureSpecError))
|
|
791
|
-
throw err;
|
|
792
|
-
emitExplore(fail(err.code, err.message));
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
// Mission fixtures (#140/#144) run around the goal loop and its replays only.
|
|
797
|
-
const fixtureFlagsGiven = o.fixtures !== undefined || o.before !== undefined || o.after !== undefined;
|
|
798
|
-
if (fixtureFlagsGiven && (o.feature !== undefined || strategy !== "goal")) {
|
|
799
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--fixtures, --before and --after are supported only with --strategy goal, or with --from-journey (any anchored strategy)"));
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
// #427 pre-flight auth check: a run that starts from a session first proves the session is alive
|
|
803
|
-
// (re-minting it once from a persona's login parameters) — else it ends now, `auth-expired`.
|
|
804
|
-
if (primaryStorageState !== undefined) {
|
|
805
|
-
let expired;
|
|
806
|
-
try {
|
|
807
|
-
expired = await sessionPreflight(primaryStorageState, actors?.primary ?? projectPersona);
|
|
808
|
-
}
|
|
809
|
-
catch (err) {
|
|
810
|
-
if (err instanceof UnauthorizedExploreTargetError)
|
|
811
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
812
|
-
// The check could not open its browser (the mission could not have either): the run did not start.
|
|
813
|
-
else
|
|
814
|
-
emitExplore(fail("E_EXPLORE_RUN", `the pre-flight auth check failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
if (expired !== undefined) {
|
|
818
|
-
emitExplore(ok(withEngine(expired)), EXIT_CODES.inconclusive);
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
// Additive coverage/exploratory strategy: proof-by-induction state coverage.
|
|
823
|
-
// It takes no goal/success (the frontier itself is the objective), so it is
|
|
824
|
-
// a distinct, goal-free path that leaves the goal strategy below unchanged.
|
|
825
|
-
if (strategy === "coverage" || strategy === "exploratory") {
|
|
826
|
-
if (!o.url) {
|
|
827
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--url is required"));
|
|
828
|
-
return;
|
|
829
|
-
}
|
|
830
|
-
if (o.scope !== undefined && o.scope !== "app") {
|
|
831
|
-
emitExplore(fail("E_EXPLORE_ARGS", `--scope must be "app" (got ${JSON.stringify(o.scope)})`));
|
|
832
|
-
return;
|
|
833
|
-
}
|
|
834
|
-
const covAllowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
835
|
-
const covBounds = {};
|
|
836
|
-
if (o.maxActions !== undefined)
|
|
837
|
-
covBounds.maxActions = Number(o.maxActions);
|
|
838
|
-
if (o.maxDecisions !== undefined)
|
|
839
|
-
covBounds.maxDecisions = Number(o.maxDecisions);
|
|
840
|
-
// Scope containment (#89, reusing #64's model): the start URL's route plus --route globs;
|
|
841
|
-
// --scope app (or --route '/**') widens it to the whole app.
|
|
842
|
-
const covRouteGlobs = [...o.route, ...(o.scope === "app" ? ["/**"] : [])];
|
|
843
|
-
const covStall = stallTimeoutMs(o.stallTimeout);
|
|
844
|
-
if (covStall === null) {
|
|
845
|
-
emitExplore(fail("E_EXPLORE_ARGS", `--stall-timeout must be a positive number of seconds (got ${JSON.stringify(o.stallTimeout)})`));
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
let covJudge;
|
|
849
|
-
let covGen;
|
|
850
|
-
let covUsage;
|
|
851
|
-
try {
|
|
852
|
-
({ judge: covJudge, gen: covGen, usage: covUsage } = await buildExploreGateways(deps, {
|
|
853
|
-
real: o.real ?? false,
|
|
854
|
-
fakeAi: o.fakeAi ?? false,
|
|
855
|
-
jevProvider: o.jevProvider,
|
|
856
|
-
}));
|
|
857
|
-
}
|
|
858
|
-
catch (err) {
|
|
859
|
-
if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
|
|
860
|
-
emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
|
|
861
|
-
}
|
|
862
|
-
else {
|
|
863
|
-
emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
|
|
864
|
-
}
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
try {
|
|
868
|
-
const result = await runCoverageMission({
|
|
869
|
-
...(target === undefined ? {} : { target }),
|
|
870
|
-
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
871
|
-
url: o.url,
|
|
872
|
-
allowlist: covAllowlist,
|
|
873
|
-
judge: covJudge,
|
|
874
|
-
gen: covGen,
|
|
875
|
-
usage: covUsage,
|
|
876
|
-
bounds: Object.keys(covBounds).length > 0 ? covBounds : undefined,
|
|
877
|
-
...(covRouteGlobs.length > 0 ? { routeGlobs: covRouteGlobs } : {}),
|
|
878
|
-
strategy,
|
|
879
|
-
...(covStall === undefined ? {} : { stallTimeoutMs: covStall }),
|
|
880
|
-
outDir: o.out,
|
|
881
|
-
browserPortFactory: deps.explore?.browserPortFactory,
|
|
882
|
-
browser,
|
|
883
|
-
...screenshotsOpt,
|
|
884
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
885
|
-
overflow,
|
|
886
|
-
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
887
|
-
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
888
|
-
...withInvariants,
|
|
889
|
-
...triagedServerLog(serverLog, covJudge, o.real === true),
|
|
890
|
-
...withPrefix,
|
|
891
|
-
});
|
|
892
|
-
// Typed verdict → exit code (0 clean · 1 defects · 2 crashed; see exit-codes.ts).
|
|
893
|
-
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
894
|
-
}
|
|
895
|
-
catch (err) {
|
|
896
|
-
if (emitJourneyFailure(err))
|
|
897
|
-
return;
|
|
898
|
-
if (err instanceof UnauthorizedExploreTargetError) {
|
|
899
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
900
|
-
}
|
|
901
|
-
else if (err instanceof ScopeUnderivableError) {
|
|
902
|
-
// #224: no default route scope from --url — a usage error (64), refused before any browser.
|
|
903
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
904
|
-
}
|
|
905
|
-
else {
|
|
906
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
// Additive adversarial strategy: a bounded "try to break it" run whose
|
|
912
|
-
// stop decision comes from a trusted hard-signal oracle (never Jev's
|
|
913
|
-
// Noul). Requires only --url; --goal/--success are goal-strategy inputs.
|
|
914
|
-
if (strategy === "adversarial") {
|
|
915
|
-
if (!o.url) {
|
|
916
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--url is required for --strategy adversarial"));
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
const advAllowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
920
|
-
let advJudge;
|
|
921
|
-
let advGen;
|
|
922
|
-
let advUsage;
|
|
923
|
-
try {
|
|
924
|
-
({ judge: advJudge, gen: advGen, usage: advUsage } = await buildExploreGateways(deps, {
|
|
925
|
-
real: o.real ?? false,
|
|
926
|
-
fakeAi: o.fakeAi ?? false,
|
|
927
|
-
jevProvider: o.jevProvider,
|
|
928
|
-
}));
|
|
929
|
-
}
|
|
930
|
-
catch (err) {
|
|
931
|
-
if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
|
|
932
|
-
emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
935
|
-
emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
|
|
936
|
-
}
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
let coverageThresholds;
|
|
940
|
-
try {
|
|
941
|
-
coverageThresholds = resolveCoverageThresholds({
|
|
942
|
-
...(o.minControlCoverage === undefined ? {} : { minControlRatio: Number(o.minControlCoverage) }),
|
|
943
|
-
requireFormSubmit: o.requireFormSubmit,
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
catch (err) {
|
|
947
|
-
emitExplore(fail("E_EXPLORE_ARGS", String(err instanceof Error ? err.message : err)));
|
|
948
|
-
return;
|
|
949
|
-
}
|
|
950
|
-
const advBounds = {};
|
|
951
|
-
if (o.maxActions !== undefined)
|
|
952
|
-
advBounds.maxActions = Number(o.maxActions);
|
|
953
|
-
if (o.maxDecisions !== undefined)
|
|
954
|
-
advBounds.maxDecisions = Number(o.maxDecisions);
|
|
955
|
-
try {
|
|
956
|
-
const result = await runAdversarialCliMission({
|
|
957
|
-
...(target === undefined ? {} : { target }),
|
|
958
|
-
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
959
|
-
seedUrl: o.url,
|
|
960
|
-
allowlist: advAllowlist,
|
|
961
|
-
usage: advUsage,
|
|
962
|
-
...(o.route.length > 0 ? { routeGlobs: o.route } : {}),
|
|
963
|
-
coverageThresholds,
|
|
964
|
-
bounds: Object.keys(advBounds).length > 0 ? advBounds : undefined,
|
|
965
|
-
secrets: o.secret.length > 0 ? o.secret : undefined,
|
|
966
|
-
...(runFiling === undefined ? {} : { filing: runFiling }),
|
|
967
|
-
issueFiler,
|
|
968
|
-
...(o.hangReplays === undefined ? {} : { hangReplays: o.hangReplays }),
|
|
969
|
-
strategies: CLI_ADVERSARIAL_STRATEGIES,
|
|
970
|
-
judgment: advJudge,
|
|
971
|
-
generation: advGen,
|
|
972
|
-
browserPortFactory: deps.explore?.browserPortFactory,
|
|
973
|
-
browser,
|
|
974
|
-
...screenshotsOpt,
|
|
975
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
976
|
-
overflow,
|
|
977
|
-
outDir: o.out,
|
|
978
|
-
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
979
|
-
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
980
|
-
...withInvariants,
|
|
981
|
-
...triagedServerLog(serverLog, advJudge, o.real === true),
|
|
982
|
-
...withPrefix,
|
|
983
|
-
});
|
|
984
|
-
// The typed verdict gates CI: 0 clean · 1 defects found (a failing check) · 2 the run
|
|
985
|
-
// itself broke (inconclusive/crashed) — see exit-codes.ts.
|
|
986
|
-
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
987
|
-
}
|
|
988
|
-
catch (err) {
|
|
989
|
-
if (emitJourneyFailure(err))
|
|
990
|
-
return;
|
|
991
|
-
if (err instanceof UnauthorizedExploreTargetError) {
|
|
992
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
993
|
-
}
|
|
994
|
-
else if (err instanceof ScopeUnderivableError) {
|
|
995
|
-
// #224: no default route scope from --url — a usage error (64), refused before any browser.
|
|
996
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
997
|
-
}
|
|
998
|
-
else {
|
|
999
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
// Additive: `--strategy usability` (issue #30) — a UX review. Reuses the
|
|
1005
|
-
// explore loop (goal = the job) and analyzes each observed screen against
|
|
1006
|
-
// the cited @jevitate/ux rubric. Findings are ADVISORY: a UX finding never
|
|
1007
|
-
// gates the run (no non-zero exit).
|
|
1008
|
-
if (strategy === "usability") {
|
|
1009
|
-
if (!o.url || !o.goal) {
|
|
1010
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--url and --goal (the job) are required for --strategy usability"));
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
if (!o.appClass) {
|
|
1014
|
-
emitExplore(fail("E_UX_ARGS", "--app-class is required for --strategy usability"));
|
|
1015
|
-
return;
|
|
1016
|
-
}
|
|
1017
|
-
// #225: --success is never ignored — an independent completion check on the job, parsed and
|
|
1018
|
-
// validated exactly as for --strategy goal (same kinds, same --success-when / vacuous rules).
|
|
1019
|
-
let uxSuccessChecks;
|
|
1020
|
-
try {
|
|
1021
|
-
uxSuccessChecks = o.success.map(parseSuccessSpec);
|
|
1022
|
-
}
|
|
1023
|
-
catch (err) {
|
|
1024
|
-
emitExplore(fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
if (o.successWhen !== undefined && o.successWhen !== "held" && o.successWhen !== "final" && o.successWhen !== "each") {
|
|
1028
|
-
emitExplore(fail("E_EXPLORE_ARGS", `--success-when must be "final", "held" or "each", got ${JSON.stringify(o.successWhen)}`));
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
if (uxSuccessChecks.length === 0 && (o.successWhen !== undefined || o.allowVacuousChecks === true)) {
|
|
1032
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--success-when and --allow-vacuous-checks need at least one --success check"));
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
const uxAllowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
1036
|
-
const uxBounds = {};
|
|
1037
|
-
if (o.maxActions !== undefined)
|
|
1038
|
-
uxBounds.maxActions = Number(o.maxActions);
|
|
1039
|
-
if (o.maxDecisions !== undefined)
|
|
1040
|
-
uxBounds.maxDecisions = Number(o.maxDecisions);
|
|
1041
|
-
let uxJudge;
|
|
1042
|
-
let uxGen;
|
|
1043
|
-
let uxUsage;
|
|
1044
|
-
try {
|
|
1045
|
-
({ judge: uxJudge, gen: uxGen, usage: uxUsage } = await buildExploreGateways(deps, {
|
|
1046
|
-
real: o.real ?? false,
|
|
1047
|
-
fakeAi: o.fakeAi ?? false,
|
|
1048
|
-
jevProvider: o.jevProvider,
|
|
1049
|
-
}));
|
|
1050
|
-
}
|
|
1051
|
-
catch (err) {
|
|
1052
|
-
if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
|
|
1053
|
-
emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
|
|
1054
|
-
}
|
|
1055
|
-
else {
|
|
1056
|
-
emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
|
|
1057
|
-
}
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
try {
|
|
1061
|
-
const result = await runUsabilityMission({
|
|
1062
|
-
...(target === undefined ? {} : { target }),
|
|
1063
|
-
url: o.url,
|
|
1064
|
-
job: o.goal,
|
|
1065
|
-
allowlist: uxAllowlist,
|
|
1066
|
-
appContext: { appClass: o.appClass, job: o.goal },
|
|
1067
|
-
judge: uxJudge,
|
|
1068
|
-
gen: uxGen,
|
|
1069
|
-
usage: uxUsage,
|
|
1070
|
-
...(o.minConfidence !== undefined ? { minConfidence: o.minConfidence } : {}),
|
|
1071
|
-
...(o.show !== undefined ? { show: o.show } : {}),
|
|
1072
|
-
...(o.maxFindingsPerPage !== undefined ? { maxFindingsPerRoute: o.maxFindingsPerPage } : {}),
|
|
1073
|
-
...(o.product !== undefined ? { product: o.product } : {}),
|
|
1074
|
-
...(o.polish === true ? { polish: true } : {}),
|
|
1075
|
-
...(o.probeGuards === true ? { probeGuards: true } : {}),
|
|
1076
|
-
bounds: Object.keys(uxBounds).length > 0 ? uxBounds : undefined,
|
|
1077
|
-
conversation,
|
|
1078
|
-
secrets: o.secret.length > 0 ? o.secret : undefined,
|
|
1079
|
-
...(secretFields.length > 0 ? { secretFields } : {}),
|
|
1080
|
-
...(secretCommand === undefined ? {} : { secretCommand }),
|
|
1081
|
-
...(o.secretCmdAttempts === undefined ? {} : { secretCommandAttempts: o.secretCmdAttempts }),
|
|
1082
|
-
fixture: o.fixture,
|
|
1083
|
-
outDir: o.out,
|
|
1084
|
-
browserPortFactory: deps.explore?.browserPortFactory,
|
|
1085
|
-
browser,
|
|
1086
|
-
...screenshotsOpt,
|
|
1087
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
1088
|
-
overflow,
|
|
1089
|
-
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
1090
|
-
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
1091
|
-
...triagedServerLog(serverLog, uxJudge, o.real === true),
|
|
1092
|
-
...withInvariants,
|
|
1093
|
-
...(uxSuccessChecks.length === 0 ? {} : { successChecks: uxSuccessChecks }),
|
|
1094
|
-
...(o.successWhen === "held" || o.successWhen === "final" || o.successWhen === "each" ? { successWhen: o.successWhen } : {}),
|
|
1095
|
-
...(o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {}),
|
|
1096
|
-
...withPrefix,
|
|
1097
|
-
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
1098
|
-
});
|
|
1099
|
-
// UX findings are advisory (0); a failed --success check (#225) is 1, as on a goal run; a
|
|
1100
|
-
// broken run or an unavailable analysis is 2.
|
|
1101
|
-
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
1102
|
-
}
|
|
1103
|
-
catch (err) {
|
|
1104
|
-
if (emitJourneyFailure(err))
|
|
1105
|
-
return;
|
|
1106
|
-
if (err instanceof UnauthorizedExploreTargetError) {
|
|
1107
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
1108
|
-
}
|
|
1109
|
-
else if (err instanceof ScopeUnderivableError) {
|
|
1110
|
-
// #224: no default route scope from --url — a usage error (64), refused before any browser.
|
|
1111
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
1112
|
-
}
|
|
1113
|
-
else if (err instanceof FixtureNotFoundError) {
|
|
1114
|
-
emitExplore(fail("E_EXPLORE_FIXTURE", err.message));
|
|
1115
|
-
}
|
|
1116
|
-
else if (err instanceof MinConfidenceError || err instanceof QualityPolicyError || err instanceof MaxFindingsPerRouteError || err instanceof UxConfigError) {
|
|
1117
|
-
emitExplore(fail("E_UX_ARGS", err.message));
|
|
1118
|
-
}
|
|
1119
|
-
else if (err instanceof UsabilityInvariantsUnsupportedError) {
|
|
1120
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
1121
|
-
}
|
|
1122
|
-
else if (err instanceof ProductFactsError) {
|
|
1123
|
-
emitExplore(fail(err.code, err.message));
|
|
1124
|
-
}
|
|
1125
|
-
else {
|
|
1126
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
// Additive: `--feature <name>` runs the capability-scoped feature-testing
|
|
1132
|
-
// mission (ticket #2 / site #11). It is model-free, so it needs neither
|
|
1133
|
-
// --goal/--success nor a gateway selection; the goal-based path below is
|
|
1134
|
-
// untouched when --feature is absent.
|
|
1135
|
-
if (o.feature) {
|
|
1136
|
-
if (!o.url) {
|
|
1137
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--url is required with --feature"));
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
const featAllowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
1141
|
-
const featBounds = {};
|
|
1142
|
-
if (o.maxActions !== undefined)
|
|
1143
|
-
featBounds.maxActions = Number(o.maxActions);
|
|
1144
|
-
if (o.maxDecisions !== undefined)
|
|
1145
|
-
featBounds.maxDecisions = Number(o.maxDecisions);
|
|
1146
|
-
const featStall = stallTimeoutMs(o.stallTimeout);
|
|
1147
|
-
if (featStall === null) {
|
|
1148
|
-
emitExplore(fail("E_EXPLORE_ARGS", `--stall-timeout must be a positive number of seconds (got ${JSON.stringify(o.stallTimeout)})`));
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
try {
|
|
1152
|
-
const result = await runFeatureCliMission({
|
|
1153
|
-
...(featStall === undefined ? {} : { stallTimeoutMs: featStall }),
|
|
1154
|
-
seedUrl: o.url,
|
|
1155
|
-
allowlist: featAllowlist,
|
|
1156
|
-
capability: o.feature,
|
|
1157
|
-
routeGlobs: o.route ?? [],
|
|
1158
|
-
bounds: Object.keys(featBounds).length > 0 ? featBounds : undefined,
|
|
1159
|
-
outDir: o.out,
|
|
1160
|
-
browserPortFactory: deps.explore?.browserPortFactory,
|
|
1161
|
-
browser,
|
|
1162
|
-
...screenshotsOpt,
|
|
1163
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
1164
|
-
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
1165
|
-
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
1166
|
-
...withInvariants,
|
|
1167
|
-
...withServerLog,
|
|
1168
|
-
...(target?.safety === undefined ? {} : { safety: target.safety }),
|
|
1169
|
-
});
|
|
1170
|
-
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
1171
|
-
}
|
|
1172
|
-
catch (err) {
|
|
1173
|
-
if (err instanceof UnauthorizedExploreTargetError) {
|
|
1174
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
1175
|
-
}
|
|
1176
|
-
else if (err instanceof ScopeUnderivableError) {
|
|
1177
|
-
// #224: no default route scope from --url — a usage error (64), refused before any browser.
|
|
1178
|
-
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
1179
|
-
}
|
|
1180
|
-
else {
|
|
1181
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
1182
|
-
}
|
|
1183
|
-
}
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
// --success may be omitted for a find-out goal (#130d): the run is then verified by a grounded
|
|
1187
|
-
// `report` answer (#101) instead of an independent page/network check.
|
|
1188
|
-
if (!o.url || !o.goal) {
|
|
1189
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--url and --goal are required"));
|
|
1190
|
-
return;
|
|
1191
|
-
}
|
|
1192
|
-
let successChecks;
|
|
1193
|
-
try {
|
|
1194
|
-
successChecks = o.success.map(parseSuccessSpec);
|
|
1195
|
-
}
|
|
1196
|
-
catch (err) {
|
|
1197
|
-
emitExplore(fail("E_EXPLORE_ASSERTION", String(err instanceof Error ? err.message : err)));
|
|
1198
|
-
return;
|
|
1199
|
-
}
|
|
1200
|
-
let successWhen;
|
|
1201
|
-
try {
|
|
1202
|
-
successWhen = successWhenFromFlags(o);
|
|
1203
|
-
}
|
|
1204
|
-
catch (err) {
|
|
1205
|
-
if (!(err instanceof GoalRunFlagError))
|
|
1206
|
-
throw err;
|
|
1207
|
-
emitExplore(fail(err.code, err.message));
|
|
1208
|
-
return;
|
|
1209
|
-
}
|
|
1210
|
-
const allowlist = resolveExploreAllowlist(setupRefFreeUrl(o.url), o.allow);
|
|
1211
|
-
// Fixtures (#140/#144): the spec and every ${setup.x} reference are validated here, before any
|
|
1212
|
-
// browser or request; the setup itself runs just before the mission (below).
|
|
1213
|
-
let fx;
|
|
1214
|
-
try {
|
|
1215
|
-
checkUrlRefOrigin(o.url);
|
|
1216
|
-
fx = buildMissionFixtures(o, {
|
|
1217
|
-
allowlist,
|
|
1218
|
-
baseUrl: setupRefFreeUrl(o.url),
|
|
1219
|
-
...(primaryStorageState === undefined ? {} : { storageState: primaryStorageState }),
|
|
1220
|
-
secretFields,
|
|
1221
|
-
secrets: o.secret,
|
|
1222
|
-
...(target?.fixtures === undefined ? {} : { targetFixtures: target.fixtures }),
|
|
1223
|
-
...(target?.personas === undefined ? {} : { personas: target.personas }),
|
|
1224
|
-
});
|
|
1225
|
-
checkSetupRefs({ "--url": o.url, "--goal": o.goal, "--success": o.success, ...invariantSetupTexts(invariants), ...(journeyPrefix === undefined ? {} : { "--param": Object.values(o.param) }) }, fx);
|
|
1226
|
-
}
|
|
1227
|
-
catch (err) {
|
|
1228
|
-
if (!(err instanceof FixtureSpecError || err instanceof UnboundSetupRefError))
|
|
1229
|
-
throw err;
|
|
1230
|
-
emitExplore(fail(err.code, err.message));
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
const bounds = {};
|
|
1234
|
-
if (o.maxActions !== undefined)
|
|
1235
|
-
bounds.maxActions = Number(o.maxActions);
|
|
1236
|
-
if (o.maxDecisions !== undefined)
|
|
1237
|
-
bounds.maxDecisions = Number(o.maxDecisions);
|
|
1238
|
-
let judge;
|
|
1239
|
-
let gen;
|
|
1240
|
-
let usage;
|
|
1241
|
-
try {
|
|
1242
|
-
({ judge, gen, usage } = await buildExploreGateways(deps, { real: o.real ?? false, fakeAi: o.fakeAi ?? false, jevProvider: o.jevProvider }));
|
|
1243
|
-
}
|
|
1244
|
-
catch (err) {
|
|
1245
|
-
if (err instanceof MissingCredentialError || err instanceof GatewaySelectionError) {
|
|
1246
|
-
emitExplore(fail("E_AI_SETUP_REQUIRED", err.message));
|
|
1247
|
-
}
|
|
1248
|
-
else {
|
|
1249
|
-
emitExplore(fail("E_EXPLORE_SETUP", String(err instanceof Error ? err.message : err)));
|
|
1250
|
-
}
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
let url = o.url;
|
|
1254
|
-
let goal = o.goal;
|
|
1255
|
-
let runInvariants = withInvariants;
|
|
1256
|
-
if (fx !== undefined) {
|
|
1257
|
-
// Never run the mission on unknown state: a failed setup ends the run inconclusive (a
|
|
1258
|
-
// configuration error), after restoring whatever the partial setup created.
|
|
1259
|
-
try {
|
|
1260
|
-
await fx.setup();
|
|
1261
|
-
const b = fx.bindings();
|
|
1262
|
-
url = substituteUrlSetupRefs(o.url, b);
|
|
1263
|
-
goal = substituteSetupRefs(o.goal, b, { where: "--goal" });
|
|
1264
|
-
successChecks = o.success.map((spec) => parseSuccessSpec(substituteSetupRefs(spec, b, { where: "--success" })));
|
|
1265
|
-
// #187: ${setup.x} in the invariants (probe paths, deniedAs.open, capture routes), origin-fixed.
|
|
1266
|
-
if (invariants !== undefined)
|
|
1267
|
-
runInvariants = { ...withInvariants, invariants: substituteSpecSetupRefs(invariants, b, url) };
|
|
1268
|
-
// #399: `--param name=${setup.x}` for the Journey prefix is bound to the fixture's output, and the
|
|
1269
|
-
// prefix re-resolved so its secret params (and so every redaction) hold the bound value.
|
|
1270
|
-
if (journeyPrefix !== undefined && prefixRequest !== undefined && Object.values(o.param).some((v) => v.includes("${setup."))) {
|
|
1271
|
-
const params = Object.fromEntries(Object.entries(o.param).map(([k, v]) => [k, substituteSetupRefs(v, b, { where: `--param ${k}` })]));
|
|
1272
|
-
journeyPrefix = await resolveJourneyPrefix({ ...prefixRequest, params });
|
|
1273
|
-
o.secret = [...new Set([...o.secret, ...journeyPrefix.secrets])];
|
|
1274
|
-
}
|
|
1275
|
-
}
|
|
1276
|
-
catch (err) {
|
|
1277
|
-
if (!(err instanceof FixtureSetupError || err instanceof UnboundSetupRefError)) {
|
|
1278
|
-
await fx.restore();
|
|
1279
|
-
throw err;
|
|
1280
|
-
}
|
|
1281
|
-
await fx.restore();
|
|
1282
|
-
emitExplore(ok(withEngine(fixtureSetupFailedResult(err, fx))), EXIT_CODES.inconclusive);
|
|
1283
|
-
return;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
try {
|
|
1287
|
-
const result = await runExploration({
|
|
1288
|
-
...(target === undefined ? {} : { target }),
|
|
1289
|
-
url,
|
|
1290
|
-
goal,
|
|
1291
|
-
successChecks,
|
|
1292
|
-
...(successWhen === undefined ? {} : { successWhen }),
|
|
1293
|
-
...(o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {}),
|
|
1294
|
-
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
1295
|
-
allowlist,
|
|
1296
|
-
judge,
|
|
1297
|
-
gen,
|
|
1298
|
-
usage,
|
|
1299
|
-
bounds: Object.keys(bounds).length > 0 ? bounds : undefined,
|
|
1300
|
-
...(o.minActions === undefined && o.minDistinctStates === undefined
|
|
1301
|
-
? {}
|
|
1302
|
-
: {
|
|
1303
|
-
minEffort: {
|
|
1304
|
-
...(o.minActions === undefined ? {} : { minActions: Number(o.minActions) }),
|
|
1305
|
-
...(o.minDistinctStates === undefined ? {} : { minDistinctStates: Number(o.minDistinctStates) }),
|
|
1306
|
-
},
|
|
1307
|
-
}),
|
|
1308
|
-
secrets: o.secret.length > 0 ? o.secret : undefined,
|
|
1309
|
-
...(secretFields.length > 0 ? { secretFields } : {}),
|
|
1310
|
-
...(secretCommand === undefined ? {} : { secretCommand }),
|
|
1311
|
-
...(o.secretCmdAttempts === undefined ? {} : { secretCommandAttempts: o.secretCmdAttempts }),
|
|
1312
|
-
...(typeFixtures.length > 0 ? { typeFixtures } : {}),
|
|
1313
|
-
fixture: o.fixture,
|
|
1314
|
-
outDir: o.out,
|
|
1315
|
-
browserPortFactory: deps.explore?.browserPortFactory,
|
|
1316
|
-
browser,
|
|
1317
|
-
...screenshotsOpt,
|
|
1318
|
-
...(emulation === undefined ? {} : { emulation }),
|
|
1319
|
-
...(primaryStorageState !== undefined ? { storageState: primaryStorageState } : {}),
|
|
1320
|
-
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
1321
|
-
...(actors === null ? {} : { actors }),
|
|
1322
|
-
...(runFiling === undefined ? {} : { filing: runFiling }),
|
|
1323
|
-
issueFiler,
|
|
1324
|
-
...(o.hangReplays === undefined ? {} : { hangReplays: o.hangReplays }),
|
|
1325
|
-
conversation,
|
|
1326
|
-
...runInvariants,
|
|
1327
|
-
...triagedServerLog(serverLog, judge, o.real === true),
|
|
1328
|
-
...(fx === undefined ? {} : { fixtures: fx }),
|
|
1329
|
-
...(journeyPrefix === undefined ? {} : { journeyPrefix }), // #399: re-resolved when a --param was bound
|
|
1330
|
-
});
|
|
1331
|
-
// 0 succeeded · 1 assertion not met · 2 the run broke (inconclusive/crashed).
|
|
1332
|
-
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
1333
|
-
}
|
|
1334
|
-
catch (err) {
|
|
1335
|
-
if (emitJourneyFailure(err))
|
|
1336
|
-
return;
|
|
1337
|
-
if (err instanceof UnauthorizedExploreTargetError) {
|
|
1338
|
-
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
1339
|
-
}
|
|
1340
|
-
else if (err instanceof FixtureNotFoundError) {
|
|
1341
|
-
emitExplore(fail("E_EXPLORE_FIXTURE", err.message));
|
|
1342
|
-
}
|
|
1343
|
-
else {
|
|
1344
|
-
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
finally {
|
|
1348
|
-
// Every exit path restores the fixture state (a no-op when the mission already did).
|
|
1349
|
-
await fx?.restore();
|
|
1350
|
-
}
|
|
1351
|
-
}));
|
|
7
|
+
registerExploreRun(program, deps, buildProgram);
|
|
1352
8
|
}
|
|
1353
9
|
//# sourceMappingURL=explore-cli.js.map
|