// Generate a committed scenario from a recorded journey. The output is a normal // scenario file (the same shape a human writes, see e2e/AGENTS.md): an Effect // body that yields Target + Browser, mints a fresh identity, and drives one // browser session of labelled steps. Terminal (`run`) and HTTP (`request`) steps // run INSIDE that session, interleaved with the UI, so page state is never lost // between them. It is meant to be read and edited after generation — promotion // is the START of a scenario's life, not a frozen artifact. import { codegenStep, isAssertion, isBrowserStep, stepLabel, type Step } from "./steps"; export interface JourneyFile { readonly target: string; readonly org: boolean; readonly steps: ReadonlyArray; } const INDENT = " "; const stepBlock = (step: Step): string => { const label = JSON.stringify(stepLabel(step)); const body = codegenStep(step) .split("\n") .map((line) => `${INDENT} ${line}`) .join("\n"); return `${INDENT}await step(${label}, async () => {\n${body}\n${INDENT}});`; }; /** A journey with no assertion proves nothing; one with no browser step isn't * what this tool generates (write a CLI/API test directly). `promote` checks * both. */ export const journeyHasAssertion = (journey: JourneyFile): boolean => journey.steps.some(isAssertion); export const journeyHasBrowserStep = (journey: JourneyFile): boolean => journey.steps.some(isBrowserStep); export const codegenScenario = (name: string, journey: JourneyFile): string => { const identityArg = journey.org ? "" : "{ org: false }"; const body = journey.steps.map(stepBlock).join("\n"); const needsExec = journey.steps.some((step) => step.kind === "run"); const needsExpect = journey.steps.some( (step) => step.kind === "request" || (step.kind === "run" && step.contains !== undefined), ); const imports: string[] = []; if (needsExec) { imports.push(`import { execFile } from "node:child_process";`); imports.push(`import { promisify } from "node:util";`); imports.push(""); } if (needsExpect) imports.push(`import { expect } from "@effect/vitest";`); imports.push(`import { Effect } from "effect";`); imports.push(""); imports.push(`import { scenario } from "../src/scenario";`); imports.push(`import { Browser, Target } from "../src/services";`); const execHelper = needsExec ? "\nconst execFileAsync = promisify(execFile);\n" : ""; return `// Generated from an interactive browser journey: \`bun scripts/cli.ts promote ${journey.target} ""\`. // This is now an ordinary scenario — edit it freely. It drives the same Browser // surface the exploration used, so a reviewer can judge the guarantee by // reading it. Re-run with: E2E_${journey.target.toUpperCase()}_URL= vitest run --project ${journey.target} ${imports.join("\n")} ${execHelper} scenario( ${JSON.stringify(name)}, { timeout: 120_000 }, Effect.gen(function* () { const target = yield* Target; const browser = yield* Browser; const identity = yield* target.newIdentity(${identityArg}); yield* browser.session(identity, async ({ page, step }) => { ${body} }); }), ); `; };