@jevitate/cli 0.12.0 → 0.14.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/author-journey-cli.d.ts +9 -0
- package/dist/author-journey-cli.js +18 -1
- package/dist/author-journey-cli.js.map +1 -1
- package/dist/bin.js +25673 -24216
- package/dist/build-info.generated.js +2 -2
- package/dist/catalog-analysis.js +4 -2
- package/dist/catalog-analysis.js.map +1 -1
- package/dist/catalog-bundle-api.js +1 -3
- package/dist/catalog-bundle-api.js.map +1 -1
- package/dist/catalog-bundle-findings.js +1 -3
- package/dist/catalog-bundle-findings.js.map +1 -1
- package/dist/catalog-bundle.js +1 -4
- package/dist/catalog-bundle.js.map +1 -1
- package/dist/catalog-cli.js +48 -6
- package/dist/catalog-cli.js.map +1 -1
- package/dist/catalog-review.d.ts +8 -0
- package/dist/catalog-review.js +26 -0
- package/dist/catalog-review.js.map +1 -1
- package/dist/catalog.d.ts +42 -0
- package/dist/catalog.js +67 -4
- package/dist/catalog.js.map +1 -1
- package/dist/check-execute.js +9 -0
- package/dist/check-execute.js.map +1 -1
- package/dist/check-plan.d.ts +5 -2
- package/dist/check-plan.js +9 -5
- package/dist/check-plan.js.map +1 -1
- package/dist/check-suite.js +1 -3
- package/dist/check-suite.js.map +1 -1
- package/dist/cli-output.js +1 -3
- package/dist/cli-output.js.map +1 -1
- package/dist/cli-shared.d.ts +1 -1
- package/dist/cli-shared.js +2 -0
- package/dist/cli-shared.js.map +1 -1
- package/dist/defect-evidence.js +4 -8
- package/dist/defect-evidence.js.map +1 -1
- package/dist/environments.d.ts +1 -1
- package/dist/environments.js +1 -1
- package/dist/environments.js.map +1 -1
- package/dist/exit-codes.d.ts +0 -1
- package/dist/exit-codes.js +2 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/explore-adversarial.d.ts +10 -1
- package/dist/explore-adversarial.js +3 -0
- package/dist/explore-adversarial.js.map +1 -1
- package/dist/explore-cli/adversarial-strategy.d.ts +3 -0
- package/dist/explore-cli/adversarial-strategy.js +101 -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 +97 -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 +60 -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 +185 -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 +157 -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 +702 -0
- package/dist/explore-cli/run.js.map +1 -0
- package/dist/explore-cli/scope-region.d.ts +17 -0
- package/dist/explore-cli/scope-region.js +24 -0
- package/dist/explore-cli/scope-region.js.map +1 -0
- package/dist/explore-cli/shared.d.ts +167 -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 +137 -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 -1347
- package/dist/explore-cli.js.map +1 -1
- package/dist/explore-coverage.d.ts +10 -1
- package/dist/explore-coverage.js +3 -0
- package/dist/explore-coverage.js.map +1 -1
- package/dist/explore-feature.d.ts +6 -0
- package/dist/explore-feature.js +1 -0
- package/dist/explore-feature.js.map +1 -1
- package/dist/explore-goal.d.ts +6 -0
- package/dist/explore-goal.js +1 -0
- package/dist/explore-goal.js.map +1 -1
- package/dist/explore-specs.d.ts +3 -1
- package/dist/explore-specs.js +2 -1
- package/dist/explore-specs.js.map +1 -1
- package/dist/github-issue-filer.js +1 -4
- package/dist/github-issue-filer.js.map +1 -1
- package/dist/goal-run-flags.d.ts +0 -1
- package/dist/goal-run-flags.js.map +1 -1
- package/dist/inbox-cli.js +1 -3
- package/dist/inbox-cli.js.map +1 -1
- package/dist/init-skills.js +1 -4
- package/dist/init-skills.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 +22 -8
- package/dist/journey-anchor-cli.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 +84 -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 +108 -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 +3 -2
- package/dist/journey-cli.js +25 -950
- package/dist/journey-cli.js.map +1 -1
- package/dist/journey-link-api.d.ts +40 -0
- package/dist/journey-link-api.js +90 -0
- package/dist/journey-link-api.js.map +1 -0
- package/dist/journey-link-cli.d.ts +7 -0
- package/dist/journey-link-cli.js +58 -0
- package/dist/journey-link-cli.js.map +1 -0
- package/dist/journey-link-hints.d.ts +8 -0
- package/dist/journey-link-hints.js +68 -0
- package/dist/journey-link-hints.js.map +1 -0
- package/dist/journey-lint-sarif.js +3 -0
- package/dist/journey-lint-sarif.js.map +1 -1
- package/dist/journey-network-checks.d.ts +9 -1
- package/dist/journey-network-checks.js +55 -12
- package/dist/journey-network-checks.js.map +1 -1
- package/dist/journey-prefix.d.ts +9 -1
- package/dist/journey-prefix.js +12 -0
- package/dist/journey-prefix.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-api.d.ts +1 -0
- package/dist/journey-review-api.js +7 -0
- package/dist/journey-review-api.js.map +1 -1
- package/dist/journey-review-store.js +1 -0
- package/dist/journey-review-store.js.map +1 -1
- package/dist/journey-review.d.ts +6 -4
- package/dist/journey-review.js +18 -393
- package/dist/journey-review.js.map +1 -1
- package/dist/journey-verify.js +2 -1
- package/dist/journey-verify.js.map +1 -1
- package/dist/ledger-api.js +1 -4
- package/dist/ledger-api.js.map +1 -1
- package/dist/locator-health-api.js +1 -3
- package/dist/locator-health-api.js.map +1 -1
- package/dist/locator-health.d.ts +0 -1
- package/dist/locator-health.js +0 -3
- package/dist/locator-health.js.map +1 -1
- package/dist/log-classes.js +1 -3
- package/dist/log-classes.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 +48 -7
- 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/mission-fixtures.js +1 -8
- package/dist/mission-fixtures.js.map +1 -1
- package/dist/mission-queue-cli.js +1 -3
- package/dist/mission-queue-cli.js.map +1 -1
- package/dist/multi-run.js +1 -3
- package/dist/multi-run.js.map +1 -1
- package/dist/pre-approval.js +4 -4
- package/dist/pre-approval.js.map +1 -1
- package/dist/readiness.js +2 -2
- package/dist/readiness.js.map +1 -1
- package/dist/report-api.js +1 -4
- package/dist/report-api.js.map +1 -1
- package/dist/resource-preflight.js.map +1 -1
- package/dist/run-metadata.js +1 -3
- package/dist/run-metadata.js.map +1 -1
- package/dist/run-tags-cli.d.ts +0 -2
- package/dist/run-tags-cli.js +0 -4
- package/dist/run-tags-cli.js.map +1 -1
- package/dist/session-check.js +1 -3
- package/dist/session-check.js.map +1 -1
- package/dist/signal-triage.js +6 -2
- package/dist/signal-triage.js.map +1 -1
- package/dist/suite-explore-options.d.ts +8 -0
- package/dist/suite-explore-options.js +4 -0
- package/dist/suite-explore-options.js.map +1 -1
- package/dist/sweep-api.d.ts +9 -0
- package/dist/sweep-api.js +70 -4
- 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/transcript-file.d.ts +0 -3
- package/dist/transcript-file.js +0 -9
- package/dist/transcript-file.js.map +1 -1
- package/dist/ui-api.js +1 -4
- package/dist/ui-api.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 +76 -0
- package/dist/ux-usability/setup.js +204 -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 +20 -17
- package/dist/ux-usability.js +15 -589
- package/dist/ux-usability.js.map +1 -1
- package/dist/verify-fix-api.js +1 -4
- package/dist/verify-fix-api.js.map +1 -1
- package/package.json +5 -6
- package/skills/jevitate-explore/SKILL.md +9 -1
- package/skills/jevitate-getting-started/SKILL.md +1 -0
- package/skills/jevitate-promotion-ready/SKILL.md +76 -0
- package/skills/jevitate-record/SKILL.md +3 -0
- package/skills/jevitate-run-journey/SKILL.md +3 -0
- package/skills/jevitate-test-campaign/SKILL.md +5 -3
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { taggedAction } from "../run-tags-cli.js";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { envCredentialStore } from "@jevitate/ai-core";
|
|
4
|
+
import { loadLocalCredentials } from "../credentials-file.js";
|
|
5
|
+
import { UnauthorizedExploreTargetError, TypeFixtureSpecError, validateAllowControlPatterns, validateDenyPatterns } from "@jevitate/explore";
|
|
6
|
+
import { loadTypeFixtures } from "../type-fixture-file.js";
|
|
7
|
+
import { secretCommandRunner } from "../secret-command.js";
|
|
8
|
+
import { ok, fail } from "../envelope.js";
|
|
9
|
+
import { SessionFileInProjectError, assertSessionFileOutsideProject } from "../project-dir.js";
|
|
10
|
+
import { InvariantsFileError, loadInvariantFiles, resolveInvariantAuthTokens } from "../invariants-file.js";
|
|
11
|
+
import { FilingConfigError, loadFilingFileConfig, resolveFilingConfig } from "../findings-filing.js";
|
|
12
|
+
import { fileDraftsWithEvidence } from "../defect-evidence.js";
|
|
13
|
+
import { parseScreenshotsArg } from "../run-screenshots.js";
|
|
14
|
+
import { GitHubIssueFiler } from "../github-issue-filer.js";
|
|
15
|
+
import { TargetConfigError, loadTargetsFile, resolveTargetConfig } from "../target-config.js";
|
|
16
|
+
import { setupRefFreeUrl } from "../fixture-cli.js";
|
|
17
|
+
import { LITERAL_SECRET_WARNING, SecretArgError, resolveSecretArgs } from "../secret-args.js";
|
|
18
|
+
import { withEngine } from "../engine.js";
|
|
19
|
+
import { setKillSwitchOutput } from "../kill-signal.js";
|
|
20
|
+
import { EXIT_CODES } from "../exit-codes.js";
|
|
21
|
+
import { formatMissionHuman, formatMultiRunHuman } from "../cli-output.js";
|
|
22
|
+
import { resolveExploreAllowlist } from "../explore-api.js";
|
|
23
|
+
import { parseLogSourceSpecs, LogSourceSpecError } from "../log-sources.js";
|
|
24
|
+
import { parseLogDefectSpecs, parseLogIgnoreSpecs, parseLogScopeSpecs } from "../log-correlation.js";
|
|
25
|
+
import { LogClassesError, loadLogClassRules } from "../log-classes.js";
|
|
26
|
+
import { parseCorrelationHeaders, parseLogIdPatterns } from "../log-trace.js";
|
|
27
|
+
import { LogSpecError } from "../log-lines.js";
|
|
28
|
+
import { MultiRunArgsError, projectPersonaFor, resolveMultiRunPlan, wantsMultiRun } from "../multi-run.js";
|
|
29
|
+
import { authExpiredResult, ensurePersonaSession, LoginArgsError, parseAuthCheck } from "../persona-login.js";
|
|
30
|
+
import { MultiRunAbortedError, runExploreMultiRun } from "../multi-run-cli.js";
|
|
31
|
+
import { checkActorsAgainstSpec, resolveMissionActors } from "../mission-actors.js";
|
|
32
|
+
import { PlaywrightBrowserPort } from "@jevitate/playwright";
|
|
33
|
+
import { browserRunFromFlags, emulationFromFlags, emitCommandResult, environmentSeams, resolveDbPath, resolveJourneysDir, resolveMissionTargetsDir, EXPLORE_STRATEGIES } from "../cli-shared.js";
|
|
34
|
+
import { allowWithExtensions, assertExtensionTargetLoaded, multiWindowWarning } from "../browser-run-options.js";
|
|
35
|
+
import { ParamValidationError } from "@jevitate/journey";
|
|
36
|
+
import { SiteGateRefusedError } from "@jevitate/runtime";
|
|
37
|
+
import { JourneyRequiresAuthError, UnknownJourneyError } from "../journey-api.js";
|
|
38
|
+
import { environmentFromFlags, isEnvironmentError } from "../environments.js";
|
|
39
|
+
import { resolve as resolvePath } from "node:path";
|
|
40
|
+
import { CAMPAIGN_LIMITS, isSweepMode } from "@jevitate/journey";
|
|
41
|
+
import { CampaignSpecError, runCampaign, validateCampaignSpec } from "../campaign-api.js";
|
|
42
|
+
import { formatCampaignHuman } from "../campaign-cli.js";
|
|
43
|
+
import { forwardedArgv } from "../multi-run-cli.js";
|
|
44
|
+
import { resolveScopeRegions } from "./scope-region.js";
|
|
45
|
+
import { ANCHORED_STRATEGIES, JourneyPrefixArgsError, JourneyPrefixStaleError, journeyStaleResult, resolveJourneyPrefix } from "../journey-prefix.js";
|
|
46
|
+
import { clock } from "@jevitate/domain";
|
|
47
|
+
import { GoalRunFlagError, conversationFromFlags, dialogsFromFlags, secretFieldsFromFlags, targetFlagsFromFlags } from "../goal-run-flags.js";
|
|
48
|
+
import { declareExploreCommand } from "./options.js";
|
|
49
|
+
import { runCoverageStrategy } from "./coverage-strategy.js";
|
|
50
|
+
import { runAdversarialStrategy } from "./adversarial-strategy.js";
|
|
51
|
+
import { runUsabilityStrategy } from "./usability-strategy.js";
|
|
52
|
+
import { runFeatureStrategy } from "./feature-strategy.js";
|
|
53
|
+
import { runGoalStrategy } from "./goal-strategy.js";
|
|
54
|
+
import { AUTH_PREFLIGHT_TIMEOUT_MS, SWEEP_OWNED, fixtureBoundUrlRoot } from "./shared.js";
|
|
55
|
+
/**
|
|
56
|
+
* Registers `jevitate explore` (every strategy: goal, coverage, exploratory, adversarial, usability, feature, multi-run).
|
|
57
|
+
* `buildProgram` builds a fresh program for each run of a multi-run (#141/#143).
|
|
58
|
+
*/
|
|
59
|
+
export function registerExploreRun(program, deps, buildProgram) {
|
|
60
|
+
declareExploreCommand(program)
|
|
61
|
+
.action(taggedAction(program, "explore", async function () {
|
|
62
|
+
const o = this.opts();
|
|
63
|
+
// #210: one output rule for every strategy — the envelope with --json, a human summary without.
|
|
64
|
+
const emitExplore = (envelope, exitCode, human = formatMissionHuman) => emitCommandResult(program, envelope, { json: o.json === true, command: "explore", human, ...(exitCode === undefined ? {} : { exitCode }) });
|
|
65
|
+
// #230: an unknown --strategy must be refused before any other required-option message — it
|
|
66
|
+
// would otherwise fall through to the default goal-strategy path and silently run a goal
|
|
67
|
+
// mission. Checked first, ahead of every other validation below.
|
|
68
|
+
if (o.strategy !== undefined && !EXPLORE_STRATEGIES.includes(o.strategy)) {
|
|
69
|
+
emitExplore(fail("E_EXPLORE_ARGS", `unknown strategy ${JSON.stringify(o.strategy)} (one of ${EXPLORE_STRATEGIES.join(", ")})`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// #334: a mistyped --dialogs never silently means "dismiss".
|
|
73
|
+
let dialogs;
|
|
74
|
+
try {
|
|
75
|
+
dialogs = dialogsFromFlags(o);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (!(err instanceof GoalRunFlagError))
|
|
79
|
+
throw err;
|
|
80
|
+
emitExplore(fail(err.code, err.message));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// #427: an unusable --auth-check is a usage error, refused before anything runs.
|
|
84
|
+
let authCheck;
|
|
85
|
+
try {
|
|
86
|
+
authCheck = parseAuthCheck(o.authCheck);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
if (!(err instanceof LoginArgsError))
|
|
90
|
+
throw err;
|
|
91
|
+
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
// #195: a session file never lands in the repo's .jevitate/ (refused before any run, multi-runs included).
|
|
95
|
+
if (o.saveStorageState !== undefined) {
|
|
96
|
+
try {
|
|
97
|
+
assertSessionFileOutsideProject(o.saveStorageState, "--save-storage-state");
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (!(err instanceof SessionFileInProjectError))
|
|
101
|
+
throw err;
|
|
102
|
+
emitExplore(fail(err.code, err.message));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const strategy = o.strategy ?? "goal";
|
|
107
|
+
// #519: --scope-region on a strategy without a coverage gate, or one that does not parse, is refused up front
|
|
108
|
+
// (also ahead of a --at-step sweep, whose per-stop runs would each refuse it).
|
|
109
|
+
try {
|
|
110
|
+
resolveScopeRegions({ scopeRegion: o.scopeRegion, strategy, ...(o.feature === undefined ? {} : { feature: o.feature }), ...(o.goal === undefined ? {} : { goal: o.goal }) });
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// #293 journey-anchored exploration: the Journey, step, params and environment are resolved (and
|
|
117
|
+
// refused, exit 64) before anything else — the start URL every later check uses is where the
|
|
118
|
+
// Journey's prefix lands.
|
|
119
|
+
let journeyPrefix;
|
|
120
|
+
/** #399: how the prefix was resolved — re-resolved once `${setup.x}` in a --param is bound (goal + --fixtures). */
|
|
121
|
+
let prefixRequest;
|
|
122
|
+
const anchoredFlags = [o.fromJourney, o.atStep, o.journeysDir, o.env, o.baseUrl].some((v) => v !== undefined);
|
|
123
|
+
if (anchoredFlags || Object.keys(o.param).length > 0) {
|
|
124
|
+
const refuse = (message) => emitExplore(fail("E_EXPLORE_ARGS", message));
|
|
125
|
+
if (o.fromJourney === undefined || o.atStep === undefined) {
|
|
126
|
+
refuse("--from-journey and --at-step go together (and --param, --env, --base-url and --journeys-dir need them)");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (o.url !== undefined) {
|
|
130
|
+
refuse("--url cannot be combined with --from-journey: the mission starts where the Journey's prefix leaves the page");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (o.feature !== undefined || !ANCHORED_STRATEGIES.includes(strategy)) {
|
|
134
|
+
refuse(`--from-journey supports --strategy ${ANCHORED_STRATEGIES.join(", ")} (not --feature)`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (wantsMultiRun(o) || o.actor.length > 0) {
|
|
138
|
+
refuse("--from-journey runs one anchored mission: --repeat, --persona, --personas and --actor are not supported with it (a campaign runs several)");
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// #293 sweep: `--at-step all|anchors` runs the strategy from EVERY step (or anchor), each in a
|
|
142
|
+
// fresh session with --fixtures restored around it, --max-actions/--max-decisions split evenly
|
|
143
|
+
// over the stop points, and reads them as one deduped report (a one-job campaign).
|
|
144
|
+
// #312: one anchored step with a state restore (--fixtures/--before/--after) on a non-goal
|
|
145
|
+
// strategy runs the same way — as a one-stop campaign, whose runner restores around the run.
|
|
146
|
+
const restoredSingle = !isSweepMode(o.atStep) && strategy !== "goal" && (o.fixtures !== undefined || o.before !== undefined || o.after !== undefined);
|
|
147
|
+
if ((isSweepMode(o.atStep) || restoredSingle) && Object.values(o.param).some((v) => v.includes("${setup."))) {
|
|
148
|
+
refuse("a --param holding ${setup.x} is bound from --fixtures only by a single --strategy goal run; pass the value itself here");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (isSweepMode(o.atStep) || restoredSingle) {
|
|
152
|
+
if (o.real !== true && o.fakeAi !== true) {
|
|
153
|
+
emitExplore(fail("E_AI_SETUP_REQUIRED", "a sweep's missions are model-driven: pass --real or --fake-ai"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const journeysDir = resolveJourneysDir(deps, o.journeysDir);
|
|
157
|
+
const abs = (p) => resolvePath(p);
|
|
158
|
+
const spec = {
|
|
159
|
+
version: 1,
|
|
160
|
+
name: restoredSingle ? `${o.fromJourney} at step ${o.atStep.trim()} (${strategy}, restored)` : `sweep of ${o.fromJourney} (${o.atStep}, ${strategy})`,
|
|
161
|
+
...(o.env === undefined ? {} : { env: o.env }),
|
|
162
|
+
...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }),
|
|
163
|
+
...(o.storageState === undefined ? {} : { storageState: abs(o.storageState) }),
|
|
164
|
+
...(o.fixtures === undefined ? {} : { fixtures: abs(o.fixtures) }),
|
|
165
|
+
...(o.before === undefined ? {} : { before: o.before }),
|
|
166
|
+
...(o.after === undefined ? {} : { after: o.after }),
|
|
167
|
+
discovery: false,
|
|
168
|
+
maxRuns: CAMPAIGN_LIMITS.maxRuns,
|
|
169
|
+
jobs: [
|
|
170
|
+
{
|
|
171
|
+
id: "sweep",
|
|
172
|
+
journey: o.fromJourney,
|
|
173
|
+
params: o.param,
|
|
174
|
+
anchors: restoredSingle ? [o.atStep.trim()] : o.atStep.trim(),
|
|
175
|
+
strategies: [strategy],
|
|
176
|
+
...(o.goal === undefined ? {} : { goal: o.goal }),
|
|
177
|
+
...(o.appClass === undefined ? {} : { appClass: o.appClass }),
|
|
178
|
+
...(o.success.length === 0 ? {} : { success: o.success }),
|
|
179
|
+
...(o.maxActions === undefined ? {} : { maxActions: Number(o.maxActions) }),
|
|
180
|
+
...(o.maxDecisions === undefined ? {} : { maxDecisions: Number(o.maxDecisions) }),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
try {
|
|
185
|
+
const plan = await validateCampaignSpec(spec, resolvePath("explore-sweep.json"), {
|
|
186
|
+
journeysDir,
|
|
187
|
+
allowShellHooks: o.allowShellHooks === true,
|
|
188
|
+
...(o.hookTimeoutMs === undefined ? {} : { hookTimeoutMs: Number(o.hookTimeoutMs) }),
|
|
189
|
+
environmentSeams: environmentSeams(deps),
|
|
190
|
+
});
|
|
191
|
+
const result = await runCampaign(plan, {
|
|
192
|
+
newProgram: () => buildProgram(deps),
|
|
193
|
+
journeysDir,
|
|
194
|
+
missionTargetsDir: resolveMissionTargetsDir(deps),
|
|
195
|
+
...(o.out === undefined ? {} : { outDir: o.out }),
|
|
196
|
+
...(o.real === true ? { real: true } : {}),
|
|
197
|
+
...(o.fakeAi === true ? { fakeAi: true } : {}),
|
|
198
|
+
missionArgs: forwardedArgv(this, SWEEP_OWNED),
|
|
199
|
+
});
|
|
200
|
+
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);
|
|
201
|
+
}
|
|
202
|
+
catch (err) {
|
|
203
|
+
if (err instanceof CampaignSpecError)
|
|
204
|
+
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
205
|
+
else
|
|
206
|
+
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
const environment = environmentFromFlags({ ...(o.env === undefined ? {} : { env: o.env }), ...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }) }, environmentSeams(deps));
|
|
212
|
+
// The environment's own session applies when --storage-state names none (as `journey run`).
|
|
213
|
+
if (o.storageState === undefined && environment?.storageState !== undefined)
|
|
214
|
+
o.storageState = environment.storageState;
|
|
215
|
+
prefixRequest = {
|
|
216
|
+
dir: resolveJourneysDir(deps, o.journeysDir),
|
|
217
|
+
id: o.fromJourney,
|
|
218
|
+
atStep: o.atStep,
|
|
219
|
+
params: o.param,
|
|
220
|
+
...(environment === undefined ? {} : { environment }),
|
|
221
|
+
...(o.storageState === undefined ? {} : { storageState: o.storageState }),
|
|
222
|
+
dbPath: resolveDbPath(deps),
|
|
223
|
+
environmentFlags: { ...(o.env === undefined ? {} : { env: o.env }), ...(o.baseUrl === undefined ? {} : { baseUrl: o.baseUrl }) },
|
|
224
|
+
};
|
|
225
|
+
journeyPrefix = await resolveJourneyPrefix(prefixRequest);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
if (isEnvironmentError(err) || err instanceof JourneyPrefixArgsError)
|
|
229
|
+
emitExplore(fail(err.code, err.message));
|
|
230
|
+
else if (err instanceof UnknownJourneyError)
|
|
231
|
+
emitExplore(fail("E_UNKNOWN_JOURNEY", err.message));
|
|
232
|
+
else if (err instanceof ParamValidationError)
|
|
233
|
+
emitExplore(fail("E_INVALID_PARAMS", err.message));
|
|
234
|
+
else if (err instanceof JourneyRequiresAuthError)
|
|
235
|
+
emitExplore(fail("E_JOURNEY_REQUIRES_AUTH", err.message));
|
|
236
|
+
else
|
|
237
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
// The prefix types its secret params into the page the mission perceives: they are redacted like
|
|
241
|
+
// --secret. Coverage/exploratory carry no redaction set, so a prefix with secrets refuses them.
|
|
242
|
+
if (journeyPrefix.secrets.length > 0 && (strategy === "coverage" || strategy === "exploratory")) {
|
|
243
|
+
refuse(`journey '${o.fromJourney}' types a secret param before step ${journeyPrefix.branch.step}: --strategy ${strategy} cannot redact it — use goal, adversarial or usability`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
o.url = journeyPrefix.startUrl;
|
|
247
|
+
// Default allowlist: the origins the Journey's steps may be on (the environment's), not just the landing's.
|
|
248
|
+
if (o.allow.length === 0)
|
|
249
|
+
o.allow = [...journeyPrefix.allowedOrigins];
|
|
250
|
+
}
|
|
251
|
+
const withPrefix = journeyPrefix === undefined ? {} : { journeyPrefix };
|
|
252
|
+
// #519: the scope region — the anchor's (authoritative) or --scope-region's — resolved and refused before any browser opens.
|
|
253
|
+
let withScopeRegion;
|
|
254
|
+
try {
|
|
255
|
+
const region = resolveScopeRegions({ scopeRegion: o.scopeRegion, strategy, ...(o.feature === undefined ? {} : { feature: o.feature }), ...(o.goal === undefined ? {} : { goal: o.goal }) }, journeyPrefix);
|
|
256
|
+
withScopeRegion = region === undefined ? {} : { scopeRegions: region.descriptors, scopeRegionSpec: region.spec };
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
/** #293: a prefix that no longer replays, or a site policy that refused it — typed, handled once for every strategy. */
|
|
263
|
+
const emitJourneyFailure = (err) => {
|
|
264
|
+
if (err instanceof JourneyPrefixStaleError) {
|
|
265
|
+
emitExplore(ok(withEngine(journeyStaleResult(err, strategy))), EXIT_CODES.inconclusive);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
if (err instanceof SiteGateRefusedError) {
|
|
269
|
+
emitExplore(fail(err.code, err.message));
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
};
|
|
274
|
+
// #195: `--secret env:VAR` is resolved from the environment before anything runs (fail closed).
|
|
275
|
+
try {
|
|
276
|
+
const resolved = resolveSecretArgs(o.secret, process.env, "--secret");
|
|
277
|
+
if (resolved.literals > 0)
|
|
278
|
+
program.configureOutput().writeErr?.(LITERAL_SECRET_WARNING);
|
|
279
|
+
o.secret = [...resolved.secrets, ...(journeyPrefix?.secrets ?? [])];
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
if (!(err instanceof SecretArgError))
|
|
283
|
+
throw err;
|
|
284
|
+
emitExplore(fail("E_EXPLORE_ARGS", err.message));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// #245 demo mode: resolved (and a headed run without a display refused, exit 64) before any
|
|
288
|
+
// browser opens — a multi-run's every run re-resolves the same flags.
|
|
289
|
+
let browser;
|
|
290
|
+
try {
|
|
291
|
+
browser = browserRunFromFlags(o, deps.explore?.env ?? process.env);
|
|
292
|
+
// #256: a chrome-extension:// --url must be a loaded extension's; loaded extensions' origins are allowed.
|
|
293
|
+
assertExtensionTargetLoaded(o.url, browser);
|
|
294
|
+
o.allow = allowWithExtensions(o.url, o.allow, browser);
|
|
295
|
+
}
|
|
296
|
+
catch (err) {
|
|
297
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// #251: an unusable --screenshots value is a usage error (64), refused before any browser opens.
|
|
301
|
+
let screenshots;
|
|
302
|
+
try {
|
|
303
|
+
screenshots = parseScreenshotsArg(o.screenshots);
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// #251/#250: the capture options every strategy's runner takes.
|
|
310
|
+
const screenshotsOpt = { ...(screenshots === undefined ? {} : { screenshots }), ...(o.evidenceVideo === true ? { evidenceVideo: true } : {}) };
|
|
311
|
+
if (browser?.headed === true && (wantsMultiRun(o) || o.actor.length > 1)) {
|
|
312
|
+
program.configureOutput().writeErr?.(multiWindowWarning(o.actor.length > 1 ? "several --actor sessions" : "--repeat/--persona"));
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* #427: the pre-flight auth check of one session — undefined when it is alive (or refreshed, or
|
|
316
|
+
* not checked: --auth-check off, no --url, a --from-journey run whose prefix replay is its own
|
|
317
|
+
* check), or the `auth-expired` result when it is not. Throws `UnauthorizedExploreTargetError`.
|
|
318
|
+
*/
|
|
319
|
+
const sessionPreflight = async (storageState, persona) => {
|
|
320
|
+
if (authCheck.mode === "off" || o.url === undefined || journeyPrefix !== undefined)
|
|
321
|
+
return undefined;
|
|
322
|
+
const allowlist = resolveExploreAllowlist(o.url, o.allow);
|
|
323
|
+
// A start URL bound from --fixtures (`${setup.x}`) does not exist before the setup runs: the
|
|
324
|
+
// check opens the app's root instead (the mission itself starts after the setup, as before).
|
|
325
|
+
const checkUrl = o.url.includes("${") ? fixtureBoundUrlRoot(o.url) : o.url;
|
|
326
|
+
if (checkUrl === undefined)
|
|
327
|
+
return undefined; // no origin before the placeholder: the mission's own URL check refuses it
|
|
328
|
+
const r = await ensurePersonaSession({
|
|
329
|
+
...(persona === undefined ? {} : { persona: persona.name }),
|
|
330
|
+
...(persona?.login === undefined ? {} : { login: persona.login }),
|
|
331
|
+
storageState,
|
|
332
|
+
url: checkUrl,
|
|
333
|
+
allowlist,
|
|
334
|
+
check: authCheck,
|
|
335
|
+
port: (deps.explore?.browserPortFactory ?? (() => new PlaywrightBrowserPort()))(),
|
|
336
|
+
...(browser === undefined ? {} : { browser }),
|
|
337
|
+
env: deps.explore?.env ?? process.env,
|
|
338
|
+
timeoutMs: AUTH_PREFLIGHT_TIMEOUT_MS,
|
|
339
|
+
note: (line) => program.configureOutput().writeErr?.(line),
|
|
340
|
+
});
|
|
341
|
+
return r.ok ? undefined : authExpiredResult(strategy, o.url, r, persona?.name);
|
|
342
|
+
};
|
|
343
|
+
// Repeat-and-vote (#141) / persona matrix (#143): the same command, run sequentially and aggregated.
|
|
344
|
+
if (wantsMultiRun(o)) {
|
|
345
|
+
try {
|
|
346
|
+
const plan = resolveMultiRunPlan(o);
|
|
347
|
+
const result = await runExploreMultiRun({
|
|
348
|
+
cmd: this,
|
|
349
|
+
newProgram: () => buildProgram(deps),
|
|
350
|
+
plan,
|
|
351
|
+
strategy,
|
|
352
|
+
...(o.out === undefined ? {} : { out: o.out }),
|
|
353
|
+
// #220: a killed multi-run prints ITS partial summary, by this command's own output rule.
|
|
354
|
+
killOutput: (partial) => (o.json === true ? `${JSON.stringify(ok(withEngine(partial)))}\n` : formatMultiRunHuman(partial)),
|
|
355
|
+
// #427: each run's session (its persona's, or the mission's own) is checked before it starts.
|
|
356
|
+
...(authCheck.mode === "off"
|
|
357
|
+
? {}
|
|
358
|
+
: {
|
|
359
|
+
authPreflight: async (p) => {
|
|
360
|
+
const state = p?.storageState ?? o.storageState;
|
|
361
|
+
if (state === undefined)
|
|
362
|
+
return undefined;
|
|
363
|
+
const r = await sessionPreflight(state, p ?? projectPersonaFor(state));
|
|
364
|
+
return r === undefined ? undefined : withEngine(r);
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
});
|
|
368
|
+
emitExplore(ok(withEngine(result)), result.exitCode, formatMultiRunHuman);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
if (err instanceof MultiRunArgsError)
|
|
372
|
+
emitExplore(fail(err.code, err.message));
|
|
373
|
+
else if (err instanceof UnauthorizedExploreTargetError)
|
|
374
|
+
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
375
|
+
else if (err instanceof MultiRunAbortedError)
|
|
376
|
+
emitExplore(fail(err.envelope.error.code, err.envelope.error.message));
|
|
377
|
+
else
|
|
378
|
+
emitExplore(fail("E_EXPLORE_RUN", String(err instanceof Error ? err.message : err)));
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// #120: a killed run prints what this command would have printed — the envelope with --json,
|
|
383
|
+
// else the human summary (#210) — before it exits.
|
|
384
|
+
setKillSwitchOutput(o.json === true ? "envelope" : "human");
|
|
385
|
+
let conversation;
|
|
386
|
+
try {
|
|
387
|
+
conversation = conversationFromFlags(o);
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
if (!(err instanceof GoalRunFlagError))
|
|
391
|
+
throw err;
|
|
392
|
+
emitExplore(fail(err.code, err.message));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
// #154: refused BEFORE any browser opens — `nonNegativeIntArg` already rejects a bad value at
|
|
396
|
+
// parse time (#275: the value is a number here). 0 is valid: "don't replay" — a hang is then
|
|
397
|
+
// reported unconfirmed (inconclusive), never replayed and never a crash.
|
|
398
|
+
try {
|
|
399
|
+
validateDenyPatterns(o.deny);
|
|
400
|
+
validateDenyPatterns(o.paid, "--paid");
|
|
401
|
+
validateAllowControlPatterns(o.allowControl ?? []);
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// #149: refused BEFORE any browser opens (an unknown --device, or --viewport + --device together).
|
|
408
|
+
let emulation;
|
|
409
|
+
try {
|
|
410
|
+
emulation = emulationFromFlags(o);
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
emitExplore(fail("E_EXPLORE_ARGS", err instanceof Error ? err.message : String(err)));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const overflow = { checkOverflow: o.checkOverflow ?? false, ignoreSelectors: o.ignoreOverflow };
|
|
417
|
+
// Issue filing: drafts are always written; filing needs --file-issues (or config) AND a repo.
|
|
418
|
+
let filing;
|
|
419
|
+
if (o.url !== undefined) {
|
|
420
|
+
try {
|
|
421
|
+
filing = resolveFilingConfig(loadFilingFileConfig(deps.explore?.filingConfigPath), {
|
|
422
|
+
...(o.fileIssues === undefined ? {} : { fileIssues: o.fileIssues }),
|
|
423
|
+
...(o.issueRepo === undefined ? {} : { issueRepo: o.issueRepo }),
|
|
424
|
+
...(o.jevitateRepo === undefined ? {} : { jevitateRepo: o.jevitateRepo }),
|
|
425
|
+
}, new URL(setupRefFreeUrl(o.url)).origin);
|
|
426
|
+
}
|
|
427
|
+
catch (err) {
|
|
428
|
+
if (err instanceof FilingConfigError) {
|
|
429
|
+
emitExplore(fail(err.code, err.message));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (!(err instanceof TypeError))
|
|
433
|
+
throw err;
|
|
434
|
+
// An unparseable --url is refused by the authorized-target guard below.
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
// Per-target settle/hang configuration: ~/.jevitate/targets.json by origin, plus flags.
|
|
438
|
+
let target;
|
|
439
|
+
if (o.url !== undefined) {
|
|
440
|
+
try {
|
|
441
|
+
target = resolveTargetConfig(loadTargetsFile(deps.explore?.targetsConfigPath), new URL(setupRefFreeUrl(o.url)).origin, targetFlagsFromFlags(o, dialogs));
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
if (err instanceof TargetConfigError) {
|
|
445
|
+
emitExplore(fail(err.code, err.message));
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (!(err instanceof TypeError))
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const issueFiler = deps.explore?.issueFiler ??
|
|
453
|
+
(() => new GitHubIssueFiler({ store: envCredentialStore(process.env, loadLocalCredentials()) }));
|
|
454
|
+
// #250 --evidence-video: the runner attaches each defect's captioned repro clip + key
|
|
455
|
+
// screenshots; drafts are filed only AFTER their media is linked (the run writes drafts only).
|
|
456
|
+
const evidenceOn = o.evidenceVideo === true;
|
|
457
|
+
const runFiling = filing === undefined ? undefined : evidenceOn ? { ...filing, enabled: false } : filing;
|
|
458
|
+
const withEvidence = async (result) => {
|
|
459
|
+
if (!evidenceOn)
|
|
460
|
+
return result;
|
|
461
|
+
let out = result;
|
|
462
|
+
if (filing?.enabled === true)
|
|
463
|
+
out = await fileDraftsWithEvidence(out, filing, issueFiler, clock.nowIso());
|
|
464
|
+
return out;
|
|
465
|
+
};
|
|
466
|
+
// `--fixture` feeds the upload op, which only the explore loop (goal and
|
|
467
|
+
// usability strategies) can issue. Refuse it elsewhere rather than
|
|
468
|
+
// silently ignoring a file the user expected to be uploaded.
|
|
469
|
+
// #424: the minimum effort gates the goal loop's own endings (report / blocked / done) — a goal run's.
|
|
470
|
+
if ((o.minActions !== undefined || o.minDistinctStates !== undefined) && (o.feature !== undefined || strategy !== "goal")) {
|
|
471
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--min-actions and --min-distinct-states are supported only with --strategy goal (a goal or find-out run)"));
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (o.fixture !== undefined && (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability"))) {
|
|
475
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--fixture is supported only with --strategy goal or usability"));
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
// #198: product facts and polish shape the UX review's findings only.
|
|
479
|
+
if ((o.product !== undefined || o.polish === true || o.probeGuards === true) && (o.feature !== undefined || strategy !== "usability")) {
|
|
480
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--product, --polish and --probe-guards are supported only with --strategy usability"));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// #225: success checks judge a goal / a usability job — every other strategy (and --feature) would
|
|
484
|
+
// silently ignore them, so they are refused up front, never dropped.
|
|
485
|
+
if ((o.success.length > 0 || o.successWhen !== undefined || o.allowVacuousChecks === true) &&
|
|
486
|
+
(o.feature !== undefined || (strategy !== "goal" && strategy !== "usability"))) {
|
|
487
|
+
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}`})`));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// #303: action deltas are recorded by the goal loop (goal and usability runs) only — refused
|
|
491
|
+
// elsewhere, never silently ignored.
|
|
492
|
+
if (o.actionDeltas === true && o.feature !== undefined) {
|
|
493
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--action-deltas is not supported with --feature (goal, usability, coverage, exploratory and adversarial runs record deltas)"));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
// #427: the project persona (.jevitate/personas.json) this --storage-state is, when it is one — its
|
|
497
|
+
// session may not exist yet: the pre-flight mints it from its login parameters.
|
|
498
|
+
let projectPersona;
|
|
499
|
+
try {
|
|
500
|
+
projectPersona = o.storageState === undefined ? undefined : projectPersonaFor(o.storageState);
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
if (!(err instanceof MultiRunArgsError))
|
|
504
|
+
throw err;
|
|
505
|
+
emitExplore(fail(err.code, err.message));
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (o.storageState !== undefined && !existsSync(o.storageState) && (authCheck.mode === "off" || projectPersona?.login === undefined)) {
|
|
509
|
+
emitExplore(fail("E_EXPLORE_ARGS", `storage state not found: ${o.storageState}`));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// Multi-actor missions (#147): the first --actor is the primary, the rest are observers.
|
|
513
|
+
let actors;
|
|
514
|
+
try {
|
|
515
|
+
actors = resolveMissionActors(o.actor);
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
if (!(err instanceof MultiRunArgsError))
|
|
519
|
+
throw err;
|
|
520
|
+
emitExplore(fail(err.code, err.message));
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (actors !== null) {
|
|
524
|
+
if (o.feature !== undefined || strategy !== "goal") {
|
|
525
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--actor is supported only with --strategy goal"));
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (o.storageState !== undefined) {
|
|
529
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--storage-state cannot be combined with --actor (the first --actor is the primary's session)"));
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// The primary's session: its --actor state, else --storage-state.
|
|
534
|
+
const primaryStorageState = actors?.primary.storageState ?? o.storageState;
|
|
535
|
+
// App-declared invariants (#86): validated (schema, observables, probe origins) BEFORE any browser.
|
|
536
|
+
let invariants;
|
|
537
|
+
// #135: authFrom.secret refs (env:VAR), resolved from the environment HERE — the one place this
|
|
538
|
+
// package reads process.env for invariants — never inside @jevitate/explore or @jevitate/recording.
|
|
539
|
+
let invariantAuthTokens;
|
|
540
|
+
if (o.invariants.length > 0) {
|
|
541
|
+
if (o.url !== undefined) {
|
|
542
|
+
try {
|
|
543
|
+
const loaded = loadInvariantFiles(o.invariants, {
|
|
544
|
+
allowlist: resolveExploreAllowlist(o.url, o.allow),
|
|
545
|
+
baseUrl: o.url,
|
|
546
|
+
observers: actors?.observers.map((a) => a.name) ?? [],
|
|
547
|
+
});
|
|
548
|
+
invariants = loaded;
|
|
549
|
+
invariantAuthTokens = loaded === undefined ? undefined : resolveInvariantAuthTokens(loaded, process.env);
|
|
550
|
+
checkActorsAgainstSpec(actors, loaded);
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
if (err instanceof MultiRunArgsError) {
|
|
554
|
+
emitExplore(fail(err.code, err.message));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (!(err instanceof InvariantsFileError))
|
|
558
|
+
throw err;
|
|
559
|
+
emitExplore(fail(err.code, err.message));
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// #147: captures and cross-actor checks run in the goal loop only — never silently skipped elsewhere.
|
|
564
|
+
if (invariants?.capture !== undefined && (o.feature !== undefined || strategy !== "goal")) {
|
|
565
|
+
emitExplore(fail("E_EXPLORE_ARGS", "invariants with capture (cross-actor checks) are supported only with --strategy goal"));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
const withInvariants = {
|
|
570
|
+
...(invariants === undefined ? {} : { invariants }),
|
|
571
|
+
...(invariantAuthTokens === undefined || invariantAuthTokens.size === 0 ? {} : { invariantAuthTokens }),
|
|
572
|
+
};
|
|
573
|
+
// Backend log sources (#142): validated (spec shape, --allow-log-cmd gate, matcher regexes)
|
|
574
|
+
// BEFORE any browser opens — the same fail-closed discipline as --invariants above. Supported
|
|
575
|
+
// on every strategy, INCLUDING usability (#142 follow-up): lines attach to usability steps the
|
|
576
|
+
// same way, though a UX run's own outcome stays advisory (a server-log defect is still reported,
|
|
577
|
+
// never gates the exit code — the same rule as every other UX finding).
|
|
578
|
+
let serverLog;
|
|
579
|
+
if (o.logSource.length > 0 || o.logDefect.length > 0) {
|
|
580
|
+
try {
|
|
581
|
+
const sources = parseLogSourceSpecs(o.logSource, o.allowLogCmd ?? false);
|
|
582
|
+
const logDefect = parseLogDefectSpecs(o.logDefect);
|
|
583
|
+
const logIgnore = parseLogIgnoreSpecs(o.logIgnore);
|
|
584
|
+
const logScope = parseLogScopeSpecs(o.logScope);
|
|
585
|
+
const correlationHeaders = parseCorrelationHeaders(o.logCorrelationHeader);
|
|
586
|
+
const idPatterns = parseLogIdPatterns(o.logIdPattern);
|
|
587
|
+
// #422: the project's .jevitate/log-classes.json, validated before any browser opens.
|
|
588
|
+
const logClasses = loadLogClassRules();
|
|
589
|
+
serverLog = {
|
|
590
|
+
sources,
|
|
591
|
+
logDefect,
|
|
592
|
+
allowLogCmd: o.allowLogCmd ?? false,
|
|
593
|
+
quietOk: o.logQuietOk,
|
|
594
|
+
logIgnore,
|
|
595
|
+
logScope,
|
|
596
|
+
correlationHeaders,
|
|
597
|
+
idPatterns,
|
|
598
|
+
logClasses,
|
|
599
|
+
...(o.serverLogDrainMs === undefined ? {} : { drainMs: Number(o.serverLogDrainMs) }),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
catch (err) {
|
|
603
|
+
if (err instanceof LogSourceSpecError || err instanceof LogSpecError || err instanceof LogClassesError) {
|
|
604
|
+
emitExplore(fail(err.code, err.message));
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
throw err;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (o.logTriage === true) {
|
|
611
|
+
if (serverLog === undefined) {
|
|
612
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--log-triage triages the run's backend and browser signals: it needs at least one --log-source"));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
serverLog = { ...serverLog, triage: {} };
|
|
616
|
+
}
|
|
617
|
+
const withServerLog = serverLog === undefined ? {} : { serverLog };
|
|
618
|
+
// Secret field bindings (#72): resolved from the environment here, typed by code in the goal loop.
|
|
619
|
+
let secretFields = [];
|
|
620
|
+
if (o.secretField.length > 0 || o.totp.length > 0) {
|
|
621
|
+
if (o.feature !== undefined || (strategy !== "goal" && strategy !== "usability")) {
|
|
622
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--secret-field and --totp are supported only with --strategy goal or usability"));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
secretFields = secretFieldsFromFlags(o);
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
if (!(err instanceof GoalRunFlagError))
|
|
630
|
+
throw err;
|
|
631
|
+
emitExplore(fail(err.code, err.message));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// #324: a cmd: binding's command runs at type time; its output becomes a run secret at once.
|
|
636
|
+
const secretCommand = secretFields.some((f) => f.kind === "cmd") ? secretCommandRunner() : undefined;
|
|
637
|
+
// #281: fields typed with a file's exact text — read (and checked) before any browser opens.
|
|
638
|
+
let typeFixtures = [];
|
|
639
|
+
if (o.typeFixture.length > 0) {
|
|
640
|
+
if (o.feature !== undefined || strategy !== "goal") {
|
|
641
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--type-fixture is supported only with --strategy goal"));
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
try {
|
|
645
|
+
typeFixtures = loadTypeFixtures(o.typeFixture);
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
if (!(err instanceof TypeFixtureSpecError))
|
|
649
|
+
throw err;
|
|
650
|
+
emitExplore(fail(err.code, err.message));
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Mission fixtures (#140/#144) run around the goal loop and its replays only.
|
|
655
|
+
const fixtureFlagsGiven = o.fixtures !== undefined || o.before !== undefined || o.after !== undefined;
|
|
656
|
+
if (fixtureFlagsGiven && (o.feature !== undefined || strategy !== "goal")) {
|
|
657
|
+
emitExplore(fail("E_EXPLORE_ARGS", "--fixtures, --before and --after are supported only with --strategy goal, or with --from-journey (any anchored strategy)"));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
// #427 pre-flight auth check: a run that starts from a session first proves the session is alive
|
|
661
|
+
// (re-minting it once from a persona's login parameters) — else it ends now, `auth-expired`.
|
|
662
|
+
if (primaryStorageState !== undefined) {
|
|
663
|
+
let expired;
|
|
664
|
+
try {
|
|
665
|
+
expired = await sessionPreflight(primaryStorageState, actors?.primary ?? projectPersona);
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
if (err instanceof UnauthorizedExploreTargetError)
|
|
669
|
+
emitExplore(fail("E_UNAUTHORIZED_EXPLORE_TARGET", err.message));
|
|
670
|
+
// The check could not open its browser (the mission could not have either): the run did not start.
|
|
671
|
+
else
|
|
672
|
+
emitExplore(fail("E_EXPLORE_RUN", `the pre-flight auth check failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
if (expired !== undefined) {
|
|
676
|
+
emitExplore(ok(withEngine(expired)), EXIT_CODES.inconclusive);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const ctx = {
|
|
681
|
+
deps, o, emitExplore, strategy, target, browser, screenshotsOpt, emulation, overflow, withInvariants, serverLog, withServerLog, withPrefix, withScopeRegion, emitJourneyFailure, withEvidence, runFiling, issueFiler, conversation, secretFields, secretCommand, actors, primaryStorageState, invariants, journeyPrefix, prefixRequest, typeFixtures,
|
|
682
|
+
};
|
|
683
|
+
if (strategy === "coverage" || strategy === "exploratory") {
|
|
684
|
+
await runCoverageStrategy({ ...ctx, strategy });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (strategy === "adversarial") {
|
|
688
|
+
await runAdversarialStrategy(ctx);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (strategy === "usability") {
|
|
692
|
+
await runUsabilityStrategy(ctx);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (o.feature) {
|
|
696
|
+
await runFeatureStrategy(ctx);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
await runGoalStrategy(ctx);
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
//# sourceMappingURL=run.js.map
|