Skip to content
107 changes: 107 additions & 0 deletions e2e-harness/__tests__/__snapshots__/e2e-flow-snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ exports[`e2e flow snapshot — posthog-integration > Next.js (with a setup quest
},
{
"action": "choose",
"params": {
"key": "router",
"value": "app-router",
},
"screen": "setup",
},
{
Expand All @@ -38,6 +42,9 @@ exports[`e2e flow snapshot — posthog-integration > Next.js (with a setup quest
},
{
"action": "set_mcp_outcome",
"params": {
"outcome": "skipped",
},
"screen": "mcp",
},
{
Expand All @@ -46,6 +53,9 @@ exports[`e2e flow snapshot — posthog-integration > Next.js (with a setup quest
},
{
"action": "keep_skills",
"params": {
"kept": false,
},
"screen": "keep-skills",
},
],
Expand Down Expand Up @@ -78,6 +88,9 @@ exports[`e2e flow snapshot — posthog-integration > Node (no setup question) wa
},
{
"action": "set_mcp_outcome",
"params": {
"outcome": "skipped",
},
"screen": "mcp",
},
{
Expand All @@ -86,8 +99,102 @@ exports[`e2e flow snapshot — posthog-integration > Node (no setup question) wa
},
{
"action": "keep_skills",
"params": {
"kept": false,
},
"screen": "keep-skills",
},
],
}
`;

exports[`e2e flow snapshot — self-driving > already-integrated (skips SDK setup) walks a stable path 1`] = `
{
"program": "self-driving",
"trace": [
{
"action": "confirm_setup",
"screen": "self-driving-intro",
},
{
"action": "set_integrate",
"params": {
"integrate": false,
},
"screen": "self-driving-integration-check",
},
{
"action": "dismiss_outage",
"screen": "health-check",
},
{
"action": "(external)",
"screen": "auth",
},
{
"action": "(external)",
"screen": "run",
},
{
"action": "dismiss_outro",
"screen": "outro",
},
],
}
`;

exports[`e2e flow snapshot — self-driving > integration-first (no existing PostHog) walks a stable path 1`] = `
{
"profile": {
"ask": "first",
"healthCheck": "dismiss",
"integrate": true,
"mcp": "skip",
"setup": "first",
"skills": "delete",
"slack": "skip",
},
"program": "self-driving",
"trace": [
{
"action": "confirm_setup",
"screen": "self-driving-intro",
},
{
"action": "set_integrate",
"params": {
"integrate": true,
},
"screen": "self-driving-integration-check",
},
{
"action": "dismiss_outage",
"screen": "health-check",
},
{
"action": "(external)",
"screen": "auth",
},
{
"action": "(external)",
"screen": "self-driving-integration-detect",
},
{
"action": "(external)",
"screen": "run",
},
{
"action": "confirm_self_driving_handoff",
"screen": "self-driving-handoff",
},
{
"action": "(external)",
"screen": "run",
},
{
"action": "dismiss_outro",
"screen": "outro",
},
],
}
`;
110 changes: 93 additions & 17 deletions e2e-harness/__tests__/e2e-flow-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,59 @@ import { buildSession, RunPhase } from '@lib/wizard-session';
import { Integration } from '@lib/constants';
import { FRAMEWORK_REGISTRY } from '@lib/registry';
import { WizardReadiness } from '@lib/health-checks/readiness';
import { Program } from '@lib/programs/program-registry';
import {
Program,
getProgramConfig,
type ProgramId,
} from '@lib/programs/program-registry';
import { ScreenId } from '@ui/tui/router';
import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect';
import { WizardCiDriver } from '../wizard-ci-driver';
import { decideE2eAction } from '../e2e-profile';
import { decideE2eAction, type WizardE2eProfile } from '../e2e-profile';
import { profileFor } from '../profiles';

/**
* Walk the program flow offline using its e2e profile, injecting the external
* transitions a real run gets from the runner (auth) and the agent (runPhase)
* and the health probe. Returns the ordered (screen, action) trace.
* Walk a program flow offline using an e2e profile, injecting the external
* transitions a real run gets from the runner (auth), the agent (runPhase), and
* the health probe. Returns the ordered (screen, action) trace. Stops at the
* terminal Exit screen or when a profile decision marks the run done.
*/
function traceFlow(
integration: Integration,
): Array<{ screen: string; action: string }> {
const store = new WizardStore(Program.PostHogIntegration);
program: ProgramId,
profile: WizardE2eProfile,
integration?: Integration,
): Array<{
screen: string;
action: string;
params?: Record<string, unknown>;
}> {
const store = new WizardStore(program);
setUI(new InkUI(store));
const session = buildSession({ installDir: '/tmp/e2e-snap', ci: true });
session.integration = integration;
session.frameworkConfig = FRAMEWORK_REGISTRY[integration];
if (integration) {
session.integration = integration;
session.frameworkConfig = FRAMEWORK_REGISTRY[integration];
}
store.session = session;

const driver = new WizardCiDriver(store);
const profile = profileFor(Program.PostHogIntegration);

const trace: Array<{ screen: string; action: string }> = [];
const trace: Array<{
screen: string;
action: string;
params?: Record<string, unknown>;
}> = [];
for (let guard = 0; guard < 40; guard++) {
const state = driver.readState();
const screen = state.currentScreen;
if (screen === ScreenId.Exit) break; // terminal (self-driving outro exits)

const decision = decideE2eAction(state, profile);
trace.push({ screen, action: decision.action?.id ?? '(external)' });
trace.push({
screen,
action: decision.action?.id ?? '(external)',
...(decision.action?.params ? { params: decision.action.params } : {}),
});

if (decision.action) {
driver.performAction(decision.action.id, decision.action.params ?? {});
Expand All @@ -69,8 +92,31 @@ function traceFlow(
host: 'https://us.posthog.com',
projectId: 1,
});
} else if (screen === ScreenId.SelfDrivingIntegrationDetect) {
// The detect screen self-advances in ci by picking a project; simulate
// that pick (framework + path) so the run phase can proceed.
store.setFrameworkContext(SELF_DRIVING_INTEGRATE_PATH_KEY, '.');
store.setFrameworkConfig(
Integration.javascriptNode,
FRAMEWORK_REGISTRY[Integration.javascriptNode],
);
} else if (screen === ScreenId.Run) {
store.setRunPhase(RunPhase.Completed);
// The run screen is shared by composed run steps (a step carrying its own
// `run` thunk, e.g. self-driving's integrate-run) and the program's own
// run. Complete the active run step the way the runner would: a composed
// step via completeRunStep, the main run via runPhase.
const steps = getProgramConfig(store.router.activeProgram).steps;
const runStep = steps.find(
(s) =>
s.screenId === 'run' &&
(!s.show || s.show(store.session)) &&
(!s.isComplete || !s.isComplete(store.session)),
);
if (runStep?.run) {
store.completeRunStep(runStep.id);
} else {
store.setRunPhase(RunPhase.Completed);
}
}

if (decision.done || store.session.skillsComplete) break;
Expand All @@ -79,18 +125,48 @@ function traceFlow(
}

describe('e2e flow snapshot — posthog-integration', () => {
const profile = profileFor(Program.PostHogIntegration);

it('Next.js (with a setup question) walks a stable path', () => {
expect({
program: 'posthog-integration',
profile: profileFor(Program.PostHogIntegration),
trace: traceFlow(Integration.nextjs),
profile,
trace: traceFlow(Program.PostHogIntegration, profile, Integration.nextjs),
}).toMatchSnapshot();
});

it('Node (no setup question) walks a stable path', () => {
expect({
program: 'posthog-integration',
trace: traceFlow(Integration.javascriptNode),
trace: traceFlow(
Program.PostHogIntegration,
profile,
Integration.javascriptNode,
),
}).toMatchSnapshot();
});
});

describe('e2e flow snapshot — self-driving', () => {
const profile = profileFor(Program.SelfDriving);

it('integration-first (no existing PostHog) walks a stable path', () => {
expect({
program: 'self-driving',
profile,
trace: traceFlow(Program.SelfDriving, profile),
}).toMatchSnapshot();
});

it('already-integrated (skips SDK setup) walks a stable path', () => {
// Same flow, answering "yes, already integrated" at the check.
const alreadyIntegrated: WizardE2eProfile = {
...profile,
integrate: false,
};
expect({
program: 'self-driving',
trace: traceFlow(Program.SelfDriving, alreadyIntegrated),
}).toMatchSnapshot();
});
});
40 changes: 40 additions & 0 deletions e2e-harness/__tests__/wizard-ci-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,46 @@ describe('WizardCiDriver — wizard_ask overlay', () => {
});
});

describe('WizardCiDriver — self-driving integration check', () => {
function selfDrivingStore(): WizardStore {
const store = new WizardStore(Program.SelfDriving);
setUI(new InkUI(store));
store.session = buildSession({ installDir: '/tmp/ci-driver-sd', ci: true });
return store;
}

it('exposes the integration check and commits set_integrate', () => {
const store = selfDrivingStore();
const driver = new WizardCiDriver(store);

// Intro → integration-check.
store.completeSetup();
const state = driver.readState();
expect(state.currentScreen).toBe(ScreenId.SelfDrivingIntegrationCheck);
expect(state.session.integrate).toBeNull();
expect(state.actions.map((a) => a.id)).toContain('set_integrate');

// Answer "no, set it up first" → integrate=true, advances off the screen.
const next = driver.performAction('set_integrate', { integrate: true });
expect(next.session.integrate).toBe(true);
expect(next.currentScreen).not.toBe(ScreenId.SelfDrivingIntegrationCheck);
});

it('skips the integration check when --integrate pre-resolved it', () => {
const store = selfDrivingStore();
store.session = buildSession({
installDir: '/tmp/ci-driver-sd',
integrate: true,
});
const driver = new WizardCiDriver(store);

store.completeSetup();
expect(driver.readState().currentScreen).not.toBe(
ScreenId.SelfDrivingIntegrationCheck,
);
});
});

describe('action registry exhaustiveness', () => {
it('every screen and overlay is either actionable or explicitly no-action', () => {
const allScreens = [...Object.values(ScreenId), ...Object.values(Overlay)];
Expand Down
24 changes: 24 additions & 0 deletions e2e-harness/action-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const NO_ACTION_SCREENS: ReadonlySet<ScreenName> = new Set<ScreenName>([
ScreenId.Exit,
ScreenId.AuditRun,
ScreenId.DoctorReport,
// The detector + picker are interactive; no headless e2e drives this screen.
ScreenId.SelfDrivingIntegrationDetect,
ScreenId.SourceMapsDetect,
ScreenId.SourceMapsOutro,
ScreenId.AuditOutro,
Expand Down Expand Up @@ -101,6 +103,28 @@ export const ACTION_REGISTRY: Partial<Record<ScreenName, DriverAction[]>> = {
[ScreenId.WarehouseIntro]: [confirmSetupAction],
[ScreenId.SelfDrivingIntro]: [confirmSetupAction],

// ── Self-driving integration check ────────────────────────────────────
[ScreenId.SelfDrivingIntegrationCheck]: [
{
id: 'set_integrate',
description:
'Answer the self-driving integration check. integrate=true sets up ' +
'the PostHog SDK first; false goes straight to Self-driving.',
params: { integrate: 'boolean (default false)' },
apply: (store, params) => store.setIntegrate(params.integrate === true),
},
],

// ── Self-driving handoff (after the integration run) ───────────────────
[ScreenId.SelfDrivingHandoff]: [
{
id: 'confirm_self_driving_handoff',
description:
'Acknowledge the post-integration handoff and start the Self-driving run.',
apply: (store) => store.confirmSelfDrivingHandoff(),
},
],

// ── Health check — dismiss a blocking outage ──────────────────────────
[ScreenId.HealthCheck]: [
{
Expand Down
Loading
Loading