Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 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 @@ -198,3 +198,46 @@ exports[`e2e flow snapshot — self-driving > integration-first (no existing Pos
],
}
`;

exports[`e2e flow snapshot — upload-source-maps > walks a stable path 1`] = `
{
"profile": {
"ask": "first",
"healthCheck": "dismiss",
"mcp": "skip",
"setup": "first",
"skills": "delete",
"slack": "skip",
},
"program": "error-tracking-upload-source-maps",
"trace": [
{
"action": "confirm_setup",
"screen": "source-maps-intro",
},
{
"action": "(external)",
"screen": "auth",
},
{
"action": "(external)",
"screen": "source-maps-detect",
},
{
"action": "(external)",
"screen": "run",
},
{
"action": "dismiss_outro",
"screen": "source-maps-outro",
},
{
"action": "keep_skills",
"params": {
"kept": false,
},
"screen": "keep-skills",
},
],
}
`;
19 changes: 19 additions & 0 deletions e2e-harness/__tests__/e2e-flow-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ function traceFlow(
Integration.javascriptNode,
FRAMEWORK_REGISTRY[Integration.javascriptNode],
);
} else if (screen === ScreenId.SourceMapsDetect) {
// The detect screen runs an agentic scan + an interactive pick; commit
// the pick through the driver the way the e2e host injection does.
driver.performAction('pick_source_maps_project', {
variant: 'node',
path: '.',
});
} else if (screen === ScreenId.Run) {
// 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
Expand Down Expand Up @@ -170,3 +177,15 @@ describe('e2e flow snapshot — self-driving', () => {
}).toMatchSnapshot();
});
});

describe('e2e flow snapshot — upload-source-maps', () => {
const profile = profileFor(Program.ErrorTrackingUploadSourceMaps);

it('walks a stable path', () => {
expect({
program: 'error-tracking-upload-source-maps',
profile,
trace: traceFlow(Program.ErrorTrackingUploadSourceMaps, profile),
}).toMatchSnapshot();
});
});
53 changes: 53 additions & 0 deletions e2e-harness/__tests__/wizard-ci-driver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ScreenId, Overlay } from '@ui/tui/router';
import { Program } from '@lib/programs/program-registry';
import { WizardCiDriver, UnknownActionError } from '../wizard-ci-driver';
import { ACTION_REGISTRY, NO_ACTION_SCREENS } from '../action-registry';
import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/index';

function freshStore(): WizardStore {
const store = new WizardStore(Program.PostHogIntegration);
Expand Down Expand Up @@ -212,6 +213,58 @@ describe('WizardCiDriver — self-driving integration check', () => {
});
});

describe('WizardCiDriver — source-maps project pick', () => {
function sourceMapsStore(): WizardStore {
const store = new WizardStore(Program.ErrorTrackingUploadSourceMaps);
setUI(new InkUI(store));
store.session = buildSession({ installDir: '/tmp/ci-driver-sm', ci: true });
return store;
}

function toDetectScreen(store: WizardStore): void {
// Intro → auth → detect.
store.completeSetup();
store.setCredentials({
accessToken: 'phx_x',
projectApiKey: 'phc_x',
host: 'https://us.posthog.com',
projectId: 1,
});
}

it('commits the pick the way the detect screen would and advances', () => {
const store = sourceMapsStore();
const driver = new WizardCiDriver(store);

toDetectScreen(store);
const state = driver.readState();
expect(state.currentScreen).toBe(ScreenId.SourceMapsDetect);
expect(state.actions.map((a) => a.id)).toContain(
'pick_source_maps_project',
);

const next = driver.performAction('pick_source_maps_project', {
variant: 'node',
path: '.',
});
const ctx = store.session.frameworkContext;
expect(ctx[SOURCE_MAPS_CONTEXT_KEYS.selectedVariant]).toBe('node');
expect(ctx[SOURCE_MAPS_CONTEXT_KEYS.selectedDisplayName]).toBe('Node.js');
expect(ctx[SOURCE_MAPS_CONTEXT_KEYS.selectedPath]).toBe('.');
expect(next.currentScreen).toBe(ScreenId.Run);
});

it('requires the variant and path params', () => {
const store = sourceMapsStore();
const driver = new WizardCiDriver(store);

toDetectScreen(store);
expect(() =>
driver.performAction('pick_source_maps_project', { variant: 'node' }),
).toThrow('requires param "path"');
});
});

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
48 changes: 44 additions & 4 deletions e2e-harness/action-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import type { WizardStore } from '@ui/tui/store';
import { ScreenId, Overlay, type ScreenName } from '@ui/tui/router';
import { McpOutcome } from '@lib/wizard-session';
import type { AskAnswers } from '@lib/wizard-session';
import {
SOURCE_MAPS_CONTEXT_KEYS,
VARIANT_DISPLAY_NAME,
} from '@lib/programs/error-tracking-upload-source-maps/index';

/** One commit action legal on a given screen. */
export interface DriverAction {
Expand Down Expand Up @@ -60,8 +64,7 @@ function requireString(
* (agent sets runPhase), ai-opt-in (org approval / ci auto-consent), exit,
* and the no-dismiss terminal overlays.
* - screens of programs the integration e2e profile never enters (audit,
* doctor, source-maps). source-maps-detect is interactive, so wire it into
* ACTION_REGISTRY when a source-maps profile starts driving that program.
* doctor).
*/
export const NO_ACTION_SCREENS: ReadonlySet<ScreenName> = new Set<ScreenName>([
ScreenId.Auth,
Expand All @@ -72,8 +75,6 @@ export const NO_ACTION_SCREENS: ReadonlySet<ScreenName> = new Set<ScreenName>([
ScreenId.DoctorReport,
// The detector + picker are interactive; no headless e2e drives this screen.
ScreenId.SelfDrivingIntegrationDetect,
ScreenId.SourceMapsDetect,
ScreenId.SourceMapsOutro,
ScreenId.AuditOutro,
Overlay.ManagedSettings,
Overlay.AuthError,
Expand Down Expand Up @@ -125,6 +126,45 @@ export const ACTION_REGISTRY: Partial<Record<ScreenName, DriverAction[]>> = {
},
],

// ── Source-maps project pick + outro ───────────────────────────────────
[ScreenId.SourceMapsDetect]: [
{
id: 'pick_source_maps_project',
description:
'Commit the project to wire source-map upload for, as the detect ' +
"screen's picker would. The candidate list lives in the screen's " +
'agentic report, so the caller supplies the pick.',
params: {
variant: 'skill variant (e.g. "node", "nextjs")',
path: 'project path relative to the repo root ("." = root)',
},
apply: (store, params) => {
const variant = requireString(
'pick_source_maps_project',
params,
'variant',
);
const path = requireString('pick_source_maps_project', params, 'path');
store.setFrameworkContext(
SOURCE_MAPS_CONTEXT_KEYS.selectedVariant,
variant,
);
store.setFrameworkContext(
SOURCE_MAPS_CONTEXT_KEYS.selectedDisplayName,
(VARIANT_DISPLAY_NAME as Record<string, string>)[variant] ?? variant,
);
store.setFrameworkContext(SOURCE_MAPS_CONTEXT_KEYS.selectedPath, path);
},
},
],
[ScreenId.SourceMapsOutro]: [
{
id: 'dismiss_outro',
description: 'Dismiss the source-maps outro (sets outroDismissed).',
apply: (store) => store.setOutroDismissed(),
},
],

// ── Health check — dismiss a blocking outage ──────────────────────────
[ScreenId.HealthCheck]: [
{
Expand Down
2 changes: 2 additions & 0 deletions e2e-harness/e2e-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export function decideE2eAction(
return { action: { id: 'confirm_self_driving_handoff' } };

case ScreenId.Outro:
case ScreenId.SourceMapsOutro:
return { action: { id: 'dismiss_outro' } };

case ScreenId.Mcp:
Expand Down Expand Up @@ -166,6 +167,7 @@ export const E2E_DRIVABLE_SCREENS: readonly ScreenName[] = [
ScreenId.Setup,
ScreenId.SelfDrivingIntegrationCheck,
ScreenId.Outro,
ScreenId.SourceMapsOutro,
ScreenId.Mcp,
ScreenId.McpSuggestedPrompts,
ScreenId.SlackConnect,
Expand Down
3 changes: 3 additions & 0 deletions e2e-harness/profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import { Program, type ProgramId } from '@lib/programs/program-registry';
import { DEFAULT_E2E_PROFILE, type WizardE2eProfile } from './e2e-profile.js';
import posthogIntegrationE2e from '@lib/programs/posthog-integration/test/e2e.json';
import selfDrivingE2e from '@lib/programs/self-driving/test/e2e.json';
import sourceMapsE2e from '@lib/programs/error-tracking-upload-source-maps/test/e2e.json';

const PROFILES: Partial<Record<ProgramId, WizardE2eProfile>> = {
[Program.PostHogIntegration]:
posthogIntegrationE2e.profile as WizardE2eProfile,
[Program.SelfDriving]: selfDrivingE2e.profile as WizardE2eProfile,
[Program.ErrorTrackingUploadSourceMaps]:
sourceMapsE2e.profile as WizardE2eProfile,
};

/** The e2e profile for a program, or the happy-path default if none is set. */
Expand Down
60 changes: 59 additions & 1 deletion scripts/tui-host.no-jest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import { detectFramework } from '@lib/detection/index';
import { FRAMEWORK_REGISTRY } from '@lib/registry';
import type { Integration } from '@lib/constants';
import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect';
import { ScreenId } from '@ui/tui/router';
import {
detectSourceMapsPrerequisites,
SOURCE_MAPS_CONTEXT_KEYS,
} from '@lib/programs/error-tracking-upload-source-maps/index';
import { ScreenId, Overlay } from '@ui/tui/router';
import { WizardCiDriver } from '@e2e-harness/wizard-ci-driver';
import {
decideE2eAction,
Expand Down Expand Up @@ -253,6 +257,18 @@ async function main() {
async function fixed() {
const CTRL = process.env.SNAP_CTRL!;
const profile: WizardE2eProfile = profileFor(programId);
// Program-specific wizard_ask answers the generic 'first' strategy can't
// give. Source-maps STEP 1 wants a real upload key — the driver supplies
// the raw value and the wizard_ask tool vaults it, so the agent only ever
// sees a secretRef — and STEP 8's "test it now?" must be declined: nobody
// is at the keyboard to run a build. An unset env answer falls through to
// the generic strategy (the 'e2e' sentinel).
const askOverrides: Record<string, Record<string, string | undefined>> = {
[Program.ErrorTrackingUploadSourceMaps]: {
'api-key': process.env.SOURCE_MAPS_CLI_KEY,
'test-affordance': 'no',
},
};
const screenPath: string[] = [];
// Snapshot on key moments — a screen change, a task-list update, or a
// runPhase change — so the run screen's progression (the agent working) is
Expand Down Expand Up @@ -317,6 +333,48 @@ async function main() {
continue;
}

// Headless source-maps detect: the screen's candidate list lives in
// its own agentic report (React state), so compute the pick here with
// the static prerequisite detector — right for a single-app fixture —
// and commit it through the driver the way the picker would.
if (
state.currentScreen === ScreenId.SourceMapsDetect &&
store.session.frameworkContext[
SOURCE_MAPS_CONTEXT_KEYS.selectedVariant
] == null
) {
const ctx: Record<string, unknown> = {};
detectSourceMapsPrerequisites(store.session, (k, v) => {
ctx[k] = v;
});
const variant = ctx[SOURCE_MAPS_CONTEXT_KEYS.skillVariant];
if (typeof variant !== 'string') {
mark(
'source-maps detect found nothing to instrument: ' +
JSON.stringify(ctx[SOURCE_MAPS_CONTEXT_KEYS.detectError]),
);
process.exit(1);
}
driver.performAction('pick_source_maps_project', {
variant,
path: '.',
});
continue;
}

// Program-specific ask answers take precedence over the generic
// profile strategy.
if (state.currentScreen === Overlay.WizardAsk) {
const q = state.pendingQuestion?.questions[0];
const override = q ? askOverrides[programId]?.[q.id] : undefined;
if (q && override !== undefined) {
driver.performAction('answer_question', {
answers: { [q.id]: override },
});
continue;
}
}

let acted = false;
try {
const decision = decideE2eAction(state, profile);
Expand Down
38 changes: 38 additions & 0 deletions src/lib/programs/error-tracking-upload-source-maps/test/e2e.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"program": "error-tracking-upload-source-maps",
"summary": "Upload source maps: intro → auth → the detect agent scans the repo and the host injects the first instrumentable project as the pick → the run agent gets the upload key via wizard_ask (id \"api-key\"; the driver supplies the raw key and the ask tool vaults it, so the agent only sees a secretRef), wires build config + CI, declines the test offer → outro → keep-skills.",
"profile": {
"setup": "first",
"healthCheck": "dismiss",
"mcp": "skip",
"slack": "skip",
"skills": "delete",
"ask": "first"
},
"path": [
{
"screen": "source-maps-intro",
"auto": "confirm & continue"
},
{
"screen": "auth",
"auto": "(external) — the runner resolves credentials from the phx key"
},
{
"screen": "source-maps-detect",
"auto": "(external) — the host computes the pick with the static prerequisite detector and commits it via pick_source_maps_project (selectedVariant/DisplayName/Path); the runner's post-auth gate then releases the run"
},
{
"screen": "run",
"auto": "(external) — the upload agent; the host answers wizard_ask \"api-key\" with SOURCE_MAPS_CLI_KEY and \"test-affordance\" with \"no\""
},
{
"screen": "source-maps-outro",
"auto": "dismiss"
},
{
"screen": "keep-skills",
"auto": "delete the installed skill"
}
]
}
Loading