@monster-orange/den 0.0.0-stage → 0.1.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/CHANGELOG.md +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +104 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +406 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +566 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +178 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Journeys CLI entrypoint (plan: jrn-pilot-journeys packet).
|
|
3
|
+
*
|
|
4
|
+
* npx tsx src/pillars/journeys/run.ts --env=<env> [--journey=<repo/name>]
|
|
5
|
+
*
|
|
6
|
+
* `--journey` accepts a repo-qualified "<repo>/<name>" (exact) or a bare
|
|
7
|
+
* spec name; bare names duplicated across repos are refused loudly.
|
|
8
|
+
*
|
|
9
|
+
* The FIRST executable surface of the journeys pillar — every library
|
|
10
|
+
* packet (runner, verifier, reporting, teardown) already landed, and this
|
|
11
|
+
* module only orchestrates them:
|
|
12
|
+
*
|
|
13
|
+
* - `--env` is REQUIRED and exactly one environment per invocation
|
|
14
|
+
* (Approach §Explicit environment targeting; the make targets add the
|
|
15
|
+
* command-line-origin guard on top).
|
|
16
|
+
* - Discovery is manifest-driven via discoverJourneySpecs() — specs live
|
|
17
|
+
* in registered repos (the app's own repo), never a den-local dir. Specs whose
|
|
18
|
+
* `environments` allowlist excludes the target env are skipped with a
|
|
19
|
+
* logged reason; zero runnable specs exits with the distinct loud code
|
|
20
|
+
* {@link EXIT_NO_SPECS} — never silent green.
|
|
21
|
+
* - Per journey, the STRICT order is runJourney → verify → collect report
|
|
22
|
+
* → teardown (in finally, strictly AFTER the verification probes so they
|
|
23
|
+
* observe created state) → finalize report (teardown warnings folded
|
|
24
|
+
* in). Reporting is guaranteed in the same finally on every outcome.
|
|
25
|
+
* - Sequential in v1; exit codes: 0 all passed / 1 infra error / 2 any
|
|
26
|
+
* journey failed / 3 no specs discovered.
|
|
27
|
+
* - In-process transcript secret scan: the secrets captured for the run
|
|
28
|
+
* are compared FROM MEMORY against the persisted transcript artifacts —
|
|
29
|
+
* they never pass through argv or a child process. A hit fails the
|
|
30
|
+
* journey as infra_error (belt and suspenders over the runner's
|
|
31
|
+
* persist-path scrubber).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import * as fs from "fs";
|
|
35
|
+
import * as path from "path";
|
|
36
|
+
import type { Environment } from "../../shared/types/index.js";
|
|
37
|
+
import {
|
|
38
|
+
discoverJourneySpecs,
|
|
39
|
+
type DiscoveredJourneySpec,
|
|
40
|
+
} from "./discover.js";
|
|
41
|
+
import { extractFrictionPoints } from "./report/friction.js";
|
|
42
|
+
import {
|
|
43
|
+
collectJourneyReport,
|
|
44
|
+
finalizeJourneyReport,
|
|
45
|
+
type CollectedJourneyReport,
|
|
46
|
+
} from "./report/results.js";
|
|
47
|
+
import {
|
|
48
|
+
writeJourneysJUnitXml,
|
|
49
|
+
type JourneysJUnitCase,
|
|
50
|
+
} from "./report/junit.js";
|
|
51
|
+
import {
|
|
52
|
+
runJourney,
|
|
53
|
+
type RunnerDeps,
|
|
54
|
+
type RunnerEnvConfigLike,
|
|
55
|
+
type RunOutcome,
|
|
56
|
+
} from "./runner.js";
|
|
57
|
+
import type { JourneyPersonaRole, JourneySpec } from "./spec/schema.js";
|
|
58
|
+
import type { JourneysSessionTokenConfig } from "./app-config.js";
|
|
59
|
+
import type { CapturedSecrets } from "./sandbox.js";
|
|
60
|
+
import type { SessionApiToken } from "./tools/browser-session.js";
|
|
61
|
+
import { teardownLedger, type TeardownReport } from "./teardown.js";
|
|
62
|
+
import { allowedBaseUrlsFromEnv } from "./tools/http-tool.js";
|
|
63
|
+
import type { RunLedger } from "./tools/ledger.js";
|
|
64
|
+
import {
|
|
65
|
+
buildVerifierProbeClient,
|
|
66
|
+
type VerifierProbeClient,
|
|
67
|
+
type VerifierProbeClientOptions,
|
|
68
|
+
} from "./verify/assertions.js";
|
|
69
|
+
import { verifyJourney, type Verdict } from "./verify/verdict.js";
|
|
70
|
+
|
|
71
|
+
import { product } from "../../shared/product.js";
|
|
72
|
+
// ── Exit codes ───────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
export const EXIT_ALL_PASSED = 0;
|
|
75
|
+
export const EXIT_INFRA_ERROR = 1;
|
|
76
|
+
export const EXIT_JOURNEY_FAILED = 2;
|
|
77
|
+
/** Zero specs discovered/runnable — loud and distinct, never silent green. */
|
|
78
|
+
export const EXIT_NO_SPECS = 3;
|
|
79
|
+
|
|
80
|
+
// ── CLI args ─────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const KNOWN_ENVIRONMENTS: readonly Environment[] = [
|
|
83
|
+
"local",
|
|
84
|
+
"dev",
|
|
85
|
+
"sandbox",
|
|
86
|
+
"staging",
|
|
87
|
+
"beta",
|
|
88
|
+
"prod",
|
|
89
|
+
"production",
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
/** Operator/usage mistake — maps to {@link EXIT_INFRA_ERROR}. */
|
|
93
|
+
export class CliUsageError extends Error {
|
|
94
|
+
constructor(message: string) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = "CliUsageError";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface ParsedCliArgs {
|
|
101
|
+
env: Environment;
|
|
102
|
+
journey?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse `--env=<env> [--journey=<name>]`. `--env` is required and must
|
|
107
|
+
* appear exactly once (one environment per invocation); unknown arguments
|
|
108
|
+
* are rejected loudly rather than ignored.
|
|
109
|
+
*/
|
|
110
|
+
export function parseCliArgs(argv: readonly string[]): ParsedCliArgs {
|
|
111
|
+
const envValues: string[] = [];
|
|
112
|
+
let journey: string | undefined;
|
|
113
|
+
|
|
114
|
+
for (const arg of argv) {
|
|
115
|
+
if (arg.startsWith("--env=")) {
|
|
116
|
+
envValues.push(arg.slice("--env=".length));
|
|
117
|
+
} else if (arg.startsWith("--journey=")) {
|
|
118
|
+
journey = arg.slice("--journey=".length);
|
|
119
|
+
} else {
|
|
120
|
+
throw new CliUsageError(
|
|
121
|
+
`Unrecognized argument "${arg}". ` +
|
|
122
|
+
"Usage: run.ts --env=<env> [--journey=<name>]",
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (envValues.length === 0) {
|
|
128
|
+
throw new CliUsageError(
|
|
129
|
+
"--env is required (exactly one environment per invocation). " +
|
|
130
|
+
"Usage: run.ts --env=<env> [--journey=<name>]",
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (envValues.length > 1) {
|
|
134
|
+
throw new CliUsageError(
|
|
135
|
+
"Exactly one --env per invocation — journeys never fan out across " +
|
|
136
|
+
`environments (got: ${envValues.join(", ")}).`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const env = envValues[0];
|
|
141
|
+
if (!(KNOWN_ENVIRONMENTS as readonly string[]).includes(env)) {
|
|
142
|
+
throw new CliUsageError(
|
|
143
|
+
`Unknown environment "${env}". Known: ${KNOWN_ENVIRONMENTS.join(", ")}.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const parsed: ParsedCliArgs = { env: env as Environment };
|
|
148
|
+
if (journey !== undefined) {
|
|
149
|
+
parsed.journey = journey;
|
|
150
|
+
}
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Secret scan ──────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* In-process scan: does `contents` contain any live secret value? Values
|
|
158
|
+
* are compared from memory — never serialized to argv, a temp file, or a
|
|
159
|
+
* child process. Empty values are ignored (they would match everything).
|
|
160
|
+
*/
|
|
161
|
+
export function transcriptContainsSecret(
|
|
162
|
+
contents: string,
|
|
163
|
+
secrets: readonly string[],
|
|
164
|
+
): boolean {
|
|
165
|
+
return secrets.some(
|
|
166
|
+
(secret) => secret.length > 0 && contents.includes(secret),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Injectable dependencies ──────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
export interface EntrypointDeps {
|
|
173
|
+
discoverSpecs: () => DiscoveredJourneySpec[];
|
|
174
|
+
loadEnvironment: (env: Environment) => Promise<RunnerEnvConfigLike>;
|
|
175
|
+
/**
|
|
176
|
+
* The persona-secret source (Cursor key only). The entrypoint passes a
|
|
177
|
+
* caching wrapper of this into runJourney's deps so the captured value
|
|
178
|
+
* remains available — in memory only — for the transcript secret scan.
|
|
179
|
+
*/
|
|
180
|
+
captureSecrets: (
|
|
181
|
+
role: JourneyPersonaRole,
|
|
182
|
+
env: Environment,
|
|
183
|
+
) => Promise<CapturedSecrets>;
|
|
184
|
+
/**
|
|
185
|
+
* Derive the API assertion token + session headers from the browser
|
|
186
|
+
* session. The entrypoint passes a caching wrapper into runJourney's deps
|
|
187
|
+
* so the SAME real-user token + headers back the verifier probe client and
|
|
188
|
+
* the teardown client — one credential for navigation and assertions.
|
|
189
|
+
*/
|
|
190
|
+
deriveApiToken: (
|
|
191
|
+
authFilePath: string,
|
|
192
|
+
env: Environment,
|
|
193
|
+
sessionToken?: JourneysSessionTokenConfig,
|
|
194
|
+
) => Promise<SessionApiToken>;
|
|
195
|
+
runJourney: (
|
|
196
|
+
spec: JourneySpec,
|
|
197
|
+
env: Environment,
|
|
198
|
+
deps: Partial<RunnerDeps>,
|
|
199
|
+
) => Promise<RunOutcome>;
|
|
200
|
+
buildProbeClient: (
|
|
201
|
+
options: VerifierProbeClientOptions,
|
|
202
|
+
) => VerifierProbeClient;
|
|
203
|
+
verifyJourney: typeof verifyJourney;
|
|
204
|
+
teardownLedger: typeof teardownLedger;
|
|
205
|
+
collectReport: typeof collectJourneyReport;
|
|
206
|
+
finalizeReport: (
|
|
207
|
+
collected: CollectedJourneyReport,
|
|
208
|
+
teardown: TeardownReport | undefined,
|
|
209
|
+
) => ReturnType<typeof finalizeJourneyReport>;
|
|
210
|
+
writeJUnit: (cases: readonly JourneysJUnitCase[]) => Promise<string>;
|
|
211
|
+
extractFriction: typeof extractFrictionPoints;
|
|
212
|
+
/** Read a text file, or undefined when it does not exist. */
|
|
213
|
+
readFileIfExists: (filePath: string) => string | undefined;
|
|
214
|
+
log: (message: string) => void;
|
|
215
|
+
logError: (message: string) => void;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function defaultReadFileIfExists(filePath: string): string | undefined {
|
|
219
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
220
|
+
return fs.readFileSync(filePath, "utf8");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Production wiring — every collaborator is the real den machinery. */
|
|
224
|
+
export function defaultEntrypointDeps(): EntrypointDeps {
|
|
225
|
+
return {
|
|
226
|
+
discoverSpecs: () => discoverJourneySpecs(),
|
|
227
|
+
loadEnvironment: async (env) => {
|
|
228
|
+
const { envSelector } =
|
|
229
|
+
await import("../../shared/environment/env-selector.js");
|
|
230
|
+
return envSelector.loadEnvironment(env);
|
|
231
|
+
},
|
|
232
|
+
captureSecrets: async (role, env) => {
|
|
233
|
+
const { captureSecrets } = await import("./sandbox.js");
|
|
234
|
+
return captureSecrets(role, env);
|
|
235
|
+
},
|
|
236
|
+
deriveApiToken: async (authFilePath, env, sessionToken) => {
|
|
237
|
+
const { deriveApiTokenFromSession } =
|
|
238
|
+
await import("./tools/browser-session.js");
|
|
239
|
+
return deriveApiTokenFromSession(authFilePath, env, sessionToken);
|
|
240
|
+
},
|
|
241
|
+
runJourney,
|
|
242
|
+
buildProbeClient: buildVerifierProbeClient,
|
|
243
|
+
verifyJourney,
|
|
244
|
+
teardownLedger,
|
|
245
|
+
collectReport: collectJourneyReport,
|
|
246
|
+
finalizeReport: (collected, teardown) =>
|
|
247
|
+
finalizeJourneyReport(collected, teardown),
|
|
248
|
+
writeJUnit: (cases) => writeJourneysJUnitXml(cases),
|
|
249
|
+
extractFriction: extractFrictionPoints,
|
|
250
|
+
readFileIfExists: defaultReadFileIfExists,
|
|
251
|
+
log: (message) => console.log(message),
|
|
252
|
+
logError: (message) => console.error(message),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Per-journey execution ────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
type JourneyStatus = "passed" | "failed" | "infra";
|
|
259
|
+
|
|
260
|
+
interface JourneyExecution {
|
|
261
|
+
status: JourneyStatus;
|
|
262
|
+
junitCase?: JourneysJUnitCase;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Parse the persisted transcript JSONL into lines for friction analysis. */
|
|
266
|
+
function parseTranscriptLines(contents: string | undefined): unknown[] {
|
|
267
|
+
if (contents === undefined) return [];
|
|
268
|
+
const lines: unknown[] = [];
|
|
269
|
+
for (const line of contents.split("\n")) {
|
|
270
|
+
if (line.trim().length === 0) continue;
|
|
271
|
+
try {
|
|
272
|
+
lines.push(JSON.parse(line));
|
|
273
|
+
} catch {
|
|
274
|
+
// Non-JSON lines carry no structured signal; friction skips them.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return lines;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function statusOf(verdict: Verdict): JourneyStatus {
|
|
281
|
+
if (verdict.passed) return "passed";
|
|
282
|
+
return verdict.failure_kind === "infra_error" ? "infra" : "failed";
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function executeJourney(
|
|
286
|
+
discovered: DiscoveredJourneySpec,
|
|
287
|
+
env: Environment,
|
|
288
|
+
envConfig: RunnerEnvConfigLike,
|
|
289
|
+
deps: EntrypointDeps,
|
|
290
|
+
): Promise<JourneyExecution> {
|
|
291
|
+
const { spec } = discovered;
|
|
292
|
+
deps.log(`▶ journey ${spec.name} (${spec.surface}) — env=${env}`);
|
|
293
|
+
|
|
294
|
+
// Held in memory for verify/teardown/scan; populated by the caching
|
|
295
|
+
// wrappers the runner invokes during its provisioning phase.
|
|
296
|
+
let secrets: CapturedSecrets | undefined;
|
|
297
|
+
let sessionToken: SessionApiToken | undefined;
|
|
298
|
+
const cachingCaptureSecrets = async (
|
|
299
|
+
role: JourneyPersonaRole,
|
|
300
|
+
targetEnv: Environment,
|
|
301
|
+
): Promise<CapturedSecrets> => {
|
|
302
|
+
secrets = await deps.captureSecrets(role, targetEnv);
|
|
303
|
+
return secrets;
|
|
304
|
+
};
|
|
305
|
+
// The browser-session-derived API token + headers. Cached
|
|
306
|
+
// here so the verifier probe client and teardown client reuse the SAME
|
|
307
|
+
// real-user credential the agent navigated with.
|
|
308
|
+
const cachingDeriveApiToken = async (
|
|
309
|
+
authFilePath: string,
|
|
310
|
+
targetEnv: Environment,
|
|
311
|
+
sessionTokenConfig?: JourneysSessionTokenConfig,
|
|
312
|
+
): Promise<SessionApiToken> => {
|
|
313
|
+
sessionToken = await deps.deriveApiToken(
|
|
314
|
+
authFilePath,
|
|
315
|
+
targetEnv,
|
|
316
|
+
sessionTokenConfig,
|
|
317
|
+
);
|
|
318
|
+
return sessionToken;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
let outcome: RunOutcome | undefined;
|
|
322
|
+
let verdict: Verdict | undefined;
|
|
323
|
+
let collected: CollectedJourneyReport | undefined;
|
|
324
|
+
let junitCase: JourneysJUnitCase | undefined;
|
|
325
|
+
let crashed: Error | undefined;
|
|
326
|
+
|
|
327
|
+
const buildClient = (): VerifierProbeClient =>
|
|
328
|
+
deps.buildProbeClient({
|
|
329
|
+
allowedBaseUrls: allowedBaseUrlsFromEnv(envConfig),
|
|
330
|
+
tokenProvider: async () => {
|
|
331
|
+
if (sessionToken === undefined) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
"No session API token available — runJourney never derived one",
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return sessionToken.apiToken;
|
|
337
|
+
},
|
|
338
|
+
headersProvider: async () => {
|
|
339
|
+
if (sessionToken === undefined) {
|
|
340
|
+
throw new Error(
|
|
341
|
+
"No session headers available — runJourney never derived them",
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
return sessionToken.headers;
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
outcome = await deps.runJourney(spec, env, {
|
|
350
|
+
captureSecrets: cachingCaptureSecrets,
|
|
351
|
+
deriveApiToken: cachingDeriveApiToken,
|
|
352
|
+
app: discovered.app,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Post-ledger runner failures come back as outcomes carrying the
|
|
356
|
+
// ledger (review finding 8) — loud in the log, and the normal
|
|
357
|
+
// verify → report → teardown pipeline below still runs.
|
|
358
|
+
if (outcome.error !== undefined) {
|
|
359
|
+
deps.logError(
|
|
360
|
+
`✖ journey ${spec.name} run error (${outcome.failure_kind ?? "unknown"}): ${outcome.error}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Verification probes run strictly BEFORE teardown so they observe
|
|
365
|
+
// the created state; even a failed run gets probed for diagnostics.
|
|
366
|
+
verdict = await deps.verifyJourney(
|
|
367
|
+
spec.success.assertions,
|
|
368
|
+
outcome,
|
|
369
|
+
buildClient(),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// In-process secret scan over the persisted transcript artifacts
|
|
373
|
+
// (JSONL + rendered markdown), secrets compared from memory.
|
|
374
|
+
const transcriptContents = deps.readFileIfExists(outcome.transcriptPath);
|
|
375
|
+
const renderedContents = deps.readFileIfExists(
|
|
376
|
+
path.join(path.dirname(outcome.transcriptPath), "transcript.md"),
|
|
377
|
+
);
|
|
378
|
+
// The event log is scrubbed at emit time by the bus, but scan it here
|
|
379
|
+
// too — defense in depth alongside the transcript scan (same failure
|
|
380
|
+
// mode: a live secret in a persisted artifact fails the run closed).
|
|
381
|
+
const eventsContents = deps.readFileIfExists(outcome.eventsPath);
|
|
382
|
+
const secretValues = [secrets?.cursorApiKey, sessionToken?.apiToken].filter(
|
|
383
|
+
(value): value is string => value !== undefined,
|
|
384
|
+
);
|
|
385
|
+
const leaked = [transcriptContents, renderedContents, eventsContents].some(
|
|
386
|
+
(contents) =>
|
|
387
|
+
contents !== undefined &&
|
|
388
|
+
transcriptContainsSecret(contents, secretValues),
|
|
389
|
+
);
|
|
390
|
+
if (leaked) {
|
|
391
|
+
deps.logError(
|
|
392
|
+
`✖ journey ${spec.name}: a live secret value was found in the ` +
|
|
393
|
+
"persisted transcript — failing closed as infra_error.",
|
|
394
|
+
);
|
|
395
|
+
outcome.failure_kind = "infra_error";
|
|
396
|
+
verdict = { ...verdict, passed: false, failure_kind: "infra_error" };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const friction = deps.extractFriction({
|
|
400
|
+
transcript: parseTranscriptLines(transcriptContents),
|
|
401
|
+
snapshotIndex: outcome.snapshotIndex,
|
|
402
|
+
budgets: spec.budgets,
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
collected = deps.collectReport({
|
|
406
|
+
spec,
|
|
407
|
+
env,
|
|
408
|
+
outcome,
|
|
409
|
+
verdict,
|
|
410
|
+
frictionPoints: friction,
|
|
411
|
+
});
|
|
412
|
+
} catch (error) {
|
|
413
|
+
crashed = error instanceof Error ? error : new Error(String(error));
|
|
414
|
+
deps.logError(`✖ journey ${spec.name} crashed: ${crashed.message}`);
|
|
415
|
+
} finally {
|
|
416
|
+
// Teardown strictly AFTER verification; a run that never produced an
|
|
417
|
+
// outcome has no ledger to tear down (and nothing persisted to report).
|
|
418
|
+
let teardown: TeardownReport | undefined;
|
|
419
|
+
// Teardown needs the session-derived token/headers to authenticate its
|
|
420
|
+
// DELETE probes; skip cleanly if the run never got that far.
|
|
421
|
+
if (outcome !== undefined && sessionToken !== undefined) {
|
|
422
|
+
try {
|
|
423
|
+
teardown = await deps.teardownLedger(
|
|
424
|
+
outcome.ledger as RunLedger,
|
|
425
|
+
async (request) => {
|
|
426
|
+
const response = await buildClient()({
|
|
427
|
+
method: request.method as
|
|
428
|
+
| "GET"
|
|
429
|
+
| "POST"
|
|
430
|
+
| "PUT"
|
|
431
|
+
| "PATCH"
|
|
432
|
+
| "DELETE",
|
|
433
|
+
path: request.path,
|
|
434
|
+
});
|
|
435
|
+
return { status: response.status };
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
deps.logError(
|
|
440
|
+
`✖ journey ${spec.name} teardown crashed: ` +
|
|
441
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
442
|
+
);
|
|
443
|
+
crashed ??= error instanceof Error ? error : new Error(String(error));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Reporting guaranteed on every outcome: when the pipeline crashed
|
|
448
|
+
// after producing an outcome, persist a failed infra report anyway.
|
|
449
|
+
if (outcome !== undefined) {
|
|
450
|
+
if (collected === undefined) {
|
|
451
|
+
verdict = {
|
|
452
|
+
passed: false,
|
|
453
|
+
failure_kind: outcome.failure_kind ?? "infra_error",
|
|
454
|
+
results: [],
|
|
455
|
+
};
|
|
456
|
+
collected = deps.collectReport({
|
|
457
|
+
spec,
|
|
458
|
+
env,
|
|
459
|
+
outcome,
|
|
460
|
+
verdict,
|
|
461
|
+
frictionPoints: [],
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
const finalized = await deps.finalizeReport(collected, teardown);
|
|
466
|
+
junitCase = finalized.junitCase;
|
|
467
|
+
deps.log(` report: ${finalized.reportPath}`);
|
|
468
|
+
// Point operators at the structured timeline for post-mortem; the
|
|
469
|
+
// report embeds it too, but the raw JSONL is the forensic source.
|
|
470
|
+
if (outcome !== undefined) {
|
|
471
|
+
deps.log(` events: ${outcome.eventsPath}`);
|
|
472
|
+
}
|
|
473
|
+
} catch (error) {
|
|
474
|
+
deps.logError(
|
|
475
|
+
`✖ journey ${spec.name} report finalization failed: ` +
|
|
476
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
477
|
+
);
|
|
478
|
+
crashed ??= error instanceof Error ? error : new Error(String(error));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (crashed !== undefined || verdict === undefined) {
|
|
484
|
+
const execution: JourneyExecution = { status: "infra" };
|
|
485
|
+
if (junitCase !== undefined) execution.junitCase = junitCase;
|
|
486
|
+
return execution;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const status = statusOf(verdict);
|
|
490
|
+
deps.log(
|
|
491
|
+
status === "passed"
|
|
492
|
+
? `✔ journey ${spec.name} passed`
|
|
493
|
+
: `✖ journey ${spec.name} ${status === "infra" ? "hit an infra error" : "failed"}` +
|
|
494
|
+
(verdict.failure_kind !== undefined
|
|
495
|
+
? ` (failure_kind=${verdict.failure_kind})`
|
|
496
|
+
: ""),
|
|
497
|
+
);
|
|
498
|
+
const execution: JourneyExecution = { status };
|
|
499
|
+
if (junitCase !== undefined) execution.junitCase = junitCase;
|
|
500
|
+
return execution;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// ── CLI orchestration ────────────────────────────────────────────────────────
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Run the journeys pillar once against one environment. Returns the
|
|
507
|
+
* process exit code (see the EXIT_* constants).
|
|
508
|
+
*/
|
|
509
|
+
export async function runJourneysCli(
|
|
510
|
+
argv: readonly string[],
|
|
511
|
+
deps: EntrypointDeps = defaultEntrypointDeps(),
|
|
512
|
+
): Promise<number> {
|
|
513
|
+
let args: ParsedCliArgs;
|
|
514
|
+
try {
|
|
515
|
+
args = parseCliArgs(argv);
|
|
516
|
+
} catch (error) {
|
|
517
|
+
deps.logError(error instanceof Error ? error.message : String(error));
|
|
518
|
+
return EXIT_INFRA_ERROR;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
let discovered: DiscoveredJourneySpec[];
|
|
522
|
+
try {
|
|
523
|
+
discovered = deps.discoverSpecs();
|
|
524
|
+
} catch (error) {
|
|
525
|
+
deps.logError(
|
|
526
|
+
`Journey spec discovery failed: ` +
|
|
527
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
528
|
+
);
|
|
529
|
+
return EXIT_INFRA_ERROR;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Repo-qualified filtering (review finding 10): --journey accepts
|
|
533
|
+
// "<repo>/<name>" (exact) or a bare name — bare names must be
|
|
534
|
+
// unambiguous across repos, else the run refuses loudly instead of
|
|
535
|
+
// executing an arbitrary duplicate.
|
|
536
|
+
if (args.journey !== undefined) {
|
|
537
|
+
const requested = args.journey;
|
|
538
|
+
const slash = requested.indexOf("/");
|
|
539
|
+
if (slash !== -1) {
|
|
540
|
+
const repo = requested.slice(0, slash);
|
|
541
|
+
const name = requested.slice(slash + 1);
|
|
542
|
+
discovered = discovered.filter(
|
|
543
|
+
(entry) => entry.repoName === repo && entry.spec.name === name,
|
|
544
|
+
);
|
|
545
|
+
if (discovered.length === 0) {
|
|
546
|
+
deps.logError(
|
|
547
|
+
`No discovered journey "${name}" in repo "${repo}". Check the ` +
|
|
548
|
+
"spec name and that the repo is registered (make register-repos).",
|
|
549
|
+
);
|
|
550
|
+
return EXIT_NO_SPECS;
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
const matches = discovered.filter(
|
|
554
|
+
(entry) => entry.spec.name === requested,
|
|
555
|
+
);
|
|
556
|
+
if (matches.length === 0) {
|
|
557
|
+
deps.logError(
|
|
558
|
+
`No discovered journey named "${requested}". Check the spec name ` +
|
|
559
|
+
"and that its repo is registered (make register-repos).",
|
|
560
|
+
);
|
|
561
|
+
return EXIT_NO_SPECS;
|
|
562
|
+
}
|
|
563
|
+
const repos = [...new Set(matches.map((entry) => entry.repoName))];
|
|
564
|
+
if (repos.length > 1) {
|
|
565
|
+
deps.logError(
|
|
566
|
+
`Journey name "${requested}" is ambiguous — declared by repos ` +
|
|
567
|
+
`[${repos.join(", ")}]. Qualify it as <repo>/<name> ` +
|
|
568
|
+
`(e.g. --journey=${repos[0]}/${requested}).`,
|
|
569
|
+
);
|
|
570
|
+
return EXIT_INFRA_ERROR;
|
|
571
|
+
}
|
|
572
|
+
discovered = matches;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (discovered.length === 0) {
|
|
577
|
+
deps.logError(
|
|
578
|
+
"No journey specs discovered across registered repos. Register the " +
|
|
579
|
+
`spec-hosting repo (make register-repos) and check its ${product().manifestYml} ` +
|
|
580
|
+
"journeys: block — zero specs is never a pass.",
|
|
581
|
+
);
|
|
582
|
+
return EXIT_NO_SPECS;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const runnable: DiscoveredJourneySpec[] = [];
|
|
586
|
+
for (const entry of discovered) {
|
|
587
|
+
if (entry.spec.environments.includes(args.env)) {
|
|
588
|
+
runnable.push(entry);
|
|
589
|
+
} else {
|
|
590
|
+
deps.log(
|
|
591
|
+
`↷ skipping ${entry.spec.name}: environments allowlist ` +
|
|
592
|
+
`[${entry.spec.environments.join(", ")}] excludes "${args.env}"`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (runnable.length === 0) {
|
|
598
|
+
deps.logError(
|
|
599
|
+
`0 of ${discovered.length} discovered journeys allow env "${args.env}" ` +
|
|
600
|
+
"— nothing ran (never silent green).",
|
|
601
|
+
);
|
|
602
|
+
return EXIT_NO_SPECS;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let envConfig: RunnerEnvConfigLike;
|
|
606
|
+
try {
|
|
607
|
+
envConfig = await deps.loadEnvironment(args.env);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
deps.logError(
|
|
610
|
+
`Failed to load environment "${args.env}": ` +
|
|
611
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
612
|
+
);
|
|
613
|
+
return EXIT_INFRA_ERROR;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Sequential in v1 — one agent, one browser context at a time.
|
|
617
|
+
const executions: JourneyExecution[] = [];
|
|
618
|
+
for (const entry of runnable) {
|
|
619
|
+
executions.push(await executeJourney(entry, args.env, envConfig, deps));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const junitCases = executions.flatMap((execution) =>
|
|
623
|
+
execution.junitCase !== undefined ? [execution.junitCase] : [],
|
|
624
|
+
);
|
|
625
|
+
if (junitCases.length > 0) {
|
|
626
|
+
try {
|
|
627
|
+
const junitPath = await deps.writeJUnit(junitCases);
|
|
628
|
+
deps.log(`JUnit: ${junitPath}`);
|
|
629
|
+
} catch (error) {
|
|
630
|
+
deps.logError(
|
|
631
|
+
`Failed to write the JUnit report: ` +
|
|
632
|
+
`${error instanceof Error ? error.message : String(error)}`,
|
|
633
|
+
);
|
|
634
|
+
return EXIT_INFRA_ERROR;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const infra = executions.filter((e) => e.status === "infra").length;
|
|
639
|
+
const failed = executions.filter((e) => e.status === "failed").length;
|
|
640
|
+
const passedCount = executions.length - infra - failed;
|
|
641
|
+
deps.log(
|
|
642
|
+
`journeys: ${passedCount} passed, ${failed} failed, ${infra} infra ` +
|
|
643
|
+
`error(s) — env=${args.env}`,
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
// Infra errors dominate: the result set is unreliable, not merely red.
|
|
647
|
+
if (infra > 0) return EXIT_INFRA_ERROR;
|
|
648
|
+
if (failed > 0) return EXIT_JOURNEY_FAILED;
|
|
649
|
+
return EXIT_ALL_PASSED;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// ── Main-module gate ─────────────────────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
// No import.meta here: ts-jest compiles this module to CJS for the unit
|
|
655
|
+
// tests, where import.meta is a syntax error. Matching argv[1] against this
|
|
656
|
+
// module's stable repo path keeps the gate inert under jest (argv[1] is the
|
|
657
|
+
// jest binary) while firing under `npx tsx src/pillars/journeys/run.ts`.
|
|
658
|
+
const isMainModule = /[\\/]pillars[\\/]journeys[\\/]run\.(ts|js)$/.test(
|
|
659
|
+
process.argv[1] ?? "",
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
if (isMainModule) {
|
|
663
|
+
runJourneysCli(process.argv.slice(2))
|
|
664
|
+
.then((exitCode) => {
|
|
665
|
+
process.exitCode = exitCode;
|
|
666
|
+
})
|
|
667
|
+
.catch((error: unknown) => {
|
|
668
|
+
console.error(
|
|
669
|
+
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
|
670
|
+
);
|
|
671
|
+
process.exitCode = EXIT_INFRA_ERROR;
|
|
672
|
+
});
|
|
673
|
+
}
|