@jevitate/cli 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ai-cli.d.ts +31 -11
- package/dist/ai-cli.js +100 -67
- package/dist/ai-cli.js.map +1 -1
- package/dist/author-journey-cli.js +3 -1
- package/dist/author-journey-cli.js.map +1 -1
- package/dist/bin.js +48715 -40027
- package/dist/browser-run-options.d.ts +32 -1
- package/dist/browser-run-options.js +69 -0
- package/dist/browser-run-options.js.map +1 -1
- package/dist/build-info.generated.js +2 -2
- package/dist/campaign-api.d.ts +153 -0
- package/dist/campaign-api.js +447 -0
- package/dist/campaign-api.js.map +1 -0
- package/dist/campaign-cli.d.ts +8 -0
- package/dist/campaign-cli.js +109 -0
- package/dist/campaign-cli.js.map +1 -0
- package/dist/check-api.js +3 -2
- package/dist/check-api.js.map +1 -1
- package/dist/check-budget.js +2 -1
- package/dist/check-budget.js.map +1 -1
- package/dist/check-execute.d.ts +4 -0
- package/dist/check-execute.js +46 -10
- package/dist/check-execute.js.map +1 -1
- package/dist/check-plan.d.ts +5 -0
- package/dist/check-plan.js +77 -4
- package/dist/check-plan.js.map +1 -1
- package/dist/check-suite.d.ts +14 -1
- package/dist/check-suite.js +26 -1
- package/dist/check-suite.js.map +1 -1
- package/dist/cli-output.d.ts +4 -0
- package/dist/cli-output.js +52 -5
- package/dist/cli-output.js.map +1 -1
- package/dist/cli-shared.d.ts +13 -3
- package/dist/cli-shared.js +52 -7
- package/dist/cli-shared.js.map +1 -1
- package/dist/defect-evidence.d.ts +3 -2
- package/dist/defect-evidence.js +12 -8
- package/dist/defect-evidence.js.map +1 -1
- package/dist/demo-aspect-api.js +2 -1
- package/dist/demo-aspect-api.js.map +1 -1
- package/dist/demo-aspect-cli.js +3 -1
- package/dist/demo-aspect-cli.js.map +1 -1
- package/dist/demo-capture.d.ts +37 -3
- package/dist/demo-capture.js +119 -20
- package/dist/demo-capture.js.map +1 -1
- package/dist/explore-adversarial.d.ts +11 -4
- package/dist/explore-adversarial.js +22 -13
- package/dist/explore-adversarial.js.map +1 -1
- package/dist/explore-cli.js +282 -31
- package/dist/explore-cli.js.map +1 -1
- package/dist/explore-coverage.d.ts +17 -2
- package/dist/explore-coverage.js +20 -14
- package/dist/explore-coverage.js.map +1 -1
- package/dist/explore-feature.d.ts +0 -2
- package/dist/explore-feature.js +7 -9
- package/dist/explore-feature.js.map +1 -1
- package/dist/explore-goal.d.ts +25 -7
- package/dist/explore-goal.js +62 -19
- package/dist/explore-goal.js.map +1 -1
- package/dist/explore-shared.d.ts +53 -3
- package/dist/explore-shared.js +48 -1
- package/dist/explore-shared.js.map +1 -1
- package/dist/fixture-auth.d.ts +13 -2
- package/dist/fixture-auth.js +19 -6
- package/dist/fixture-auth.js.map +1 -1
- package/dist/fixture-cli.d.ts +29 -0
- package/dist/fixture-cli.js +121 -7
- package/dist/fixture-cli.js.map +1 -1
- package/dist/github-issue-filer.js +2 -1
- package/dist/github-issue-filer.js.map +1 -1
- package/dist/host-health-run.d.ts +2 -1
- package/dist/host-health-run.js +8 -2
- package/dist/host-health-run.js.map +1 -1
- package/dist/init-cli.js +34 -5
- package/dist/init-cli.js.map +1 -1
- package/dist/init-keys.d.ts +11 -0
- package/dist/init-keys.js +5 -2
- package/dist/init-keys.js.map +1 -1
- package/dist/journey-annotate-api.js +17 -4
- package/dist/journey-annotate-api.js.map +1 -1
- package/dist/journey-annotate-cli.js +3 -1
- package/dist/journey-annotate-cli.js.map +1 -1
- package/dist/journey-api.d.ts +26 -1
- package/dist/journey-api.js +51 -25
- package/dist/journey-api.js.map +1 -1
- package/dist/journey-cli.d.ts +1 -1
- package/dist/journey-cli.js +44 -3
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-demo-api.d.ts +2 -0
- package/dist/journey-demo-api.js +23 -10
- package/dist/journey-demo-api.js.map +1 -1
- package/dist/journey-demo-cli.js +5 -2
- package/dist/journey-demo-cli.js.map +1 -1
- package/dist/journey-prefix.d.ts +194 -0
- package/dist/journey-prefix.js +279 -0
- package/dist/journey-prefix.js.map +1 -0
- package/dist/key-report.d.ts +49 -0
- package/dist/key-report.js +101 -0
- package/dist/key-report.js.map +1 -0
- package/dist/kill-signal.js +0 -1
- package/dist/kill-signal.js.map +1 -1
- package/dist/ledger-api.js +3 -1
- package/dist/ledger-api.js.map +1 -1
- package/dist/log-correlation.d.ts +63 -1
- package/dist/log-correlation.js +125 -16
- package/dist/log-correlation.js.map +1 -1
- package/dist/log-sources.js +6 -5
- package/dist/log-sources.js.map +1 -1
- package/dist/log-trace.d.ts +80 -0
- package/dist/log-trace.js +210 -0
- package/dist/log-trace.js.map +1 -0
- package/dist/logs-cli.js +60 -1
- package/dist/logs-cli.js.map +1 -1
- package/dist/logs-retention.js +2 -1
- package/dist/logs-retention.js.map +1 -1
- package/dist/masked-input.d.ts +22 -0
- package/dist/masked-input.js +99 -0
- package/dist/masked-input.js.map +1 -0
- package/dist/mcp-api.d.ts +3 -1
- package/dist/mcp-api.js +49 -7
- package/dist/mcp-api.js.map +1 -1
- package/dist/mcp-args.d.ts +12 -0
- package/dist/mcp-args.js +33 -0
- package/dist/mcp-args.js.map +1 -1
- package/dist/mcp-cli-tools.d.ts +6 -1
- package/dist/mcp-cli-tools.js +103 -16
- package/dist/mcp-cli-tools.js.map +1 -1
- package/dist/mission-api.js +2 -1
- package/dist/mission-api.js.map +1 -1
- package/dist/mission-cli.js +3 -1
- package/dist/mission-cli.js.map +1 -1
- package/dist/mission-fixtures.d.ts +10 -1
- package/dist/mission-fixtures.js +49 -15
- package/dist/mission-fixtures.js.map +1 -1
- package/dist/mission-queue-runner.d.ts +2 -0
- package/dist/mission-queue-runner.js +18 -5
- package/dist/mission-queue-runner.js.map +1 -1
- package/dist/multi-run-cli.js +7 -1
- package/dist/multi-run-cli.js.map +1 -1
- package/dist/openrouter-call.js +3 -2
- package/dist/openrouter-call.js.map +1 -1
- package/dist/program.js +4 -0
- package/dist/program.js.map +1 -1
- package/dist/project-dir.js +3 -2
- package/dist/project-dir.js.map +1 -1
- package/dist/record-api.d.ts +3 -1
- package/dist/record-api.js +5 -2
- package/dist/record-api.js.map +1 -1
- package/dist/record-cli.js +14 -2
- package/dist/record-cli.js.map +1 -1
- package/dist/regression-api.d.ts +6 -0
- package/dist/regression-api.js +4 -2
- package/dist/regression-api.js.map +1 -1
- package/dist/regression-cli.js +56 -4
- package/dist/regression-cli.js.map +1 -1
- package/dist/report-api.js +2 -1
- package/dist/report-api.js.map +1 -1
- package/dist/resource-preflight.d.ts +87 -0
- package/dist/resource-preflight.js +160 -0
- package/dist/resource-preflight.js.map +1 -0
- package/dist/result-schema.d.ts +1 -1
- package/dist/result-schema.js +1 -1
- package/dist/run-key-preflight.d.ts +42 -0
- package/dist/run-key-preflight.js +82 -0
- package/dist/run-key-preflight.js.map +1 -0
- package/dist/run-screenshots.js +11 -5
- package/dist/run-screenshots.js.map +1 -1
- package/dist/runtime-env.js +3 -2
- package/dist/runtime-env.js.map +1 -1
- package/dist/server-log-verify.d.ts +2 -0
- package/dist/server-log-verify.js +8 -3
- package/dist/server-log-verify.js.map +1 -1
- package/dist/signal-triage.d.ts +147 -0
- package/dist/signal-triage.js +281 -0
- package/dist/signal-triage.js.map +1 -0
- package/dist/site-gate-cli.js +2 -1
- package/dist/site-gate-cli.js.map +1 -1
- package/dist/source-api.js +2 -1
- package/dist/source-api.js.map +1 -1
- package/dist/suite-explore-options.d.ts +36 -0
- package/dist/suite-explore-options.js +23 -0
- package/dist/suite-explore-options.js.map +1 -1
- package/dist/target-config.d.ts +12 -0
- package/dist/target-config.js +15 -0
- package/dist/target-config.js.map +1 -1
- package/dist/type-fixture-file.d.ts +9 -0
- package/dist/type-fixture-file.js +48 -0
- package/dist/type-fixture-file.js.map +1 -0
- package/dist/ui-api.js +4 -3
- package/dist/ui-api.js.map +1 -1
- package/dist/usability-capture.js +4 -3
- package/dist/usability-capture.js.map +1 -1
- package/dist/usage-config.d.ts +1 -1
- package/dist/usage-config.js +1 -1
- package/dist/ux-claim-probe.d.ts +45 -0
- package/dist/ux-claim-probe.js +386 -0
- package/dist/ux-claim-probe.js.map +1 -0
- package/dist/ux-cli.js +8 -1
- package/dist/ux-cli.js.map +1 -1
- package/dist/ux-product.d.ts +14 -0
- package/dist/ux-product.js +39 -0
- package/dist/ux-product.js.map +1 -0
- package/dist/ux-review.d.ts +4 -0
- package/dist/ux-review.js +27 -7
- package/dist/ux-review.js.map +1 -1
- package/dist/ux-usability.d.ts +34 -9
- package/dist/ux-usability.js +102 -16
- package/dist/ux-usability.js.map +1 -1
- package/dist/verify-fix-api.d.ts +21 -0
- package/dist/verify-fix-api.js +85 -33
- package/dist/verify-fix-api.js.map +1 -1
- package/dist/verify-fix-cli.js +7 -2
- package/dist/verify-fix-cli.js.map +1 -1
- package/package.json +11 -11
- package/skills/jevitate-explore/SKILL.md +13 -0
- package/skills/jevitate-getting-started/SKILL.md +2 -1
- package/skills/jevitate-test-campaign/SKILL.md +206 -0
package/dist/explore-cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { envCredentialStore, MissingCredentialError } from "@jevitate/ai-core";
|
|
3
3
|
import { loadLocalCredentials } from "./credentials-file.js";
|
|
4
|
-
import { FixtureNotFoundError, ScopeUnderivableError, UnauthorizedExploreTargetError, resolveCoverageThresholds, parseSecretField, SecretFieldSpecError, validateDenyPatterns, } from "@jevitate/explore";
|
|
4
|
+
import { FixtureNotFoundError, ScopeUnderivableError, UnauthorizedExploreTargetError, resolveCoverageThresholds, parseSecretField, SecretFieldSpecError, TypeFixtureSpecError, validateDenyPatterns, } from "@jevitate/explore";
|
|
5
|
+
import { loadTypeFixtures } from "./type-fixture-file.js";
|
|
5
6
|
import { ok, fail } from "./envelope.js";
|
|
6
7
|
import { SessionFileInProjectError, assertSessionFileOutsideProject } from "./project-dir.js";
|
|
7
8
|
import { InvariantsFileError, loadInvariantFiles, resolveInvariantAuthTokens } from "./invariants-file.js";
|
|
@@ -10,8 +11,8 @@ import { fileDraftsWithEvidence } from "./defect-evidence.js";
|
|
|
10
11
|
import { parseScreenshotsArg } from "./run-screenshots.js";
|
|
11
12
|
import { GitHubIssueFiler } from "./github-issue-filer.js";
|
|
12
13
|
import { TargetConfigError, loadTargetsFile, resolveTargetConfig } from "./target-config.js";
|
|
13
|
-
import { buildMissionFixtures, checkSetupRefs, checkUrlRefOrigin, invariantSetupTexts, substituteSpecSetupRefs, fixtureSetupFailedResult, withFixtureFlags, } from "./fixture-cli.js";
|
|
14
|
-
import { FixtureSetupError, FixtureSpecError,
|
|
14
|
+
import { buildMissionFixtures, checkSetupRefs, checkUrlRefOrigin, substituteUrlSetupRefs, setupRefFreeUrl, invariantSetupTexts, substituteSpecSetupRefs, fixtureSetupFailedResult, withFixtureFlags, } from "./fixture-cli.js";
|
|
15
|
+
import { FixtureSetupError, FixtureSpecError, UnboundSetupRefError, substituteSetupRefs, } from "./mission-fixtures.js";
|
|
15
16
|
import { LITERAL_SECRET_WARNING, SecretArgError, resolveSecretArgs } from "./secret-args.js";
|
|
16
17
|
import { withEngine } from "./engine.js";
|
|
17
18
|
import { setKillSwitchOutput } from "./kill-signal.js";
|
|
@@ -20,31 +21,63 @@ import { intArg, positiveNumberArg, nonNegativeIntArg, positiveIntArg, ratioArg
|
|
|
20
21
|
import { formatMissionHuman, formatMultiRunHuman } from "./cli-output.js";
|
|
21
22
|
import { runExploration, runCoverageMission, runAdversarialCliMission, CLI_ADVERSARIAL_STRATEGIES, runFeatureCliMission, parseSuccessSpec, resolveExploreAllowlist, } from "./explore-api.js";
|
|
22
23
|
import { parseLogSourceSpecs, LogSourceSpecError } from "./log-sources.js";
|
|
23
|
-
import {
|
|
24
|
+
import { triagedServerLog } from "./explore-shared.js";
|
|
25
|
+
import { parseLogDefectSpecs, parseLogIgnoreSpecs, parseLogScopeSpecs } from "./log-correlation.js";
|
|
26
|
+
import { parseCorrelationHeaders, parseLogIdPatterns } from "./log-trace.js";
|
|
24
27
|
import { LogSpecError } from "./log-lines.js";
|
|
25
28
|
import { MultiRunArgsError, resolveMultiRunPlan, wantsMultiRun } from "./multi-run.js";
|
|
26
29
|
import { MultiRunAbortedError, runExploreMultiRun } from "./multi-run-cli.js";
|
|
27
30
|
import { checkActorsAgainstSpec, resolveMissionActors } from "./mission-actors.js";
|
|
28
31
|
import { runUsabilityMission, UsabilityInvariantsUnsupportedError } from "./ux-api.js";
|
|
29
32
|
import { UxConfigError } from "./ux-config.js";
|
|
30
|
-
import { MinConfidenceError, QualityPolicyError, MaxFindingsPerRouteError } from "@jevitate/ux";
|
|
31
|
-
import { withBrowserLaunchFlags, withDemoFlags, withScreenshotsFlag, browserRunFromFlags, withEmulationFlags, emulationFromFlags, emitCommandResult, stallTimeoutMs, EXPLORE_STRATEGIES, EXPLORE_OUTCOME_HELP, GatewaySelectionError, buildExploreGateways, } from "./cli-shared.js";
|
|
32
|
-
import { multiWindowWarning } from "./browser-run-options.js";
|
|
33
|
+
import { MinConfidenceError, QualityPolicyError, MaxFindingsPerRouteError, ProductFactsError } from "@jevitate/ux";
|
|
34
|
+
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";
|
|
35
|
+
import { allowWithExtensions, assertExtensionTargetLoaded, multiWindowWarning } from "./browser-run-options.js";
|
|
36
|
+
import { ParamValidationError } from "@jevitate/journey";
|
|
37
|
+
import { SiteGateRefusedError } from "@jevitate/runtime";
|
|
38
|
+
import { JourneyRequiresAuthError, UnknownJourneyError } from "./journey-api.js";
|
|
39
|
+
import { environmentFromFlags, isEnvironmentError, withEnvironmentFlags } from "./environments.js";
|
|
40
|
+
import { resolve as resolvePath } from "node:path";
|
|
41
|
+
import { CAMPAIGN_LIMITS, isSweepMode } from "@jevitate/journey";
|
|
42
|
+
import { CampaignSpecError, runCampaign, validateCampaignSpec } from "./campaign-api.js";
|
|
43
|
+
import { formatCampaignHuman } from "./campaign-cli.js";
|
|
44
|
+
import { forwardedArgv } from "./multi-run-cli.js";
|
|
45
|
+
import { ANCHORED_STRATEGIES, JourneyPrefixArgsError, JourneyPrefixStaleError, journeyStaleResult, resolveJourneyPrefix, } from "./journey-prefix.js";
|
|
46
|
+
import { clock } from "@jevitate/domain";
|
|
47
|
+
/**
|
|
48
|
+
* #293: the flags a sweep sets on each of its missions itself (the rest of the command line is
|
|
49
|
+
* forwarded to every mission as given).
|
|
50
|
+
*/
|
|
51
|
+
const SWEEP_OWNED = new Set([
|
|
52
|
+
"fromJourney", "atStep", "strategy", "journeysDir", "param", "env", "baseUrl", "storageState", "maxActions", "maxDecisions",
|
|
53
|
+
"goal", "success", "appClass", "real", "fakeAi", "fixtures", "before", "after", "allowShellHooks", "hookTimeoutMs",
|
|
54
|
+
]);
|
|
33
55
|
/**
|
|
34
56
|
* Registers `jevitate explore` (every strategy: goal, coverage, exploratory, adversarial, usability, feature, multi-run).
|
|
35
57
|
* `buildProgram` builds a fresh program for each run of a multi-run (#141/#143).
|
|
36
58
|
*/
|
|
37
59
|
export function registerExploreCommands(program, deps, buildProgram) {
|
|
38
|
-
withScreenshotsFlag(withEmulationFlags(withFixtureFlags(withDemoFlags(withBrowserLaunchFlags(program
|
|
60
|
+
withEnvironmentFlags(withScreenshotsFlag(withEmulationFlags(withFixtureFlags(withDemoFlags(withBrowserLaunchFlags(program
|
|
39
61
|
.command("explore")
|
|
40
|
-
.description("goal-directed exploration -> a deterministic Recording (authoring/test plane)")), { recordVideo: true, overlay: true }))))
|
|
62
|
+
.description("goal-directed exploration -> a deterministic Recording (authoring/test plane)")), { recordVideo: true, overlay: true })))))
|
|
41
63
|
.option("--url <url>", "target URL (must be an authorized origin)")
|
|
64
|
+
.option("--from-journey <id>", "journey-anchored exploration (#293): start from a PROMOTED Journey instead of --url — its first --at-step steps are replayed " +
|
|
65
|
+
"in the mission's own browser context (page, form contents and session kept; fail-closed, never self-healed; --env/--base-url apply), " +
|
|
66
|
+
"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). " +
|
|
67
|
+
"Strategies: goal, coverage, exploratory, adversarial, usability")
|
|
68
|
+
.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>`); " +
|
|
69
|
+
"`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")
|
|
70
|
+
.option("--param <kv>", "with --from-journey: a Journey param as key=value (repeatable); only the prefix's own params are required", collectParam, {})
|
|
71
|
+
.option("--journeys-dir <path>", "with --from-journey: the journeys directory (default: the repo's .jevitate/journeys; outside a repo ~/.jevitate/journeys)")
|
|
42
72
|
.option("--strategy <name>", "exploration strategy: goal (default) | coverage | exploratory | adversarial | usability (UX review: ranked, cited findings)", "goal")
|
|
43
73
|
.option("--goal <text>", "natural-language goal / job (required for --strategy goal and usability)")
|
|
44
74
|
.option("--app-class <class>", "app class for UX calibration (required for --strategy usability), e.g. consumer|admin|internal")
|
|
45
75
|
.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")
|
|
46
76
|
.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")
|
|
47
77
|
.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")
|
|
78
|
+
.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)")
|
|
79
|
+
.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)")
|
|
80
|
+
.option("--polish", "(--strategy usability) polish each verified UX finding's recommendation with one generation call (opt-in; the default prose is built from templates)")
|
|
48
81
|
.option("--success <spec>", [
|
|
49
82
|
"independent success check (repeatable; every one must hold; --strategy goal and usability). Kinds:",
|
|
50
83
|
"urlIncludes:<text> | visible:<d> | textIncludes:<d>|<text> (case-insensitive) | count:<d>|min=<n>,max=<n>",
|
|
@@ -64,6 +97,9 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
64
97
|
.option("--success-when <when>", "when the --success page checks must hold: final (default; on the final page) | held (on the final page, or all together at any settled step — a one-time secret, a toast). reloadThen is always final")
|
|
65
98
|
.option("--allow-vacuous-checks", "downgrade a vacuous --success check to a warning. By default a check satisfied before the run's first action — a page check that held on the seed page and never changed " +
|
|
66
99
|
"(an empty result container), a requestMade/responseStatus matched only by a page-load or polling request — FAILS: it cannot verify the goal")
|
|
100
|
+
.option("--action-deltas", "opt-in (#303; every --strategy, not --feature): record what each action changed on the page — an accessibility snapshot before and after, announcements, " +
|
|
101
|
+
"the action's requests — redacted, with a code verdict per step (no-change | relevant-change | inconclusive) used by the goal loop's no-progress check and a persistence re-check after writes (goal), and as defect evidence (adversarial, coverage); " +
|
|
102
|
+
"adds `delta` to every transcript step (and Recording step, goal) and `actionDeltas` to the result. Costs about 50-100 ms per action on a small page, 0.3-0.5 s on a large one")
|
|
67
103
|
.option("--feature <name>", "run the capability-scoped feature-testing mission (instead of --goal/--success)")
|
|
68
104
|
.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], [])
|
|
69
105
|
.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")
|
|
@@ -71,6 +107,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
71
107
|
.option("--secret <value|env:VAR>", "REDACTION ONLY: a secret/PII value kept out of every model call and artifact (repeatable); env:VAR reads it from the environment (preferred: a literal is visible in the process list and shell history). It is never typed into a field — to log in, bind it with --secret-field (or start from --storage-state)", (v, prev) => [...prev, v], [])
|
|
72
108
|
.option("--secret-field <binding>", "goal/usability strategy: '<label|testId|type|id|name>=<value>=env:<VAR>' (repeatable), e.g. 'label=Password=env:APP_PASSWORD'. When the run types into a matching field, code types $VAR itself; the model sees only «secret:VAR» and the Recording {redacted:true}", (v, prev) => [...prev, v], [])
|
|
73
109
|
.option("--totp <binding>", "goal/usability strategy: '<descriptor>=env:<VAR>' with $VAR a base32 TOTP seed (repeatable), e.g. 'label=Authentication code=env:APP_TOTP_SEED'. The 6-digit code is computed locally (RFC 6238) when the field is typed; the seed never reaches a model or disk", (v, prev) => [...prev, v], [])
|
|
110
|
+
.option("--type-fixture <binding>", "goal strategy: '<label|testId|type|id|name>=<value>=<file>' (repeatable), e.g. 'label=Paste your text=./fixtures/import.txt'. When the run types into a matching field, code types the file's exact text verbatim (line breaks kept, never paraphrased or capped); the model sees only «fixture:<file name>». Recorded as typed unless it holds a --secret", (v, prev) => [...prev, v], [])
|
|
74
111
|
.option("--fixture <path>", "local file the upload op attaches to a file input (goal and usability strategies); must exist")
|
|
75
112
|
.option("--storage-state <file>", "Playwright storageState JSON to start the session authenticated (deterministic login pre-step); must exist")
|
|
76
113
|
.option("--actor <name=storageState>", "multi-actor mission (#147, goal only; repeatable): the FIRST actor is the primary (the only one the model drives, " +
|
|
@@ -91,7 +128,9 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
91
128
|
.option("--reply-ceiling-ms <ms>", "conversational pages: hard ceiling on one reply wait, however busy the page stays (default 180000; never below --reply-wait-ms)", positiveIntArg)
|
|
92
129
|
.option("--reply-max-chars <n>", "conversational pages: cap on each generated chat message (goal and usability; default 300)", intArg({ min: 20, max: 2000 }))
|
|
93
130
|
.option("--job-wait-ms <ms>", "goal and usability: while the page shows an in-progress status (\"Simulating…\", aria-busy, a job \"is running\"), " +
|
|
94
|
-
"waits keep waiting with backoff — and a model 'blocked' is deferred — up to this budget (default: --reply-ceiling-ms, 180000)"
|
|
131
|
+
"waits keep waiting with backoff — and a model 'blocked' is deferred — up to this budget (default: --reply-ceiling-ms, 180000); " +
|
|
132
|
+
"it also bounds a busy indicator the app visibly keeps working behind (live progress, a job poll) before it is a hang, " +
|
|
133
|
+
"and a wait the page documents (\"usually takes a minute\") can raise it", positiveIntArg)
|
|
95
134
|
.option("--deny <pattern>", "a control no mission may click (repeatable): an accessible-name regex (/Archive/i or Archive) or a descriptor role=button;name=Archive. " +
|
|
96
135
|
"Session-ending (Sign out), destructive (Delete, Revoke, Rotate) and paid (Buy, Run simulation, Generate, Send invite) controls are refused by default", (v, prev) => [...prev, v], [])
|
|
97
136
|
.option("--paid <pattern>", "an app control that costs money or credits (repeatable; same syntax as --deny), e.g. /^(Analyze|Draft|Improve)\\b/i: treated like the built-in paid " +
|
|
@@ -124,15 +163,21 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
124
163
|
.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], [])
|
|
125
164
|
.option("--log-quiet-ok <spec>", "declares a --log-source spec (exact match, repeatable) as legitimately quiet: zero lines from it does not make the --log-defect oracle unhealthy (#169). Without it, a declared source that opened but delivered not one line makes an otherwise-clean run inconclusive, same as one that failed to open", (v, prev) => [...prev, v], [])
|
|
126
165
|
.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], [])
|
|
166
|
+
.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], [])
|
|
167
|
+
.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], [])
|
|
168
|
+
.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], [])
|
|
169
|
+
.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, " +
|
|
170
|
+
"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, " +
|
|
171
|
+
"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")
|
|
127
172
|
.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)
|
|
128
173
|
.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")
|
|
129
174
|
.option("--min-agreement <k>", "with --repeat: runs a finding (and the outcome) must recur in to count (default: a majority of N)")
|
|
130
175
|
.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], [])
|
|
131
176
|
.option("--personas <file>", "personas JSON: {\"<name>\": \"<storageState>\"} or {\"personas\": [{\"name\", \"storageState\"}]}")
|
|
132
|
-
.option("--check-overflow", "check the horizontal-overflow
|
|
177
|
+
.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 " +
|
|
133
178
|
"(a defect), adversarial (a defect) or usability (a signal finding). " +
|
|
134
179
|
"On by default whenever --viewport/--device emulates a viewport narrower than 1024px")
|
|
135
|
-
.option("--ignore-overflow <selector>", "a CSS selector (repeatable) whose overflow is intentional — excluded from the horizontal-overflow
|
|
180
|
+
.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], [])
|
|
136
181
|
.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)")
|
|
137
182
|
.option("--json", "emit the JSON envelope (default: a human summary)")
|
|
138
183
|
.addHelpText("after", [
|
|
@@ -178,12 +223,153 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
178
223
|
}
|
|
179
224
|
}
|
|
180
225
|
const strategy = o.strategy ?? "goal";
|
|
226
|
+
// #293 journey-anchored exploration: the Journey, step, params and environment are resolved (and
|
|
227
|
+
// refused, exit 64) before anything else — the start URL every later check uses is where the
|
|
228
|
+
// Journey's prefix lands.
|
|
229
|
+
let journeyPrefix;
|
|
230
|
+
const anchoredFlags = [o.fromJourney, o.atStep, o.journeysDir, o.env, o.baseUrl].some((v) => v !== undefined);
|
|
231
|
+
if (anchoredFlags || Object.keys(o.param).length > 0) {
|
|
232
|
+
const refuse = (message) => emitExplore(fail("E_EXPLORE_ARGS", message));
|
|
233
|
+
if (o.fromJourney === undefined || o.atStep === undefined) {
|
|
234
|
+
refuse("--from-journey and --at-step go together (and --param, --env, --base-url and --journeys-dir need them)");
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (o.url !== undefined) {
|
|
238
|
+
refuse("--url cannot be combined with --from-journey: the mission starts where the Journey's prefix leaves the page");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (o.feature !== undefined || !ANCHORED_STRATEGIES.includes(strategy)) {
|
|
242
|
+
refuse(`--from-journey supports --strategy ${ANCHORED_STRATEGIES.join(", ")} (not --feature)`);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (wantsMultiRun(o) || o.actor.length > 0) {
|
|
246
|
+
refuse("--from-journey runs one anchored mission: --repeat, --persona, --personas and --actor are not supported with it (a campaign runs several)");
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// #293 sweep: `--at-step all|anchors` runs the strategy from EVERY step (or anchor), each in a
|
|
250
|
+
// fresh session with --fixtures restored around it, --max-actions/--max-decisions split evenly
|
|
251
|
+
// over the stop points, and reads them as one deduped report (a one-job campaign).
|
|
252
|
+
// #312: one anchored step with a state restore (--fixtures/--before/--after) on a non-goal
|
|
253
|
+
// strategy runs the same way — as a one-stop campaign, whose runner restores around the run.
|
|
254
|
+
const restoredSingle = !isSweepMode(o.atStep) && strategy !== "goal" && (o.fixtures !== undefined || o.before !== undefined || o.after !== undefined);
|
|
255
|
+
if (isSweepMode(o.atStep) || restoredSingle) {
|
|
256
|
+
if (o.real !== true && o.fakeAi !== true) {
|
|
257
|
+
emitExplore(fail("E_AI_SETUP_REQUIRED", "a sweep's missions are model-driven: pass --real or --fake-ai"));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const journeysDir = resolveJourneysDir(deps, o.journeysDir);
|
|
261
|
+
const abs = (p) => resolvePath(p);
|
|
262
|
+
const spec = {
|
|
263
|
+
version: 1,
|
|
264
|
+
name: restoredSingle ? `${o.fromJourney} at step ${o.atStep.trim()} (${strategy}, restored)` : `sweep of ${o.fromJourney} (${o.atStep}, ${strategy})`,
|
|
265
|
+
...(o.env === undefined ? {} : { env: o.env }),
|
|
266
|
+
...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }),
|
|
267
|
+
...(o.storageState === undefined ? {} : { storageState: abs(o.storageState) }),
|
|
268
|
+
...(o.fixtures === undefined ? {} : { fixtures: abs(o.fixtures) }),
|
|
269
|
+
...(o.before === undefined ? {} : { before: o.before }),
|
|
270
|
+
...(o.after === undefined ? {} : { after: o.after }),
|
|
271
|
+
discovery: false,
|
|
272
|
+
maxRuns: CAMPAIGN_LIMITS.maxRuns,
|
|
273
|
+
jobs: [
|
|
274
|
+
{
|
|
275
|
+
id: "sweep",
|
|
276
|
+
journey: o.fromJourney,
|
|
277
|
+
params: o.param,
|
|
278
|
+
anchors: restoredSingle ? [o.atStep.trim()] : o.atStep.trim(),
|
|
279
|
+
strategies: [strategy],
|
|
280
|
+
...(o.goal === undefined ? {} : { goal: o.goal }),
|
|
281
|
+
...(o.appClass === undefined ? {} : { appClass: o.appClass }),
|
|
282
|
+
...(o.success.length === 0 ? {} : { success: o.success }),
|
|
283
|
+
...(o.maxActions === undefined ? {} : { maxActions: Number(o.maxActions) }),
|
|
284
|
+
...(o.maxDecisions === undefined ? {} : { maxDecisions: Number(o.maxDecisions) }),
|
|
285
|
+
},
|
|
286
|
+
],
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
const plan = await validateCampaignSpec(spec, resolvePath("explore-sweep.json"), {
|
|
290
|
+
journeysDir,
|
|
291
|
+
allowShellHooks: o.allowShellHooks === true,
|
|
292
|
+
...(o.hookTimeoutMs === undefined ? {} : { hookTimeoutMs: Number(o.hookTimeoutMs) }),
|
|
293
|
+
environmentSeams: environmentSeams(deps),
|
|
294
|
+
});
|
|
295
|
+
const result = await runCampaign(plan, {
|
|
296
|
+
newProgram: () => buildProgram(deps),
|
|
297
|
+
journeysDir,
|
|
298
|
+
missionTargetsDir: resolveMissionTargetsDir(deps),
|
|
299
|
+
...(o.out === undefined ? {} : { outDir: o.out }),
|
|
300
|
+
...(o.real === true ? { real: true } : {}),
|
|
301
|
+
...(o.fakeAi === true ? { fakeAi: true } : {}),
|
|
302
|
+
missionArgs: forwardedArgv(this, SWEEP_OWNED),
|
|
303
|
+
});
|
|
304
|
+
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);
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
if (err instanceof CampaignSpecError)
|
|
308
|
+
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
309
|
+
else
|
|
310
|
+
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const environment = environmentFromFlags({ ...(o.env === undefined ? {} : { env: o.env }), ...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }) }, environmentSeams(deps));
|
|
316
|
+
// The environment's own session applies when --storage-state names none (as `journey run`).
|
|
317
|
+
if (o.storageState === undefined && environment?.storageState !== undefined)
|
|
318
|
+
o.storageState = environment.storageState;
|
|
319
|
+
journeyPrefix = await resolveJourneyPrefix({
|
|
320
|
+
dir: resolveJourneysDir(deps, o.journeysDir),
|
|
321
|
+
id: o.fromJourney,
|
|
322
|
+
atStep: o.atStep,
|
|
323
|
+
params: o.param,
|
|
324
|
+
...(environment === undefined ? {} : { environment }),
|
|
325
|
+
...(o.storageState === undefined ? {} : { storageState: o.storageState }),
|
|
326
|
+
dbPath: resolveDbPath(deps),
|
|
327
|
+
environmentFlags: { ...(o.env === undefined ? {} : { env: o.env }), ...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }) },
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (err) {
|
|
331
|
+
if (isEnvironmentError(err) || err instanceof JourneyPrefixArgsError)
|
|
332
|
+
emitExplore(fail(err.code, err.message));
|
|
333
|
+
else if (err instanceof UnknownJourneyError)
|
|
334
|
+
emitExplore(fail("E_UNKNOWN_JOURNEY", err.message));
|
|
335
|
+
else if (err instanceof ParamValidationError)
|
|
336
|
+
emitExplore(fail("E_INVALID_PARAMS", err.message));
|
|
337
|
+
else if (err instanceof JourneyRequiresAuthError)
|
|
338
|
+
emitExplore(fail("E_JOURNEY_REQUIRES_AUTH", err.message));
|
|
339
|
+
else
|
|
340
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
// The prefix types its secret params into the page the mission perceives: they are redacted like
|
|
344
|
+
// --secret. Coverage/exploratory carry no redaction set, so a prefix with secrets refuses them.
|
|
345
|
+
if (journeyPrefix.secrets.length > 0 && (strategy === "coverage" || strategy === "exploratory")) {
|
|
346
|
+
refuse(`journey '${o.fromJourney}' types a secret param before step ${journeyPrefix.branch.step}: --strategy ${strategy} cannot redact it — use goal, adversarial or usability`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
o.url = journeyPrefix.startUrl;
|
|
350
|
+
// Default allowlist: the origins the Journey's steps may be on (the environment's), not just the landing's.
|
|
351
|
+
if (o.allow.length === 0)
|
|
352
|
+
o.allow = [...journeyPrefix.allowedOrigins];
|
|
353
|
+
}
|
|
354
|
+
const withPrefix = journeyPrefix === undefined ? {} : { journeyPrefix };
|
|
355
|
+
/** #293: a prefix that no longer replays, or a site policy that refused it — typed, handled once for every strategy. */
|
|
356
|
+
const emitJourneyFailure = (err) => {
|
|
357
|
+
if (err instanceof JourneyPrefixStaleError) {
|
|
358
|
+
emitExplore(ok(withEngine(journeyStaleResult(err, strategy))), EXIT_CODES.inconclusive);
|
|
359
|
+
return true;
|
|
360
|
+
}
|
|
361
|
+
if (err instanceof SiteGateRefusedError) {
|
|
362
|
+
emitExplore(fail(err.code, err.message));
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
return false;
|
|
366
|
+
};
|
|
181
367
|
// #195: `--secret env:VAR` is resolved from the environment before anything runs (fail closed).
|
|
182
368
|
try {
|
|
183
369
|
const resolved = resolveSecretArgs(o.secret, process.env, "--secret");
|
|
184
370
|
if (resolved.literals > 0)
|
|
185
371
|
program.configureOutput().writeErr?.(LITERAL_SECRET_WARNING);
|
|
186
|
-
o.secret = resolved.secrets;
|
|
372
|
+
o.secret = [...resolved.secrets, ...(journeyPrefix?.secrets ?? [])];
|
|
187
373
|
}
|
|
188
374
|
catch (err) {
|
|
189
375
|
if (!(err instanceof SecretArgError))
|
|
@@ -196,6 +382,9 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
196
382
|
let browser;
|
|
197
383
|
try {
|
|
198
384
|
browser = browserRunFromFlags(o, deps.explore?.env ?? process.env);
|
|
385
|
+
// #256: a chrome-extension:// --url must be a loaded extension's; loaded extensions' origins are allowed.
|
|
386
|
+
assertExtensionTargetLoaded(o.url, browser);
|
|
387
|
+
o.allow = allowWithExtensions(o.url, o.allow, browser);
|
|
199
388
|
}
|
|
200
389
|
catch (err) {
|
|
201
390
|
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
@@ -261,12 +450,9 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
261
450
|
emitExplore(fail("E_EXPLORE_ARGS", "--job-wait-ms must be a positive integer"));
|
|
262
451
|
return;
|
|
263
452
|
}
|
|
264
|
-
// #154: refused BEFORE any browser opens
|
|
453
|
+
// #154: refused BEFORE any browser opens — `nonNegativeIntArg` already rejects a bad value at
|
|
454
|
+
// parse time (#275: the value is a number here). 0 is valid: "don't replay" — a hang is then
|
|
265
455
|
// reported unconfirmed (inconclusive), never replayed and never a crash.
|
|
266
|
-
if (o.hangReplays !== undefined && !/^\d+$/.test(o.hangReplays.trim())) {
|
|
267
|
-
emitExplore(fail("E_EXPLORE_ARGS", `--hang-replays must be a non-negative integer (0 = don't replay; the hang is reported unconfirmed), got "${o.hangReplays}"`));
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
456
|
try {
|
|
271
457
|
validateDenyPatterns(o.deny);
|
|
272
458
|
validateDenyPatterns(o.paid, "--paid");
|
|
@@ -293,7 +479,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
293
479
|
...(o.fileIssues === undefined ? {} : { fileIssues: o.fileIssues }),
|
|
294
480
|
...(o.issueRepo === undefined ? {} : { issueRepo: o.issueRepo }),
|
|
295
481
|
...(o.jevitateRepo === undefined ? {} : { jevitateRepo: o.jevitateRepo }),
|
|
296
|
-
}, new URL(o.url).origin);
|
|
482
|
+
}, new URL(setupRefFreeUrl(o.url)).origin);
|
|
297
483
|
}
|
|
298
484
|
catch (err) {
|
|
299
485
|
if (err instanceof FilingConfigError) {
|
|
@@ -309,7 +495,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
309
495
|
let target;
|
|
310
496
|
if (o.url !== undefined) {
|
|
311
497
|
try {
|
|
312
|
-
target = resolveTargetConfig(loadTargetsFile(deps.explore?.targetsConfigPath), new URL(o.url).origin, {
|
|
498
|
+
target = resolveTargetConfig(loadTargetsFile(deps.explore?.targetsConfigPath), new URL(setupRefFreeUrl(o.url)).origin, {
|
|
313
499
|
settleIgnore: o.settleIgnore,
|
|
314
500
|
ignoreNoProgress: o.ignoreNoProgress,
|
|
315
501
|
apiPrefixes: o.apiPrefix,
|
|
@@ -343,7 +529,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
343
529
|
return result;
|
|
344
530
|
let out = result;
|
|
345
531
|
if (filing?.enabled === true)
|
|
346
|
-
out = await fileDraftsWithEvidence(out, filing, issueFiler,
|
|
532
|
+
out = await fileDraftsWithEvidence(out, filing, issueFiler, clock.nowIso());
|
|
347
533
|
return out;
|
|
348
534
|
};
|
|
349
535
|
// `--fixture` feeds the upload op, which only the explore loop (goal and
|
|
@@ -353,6 +539,11 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
353
539
|
emitExplore(fail("E_EXPLORE_ARGS", "--fixture is supported only with --strategy goal or usability"));
|
|
354
540
|
return;
|
|
355
541
|
}
|
|
542
|
+
// #198: product facts and polish shape the UX review's findings only.
|
|
543
|
+
if ((o.product !== undefined || o.polish === true || o.probeGuards === true) && (o.feature !== undefined || strategy !== "usability")) {
|
|
544
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--product, --polish and --probe-guards are supported only with --strategy usability"));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
356
547
|
// #225: success checks judge a goal / a usability job — every other strategy (and --feature) would
|
|
357
548
|
// silently ignore them, so they are refused up front, never dropped.
|
|
358
549
|
if ((o.success.length > 0 || o.successWhen !== undefined || o.allowVacuousChecks === true) &&
|
|
@@ -360,6 +551,12 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
360
551
|
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}`})`));
|
|
361
552
|
return;
|
|
362
553
|
}
|
|
554
|
+
// #303: action deltas are recorded by the goal loop (goal and usability runs) only — refused
|
|
555
|
+
// elsewhere, never silently ignored.
|
|
556
|
+
if (o.actionDeltas === true && o.feature !== undefined) {
|
|
557
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--action-deltas is not supported with --feature (goal, usability, coverage, exploratory and adversarial runs record deltas)"));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
363
560
|
if (o.storageState !== undefined && !existsSync(o.storageState)) {
|
|
364
561
|
emitExplore(fail("E_EXPLORE_ARGS", `storage state not found: ${o.storageState}`));
|
|
365
562
|
return;
|
|
@@ -436,12 +633,18 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
436
633
|
const sources = parseLogSourceSpecs(o.logSource, o.allowLogCmd ?? false);
|
|
437
634
|
const logDefect = parseLogDefectSpecs(o.logDefect);
|
|
438
635
|
const logIgnore = parseLogIgnoreSpecs(o.logIgnore);
|
|
636
|
+
const logScope = parseLogScopeSpecs(o.logScope);
|
|
637
|
+
const correlationHeaders = parseCorrelationHeaders(o.logCorrelationHeader);
|
|
638
|
+
const idPatterns = parseLogIdPatterns(o.logIdPattern);
|
|
439
639
|
serverLog = {
|
|
440
640
|
sources,
|
|
441
641
|
logDefect,
|
|
442
642
|
allowLogCmd: o.allowLogCmd ?? false,
|
|
443
643
|
quietOk: o.logQuietOk,
|
|
444
644
|
logIgnore,
|
|
645
|
+
logScope,
|
|
646
|
+
correlationHeaders,
|
|
647
|
+
idPatterns,
|
|
445
648
|
...(o.serverLogDrainMs === undefined ? {} : { drainMs: Number(o.serverLogDrainMs) }),
|
|
446
649
|
};
|
|
447
650
|
}
|
|
@@ -453,6 +656,13 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
453
656
|
throw err;
|
|
454
657
|
}
|
|
455
658
|
}
|
|
659
|
+
if (o.logTriage === true) {
|
|
660
|
+
if (serverLog === undefined) {
|
|
661
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--log-triage triages the run's backend and browser signals: it needs at least one --log-source"));
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
serverLog = { ...serverLog, triage: {} };
|
|
665
|
+
}
|
|
456
666
|
const withServerLog = serverLog === undefined ? {} : { serverLog };
|
|
457
667
|
// Secret field bindings (#72): resolved from the environment here, typed by code in the goal loop.
|
|
458
668
|
let secretFields = [];
|
|
@@ -474,10 +684,27 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
474
684
|
return;
|
|
475
685
|
}
|
|
476
686
|
}
|
|
687
|
+
// #281: fields typed with a file's exact text — read (and checked) before any browser opens.
|
|
688
|
+
let typeFixtures = [];
|
|
689
|
+
if (o.typeFixture.length > 0) {
|
|
690
|
+
if (o.feature !== undefined || strategy !== "goal") {
|
|
691
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--type-fixture is supported only with --strategy goal"));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
try {
|
|
695
|
+
typeFixtures = loadTypeFixtures(o.typeFixture);
|
|
696
|
+
}
|
|
697
|
+
catch (err) {
|
|
698
|
+
if (!(err instanceof TypeFixtureSpecError))
|
|
699
|
+
throw err;
|
|
700
|
+
emitExplore(fail(err.code, err.message));
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
477
704
|
// Mission fixtures (#140/#144) run around the goal loop and its replays only.
|
|
478
705
|
const fixtureFlagsGiven = o.fixtures !== undefined || o.before !== undefined || o.after !== undefined;
|
|
479
706
|
if (fixtureFlagsGiven && (o.feature !== undefined || strategy !== "goal")) {
|
|
480
|
-
emitExplore(fail("E_EXPLORE_ARGS", "--fixtures, --before and --after are supported only with --strategy goal"));
|
|
707
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--fixtures, --before and --after are supported only with --strategy goal, or with --from-journey (any anchored strategy)"));
|
|
481
708
|
return;
|
|
482
709
|
}
|
|
483
710
|
// Additive coverage/exploratory strategy: proof-by-induction state coverage.
|
|
@@ -527,6 +754,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
527
754
|
try {
|
|
528
755
|
const result = await runCoverageMission({
|
|
529
756
|
...(target === undefined ? {} : { target }),
|
|
757
|
+
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
530
758
|
url: o.url,
|
|
531
759
|
allowlist: covAllowlist,
|
|
532
760
|
judge: covJudge,
|
|
@@ -545,12 +773,15 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
545
773
|
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
546
774
|
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
547
775
|
...withInvariants,
|
|
548
|
-
...
|
|
776
|
+
...triagedServerLog(serverLog, covJudge, o.real === true),
|
|
777
|
+
...withPrefix,
|
|
549
778
|
});
|
|
550
779
|
// Typed verdict → exit code (0 clean · 1 defects · 2 crashed; see exit-codes.ts).
|
|
551
780
|
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
552
781
|
}
|
|
553
782
|
catch (err) {
|
|
783
|
+
if (emitJourneyFailure(err))
|
|
784
|
+
return;
|
|
554
785
|
if (err instanceof UnauthorizedExploreTargetError) {
|
|
555
786
|
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
556
787
|
}
|
|
@@ -610,6 +841,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
610
841
|
try {
|
|
611
842
|
const result = await runAdversarialCliMission({
|
|
612
843
|
...(target === undefined ? {} : { target }),
|
|
844
|
+
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
613
845
|
seedUrl: o.url,
|
|
614
846
|
allowlist: advAllowlist,
|
|
615
847
|
usage: advUsage,
|
|
@@ -619,7 +851,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
619
851
|
secrets: o.secret.length > 0 ? o.secret : undefined,
|
|
620
852
|
...(runFiling === undefined ? {} : { filing: runFiling }),
|
|
621
853
|
issueFiler,
|
|
622
|
-
...(o.hangReplays === undefined ? {} : { hangReplays:
|
|
854
|
+
...(o.hangReplays === undefined ? {} : { hangReplays: o.hangReplays }),
|
|
623
855
|
strategies: CLI_ADVERSARIAL_STRATEGIES,
|
|
624
856
|
judgment: advJudge,
|
|
625
857
|
generation: advGen,
|
|
@@ -632,13 +864,16 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
632
864
|
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
633
865
|
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
634
866
|
...withInvariants,
|
|
635
|
-
...
|
|
867
|
+
...triagedServerLog(serverLog, advJudge, o.real === true),
|
|
868
|
+
...withPrefix,
|
|
636
869
|
});
|
|
637
870
|
// The typed verdict gates CI: 0 clean · 1 defects found (a failing check) · 2 the run
|
|
638
871
|
// itself broke (inconclusive/crashed) — see exit-codes.ts.
|
|
639
872
|
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
640
873
|
}
|
|
641
874
|
catch (err) {
|
|
875
|
+
if (emitJourneyFailure(err))
|
|
876
|
+
return;
|
|
642
877
|
if (err instanceof UnauthorizedExploreTargetError) {
|
|
643
878
|
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
644
879
|
}
|
|
@@ -720,6 +955,9 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
720
955
|
...(o.minConfidence !== undefined ? { minConfidence: o.minConfidence } : {}),
|
|
721
956
|
...(o.show !== undefined ? { show: o.show } : {}),
|
|
722
957
|
...(o.maxFindingsPerPage !== undefined ? { maxFindingsPerRoute: o.maxFindingsPerPage } : {}),
|
|
958
|
+
...(o.product !== undefined ? { product: o.product } : {}),
|
|
959
|
+
...(o.polish === true ? { polish: true } : {}),
|
|
960
|
+
...(o.probeGuards === true ? { probeGuards: true } : {}),
|
|
723
961
|
bounds: Object.keys(uxBounds).length > 0 ? uxBounds : undefined,
|
|
724
962
|
conversation,
|
|
725
963
|
secrets: o.secret.length > 0 ? o.secret : undefined,
|
|
@@ -733,17 +971,21 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
733
971
|
overflow,
|
|
734
972
|
...(o.storageState !== undefined ? { storageState: o.storageState } : {}),
|
|
735
973
|
...(o.saveStorageState !== undefined ? { saveStorageState: o.saveStorageState } : {}),
|
|
736
|
-
...
|
|
974
|
+
...triagedServerLog(serverLog, uxJudge, o.real === true),
|
|
737
975
|
...withInvariants,
|
|
738
976
|
...(uxSuccessChecks.length === 0 ? {} : { successChecks: uxSuccessChecks }),
|
|
739
977
|
...(o.successWhen === "held" || o.successWhen === "final" ? { successWhen: o.successWhen } : {}),
|
|
740
978
|
...(o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {}),
|
|
979
|
+
...withPrefix,
|
|
980
|
+
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
741
981
|
});
|
|
742
982
|
// UX findings are advisory (0); a failed --success check (#225) is 1, as on a goal run; a
|
|
743
983
|
// broken run or an unavailable analysis is 2.
|
|
744
984
|
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
745
985
|
}
|
|
746
986
|
catch (err) {
|
|
987
|
+
if (emitJourneyFailure(err))
|
|
988
|
+
return;
|
|
747
989
|
if (err instanceof UnauthorizedExploreTargetError) {
|
|
748
990
|
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
749
991
|
}
|
|
@@ -760,6 +1002,9 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
760
1002
|
else if (err instanceof UsabilityInvariantsUnsupportedError) {
|
|
761
1003
|
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
762
1004
|
}
|
|
1005
|
+
else if (err instanceof ProductFactsError) {
|
|
1006
|
+
emitExplore(fail(err.code, err.message));
|
|
1007
|
+
}
|
|
763
1008
|
else {
|
|
764
1009
|
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
765
1010
|
}
|
|
@@ -840,7 +1085,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
840
1085
|
return;
|
|
841
1086
|
}
|
|
842
1087
|
const successWhen = o.successWhen === "held" || o.successWhen === "final" ? o.successWhen : undefined;
|
|
843
|
-
const allowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
1088
|
+
const allowlist = resolveExploreAllowlist(setupRefFreeUrl(o.url), o.allow);
|
|
844
1089
|
// Fixtures (#140/#144): the spec and every ${setup.x} reference are validated here, before any
|
|
845
1090
|
// browser or request; the setup itself runs just before the mission (below).
|
|
846
1091
|
let fx;
|
|
@@ -848,11 +1093,12 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
848
1093
|
checkUrlRefOrigin(o.url);
|
|
849
1094
|
fx = buildMissionFixtures(o, {
|
|
850
1095
|
allowlist,
|
|
851
|
-
baseUrl: o.url
|
|
1096
|
+
baseUrl: setupRefFreeUrl(o.url),
|
|
852
1097
|
...(primaryStorageState === undefined ? {} : { storageState: primaryStorageState }),
|
|
853
1098
|
secretFields,
|
|
854
1099
|
secrets: o.secret,
|
|
855
1100
|
...(target?.fixtures === undefined ? {} : { targetFixtures: target.fixtures }),
|
|
1101
|
+
...(target?.personas === undefined ? {} : { personas: target.personas }),
|
|
856
1102
|
});
|
|
857
1103
|
checkSetupRefs({ "--url": o.url, "--goal": o.goal, "--success": o.success, ...invariantSetupTexts(invariants) }, fx);
|
|
858
1104
|
}
|
|
@@ -891,7 +1137,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
891
1137
|
try {
|
|
892
1138
|
await fx.setup();
|
|
893
1139
|
const b = fx.bindings();
|
|
894
|
-
url =
|
|
1140
|
+
url = substituteUrlSetupRefs(o.url, b);
|
|
895
1141
|
goal = substituteSetupRefs(o.goal, b, { where: "--goal" });
|
|
896
1142
|
successChecks = o.success.map((spec) => parseSuccessSpec(substituteSetupRefs(spec, b, { where: "--success" })));
|
|
897
1143
|
// #187: ${setup.x} in the invariants (probe paths, deniedAs.open, capture routes), origin-fixed.
|
|
@@ -916,6 +1162,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
916
1162
|
successChecks,
|
|
917
1163
|
...(successWhen === undefined ? {} : { successWhen }),
|
|
918
1164
|
...(o.allowVacuousChecks === true ? { allowVacuousChecks: true } : {}),
|
|
1165
|
+
...(o.actionDeltas === true ? { actionDeltas: true } : {}),
|
|
919
1166
|
allowlist,
|
|
920
1167
|
judge,
|
|
921
1168
|
gen,
|
|
@@ -923,6 +1170,7 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
923
1170
|
bounds: Object.keys(bounds).length > 0 ? bounds : undefined,
|
|
924
1171
|
secrets: o.secret.length > 0 ? o.secret : undefined,
|
|
925
1172
|
...(secretFields.length > 0 ? { secretFields } : {}),
|
|
1173
|
+
...(typeFixtures.length > 0 ? { typeFixtures } : {}),
|
|
926
1174
|
fixture: o.fixture,
|
|
927
1175
|
outDir: o.out,
|
|
928
1176
|
browserPortFactory: deps.explore?.browserPortFactory,
|
|
@@ -934,16 +1182,19 @@ export function registerExploreCommands(program, deps, buildProgram) {
|
|
|
934
1182
|
...(actors === null ? {} : { actors }),
|
|
935
1183
|
...(runFiling === undefined ? {} : { filing: runFiling }),
|
|
936
1184
|
issueFiler,
|
|
937
|
-
...(o.hangReplays === undefined ? {} : { hangReplays:
|
|
1185
|
+
...(o.hangReplays === undefined ? {} : { hangReplays: o.hangReplays }),
|
|
938
1186
|
conversation,
|
|
939
1187
|
...runInvariants,
|
|
940
|
-
...
|
|
1188
|
+
...triagedServerLog(serverLog, judge, o.real === true),
|
|
941
1189
|
...(fx === undefined ? {} : { fixtures: fx }),
|
|
1190
|
+
...withPrefix,
|
|
942
1191
|
});
|
|
943
1192
|
// 0 succeeded · 1 assertion not met · 2 the run broke (inconclusive/crashed).
|
|
944
1193
|
emitExplore(ok(await withEvidence(result)), result.exitCode);
|
|
945
1194
|
}
|
|
946
1195
|
catch (err) {
|
|
1196
|
+
if (emitJourneyFailure(err))
|
|
1197
|
+
return;
|
|
947
1198
|
if (err instanceof UnauthorizedExploreTargetError) {
|
|
948
1199
|
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
949
1200
|
}
|