Skip to content
Merged
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
45 changes: 44 additions & 1 deletion e2e-harness/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ e2e-harness/
wizard-ci-driver.ts WizardCiDriver — read_state / perform_action over the store
action-registry.ts screen → the actions legal on it (+ NO_ACTION_SCREENS)
e2e-profile.ts WizardE2eProfile + decideE2eAction — the scripted walk policy
profiles.ts per-program profiles + profileFor(programId)
profiles.ts per-program profiles + profileFor(programId) + resolveE2eProfile(env)
e2e-result.ts the E2E_RESULT_JSON payload: E2eRunRecorder + buildE2eResult
tui-capture.ts run a command in a PTY (node-pty) + read its real screen (@xterm/headless)
scripts/
tui-host.no-jest.ts the real-TUI host: startTUI + WizardCiDriver, MODE=fixed | serve
Expand Down Expand Up @@ -78,6 +79,48 @@ the program's entry (typed by `WizardE2eProfile`); the host asks
`decideE2eAction(state, profile)` what to commit on each screen. The (screen →
decision) trace is snapshot-tested offline in `__tests__/` (`jest -u` to update).

## Driving the agent-in-the-loop layer

Two decision points ask a person to act, and the harness stands in for them.

`wizard_ask` overlay. A `ci` session normally has no ask bridge at all. The host
sets `session.e2eAsk` from `E2E_ASK=true`, which keeps the bridge wired (see
`shouldDisableAsk`). The profile then answers every question in the batch:
`askAnswers` routes a question to a value, else the first option, else the `'e2e'`
sentinel. Route credentials with `${ENV_VAR}` values, never literals.

Mark a credential rule `"secret": true`. The skill names its own questions, so a
rule matches text the agent controls — without the flag, a question called
`password` takes the value, and by omitting `sensitive: true` gets it back
unvaulted in plaintext. A secret rule answers only free text flagged sensitive
and refuses every other question shape; refusals land in the payload's
`refusedIds` / `refusedAsks`, so a run that withholds a credential says so
instead of looking like an ordinary sentinel.

Task-notice overlay. `profile.notice` decides `keep` or `decline`. `E2E_NOTICE`
overrides it per run.

Both env inputs are folded into the profile by `resolveE2eProfile`, at load.
`decideE2eAction` must stay pure — it reads no env and no store.

## The result payload

A `MODE=fixed` run writes `E2E_RESULT_JSON`: the run phase and screens walked,
plus every ask batch, every task notice, the task list, the detected warehouse
sources, the program's report file, and the abort reason. `e2e-result.ts` builds
it.

**Only question prompt text and question ids go in the payload. No answer value
ever does.** The decision function reports ids and a keep/decline verdict, so the
recorder never holds an answer. Keep it that way — the workbench scans the file
for its own injected credentials.

The report file is the one payload field whose *content* comes from the app
directory, which is a checkout the run does not control. `readReportFile` reads
only a regular file inside `appDir` — never a symlink, and never through a
symlinked parent — so a committed `posthog-warehouse-report.md` pointing at a
host file cannot copy it into the payload.

## Visual-regression snapshots (the workbench flow)

[wizard-workbench](https://github.com/PostHog/wizard-workbench) runs the CI route
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ exports[`e2e flow snapshot — posthog-integration > Next.js (with a setup quest
"ask": "first",
"healthCheck": "dismiss",
"mcp": "skip",
"notice": "keep",
"setup": "first",
"skills": "delete",
"slack": "skip",
Expand Down Expand Up @@ -171,3 +172,76 @@ exports[`e2e flow snapshot — posthog-integration > Node (no setup question) wa
],
}
`;

exports[`e2e flow snapshot — warehouse-source > walks intro → auth → run → outro → skills 1`] = `
{
"profile": {
"ask": "first",
"askAnswers": [
{
"match": "prefix",
"value": "\${E2E_SOURCE_PREFIX}",
},
{
"match": "stripe",
"secret": true,
"value": "\${E2E_STRIPE_API_KEY}",
},
{
"match": "host|hostname|server|endpoint",
"value": "\${E2E_PG_HOST}",
},
{
"match": "port",
"value": "\${E2E_PG_PORT}",
},
{
"match": "database|db_?name",
"value": "\${E2E_PG_DATABASE}",
},
{
"match": "password|passwd|pwd",
"secret": true,
"value": "\${E2E_PG_PASSWORD}",
},
{
"match": "user|username|role",
"value": "\${E2E_PG_USER}",
},
{
"match": "schema",
"value": "public",
},
],
"healthCheck": "dismiss",
"mcp": "skip",
"notice": "keep",
"setup": "first",
"skills": "delete",
"slack": "skip",
},
"program": "warehouse-source",
"trace": [
{
"action": "confirm_setup",
"screen": "warehouse-intro",
},
{
"action": "(external)",
"screen": "auth",
},
{
"action": "(external)",
"screen": "run",
},
{
"action": "dismiss_outro",
"screen": "outro",
},
{
"action": "keep_skills",
"screen": "keep-skills",
},
],
}
`;
23 changes: 23 additions & 0 deletions e2e-harness/__tests__/e2e-flow-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,26 @@ describe('e2e flow snapshot — metrics', () => {
expect(trace.length).toBeLessThan(40);
});
});

describe('e2e flow snapshot — warehouse-source', () => {
it('walks intro → auth → run → outro → skills', () => {
expect({
program: 'warehouse-source',
profile: profileFor(Program.WarehouseSource),
trace: traceFlow(Integration.javascriptNode, Program.WarehouseSource),
}).toMatchSnapshot();
});

it('reaches a terminal decision instead of stalling on the intro', () => {
const trace = traceFlow(
Integration.javascriptNode,
Program.WarehouseSource,
);
expect(trace[0]).toEqual({
screen: 'warehouse-intro',
action: 'confirm_setup',
});
expect(trace.at(-1)?.action).toBe('keep_skills');
expect(trace.length).toBeLessThan(40);
});
});
Loading
Loading