From 031ae3164f07bcd55288385b4b5257b1b613789c Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 18 Sep 2026 19:08:03 -0400 Subject: [PATCH 1/3] chore(typecheck): make tsc --noEmit pass Type-only fixes for the 32 errors tsc reported. One runtime fix rides along: RevenueIntroScreen spread the POSTHOG_SDKS and STRIPE_SDKS sets before calling array methods on them, which previously threw on those branches. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- e2e-harness/__tests__/e2e-flow-snapshot.test.ts | 3 ++- e2e-harness/__tests__/wizard-ci-driver.test.ts | 2 +- src/lib/__tests__/wizard-tools.test.ts | 7 ++++++- .../agent/runner/harness/pi/__tests__/tools.test.ts | 8 +++++--- src/lib/agent/runner/harness/pi/mcp.ts | 4 +++- .../orchestrator/__tests__/task-notice-timeout.test.ts | 2 +- src/lib/programs/__tests__/metrics-program.test.ts | 3 +-- .../programs/__tests__/warehouse-suggestion.test.ts | 8 ++++---- src/ui/tui/components/visualizer/CrateStack.tsx | 2 +- src/ui/tui/playground/demos/McpDemo.tsx | 10 +++++----- src/ui/tui/screens/MetricsIntroScreen.tsx | 2 +- src/ui/tui/screens/RevenueIntroScreen.tsx | 8 ++++---- src/utils/__tests__/ci-region.test.ts | 2 ++ src/utils/bounded-fs.ts | 2 +- src/wizard.ts | 2 +- 15 files changed, 38 insertions(+), 27 deletions(-) diff --git a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts index 2a525cfb0..243d4f830 100644 --- a/e2e-harness/__tests__/e2e-flow-snapshot.test.ts +++ b/e2e-harness/__tests__/e2e-flow-snapshot.test.ts @@ -18,6 +18,7 @@ import { InkUI } from '@ui/tui/ink-ui'; import { setUI } from '@ui/index'; import { buildSession, RunPhase } from '@lib/wizard-session'; import { Integration } from '@lib/constants'; +import { HostResolution } from '@lib/host-resolution'; import { FRAMEWORK_REGISTRY } from '@lib/registry'; import { WizardReadiness } from '@lib/health-checks/readiness'; import { @@ -89,7 +90,7 @@ function traceFlow( store.setCredentials({ accessToken: 'phx_x', projectApiKey: 'phc_x', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 1, }); } else if (screen === ScreenId.SelfDrivingGithub) { diff --git a/e2e-harness/__tests__/wizard-ci-driver.test.ts b/e2e-harness/__tests__/wizard-ci-driver.test.ts index 45d4af832..2edf4553e 100644 --- a/e2e-harness/__tests__/wizard-ci-driver.test.ts +++ b/e2e-harness/__tests__/wizard-ci-driver.test.ts @@ -251,7 +251,7 @@ describe('WizardCiDriver — source-maps project pick', () => { store.setCredentials({ accessToken: 'phx_x', projectApiKey: 'phc_x', - host: 'https://us.posthog.com', + host: HostResolution.fromApiHost('https://us.posthog.com'), projectId: 1, }); } diff --git a/src/lib/__tests__/wizard-tools.test.ts b/src/lib/__tests__/wizard-tools.test.ts index 771751234..5ddeff913 100644 --- a/src/lib/__tests__/wizard-tools.test.ts +++ b/src/lib/__tests__/wizard-tools.test.ts @@ -920,7 +920,12 @@ describe('resolveAskQuestionKinds', () => { it('reads a kind-less question as free text, which is what a credential is', () => { expect( resolveAskQuestionKinds([ - { id: 'password', prompt: 'Database password', sensitive: true }, + { id: 'password', prompt: 'Database password', sensitive: true } as { + kind?: undefined; + id: string; + prompt: string; + sensitive: boolean; + }, ]), ).toEqual([ { diff --git a/src/lib/agent/runner/harness/pi/__tests__/tools.test.ts b/src/lib/agent/runner/harness/pi/__tests__/tools.test.ts index 018e85adf..c3ef60dc1 100644 --- a/src/lib/agent/runner/harness/pi/__tests__/tools.test.ts +++ b/src/lib/agent/runner/harness/pi/__tests__/tools.test.ts @@ -66,6 +66,8 @@ const call = (tool: { execute: unknown }, args: unknown): Promise => args, ); +type ExecuteLoosely = (id: string, params: unknown) => Promise; + describe('pi wizard_ask — sensitive answers are vaulted', () => { it('returns {secretRef}, never the raw value', async () => { const { wizardAsk } = makeTools({ token: SECRET, tracker: 'linear' }); @@ -687,13 +689,13 @@ describe('audit ledger tools', () => { status: 'pending' as const, }); - await tool('audit_seed_checks').execute('1', { + await (tool('audit_seed_checks').execute as ExecuteLoosely)('1', { checks: [check('sdk-installed'), check('init-correct')], } as never); - await tool('audit_resolve_checks').execute('2', { + await (tool('audit_resolve_checks').execute as ExecuteLoosely)('2', { updates: [{ id: 'sdk-installed', status: 'pass' }], } as never); - await tool('audit_add_checks').execute('3', { + await (tool('audit_add_checks').execute as ExecuteLoosely)('3', { checks: [check('live-data-source-maps')], } as never); diff --git a/src/lib/agent/runner/harness/pi/mcp.ts b/src/lib/agent/runner/harness/pi/mcp.ts index 4e662178f..eeaa3607a 100644 --- a/src/lib/agent/runner/harness/pi/mcp.ts +++ b/src/lib/agent/runner/harness/pi/mcp.ts @@ -59,7 +59,9 @@ export async function setupPostHogMcp(opts: { // The adapter ships raw TypeScript; loading through jiti is its documented requirement. const jiti = createJiti(import.meta.url); - const mod = await jiti.import('pi-mcp-adapter'); + const mod = await jiti.import<{ + createMcpAdapter: (options: unknown) => PostHogMcpSetup['extensionFactory']; + }>('pi-mcp-adapter'); const extensionFactory = mod.createMcpAdapter({ config: { mcpServers: { diff --git a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts index c7d35a034..c6acf1014 100644 --- a/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts +++ b/src/lib/agent/runner/sequence/orchestrator/__tests__/task-notice-timeout.test.ts @@ -13,7 +13,7 @@ import type { TaskNotice } from '@lib/wizard-session'; // factory would otherwise read these before they exist. const { showTaskNotice, cancelTaskNotice, wizardCapture, captureException } = vi.hoisted(() => ({ - showTaskNotice: vi.fn<[TaskNotice], Promise>(), + showTaskNotice: vi.fn<(notice: TaskNotice) => Promise>(), cancelTaskNotice: vi.fn(), wizardCapture: vi.fn(), captureException: vi.fn(), diff --git a/src/lib/programs/__tests__/metrics-program.test.ts b/src/lib/programs/__tests__/metrics-program.test.ts index 27dbd8989..ae5936408 100644 --- a/src/lib/programs/__tests__/metrics-program.test.ts +++ b/src/lib/programs/__tests__/metrics-program.test.ts @@ -2,7 +2,6 @@ import { AGENT_SKILL_STEPS } from '@lib/programs/agent-skill/index'; import { getProgramConfig, Program } from '@lib/programs/program-registry'; import { metricsConfig } from '@lib/programs/metrics/index'; import type { ProgramRun } from '@lib/agent/agent-runner'; -import type { WizardSession } from '@lib/wizard-session'; import { metricsCommand } from '../../../commands/metrics'; @@ -38,7 +37,7 @@ describe('metrics program', () => { const run = staticRun(metricsConfig); expect(run.skillId).toBeUndefined(); - const prompt = run.customPrompt?.({} as WizardSession); + const prompt = run.customPrompt?.({} as never); expect(prompt).toContain('load_skill_menu'); expect(prompt).toContain('"metrics"'); // Every published variant the prompt teaches the agent to choose from. diff --git a/src/lib/programs/__tests__/warehouse-suggestion.test.ts b/src/lib/programs/__tests__/warehouse-suggestion.test.ts index de7a39b9c..0f6023f28 100644 --- a/src/lib/programs/__tests__/warehouse-suggestion.test.ts +++ b/src/lib/programs/__tests__/warehouse-suggestion.test.ts @@ -78,7 +78,7 @@ describe('outro suggestion', () => { const s = sessionWith([POSTGRES, STRIPE]); const runDef = await resolveRun(s); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const outro = runDef.buildOutroData!(s, CREDENTIALS as any); + const outro = runDef.buildOutroData!(s, CREDENTIALS as any)!; const text = outro.nextSteps!.items.join('\n'); // The project segment and the kind are what land the user on the source's @@ -100,7 +100,7 @@ describe('outro suggestion', () => { const s = sessionWith(many); const runDef = await resolveRun(s); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const outro = runDef.buildOutroData!(s, CREDENTIALS as any); + const outro = runDef.buildOutroData!(s, CREDENTIALS as any)!; const links = outro.nextSteps!.items.filter((i) => i.includes('new-source'), ); @@ -113,7 +113,7 @@ describe('outro suggestion', () => { const s = sessionWith([]); const runDef = await resolveRun(s); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const outro = runDef.buildOutroData!(s, CREDENTIALS as any); + const outro = runDef.buildOutroData!(s, CREDENTIALS as any)!; expect(outro.nextSteps).toBeUndefined(); }); @@ -123,7 +123,7 @@ describe('outro suggestion', () => { const s = sessionWith(sources); const runDef = await resolveRun(s); // eslint-disable-next-line @typescript-eslint/no-explicit-any - const outro = runDef.buildOutroData!(s, CREDENTIALS as any); + const outro = runDef.buildOutroData!(s, CREDENTIALS as any)!; expect(outro.message).toBe('Successfully installed PostHog!'); expect(outro.changes).toContain('Added PostHog provider'); diff --git a/src/ui/tui/components/visualizer/CrateStack.tsx b/src/ui/tui/components/visualizer/CrateStack.tsx index d002faa95..781502a4c 100644 --- a/src/ui/tui/components/visualizer/CrateStack.tsx +++ b/src/ui/tui/components/visualizer/CrateStack.tsx @@ -109,7 +109,7 @@ export const CrateStack = ({ width, height }: VisualProps) => { return ( ({ name, - status: - i === 1 ? McpClientStatus.Unchanged : McpClientStatus.Changed, + status: i === 1 ? McpClientStatus.Unchanged : McpClientStatus.Changed, })); }, async installPlugins(clientNames) { diff --git a/src/ui/tui/screens/MetricsIntroScreen.tsx b/src/ui/tui/screens/MetricsIntroScreen.tsx index 87875559a..cdda2be1e 100644 --- a/src/ui/tui/screens/MetricsIntroScreen.tsx +++ b/src/ui/tui/screens/MetricsIntroScreen.tsx @@ -23,7 +23,7 @@ export const MetricsIntroScreen = ({ store }: MetricsIntroScreenProps) => { // kubernetes, other), so there's no pre-seeded skillId here. Fall back to // the group id for the "more info" lookup. const skillId = session.skillId ?? 'metrics'; - const { skillEntry, fetchFailed } = useSkillEntry(skillId, session.localMcp); + const { skillEntry, fetchFailed } = useSkillEntry(skillId); const body = showingMoreInfo ? ( diff --git a/src/ui/tui/screens/RevenueIntroScreen.tsx b/src/ui/tui/screens/RevenueIntroScreen.tsx index bdfd0df32..3d791dd3b 100644 --- a/src/ui/tui/screens/RevenueIntroScreen.tsx +++ b/src/ui/tui/screens/RevenueIntroScreen.tsx @@ -205,11 +205,11 @@ const DetectErrorBody = ({ error }: { error: RevenueDetectError }) => { Revenue analytics requires: - {' \u2022'} A PostHog SDK ({POSTHOG_SDKS.slice(0, 3).join(', ')}, - …) + {' \u2022'} A PostHog SDK ( + {[...POSTHOG_SDKS].slice(0, 3).join(', ')}, …) - {' \u2022'} A Stripe SDK ({STRIPE_SDKS.join(', ')}) + {' \u2022'} A Stripe SDK ({[...STRIPE_SDKS].join(', ')}) @@ -247,7 +247,7 @@ const DetectErrorBody = ({ error }: { error: RevenueDetectError }) => { Install one of: - {STRIPE_SDKS.map((sdk) => ( + {[...STRIPE_SDKS].map((sdk) => ( {' \u2022'} {sdk} diff --git a/src/utils/__tests__/ci-region.test.ts b/src/utils/__tests__/ci-region.test.ts index 2f7cf9acd..d174852a5 100644 --- a/src/utils/__tests__/ci-region.test.ts +++ b/src/utils/__tests__/ci-region.test.ts @@ -55,6 +55,7 @@ describe('getOrAskForProjectData CI region', () => { it('uses the provided region and never probes @me for it', async () => { const result = await getOrAskForProjectData({ + signup: false, ci: true, apiKey: 'phx_test', projectId: 123, @@ -76,6 +77,7 @@ describe('getOrAskForProjectData CI region', () => { mockedDetect.mockResolvedValue('us'); await getOrAskForProjectData({ + signup: false, ci: true, apiKey: 'phx_test', projectId: 123, diff --git a/src/utils/bounded-fs.ts b/src/utils/bounded-fs.ts index 23c091a0f..35e6dd55c 100644 --- a/src/utils/bounded-fs.ts +++ b/src/utils/bounded-fs.ts @@ -143,7 +143,7 @@ export async function boundedGlob( ignore: [...PROJECT_IGNORE_GLOBS, ...(options.extraIgnore ?? [])], suppressErrors: true, followSymbolicLinks: false, - }); + }) as NodeJS.ReadableStream & { destroy(): void }; const matches: string[] = []; return new Promise((resolve) => { diff --git a/src/wizard.ts b/src/wizard.ts index 8671b8892..7149cc708 100644 --- a/src/wizard.ts +++ b/src/wizard.ts @@ -82,7 +82,7 @@ export class Wizard { private cli: Argv; private constructor() { - let cli = yargs(hideBin(process.argv)) + let cli: Argv = yargs(hideBin(process.argv)) .env('POSTHOG_WIZARD') .options(GLOBAL_OPTIONS); From 88571267ada132dcbdc08b891e2ab2531ca10ab7 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 18 Sep 2026 19:08:03 -0400 Subject: [PATCH 2/3] chore(test): split vitest into per-surface projects Projects: store, agent, tui, cli, harness, architecture, keyed by today's directories. New scripts test: and test:arch, a surfaces workflow that runs each project alone plus a production and CI bundle audit, aliases and empty shape files for the future src/{store,agent,tui,cli} surfaces, and a chunk manifest script for dist/. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .github/workflows/surfaces.yml | 86 +++++++++++++++++++++++++++++++ package.json | 7 +++ scripts/README.md | 1 + scripts/chunk-manifest.no-jest.ts | 40 ++++++++++++++ src/agent/index.ts | 2 + src/agent/types.ts | 2 + src/cli/index.ts | 2 + src/cli/types.ts | 2 + src/store/index.ts | 2 + src/store/types.ts | 2 + src/tui/index.ts | 2 + src/tui/types.ts | 2 + tsconfig.build.json | 61 ++++++++++++++++++---- vitest.config.ts | 56 ++++++++++++++++---- 14 files changed, 246 insertions(+), 21 deletions(-) create mode 100644 .github/workflows/surfaces.yml create mode 100644 scripts/chunk-manifest.no-jest.ts create mode 100644 src/agent/index.ts create mode 100644 src/agent/types.ts create mode 100644 src/cli/index.ts create mode 100644 src/cli/types.ts create mode 100644 src/store/index.ts create mode 100644 src/store/types.ts create mode 100644 src/tui/index.ts create mode 100644 src/tui/types.ts diff --git a/.github/workflows/surfaces.yml b/.github/workflows/surfaces.yml new file mode 100644 index 000000000..546b9c9ea --- /dev/null +++ b/.github/workflows/surfaces.yml @@ -0,0 +1,86 @@ +name: 'Surfaces' +on: + push: + branches: + - main + pull_request: + +jobs: + architecture: + name: Architecture + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 + with: + version: 10.34.5 + run_install: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: 'package.json' + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - run: node scripts/generate-version.cjs + - run: pnpm typecheck + - run: pnpm lint + - run: pnpm test:arch + + surface: + name: ${{ matrix.project }} + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + project: [store, agent, tui, cli, harness] + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 + with: + version: 10.34.5 + run_install: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: 'package.json' + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - run: node scripts/generate-version.cjs + - run: pnpm test:${{ matrix.project }} --coverage + - uses: codecov/codecov-action@13ce06bfc6bbe3ecf90edbbf1bc32fe5978ca1d3 # pin@v5.3.1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false + flags: ${{ matrix.project }} + + bundle: + name: Bundle audit + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: pnpm/action-setup@eae0cfeb286e66ffb5155f1a79b90583a127a68b # v2.4.1 + with: + version: 10.34.5 + run_install: false + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version-file: 'package.json' + cache: 'pnpm' + - run: pnpm install --frozen-lockfile + - name: Production build and smoke test + run: pnpm build + - name: Production chunk manifest + run: + npx tsx scripts/chunk-manifest.no-jest.ts dist > + chunk-manifest.prod.json + - name: CI build and smoke test + run: pnpm build:ci + - name: CI chunk manifest + run: + npx tsx scripts/chunk-manifest.no-jest.ts dist > + chunk-manifest.ci.json + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: chunk-manifests-${{ github.sha }} + path: chunk-manifest.*.json diff --git a/package.json b/package.json index 617175ae8..fb34428d9 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,13 @@ "fix:prettier": "prettier --write \"{lib,src,test}/**/*.ts\"", "fix:eslint": "eslint . --cache --format stylish --fix", "test": "pnpm build && vitest run", + "test:store": "vitest run --project store", + "test:agent": "vitest run --project agent", + "test:tui": "vitest run --project tui", + "test:cli": "vitest run --project cli", + "test:harness": "vitest run --project harness", + "test:arch": "vitest run --project architecture", + "test:goldens:update": "vitest run --project tui --project store -u", "test:coverage": "pnpm build && vitest run --coverage", "test:e2e": "pnpm build && ./e2e-tests/run.sh", "test:e2e-record": "export RECORD_FIXTURES=true && pnpm build && ./e2e-tests/run.sh", diff --git a/scripts/README.md b/scripts/README.md index a136ce8f5..725eda866 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -17,6 +17,7 @@ real ink render) and is driven purely by store state manipulation; a PTY parent | **`tui-host.no-jest.ts`** | Real TUI host: `MODE=fixed` follows a profile; `MODE=serve` accepts socket commands. | `APP_DIR`, `PROJECT_ID`, key for a full run, `SNAP_CTRL`; run under a PTY, with `CONTROL_SOCK` in serve mode | | **`tui-snapshots.no-jest.ts`** | Runs the fixed host and saves colored `SNAP_OUT/NN-.ans` frames, including within-screen progress. | `SNAP_OUT`, `APP_DIR`, `PROJECT_ID`, `POSTHOG_KEY_FILE` or `POSTHOG_PERSONAL_API_KEY` | | **`wizard-ci-mcp.no-jest.ts`** | Stdio MCP server: `open_app`, `read_state`, `perform_action`, `render_screen`, `run_agent`. Screen output is plain text. | Spawns the host; `open_app` requires `appDir` and `projectId`, with optional `keyFile`, `apiKey`, `region` | +| **`chunk-manifest.no-jest.ts`** | Prints a structural manifest of `dist/`: per chunk, the source files it contains and the chunks it imports, hash suffixes stripped. Baselines live in `scripts/__fixtures__/chunk-manifest.{prod,ci}.json`. | A built `dist/` | | **`wizard-ci-explore.no-jest.ts`** | `pnpm wizard-ci-explore`: opens an app, confirms setup, reads state, prints one frame, and exits. It does not run the agent. | `APP_DIR`, `PROJECT_ID`; optional `POSTHOG_KEY_FILE` | > You usually don't call these directly — `pnpm wizard-ci-snapshots` (in diff --git a/scripts/chunk-manifest.no-jest.ts b/scripts/chunk-manifest.no-jest.ts new file mode 100644 index 000000000..623c2e064 --- /dev/null +++ b/scripts/chunk-manifest.no-jest.ts @@ -0,0 +1,40 @@ +/** + * Structural manifest of the built bundle: for every chunk in dist/, the + * source files it contains (from its sourcemap) and the chunks it imports. + * Hash suffixes are stripped so the output is stable across builds. + * + * tsx scripts/chunk-manifest.no-jest.ts [distDir] > manifest.json + */ +import fs from 'fs'; +import path from 'path'; + +const dist = path.resolve(process.argv[2] ?? 'dist'); +const root = path.resolve(dist, '..'); + +const stripHash = (file: string): string => + file.replace(/-[A-Za-z0-9_-]{8}\.js$/, '.js'); + +const importRe = /(?:from\s*|import\s*\(\s*)["']\.\/([^"']+\.js)["']/g; + +const manifest: Record = {}; + +for (const file of fs + .readdirSync(dist) + .filter((f) => f.endsWith('.js')) + .sort()) { + const code = fs.readFileSync(path.join(dist, file), 'utf8'); + const mapPath = path.join(dist, `${file}.map`); + const sources = fs.existsSync(mapPath) + ? ( + JSON.parse(fs.readFileSync(mapPath, 'utf8')) as { sources: string[] } + ).sources + .map((s) => path.relative(root, path.resolve(dist, s))) + .filter((s) => !s.includes('node_modules')) + .sort() + : []; + const imports = new Set(); + for (const m of code.matchAll(importRe)) imports.add(stripHash(m[1])); + manifest[stripHash(file)] = { sources, imports: [...imports].sort() }; +} + +process.stdout.write(JSON.stringify(manifest, null, 2) + '\n'); diff --git a/src/agent/index.ts b/src/agent/index.ts new file mode 100644 index 000000000..00b26d0da --- /dev/null +++ b/src/agent/index.ts @@ -0,0 +1,2 @@ +/** Public runtime API of the agent surface. Populated by the surface split. */ +export {}; diff --git a/src/agent/types.ts b/src/agent/types.ts new file mode 100644 index 000000000..e2742b91f --- /dev/null +++ b/src/agent/types.ts @@ -0,0 +1,2 @@ +/** Shape of the agent surface. Populated by the surface split (P1 to P3). */ +export {}; diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 000000000..7a07c7eb7 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,2 @@ +/** Entry of the cli composition root. Populated by the surface split (P3). */ +export {}; diff --git a/src/cli/types.ts b/src/cli/types.ts new file mode 100644 index 000000000..f474d6138 --- /dev/null +++ b/src/cli/types.ts @@ -0,0 +1,2 @@ +/** Shape of the cli composition root. Populated by the surface split (P3). */ +export {}; diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 000000000..5a19657a8 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,2 @@ +/** Public runtime API of the store surface. Populated by the surface split. */ +export {}; diff --git a/src/store/types.ts b/src/store/types.ts new file mode 100644 index 000000000..b6e686418 --- /dev/null +++ b/src/store/types.ts @@ -0,0 +1,2 @@ +/** Shape of the store surface. Populated by the surface split (P1 to P3). */ +export {}; diff --git a/src/tui/index.ts b/src/tui/index.ts new file mode 100644 index 000000000..f0d66defc --- /dev/null +++ b/src/tui/index.ts @@ -0,0 +1,2 @@ +/** Public runtime API of the tui surface. Populated by the surface split. */ +export {}; diff --git a/src/tui/types.ts b/src/tui/types.ts new file mode 100644 index 000000000..e230010f6 --- /dev/null +++ b/src/tui/types.ts @@ -0,0 +1,2 @@ +/** Shape of the tui surface. Populated by the surface split (P1 to P3). */ +export {}; diff --git a/tsconfig.build.json b/tsconfig.build.json index 8618277c1..12900178b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -5,7 +5,9 @@ "noImplicitAny": true, "strictNullChecks": true, "noFallthroughCasesInSwitch": true, - "lib": ["ES2022"], + "lib": [ + "ES2022" + ], "module": "ESNext", "moduleResolution": "bundler", "target": "ES2022", @@ -18,15 +20,54 @@ "esModuleInterop": true, "jsx": "react-jsx", "paths": { - "@env": ["./src/env.ts"], - "@lib/*": ["./src/lib/*"], - "@e2e-harness/*": ["./e2e-harness/*"], - "@utils/*": ["./src/utils/*"], - "@ui": ["./src/ui/index.ts"], - "@ui/*": ["./src/ui/*"], - "@steps": ["./src/steps/index.ts"], - "@steps/*": ["./src/steps/*"], - "@frameworks/*": ["./src/frameworks/*"] + "@env": [ + "./src/env.ts" + ], + "@lib/*": [ + "./src/lib/*" + ], + "@e2e-harness/*": [ + "./e2e-harness/*" + ], + "@utils/*": [ + "./src/utils/*" + ], + "@ui": [ + "./src/ui/index.ts" + ], + "@ui/*": [ + "./src/ui/*" + ], + "@steps": [ + "./src/steps/index.ts" + ], + "@steps/*": [ + "./src/steps/*" + ], + "@frameworks/*": [ + "./src/frameworks/*" + ], + "@store": [ + "./src/store/index.ts" + ], + "@store/types": [ + "./src/store/types.ts" + ], + "@agent": [ + "./src/agent/index.ts" + ], + "@agent/types": [ + "./src/agent/types.ts" + ], + "@tui": [ + "./src/tui/index.ts" + ], + "@tui/types": [ + "./src/tui/types.ts" + ], + "@cli/*": [ + "./src/cli/*" + ] } } } diff --git a/vitest.config.ts b/vitest.config.ts index 817d2577d..ee593af0b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -29,6 +29,40 @@ function resolveTsForJs(): Plugin { }; } +// Per-surface Vitest projects keyed by today's directories. Each project runs +// alone with `vitest run --project `; `vitest run` runs them all. +const TESTS = '__tests__/**/*.{js,jsx,ts,tsx}'; +const AGENT_TESTS = [ + `src/lib/agent/**/${TESTS}`, + `src/lib/middleware/**/${TESTS}`, + 'src/lib/__tests__/agent-*.test.ts', + 'src/lib/__tests__/gateway-session.test.ts', + 'src/lib/__tests__/wizard-can-use-tool.test.ts', + 'src/lib/__tests__/yara-*.test.ts', +]; +const TUI_TESTS = [`src/ui/tui/**/${TESTS}`]; +const CLI_TESTS = [ + `src/commands/**/${TESTS}`, + `src/lib/runners/${TESTS}`, + 'src/__tests__/*cli*.test.ts', + 'src/__tests__/wizard.test.ts', + 'src/__tests__/headless-scope.test.ts', +]; +const HARNESS_TESTS = [`e2e-harness/${TESTS}`]; +const ARCH_TESTS = ['src/__tests__/architecture/**/*.{ts,tsx}']; +const EXCLUDE = [ + '**/node_modules/**', + '**/dist/**', + '**/e2e-tests/**', + '**/*.no-jest.*', + '**/*.d.ts', +]; + +const project = (name: string, include: string[], exclude: string[] = []) => ({ + extends: true as const, + test: { name, include, exclude: [...EXCLUDE, ...exclude] }, +}); + export default defineConfig({ plugins: [resolveTsForJs()], // The source targets the React 19 automatic JSX runtime (tsconfig @@ -63,17 +97,17 @@ export default defineConfig({ test: { globals: true, environment: 'node', - include: [ - '**/__tests__/**/*.{test,spec}.{js,jsx,ts,tsx}', - '**/__tests__/**/*.{js,jsx,ts,tsx}', - '**/*.{test,spec}.{js,jsx,ts,tsx}', - ], - exclude: [ - '**/node_modules/**', - '**/dist/**', - '**/e2e-tests/**', - '**/*.no-jest.*', - '**/*.d.ts', + projects: [ + project('agent', AGENT_TESTS), + project('tui', TUI_TESTS), + project('cli', CLI_TESTS), + project('harness', HARNESS_TESTS), + project('architecture', ARCH_TESTS), + project( + 'store', + [`src/**/${TESTS}`], + [...AGENT_TESTS, ...TUI_TESTS, ...CLI_TESTS, ...ARCH_TESTS], + ), ], coverage: { provider: 'v8', From 87cd9cdd21788075a1f23c4ea604db73d369a409 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Fri, 18 Sep 2026 19:08:03 -0400 Subject: [PATCH 3/3] chore(test): capture behavior baselines before the surface split Goldens recorded from the current tree: a frame per screen and overlay at two sizes through the real ScreenContainer, keyboard versus control action session diffs, store invariants, flow traces and screen analytics per program, post auth gate ids, the MCP control state sequence, and prod and CI chunk manifests. The import boundaries test seeds the 72 current violations and fails on new ones or stale entries. Generated-By: PostHog Desktop Task-Id: d14e92bb-6ee1-49b5-8502-39cb80079589 --- .../__fixtures__/control-frame-auth.txt | 49 + .../__fixtures__/control-frame-intro.txt | 49 + .../__fixtures__/control-state-baseline.json | 107 ++ scripts/__fixtures__/chunk-manifest.ci.json | 999 +++++++++++++++ scripts/__fixtures__/chunk-manifest.prod.json | 995 +++++++++++++++ .../architecture/import-boundaries.test.ts | 413 ++++++ .../architecture/known-violations.json | 76 ++ .../__snapshots__/flow-traces.test.ts.snap | 1104 +++++++++++++++++ .../post-auth-gates.test.ts.snap | 28 + .../programs/__tests__/flow-traces.test.ts | 231 ++++ .../__tests__/post-auth-gates.test.ts | 26 + .../frames/agent-skill-intro-120x40.txt | 39 + .../frames/agent-skill-intro-60x15.txt | 9 + .../frames/ai-observability-intro-120x40.txt | 39 + .../frames/ai-observability-intro-60x15.txt | 9 + .../__snapshots__/frames/ai-opt-in-120x40.txt | 39 + .../__snapshots__/frames/ai-opt-in-60x15.txt | 9 + .../frames/audit-intro-120x40.txt | 39 + .../frames/audit-intro-60x15.txt | 9 + .../frames/audit-outro-120x40.txt | 17 + .../frames/audit-outro-60x15.txt | 9 + .../__snapshots__/frames/audit-run-120x40.txt | 39 + .../__snapshots__/frames/audit-run-60x15.txt | 9 + .../__snapshots__/frames/auth-120x40.txt | 39 + .../__snapshots__/frames/auth-60x15.txt | 9 + .../frames/auth-error-120x40.txt | 18 + .../__snapshots__/frames/auth-error-60x15.txt | 9 + .../frames/doctor-intro-120x40.txt | 39 + .../frames/doctor-intro-60x15.txt | 9 + .../frames/doctor-report-120x40.txt | 16 + .../frames/doctor-report-60x15.txt | 9 + .../frames/error-tracking-detect-120x40.txt | 9 + .../frames/error-tracking-detect-60x15.txt | 9 + .../frames/error-tracking-intro-120x40.txt | 39 + .../frames/error-tracking-intro-60x15.txt | 9 + .../__snapshots__/frames/exit-120x40.txt | 3 + .../__snapshots__/frames/exit-60x15.txt | 9 + .../frames/health-check-120x40.txt | 39 + .../frames/health-check-60x15.txt | 9 + .../__snapshots__/frames/intro-120x40.txt | 39 + .../__snapshots__/frames/intro-60x15.txt | 9 + .../frames/keep-skills-120x40.txt | 7 + .../frames/keep-skills-60x15.txt | 9 + .../frames/managed-settings-120x40.txt | 39 + .../frames/managed-settings-60x15.txt | 9 + .../frames/manual-auth-code-120x40.txt | 11 + .../frames/manual-auth-code-60x15.txt | 9 + .../__snapshots__/frames/mcp-120x40.txt | 15 + .../__snapshots__/frames/mcp-60x15.txt | 9 + .../__snapshots__/frames/mcp-add-120x40.txt | 15 + .../__snapshots__/frames/mcp-add-60x15.txt | 9 + .../frames/mcp-remove-120x40.txt | 11 + .../__snapshots__/frames/mcp-remove-60x15.txt | 9 + .../frames/mcp-suggested-prompts-120x40.txt | 39 + .../frames/mcp-suggested-prompts-60x15.txt | 9 + .../frames/metrics-intro-120x40.txt | 39 + .../frames/metrics-intro-60x15.txt | 9 + .../frames/migration-intro-120x40.txt | 39 + .../frames/migration-intro-60x15.txt | 9 + .../frames/mint-failure-120x40.txt | 39 + .../frames/mint-failure-60x15.txt | 9 + .../__snapshots__/frames/outro-120x40.txt | 20 + .../__snapshots__/frames/outro-60x15.txt | 9 + .../frames/port-conflict-120x40.txt | 39 + .../frames/port-conflict-60x15.txt | 9 + .../frames/revenue-intro-120x40.txt | 39 + .../frames/revenue-intro-60x15.txt | 9 + .../__snapshots__/frames/run-120x40.txt | 39 + .../__snapshots__/frames/run-60x15.txt | 9 + .../frames/self-driving-github-120x40.txt | 39 + .../frames/self-driving-github-60x15.txt | 9 + .../frames/self-driving-handoff-120x40.txt | 39 + .../frames/self-driving-handoff-60x15.txt | 9 + .../self-driving-integration-check-120x40.txt | 39 + .../self-driving-integration-check-60x15.txt | 9 + ...self-driving-integration-detect-120x40.txt | 9 + .../self-driving-integration-detect-60x15.txt | 9 + .../frames/self-driving-intro-120x40.txt | 39 + .../frames/self-driving-intro-60x15.txt | 9 + .../frames/session-timeout-120x40.txt | 11 + .../frames/session-timeout-60x15.txt | 9 + .../frames/settings-override-120x40.txt | 39 + .../frames/settings-override-60x15.txt | 9 + .../__snapshots__/frames/setup-120x40.txt | 10 + .../__snapshots__/frames/setup-60x15.txt | 9 + .../frames/slack-connect-120x40.txt | 39 + .../frames/slack-connect-60x15.txt | 9 + .../frames/source-maps-detect-120x40.txt | 9 + .../frames/source-maps-detect-60x15.txt | 9 + .../frames/source-maps-intro-120x40.txt | 39 + .../frames/source-maps-intro-60x15.txt | 9 + .../frames/source-maps-outro-120x40.txt | 24 + .../frames/source-maps-outro-60x15.txt | 9 + .../frames/task-notice-120x40.txt | 39 + .../frames/task-notice-60x15.txt | 9 + .../frames/warehouse-intro-120x40.txt | 39 + .../frames/warehouse-intro-60x15.txt | 9 + .../frames/wizard-ask-120x40.txt | 39 + .../__snapshots__/frames/wizard-ask-60x15.txt | 9 + .../keyboard-equivalence.test.tsx.snap | 241 ++++ src/ui/tui/__tests__/frames.test.tsx | 587 +++++++++ .../helpers/render-screen.no-jest.tsx | 60 + .../__tests__/keyboard-equivalence.test.tsx | 457 +++++++ src/ui/tui/__tests__/store-invariants.test.ts | 873 +++++++++++++ 104 files changed, 7988 insertions(+) create mode 100644 e2e-harness/__fixtures__/control-frame-auth.txt create mode 100644 e2e-harness/__fixtures__/control-frame-intro.txt create mode 100644 e2e-harness/__fixtures__/control-state-baseline.json create mode 100644 scripts/__fixtures__/chunk-manifest.ci.json create mode 100644 scripts/__fixtures__/chunk-manifest.prod.json create mode 100644 src/__tests__/architecture/import-boundaries.test.ts create mode 100644 src/__tests__/architecture/known-violations.json create mode 100644 src/lib/programs/__tests__/__snapshots__/flow-traces.test.ts.snap create mode 100644 src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap create mode 100644 src/lib/programs/__tests__/flow-traces.test.ts create mode 100644 src/lib/programs/__tests__/post-auth-gates.test.ts create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/audit-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/audit-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/audit-outro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/audit-outro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/audit-run-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/audit-run-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/auth-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/auth-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/auth-error-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/auth-error-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/doctor-report-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/doctor-report-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/exit-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/exit-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/health-check-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/health-check-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/keep-skills-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/keep-skills-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/managed-settings-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/managed-settings-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-add-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-add-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/migration-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/migration-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mint-failure-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/mint-failure-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/outro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/outro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/port-conflict-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/port-conflict-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/run-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/run-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/session-timeout-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/session-timeout-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/settings-override-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/settings-override-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/setup-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/setup-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/slack-connect-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/slack-connect-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/task-notice-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/task-notice-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-120x40.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-60x15.txt create mode 100644 src/ui/tui/__tests__/__snapshots__/keyboard-equivalence.test.tsx.snap create mode 100644 src/ui/tui/__tests__/frames.test.tsx create mode 100644 src/ui/tui/__tests__/helpers/render-screen.no-jest.tsx create mode 100644 src/ui/tui/__tests__/keyboard-equivalence.test.tsx create mode 100644 src/ui/tui/__tests__/store-invariants.test.ts diff --git a/e2e-harness/__fixtures__/control-frame-auth.txt b/e2e-harness/__fixtures__/control-frame-auth.txt new file mode 100644 index 000000000..644e86286 --- /dev/null +++ b/e2e-harness/__fixtures__/control-frame-auth.txt @@ -0,0 +1,49 @@ + PostHog Wizard v2.76.0 Feedback: wizard@posthog.com + + PostHog Setup Wizard + ✔ Framework: Node.js + + Privacy & data + • Source files are read by Claude for AI context + • .env* and secrets stay on your machine + • Press [I] for full privacy & usage info + + ⠴ Waiting for authentication... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + I privacy & data diff --git a/e2e-harness/__fixtures__/control-frame-intro.txt b/e2e-harness/__fixtures__/control-frame-intro.txt new file mode 100644 index 000000000..493bdcb50 --- /dev/null +++ b/e2e-harness/__fixtures__/control-frame-intro.txt @@ -0,0 +1,49 @@ + PostHog Wizard v2.76.0 Feedback: wizard@posthog.com + + + + + + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + Review what data is shared in "Privacy & data." + .env* values stay on your machine. + + Let's do two hours of work in eight minutes. + + Directory ✔ /wz-baseline-express-todo + Framework ✔ Node.js (detected) + + ▸ Continue + Change framework + More info + Privacy & data + Cancel + + + + + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/e2e-harness/__fixtures__/control-state-baseline.json b/e2e-harness/__fixtures__/control-state-baseline.json new file mode 100644 index 000000000..9797a94c0 --- /dev/null +++ b/e2e-harness/__fixtures__/control-state-baseline.json @@ -0,0 +1,107 @@ +{ + "recordedFrom": "scripts/tui-host.no-jest.ts MODE=serve via scripts/wizard-ci-mcp.no-jest.ts, wizard 2.76.0", + "app": "wizard-workbench/apps/basic-integration/javascript-node/express-todo (throwaway copy, detection only, no key)", + "projectId": "228144", + "steps": [ + { + "call": "open_app", + "state": { + "currentScreen": "intro", + "hasOverlay": false, + "runPhase": "idle", + "session": { + "installDir": "", + "integration": "javascript_node", + "detectedFrameworkLabel": "Node.js", + "detectionComplete": true, + "setupConfirmed": false, + "integrate": null, + "hasCredentials": false, + "projectId": null, + "mcpComplete": false, + "slackStepDismissed": false, + "skillsComplete": false, + "outroDismissed": false, + "llmOptIn": false, + "discoveredFeatures": [] + }, + "tasks": [], + "statusMessages": [], + "eventPlan": [], + "pendingQuestion": null, + "taskNotice": null, + "setupQuestions": [], + "actions": [ + { "id": "confirm_setup", "description": "Confirm the intro and continue (sets setupConfirmed)." } + ], + "integration": "idle", + "integrationError": null + } + }, + { + "call": "perform_action", + "action": "confirm_setup", + "state": { + "currentScreen": "auth", + "hasOverlay": false, + "runPhase": "idle", + "session": { + "installDir": "", + "integration": "javascript_node", + "detectedFrameworkLabel": "Node.js", + "detectionComplete": true, + "setupConfirmed": true, + "integrate": null, + "hasCredentials": false, + "projectId": null, + "mcpComplete": false, + "slackStepDismissed": false, + "skillsComplete": false, + "outroDismissed": false, + "llmOptIn": false, + "discoveredFeatures": [] + }, + "tasks": [], + "statusMessages": [], + "eventPlan": [], + "pendingQuestion": null, + "taskNotice": null, + "setupQuestions": [], + "actions": [] + } + }, + { + "call": "read_state", + "state": { + "currentScreen": "auth", + "hasOverlay": false, + "runPhase": "idle", + "session": { + "installDir": "", + "integration": "javascript_node", + "detectedFrameworkLabel": "Node.js", + "detectionComplete": true, + "setupConfirmed": true, + "integrate": null, + "hasCredentials": false, + "projectId": null, + "mcpComplete": false, + "slackStepDismissed": false, + "skillsComplete": false, + "outroDismissed": false, + "llmOptIn": false, + "discoveredFeatures": [] + }, + "tasks": [], + "statusMessages": [], + "eventPlan": [], + "pendingQuestion": null, + "taskNotice": null, + "setupQuestions": [], + "actions": [], + "integration": "idle", + "integrationError": null + } + } + ] +} diff --git a/scripts/__fixtures__/chunk-manifest.ci.json b/scripts/__fixtures__/chunk-manifest.ci.json new file mode 100644 index 000000000..7ef40a63d --- /dev/null +++ b/scripts/__fixtures__/chunk-manifest.ci.json @@ -0,0 +1,999 @@ +{ + "add-mcp-server-to-clients.js": { + "sources": [ + "src/steps/add-mcp-server-to-clients/MCPClient.ts", + "src/steps/add-mcp-server-to-clients/clients/claude-code.ts", + "src/steps/add-mcp-server-to-clients/clients/claude-web.ts", + "src/steps/add-mcp-server-to-clients/clients/codex.ts", + "src/steps/add-mcp-server-to-clients/clients/cursor.ts", + "src/steps/add-mcp-server-to-clients/clients/opencode.ts", + "src/steps/add-mcp-server-to-clients/clients/visual-studio-code.ts", + "src/steps/add-mcp-server-to-clients/clients/zed.ts", + "src/steps/add-mcp-server-to-clients/index.ts", + "src/steps/add-mcp-server-to-clients/plugin-client.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "defaults.js", + "rolldown-runtime.js", + "telemetry.js" + ] + }, + "agent-interface.js": { + "sources": [ + "src/lib/agent/agent-env-isolation.ts", + "src/lib/agent/agent-interface.ts", + "src/lib/agent/agent-phase.ts", + "src/lib/agent/bash-fence.ts", + "src/lib/agent/claude-settings.ts", + "src/lib/agent/commandments.ts", + "src/lib/agent/output-signals.ts", + "src/lib/agent/runner/harness/pi/gateway.ts", + "src/lib/agent/runner/harness/pi/runtime-notes.ts", + "src/lib/agent/runner/switchboard/commandments.ts", + "src/lib/agent/stored-login.ts", + "src/lib/agent/triage-provider.ts", + "src/lib/auth-session-state.ts", + "src/lib/fetch-retry.ts", + "src/lib/safe-tools.ts", + "src/lib/secret-vault.ts", + "src/lib/wizard-ask-bridge.ts", + "src/lib/wizard-tools/handoff.ts", + "src/lib/wizard-tools/mcp.ts", + "src/lib/wizard-tools/tools.ts", + "src/utils/custom-headers.ts", + "src/utils/env-scan.ts" + ], + "imports": [ + "analytics.js", + "bounded-fs.js", + "debug.js", + "errors.js", + "gateway-session.js", + "queue-tools.js", + "rolldown-runtime.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "agent-prompt-loader.js": { + "sources": [ + "src/lib/agent/agent-prompt-loader.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "queue-tools.js" + ] + }, + "agent-runner.js": { + "sources": [ + "src/lib/agent/agent-prompt.ts", + "src/lib/agent/agent-runner.ts", + "src/lib/agent/runner/harness/anthropic/index.ts", + "src/lib/agent/runner/index.ts", + "src/lib/agent/runner/sequence/linear.ts", + "src/lib/agent/runner/sequence/orchestrator/executor.ts", + "src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts", + "src/lib/agent/runner/sequence/orchestrator/run-metrics.ts", + "src/lib/agent/runner/sequence/orchestrator/seeded-deps.ts", + "src/lib/agent/runner/shared/authenticate.ts", + "src/lib/agent/runner/shared/bootstrap.ts", + "src/lib/agent/runner/shared/errors.ts", + "src/lib/agent/runner/switchboard/flags/index.ts", + "src/lib/agent/runner/switchboard/flags/orchestrator.ts", + "src/lib/agent/runner/switchboard/flags/schemes.ts", + "src/lib/agent/runner/switchboard/flags/self-driving.ts", + "src/lib/agent/runner/switchboard/harness.ts", + "src/lib/agent/runner/switchboard/index.ts", + "src/lib/agent/runner/switchboard/sequence.ts", + "src/lib/agent/token-pricing.ts", + "src/lib/detection/agentic.ts", + "src/lib/detection/context.ts", + "src/lib/detection/features.ts", + "src/lib/detection/framework.ts", + "src/lib/middleware/benchmark.ts", + "src/lib/middleware/benchmarks/cache-tracker.ts", + "src/lib/middleware/benchmarks/compaction-tracker.ts", + "src/lib/middleware/benchmarks/context-size-tracker.ts", + "src/lib/middleware/benchmarks/cost-tracker.ts", + "src/lib/middleware/benchmarks/duration-tracker.ts", + "src/lib/middleware/benchmarks/index.ts", + "src/lib/middleware/benchmarks/json-writer.ts", + "src/lib/middleware/benchmarks/summary.ts", + "src/lib/middleware/benchmarks/token-tracker.ts", + "src/lib/middleware/benchmarks/turn-counter.ts", + "src/lib/middleware/config.ts", + "src/lib/middleware/phase-detector.ts", + "src/lib/middleware/pipeline.ts", + "src/lib/programs/audit/ledger-watcher.ts", + "src/lib/programs/posthog-integration/detect.ts", + "src/lib/programs/shared/package-scanning.ts", + "src/lib/programs/warehouse-source/detect.ts", + "src/lib/warehouse-sources/detect.ts", + "src/lib/warehouse-sources/registry.ts", + "src/utils/terminal-bell.ts" + ], + "imports": [ + "agent-interface.js", + "agent-prompt-loader.js", + "analytics.js", + "bounded-fs.js", + "debug.js", + "environment.js", + "errors.js", + "file-watcher.js", + "gateway-session.js", + "package-manager.js", + "pi.js", + "queue-tools.js", + "registry.js", + "rolldown-runtime.js", + "setup-utils.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "analytics.js": { + "sources": [ + "src/utils/analytics.ts", + "src/utils/ci-flag-overrides.ts" + ], + "imports": [ + "debug.js", + "wizard-session.js" + ] + }, + "api.js": { + "sources": [ + "src/lib/api.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "rolldown-runtime.js" + ] + }, + "bin.js": { + "sources": [ + "bin.ts", + "src/commands/ai-observability.ts", + "src/commands/audit.ts", + "src/commands/basic-integration/index.ts", + "src/commands/basic-integration/skill.ts", + "src/commands/cli/add.ts", + "src/commands/cli/index.ts", + "src/commands/command.ts", + "src/commands/doctor.ts", + "src/commands/error-tracking.ts", + "src/commands/factories/family-command-factory.ts", + "src/commands/factories/family-picker.tsx", + "src/commands/factories/native-command-factory.ts", + "src/commands/factories/shared.ts", + "src/commands/mcp-analytics.ts", + "src/commands/mcp/add.ts", + "src/commands/mcp/index.ts", + "src/commands/mcp/remove.ts", + "src/commands/mcp/tui-availability.ts", + "src/commands/mcp/tutorial.ts", + "src/commands/metrics.ts", + "src/commands/migrate.ts", + "src/commands/provision.ts", + "src/commands/replay-vision.ts", + "src/commands/revenue.ts", + "src/commands/self-driving.ts", + "src/commands/skill-program-options.ts", + "src/commands/skill.ts", + "src/commands/slack.ts", + "src/commands/upload-sourcemaps.ts", + "src/commands/warehouse.ts", + "src/lib/programs/dispatch-family.ts", + "src/lib/runners/resolve-no-telemetry.ts", + "src/lib/runners/run-non-interactive.ts", + "src/lib/runners/run-wizard-ci.ts", + "src/lib/runners/run-wizard-headless.ts", + "src/lib/runners/run-wizard.ts", + "src/ui/tui/hooks/keyboard-hints-utils.ts", + "src/ui/tui/hooks/useKeyBindings.ts", + "src/ui/tui/hooks/useKeyboardHints.tsx", + "src/ui/tui/hooks/useStdoutDimensions.ts", + "src/ui/tui/primitives/ConfirmButton.tsx", + "src/ui/tui/primitives/PickerMenu.tsx", + "src/ui/tui/primitives/PromptLabel.tsx", + "src/ui/tui/primitives/picker-filter.ts", + "src/wizard.ts" + ], + "imports": [ + "add-mcp-server-to-clients.js", + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "ci-install.js", + "debug.js", + "env-api-key.js", + "environment.js", + "errors.js", + "file.js", + "gateway-session.js", + "headless-ui.js", + "interactive.js", + "local-dev.js", + "mint-failure.js", + "non-interactive.js", + "playground.js", + "posthog.js", + "provisioning.js", + "setup-utils.js", + "start-tui.js", + "store.js", + "task-stream.js", + "telemetry.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "bounded-fs.js": { + "sources": [ + "src/utils/bounded-fs.ts" + ], + "imports": [ + "analytics.js", + "debug.js" + ] + }, + "ci-install.js": { + "sources": [ + "src/commands/basic-integration/ci-install.ts" + ], + "imports": [ + "bin.js", + "debug.js", + "errors.js", + "mint-failure.js", + "provisioning.js" + ] + }, + "debug.js": { + "sources": [ + "src/env.ts", + "src/lib/constants.ts", + "src/lib/headless-mode.ts", + "src/lib/health-checks/endpoints.ts", + "src/lib/health-checks/readiness.ts", + "src/lib/version.ts", + "src/ui/index.ts", + "src/ui/logging-ui.ts", + "src/ui/wizard-ui.ts", + "src/utils/debug.ts", + "src/utils/paths.ts" + ], + "imports": [ + "analytics.js", + "local-dev.js" + ] + }, + "defaults.js": { + "sources": [ + "src/steps/add-mcp-server-to-clients/defaults.ts", + "src/steps/add-mcp-server-to-clients/results.ts" + ], + "imports": [] + }, + "env-api-key.js": { + "sources": [ + "src/utils/env-api-key.ts" + ], + "imports": [ + "bounded-fs.js", + "rolldown-runtime.js" + ] + }, + "environment.js": { + "sources": [ + "src/utils/environment.ts" + ], + "imports": [ + "rolldown-runtime.js" + ] + }, + "errors.js": { + "sources": [ + "src/lib/agent/signals.ts", + "src/lib/errors/agent-map.ts", + "src/lib/errors/auth.ts", + "src/lib/errors/catalog.ts", + "src/lib/errors/codes.ts", + "src/lib/errors/detect-map.ts", + "src/lib/errors/emit.ts", + "src/lib/errors/run-failure.ts", + "src/lib/errors/sanitize.ts", + "src/lib/errors/skill-map.ts" + ], + "imports": [] + }, + "file-watcher.js": { + "sources": [ + "src/lib/file-watcher.ts" + ], + "imports": [ + "debug.js" + ] + }, + "file.js": { + "sources": [ + "src/lib/task-stream/destinations/file.ts" + ], + "imports": [ + "debug.js" + ] + }, + "gateway-session.js": { + "sources": [ + "src/lib/gateway-session.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "errors.js", + "wizard-abort.js" + ] + }, + "headless-ui.js": { + "sources": [ + "src/ui/headless-ui.ts" + ], + "imports": [ + "debug.js" + ] + }, + "interactive.js": { + "sources": [ + "src/commands/basic-integration/interactive.ts" + ], + "imports": [ + "bin.js", + "mint-failure.js" + ] + }, + "local-dev.js": { + "sources": [ + "src/lib/local-dev.ts" + ], + "imports": [] + }, + "mcp.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/mcp.ts" + ], + "imports": [ + "debug.js" + ] + }, + "mcp-prompt-streaming.js": { + "sources": [ + "src/lib/agent/mcp-prompt-streaming.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "gateway-session.js" + ] + }, + "mint-failure.js": { + "sources": [ + "src/lib/detection/project-scope.ts", + "src/lib/programs/agent-skill/content/index.tsx", + "src/lib/programs/agent-skill/index.ts", + "src/lib/programs/agent-skill/steps.ts", + "src/lib/programs/ai-observability/index.ts", + "src/lib/programs/audit/detect.ts", + "src/lib/programs/audit/index.ts", + "src/lib/programs/audit/seed.ts", + "src/lib/programs/error-tracking-upload-source-maps/content/index.tsx", + "src/lib/programs/error-tracking-upload-source-maps/detect.ts", + "src/lib/programs/error-tracking-upload-source-maps/index.ts", + "src/lib/programs/error-tracking-upload-source-maps/prompt.ts", + "src/lib/programs/error-tracking-upload-source-maps/steps.ts", + "src/lib/programs/error-tracking/content/index.tsx", + "src/lib/programs/error-tracking/content/tips.ts", + "src/lib/programs/error-tracking/detect-agentic.ts", + "src/lib/programs/error-tracking/index.ts", + "src/lib/programs/events-audit/index.ts", + "src/lib/programs/events-audit/seed.ts", + "src/lib/programs/events-audit/steps.ts", + "src/lib/programs/mcp-analytics/index.ts", + "src/lib/programs/mcp/index.ts", + "src/lib/programs/metrics/index.ts", + "src/lib/programs/migration/content/free-tier.tsx", + "src/lib/programs/migration/content/index.tsx", + "src/lib/programs/migration/content/pricing-structure.tsx", + "src/lib/programs/migration/content/vendor-stack.tsx", + "src/lib/programs/migration/index.ts", + "src/lib/programs/migration/steps.ts", + "src/lib/programs/posthog-doctor/fetch.ts", + "src/lib/programs/posthog-doctor/index.ts", + "src/lib/programs/posthog-doctor/kind-metadata.ts", + "src/lib/programs/posthog-doctor/steps.ts", + "src/lib/programs/posthog-doctor/types.ts", + "src/lib/programs/posthog-integration/content/data-flow.tsx", + "src/lib/programs/posthog-integration/content/funnel.tsx", + "src/lib/programs/posthog-integration/content/index.tsx", + "src/lib/programs/posthog-integration/content/line-chart.tsx", + "src/lib/programs/posthog-integration/content/product-suite.tsx", + "src/lib/programs/posthog-integration/handoff.ts", + "src/lib/programs/posthog-integration/index.ts", + "src/lib/programs/posthog-integration/steps.ts", + "src/lib/programs/program-registry.ts", + "src/lib/programs/replay-vision/index.ts", + "src/lib/programs/revenue-analytics/detect.ts", + "src/lib/programs/revenue-analytics/index.ts", + "src/lib/programs/revenue-analytics/steps.ts", + "src/lib/programs/self-driving/content/index.tsx", + "src/lib/programs/self-driving/content/pipeline-diagram.tsx", + "src/lib/programs/self-driving/content/pricing.ts", + "src/lib/programs/self-driving/content/tips.ts", + "src/lib/programs/self-driving/detect-agentic.ts", + "src/lib/programs/self-driving/detect.ts", + "src/lib/programs/self-driving/index.ts", + "src/lib/programs/self-driving/prompt.ts", + "src/lib/programs/self-driving/step-keys.ts", + "src/lib/programs/self-driving/steps.ts", + "src/lib/programs/shared/health-check-step.ts", + "src/lib/programs/shared/posthog-cli-preinstall.ts", + "src/lib/programs/slack/index.ts", + "src/lib/programs/warehouse-source/index.ts", + "src/lib/programs/warehouse-source/steps.ts", + "src/lib/programs/web-analytics-doctor/detect.ts", + "src/lib/programs/web-analytics-doctor/index.ts", + "src/lib/programs/web-analytics-doctor/steps.ts", + "src/steps/install-cli-steering/index.ts", + "src/ui/mint-failure.ts", + "src/ui/tui/components/StatusPeekTrigger.tsx", + "src/ui/tui/primitives/TextBlock.tsx", + "src/ui/tui/primitives/content-types.ts", + "src/ui/tui/primitives/layout-helpers.ts", + "src/ui/tui/primitives/text-helpers.ts", + "src/ui/tui/styles.ts" + ], + "imports": [ + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "debug.js", + "errors.js", + "package-json.js", + "provisioning.js", + "queue-tools.js", + "registry.js", + "setup-utils.js", + "steps.js", + "telemetry.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "non-interactive.js": { + "sources": [ + "src/commands/basic-integration/non-interactive.ts" + ], + "imports": [ + "debug.js", + "errors.js" + ] + }, + "orchestrator-tools.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/orchestrator-tools.ts" + ], + "imports": [ + "analytics.js", + "queue-tools.js" + ] + }, + "package-json.js": { + "sources": [ + "src/utils/package-json.ts" + ], + "imports": [ + "rolldown-runtime.js" + ] + }, + "package-manager.js": { + "sources": [ + "src/frameworks/python/utils.ts", + "src/lib/detection/package-manager.ts" + ], + "imports": [ + "setup-utils.js" + ] + }, + "pi.js": { + "sources": [ + "src/lib/agent/aio-capture.ts", + "src/lib/agent/runner/harness/pi/completion.ts", + "src/lib/agent/runner/harness/pi/index.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "errors.js", + "gateway-session.js", + "mcp.js", + "security.js", + "subagent.js", + "task.js", + "tasks.js", + "tools.js", + "yara-hooks.js" + ] + }, + "playground.js": { + "sources": [ + "src/commands/basic-integration/playground.ts", + "src/ui/tui/playground/PlaygroundApp.tsx", + "src/ui/tui/playground/demos/AiOptInDemo.tsx", + "src/ui/tui/playground/demos/AskModalDemo.tsx", + "src/ui/tui/playground/demos/AuditChecksDemo.tsx", + "src/ui/tui/playground/demos/DoctorReportDemo.tsx", + "src/ui/tui/playground/demos/EndScreensDemo.tsx", + "src/ui/tui/playground/demos/HealthCheckDemo.tsx", + "src/ui/tui/playground/demos/InputDemo.tsx", + "src/ui/tui/playground/demos/KeyboardHintsDemo.tsx", + "src/ui/tui/playground/demos/LayoutDemo.tsx", + "src/ui/tui/playground/demos/LearnDeckDemo.tsx", + "src/ui/tui/playground/demos/LogDemo.tsx", + "src/ui/tui/playground/demos/McpDemo.tsx", + "src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx", + "src/ui/tui/playground/demos/ModalDemo.tsx", + "src/ui/tui/playground/demos/ProgressDemo.tsx", + "src/ui/tui/playground/demos/RunScreenDemo.tsx", + "src/ui/tui/playground/demos/ViewportGuardDemo.tsx", + "src/ui/tui/playground/demos/WelcomeDemo.tsx", + "src/ui/tui/playground/start-playground.ts" + ], + "imports": [ + "bin.js", + "debug.js", + "mint-failure.js", + "setup-utils.js", + "store.js", + "terminal.js" + ] + }, + "posthog.js": { + "sources": [ + "src/lib/task-stream/destinations/posthog.ts" + ], + "imports": [ + "debug.js" + ] + }, + "provisioning.js": { + "sources": [ + "src/utils/provisioning.ts", + "src/utils/urls.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "rolldown-runtime.js" + ] + }, + "queue-tools.js": { + "sources": [ + "src/lib/agent/runner/sequence/orchestrator/queue-tools.ts", + "src/lib/agent/runner/sequence/orchestrator/queue.ts", + "src/lib/agent/runner/switchboard/models.ts", + "src/utils/atomic-ledger.ts" + ], + "imports": [ + "analytics.js", + "debug.js" + ] + }, + "registry.js": { + "sources": [ + "src/frameworks/android/android-wizard-agent.ts", + "src/frameworks/android/utils.ts", + "src/frameworks/angular/angular-wizard-agent.ts", + "src/frameworks/angular/utils.ts", + "src/frameworks/astro/astro-wizard-agent.ts", + "src/frameworks/astro/utils.ts", + "src/frameworks/django/django-wizard-agent.ts", + "src/frameworks/django/utils.ts", + "src/frameworks/elixir/elixir-wizard-agent.ts", + "src/frameworks/fastapi/fastapi-wizard-agent.ts", + "src/frameworks/fastapi/utils.ts", + "src/frameworks/flask/flask-wizard-agent.ts", + "src/frameworks/flask/utils.ts", + "src/frameworks/flutter/flutter-wizard-agent.ts", + "src/frameworks/go/go-wizard-agent.ts", + "src/frameworks/java/java-wizard-agent.ts", + "src/frameworks/javascript-node/javascript-node-wizard-agent.ts", + "src/frameworks/javascript-web/javascript-web-wizard-agent.ts", + "src/frameworks/javascript-web/utils.ts", + "src/frameworks/kmp/kmp-wizard-agent.ts", + "src/frameworks/laravel/laravel-wizard-agent.ts", + "src/frameworks/laravel/utils.ts", + "src/frameworks/nextjs/nextjs-wizard-agent.ts", + "src/frameworks/nextjs/utils.ts", + "src/frameworks/nuxt/nuxt-wizard-agent.ts", + "src/frameworks/python/python-wizard-agent.ts", + "src/frameworks/rails/rails-wizard-agent.ts", + "src/frameworks/rails/utils.ts", + "src/frameworks/react-native/react-native-wizard-agent.ts", + "src/frameworks/react-native/utils.ts", + "src/frameworks/react-router/react-router-wizard-agent.ts", + "src/frameworks/react-router/utils.ts", + "src/frameworks/ruby/ruby-wizard-agent.ts", + "src/frameworks/ruby/utils.ts", + "src/frameworks/rust/rust-wizard-agent.ts", + "src/frameworks/svelte/svelte-wizard-agent.ts", + "src/frameworks/swift/swift-wizard-agent.ts", + "src/frameworks/swift/utils.ts", + "src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts", + "src/frameworks/tanstack-router/utils.ts", + "src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts", + "src/frameworks/tanstack-start/utils.ts", + "src/frameworks/vue/vue-wizard-agent.ts", + "src/lib/framework-config.ts", + "src/lib/registry.ts" + ], + "imports": [ + "bounded-fs.js", + "debug.js", + "package-json.js", + "package-manager.js", + "rolldown-runtime.js", + "setup-utils.js" + ] + }, + "rolldown-runtime.js": { + "sources": [], + "imports": [] + }, + "security.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/security.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "yara-hooks.js" + ] + }, + "setup-utils.js": { + "sources": [ + "src/lib/host-resolution.ts", + "src/lib/oauth/program-scopes.ts", + "src/utils/oauth-errors.ts", + "src/utils/oauth.ts", + "src/utils/package-manager.ts", + "src/utils/project-resolution.ts", + "src/utils/semver.ts", + "src/utils/setup-utils.ts" + ], + "imports": [ + "analytics.js", + "api.js", + "bounded-fs.js", + "debug.js", + "local-dev.js", + "provisioning.js", + "rolldown-runtime.js", + "telemetry.js", + "wizard-abort.js" + ] + }, + "start-tui.js": { + "sources": [ + "src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts", + "src/lib/wizard-spellbook.ts", + "src/steps/add-mcp-server-to-clients/browser-client.ts", + "src/steps/add-mcp-server-to-clients/login-client.ts", + "src/ui/tui/App.tsx", + "src/ui/tui/components/PrivacyPanel.tsx", + "src/ui/tui/exit-line.ts", + "src/ui/tui/hooks/useGithubConnection.ts", + "src/ui/tui/ink-ui.ts", + "src/ui/tui/posthog-integration-intro.ts", + "src/ui/tui/screen-registry.tsx", + "src/ui/tui/screens/AgentSkillIntroScreen.tsx", + "src/ui/tui/screens/AiObservabilityIntroScreen.tsx", + "src/ui/tui/screens/AuthErrorScreen.tsx", + "src/ui/tui/screens/AuthScreen.tsx", + "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx", + "src/ui/tui/screens/ErrorTrackingIntroScreen.tsx", + "src/ui/tui/screens/ExitScreen.tsx", + "src/ui/tui/screens/IntroScreenLayout.tsx", + "src/ui/tui/screens/KeepSkillsScreen.tsx", + "src/ui/tui/screens/ManagedSettingsScreen.tsx", + "src/ui/tui/screens/ManualAuthCodeScreen.tsx", + "src/ui/tui/screens/MetricsIntroScreen.tsx", + "src/ui/tui/screens/MigrationIntroScreen.tsx", + "src/ui/tui/screens/MintFailureScreen.tsx", + "src/ui/tui/screens/PortConflictScreen.tsx", + "src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx", + "src/ui/tui/screens/RevenueIntroScreen.tsx", + "src/ui/tui/screens/RunScreen.tsx", + "src/ui/tui/screens/SelfDrivingGitHubScreen.tsx", + "src/ui/tui/screens/SelfDrivingHandoffScreen.tsx", + "src/ui/tui/screens/SelfDrivingIntegrationCheckScreen.tsx", + "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx", + "src/ui/tui/screens/SelfDrivingIntroScreen.tsx", + "src/ui/tui/screens/SessionTimeoutScreen.tsx", + "src/ui/tui/screens/SettingsOverrideScreen.tsx", + "src/ui/tui/screens/SetupScreen.tsx", + "src/ui/tui/screens/SourceMapsDetectScreen.tsx", + "src/ui/tui/screens/SourceMapsIntroScreen.tsx", + "src/ui/tui/screens/SourceMapsOutroScreen.tsx", + "src/ui/tui/screens/TaskNoticeScreen.tsx", + "src/ui/tui/screens/WarehouseIntroScreen.tsx", + "src/ui/tui/screens/WizardAskScreen.tsx", + "src/ui/tui/screens/audit/AuditAreaPane.tsx", + "src/ui/tui/screens/audit/AuditChecksOutroSection.tsx", + "src/ui/tui/screens/audit/AuditIntroScreen.tsx", + "src/ui/tui/screens/audit/AuditOutroScreen.tsx", + "src/ui/tui/screens/audit/AuditRunScreen.tsx", + "src/ui/tui/screens/audit/PendingChecksList.tsx", + "src/ui/tui/screens/audit/slides/events-audit/createDashboard.tsx", + "src/ui/tui/screens/audit/slides/events-audit/detectSdk.tsx", + "src/ui/tui/screens/audit/slides/events-audit/enrichSites.tsx", + "src/ui/tui/screens/audit/slides/events-audit/index.ts", + "src/ui/tui/screens/audit/slides/events-audit/queryVolume.tsx", + "src/ui/tui/screens/audit/slides/events-audit/scanSites.tsx", + "src/ui/tui/screens/audit/slides/events-audit/uploadNotebook.tsx", + "src/ui/tui/screens/audit/slides/events-audit/writeReport.tsx", + "src/ui/tui/screens/doctor/DoctorIntroScreen.tsx", + "src/ui/tui/screens/doctor/DoctorReportScreen.tsx", + "src/ui/tui/screens/health/HealthCheckScreen.tsx", + "src/ui/tui/services/coding-agent-launcher.ts", + "src/ui/tui/services/mcp-installer.ts", + "src/ui/tui/services/mcp-suggested-prompts-services.ts", + "src/ui/tui/start-tui.ts", + "src/utils/clipboard.ts" + ], + "imports": [ + "add-mcp-server-to-clients.js", + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "bin.js", + "bounded-fs.js", + "debug.js", + "defaults.js", + "errors.js", + "mcp-prompt-streaming.js", + "mint-failure.js", + "provisioning.js", + "registry.js", + "setup-utils.js", + "store.js", + "telemetry.js", + "terminal.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "steps.js": { + "sources": [ + "src/steps/upload-environment-variables/EnvironmentProvider.ts", + "src/steps/upload-environment-variables/index.ts", + "src/steps/upload-environment-variables/providers/vercel.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "telemetry.js" + ] + }, + "store.js": { + "sources": [ + "src/lib/programs/ai-opt-in-gate.ts", + "src/lib/programs/program-step.ts", + "src/ui/tui/router.ts", + "src/ui/tui/screen-sequences.ts", + "src/ui/tui/store.ts" + ], + "imports": [ + "agent-runner.js", + "analytics.js", + "debug.js", + "mint-failure.js", + "rolldown-runtime.js", + "wizard-session.js" + ] + }, + "subagent.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/subagent.ts" + ], + "imports": [ + "debug.js" + ] + }, + "task.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/task.ts" + ], + "imports": [ + "agent-interface.js", + "agent-prompt-loader.js", + "analytics.js", + "debug.js", + "errors.js", + "gateway-session.js", + "mcp.js", + "orchestrator-tools.js", + "pi.js", + "queue-tools.js", + "security.js", + "tools.js", + "yara-hooks.js" + ] + }, + "task-stream.js": { + "sources": [ + "src/lib/task-stream/audit-areas.ts", + "src/lib/task-stream/event-plan-watcher.ts", + "src/lib/task-stream/task-stream-push.ts" + ], + "imports": [ + "debug.js", + "errors.js", + "file-watcher.js", + "file.js", + "posthog.js" + ] + }, + "tasks.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/tasks.ts" + ], + "imports": [ + "debug.js" + ] + }, + "telemetry.js": { + "sources": [ + "src/telemetry.ts", + "src/utils/links.ts" + ], + "imports": [ + "analytics.js" + ] + }, + "terminal.js": { + "sources": [ + "src/lib/mcp-project-profile.ts", + "src/lib/mcp-role-prompts.copy.json", + "src/lib/mcp-role-prompts.ts", + "src/lib/mcp-seed-events.ts", + "src/ui/tui/components/LearnCard.tsx", + "src/ui/tui/components/PhaseVisuals.tsx", + "src/ui/tui/components/ServiceHealthList.tsx", + "src/ui/tui/components/TipsCard.tsx", + "src/ui/tui/components/TitleBar.tsx", + "src/ui/tui/components/TokenCostHud.tsx", + "src/ui/tui/components/visualizer/CrateStack.tsx", + "src/ui/tui/components/visualizer/DashboardGrid.tsx", + "src/ui/tui/components/visualizer/DiffCascade.tsx", + "src/ui/tui/components/visualizer/LibraryShelf.tsx", + "src/ui/tui/components/visualizer/MatrixRain.tsx", + "src/ui/tui/components/visualizer/Tumblers.tsx", + "src/ui/tui/components/visualizer/grid.ts", + "src/ui/tui/components/visualizer/palette.ts", + "src/ui/tui/components/visualizer/panel.tsx", + "src/ui/tui/hooks/useDismissOnAnyKey.ts", + "src/ui/tui/hooks/useTick.ts", + "src/ui/tui/primitives/CardLayout.tsx", + "src/ui/tui/primitives/ConfirmationInput.tsx", + "src/ui/tui/primitives/ContentSequencer.tsx", + "src/ui/tui/primitives/DissolveTransition.tsx", + "src/ui/tui/primitives/Divider.tsx", + "src/ui/tui/primitives/EventPlanViewer.tsx", + "src/ui/tui/primitives/GroupedPickerMenu.tsx", + "src/ui/tui/primitives/HNViewer.tsx", + "src/ui/tui/primitives/KeyboardHintsBar.tsx", + "src/ui/tui/primitives/LinesBlock.tsx", + "src/ui/tui/primitives/LinkText.tsx", + "src/ui/tui/primitives/LoadingBox.tsx", + "src/ui/tui/primitives/LogViewer.tsx", + "src/ui/tui/primitives/ModalOverlay.tsx", + "src/ui/tui/primitives/NodeBlock.tsx", + "src/ui/tui/primitives/ProgressList.tsx", + "src/ui/tui/primitives/ScreenContainer.tsx", + "src/ui/tui/primitives/ScreenErrorBoundary.tsx", + "src/ui/tui/primitives/SplitView.tsx", + "src/ui/tui/primitives/TabContainer.tsx", + "src/ui/tui/primitives/ViewportTooSmall.tsx", + "src/ui/tui/primitives/link-helpers.ts", + "src/ui/tui/screens/AiOptInRequiredScreen.tsx", + "src/ui/tui/screens/McpScreen.tsx", + "src/ui/tui/screens/McpSuggestedPromptsScreen.tsx", + "src/ui/tui/screens/OutroScreen.tsx", + "src/ui/tui/screens/SkillSourceInfo.tsx", + "src/ui/tui/screens/SlackConnectScreen.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/AreaHeaderRow.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/Legend.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/layout.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/sort.ts", + "src/ui/tui/screens/audit/slides/eventCapture.tsx", + "src/ui/tui/screens/audit/slides/identification.tsx", + "src/ui/tui/screens/audit/slides/index.ts", + "src/ui/tui/screens/audit/slides/installation.tsx", + "src/ui/tui/screens/audit/slides/liveData.tsx", + "src/ui/tui/screens/audit/slides/shared.tsx", + "src/ui/tui/screens/audit/slides/uploadNotebook.tsx", + "src/ui/tui/screens/audit/slides/writeReport.tsx", + "src/ui/tui/screens/doctor/IssueTable.tsx", + "src/ui/tui/terminal.ts" + ], + "imports": [ + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "bin.js", + "debug.js", + "defaults.js", + "mint-failure.js", + "setup-utils.js", + "store.js", + "telemetry.js", + "yara-hooks.js" + ] + }, + "tools.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/tools.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "package-manager.js", + "pi.js", + "queue-tools.js", + "yara-hooks.js" + ] + }, + "wizard-abort.js": { + "sources": [], + "imports": [ + "wizard-abort.js" + ] + }, + "wizard-session.js": { + "sources": [ + "src/lib/wizard-session.ts" + ], + "imports": [ + "local-dev.js" + ] + }, + "yara-hooks.js": { + "sources": [], + "imports": [ + "yara-hooks.js" + ] + } +} diff --git a/scripts/__fixtures__/chunk-manifest.prod.json b/scripts/__fixtures__/chunk-manifest.prod.json new file mode 100644 index 000000000..d93825d0a --- /dev/null +++ b/scripts/__fixtures__/chunk-manifest.prod.json @@ -0,0 +1,995 @@ +{ + "add-mcp-server-to-clients.js": { + "sources": [ + "src/steps/add-mcp-server-to-clients/MCPClient.ts", + "src/steps/add-mcp-server-to-clients/clients/claude-code.ts", + "src/steps/add-mcp-server-to-clients/clients/claude-web.ts", + "src/steps/add-mcp-server-to-clients/clients/codex.ts", + "src/steps/add-mcp-server-to-clients/clients/cursor.ts", + "src/steps/add-mcp-server-to-clients/clients/opencode.ts", + "src/steps/add-mcp-server-to-clients/clients/visual-studio-code.ts", + "src/steps/add-mcp-server-to-clients/clients/zed.ts", + "src/steps/add-mcp-server-to-clients/index.ts", + "src/steps/add-mcp-server-to-clients/plugin-client.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "defaults.js", + "rolldown-runtime.js", + "telemetry.js" + ] + }, + "agent-interface.js": { + "sources": [ + "src/lib/agent/agent-env-isolation.ts", + "src/lib/agent/agent-interface.ts", + "src/lib/agent/agent-phase.ts", + "src/lib/agent/bash-fence.ts", + "src/lib/agent/claude-settings.ts", + "src/lib/agent/commandments.ts", + "src/lib/agent/output-signals.ts", + "src/lib/agent/runner/harness/pi/gateway.ts", + "src/lib/agent/runner/harness/pi/runtime-notes.ts", + "src/lib/agent/runner/switchboard/commandments.ts", + "src/lib/agent/stored-login.ts", + "src/lib/agent/triage-provider.ts", + "src/lib/auth-session-state.ts", + "src/lib/fetch-retry.ts", + "src/lib/safe-tools.ts", + "src/lib/secret-vault.ts", + "src/lib/wizard-ask-bridge.ts", + "src/lib/wizard-tools/handoff.ts", + "src/lib/wizard-tools/mcp.ts", + "src/lib/wizard-tools/tools.ts", + "src/utils/custom-headers.ts", + "src/utils/env-scan.ts" + ], + "imports": [ + "analytics.js", + "bounded-fs.js", + "debug.js", + "errors.js", + "gateway-session.js", + "queue-tools.js", + "rolldown-runtime.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "agent-prompt-loader.js": { + "sources": [ + "src/lib/agent/agent-prompt-loader.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "queue-tools.js" + ] + }, + "agent-runner.js": { + "sources": [ + "src/lib/agent/agent-prompt.ts", + "src/lib/agent/agent-runner.ts", + "src/lib/agent/runner/harness/anthropic/index.ts", + "src/lib/agent/runner/index.ts", + "src/lib/agent/runner/sequence/linear.ts", + "src/lib/agent/runner/sequence/orchestrator/executor.ts", + "src/lib/agent/runner/sequence/orchestrator/orchestrator-runner.ts", + "src/lib/agent/runner/sequence/orchestrator/run-metrics.ts", + "src/lib/agent/runner/sequence/orchestrator/seeded-deps.ts", + "src/lib/agent/runner/shared/authenticate.ts", + "src/lib/agent/runner/shared/bootstrap.ts", + "src/lib/agent/runner/shared/errors.ts", + "src/lib/agent/runner/switchboard/flags/index.ts", + "src/lib/agent/runner/switchboard/flags/orchestrator.ts", + "src/lib/agent/runner/switchboard/flags/schemes.ts", + "src/lib/agent/runner/switchboard/flags/self-driving.ts", + "src/lib/agent/runner/switchboard/harness.ts", + "src/lib/agent/runner/switchboard/index.ts", + "src/lib/agent/runner/switchboard/sequence.ts", + "src/lib/agent/token-pricing.ts", + "src/lib/detection/agentic.ts", + "src/lib/detection/context.ts", + "src/lib/detection/features.ts", + "src/lib/detection/framework.ts", + "src/lib/middleware/benchmark.ts", + "src/lib/middleware/benchmarks/cache-tracker.ts", + "src/lib/middleware/benchmarks/compaction-tracker.ts", + "src/lib/middleware/benchmarks/context-size-tracker.ts", + "src/lib/middleware/benchmarks/cost-tracker.ts", + "src/lib/middleware/benchmarks/duration-tracker.ts", + "src/lib/middleware/benchmarks/index.ts", + "src/lib/middleware/benchmarks/json-writer.ts", + "src/lib/middleware/benchmarks/summary.ts", + "src/lib/middleware/benchmarks/token-tracker.ts", + "src/lib/middleware/benchmarks/turn-counter.ts", + "src/lib/middleware/config.ts", + "src/lib/middleware/phase-detector.ts", + "src/lib/middleware/pipeline.ts", + "src/lib/programs/audit/ledger-watcher.ts", + "src/lib/programs/posthog-integration/detect.ts", + "src/lib/programs/shared/package-scanning.ts", + "src/lib/programs/warehouse-source/detect.ts", + "src/lib/warehouse-sources/detect.ts", + "src/lib/warehouse-sources/registry.ts", + "src/utils/terminal-bell.ts" + ], + "imports": [ + "agent-interface.js", + "agent-prompt-loader.js", + "analytics.js", + "bounded-fs.js", + "debug.js", + "environment.js", + "errors.js", + "file-watcher.js", + "gateway-session.js", + "package-manager.js", + "pi.js", + "queue-tools.js", + "registry.js", + "rolldown-runtime.js", + "setup-utils.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "analytics.js": { + "sources": [], + "imports": [ + "analytics.js" + ] + }, + "api.js": { + "sources": [ + "src/lib/api.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "rolldown-runtime.js" + ] + }, + "bin.js": { + "sources": [ + "bin.ts", + "src/commands/ai-observability.ts", + "src/commands/audit.ts", + "src/commands/basic-integration/index.ts", + "src/commands/basic-integration/skill.ts", + "src/commands/cli/add.ts", + "src/commands/cli/index.ts", + "src/commands/command.ts", + "src/commands/doctor.ts", + "src/commands/error-tracking.ts", + "src/commands/factories/family-command-factory.ts", + "src/commands/factories/family-picker.tsx", + "src/commands/factories/native-command-factory.ts", + "src/commands/factories/shared.ts", + "src/commands/mcp-analytics.ts", + "src/commands/mcp/add.ts", + "src/commands/mcp/index.ts", + "src/commands/mcp/remove.ts", + "src/commands/mcp/tui-availability.ts", + "src/commands/mcp/tutorial.ts", + "src/commands/metrics.ts", + "src/commands/migrate.ts", + "src/commands/provision.ts", + "src/commands/replay-vision.ts", + "src/commands/revenue.ts", + "src/commands/self-driving.ts", + "src/commands/skill-program-options.ts", + "src/commands/skill.ts", + "src/commands/slack.ts", + "src/commands/upload-sourcemaps.ts", + "src/commands/warehouse.ts", + "src/lib/programs/dispatch-family.ts", + "src/lib/runners/resolve-no-telemetry.ts", + "src/lib/runners/run-non-interactive.ts", + "src/lib/runners/run-wizard-ci.ts", + "src/lib/runners/run-wizard-headless.ts", + "src/lib/runners/run-wizard.ts", + "src/ui/tui/hooks/keyboard-hints-utils.ts", + "src/ui/tui/hooks/useKeyBindings.ts", + "src/ui/tui/hooks/useKeyboardHints.tsx", + "src/ui/tui/hooks/useStdoutDimensions.ts", + "src/ui/tui/primitives/ConfirmButton.tsx", + "src/ui/tui/primitives/PickerMenu.tsx", + "src/ui/tui/primitives/PromptLabel.tsx", + "src/ui/tui/primitives/picker-filter.ts", + "src/wizard.ts" + ], + "imports": [ + "add-mcp-server-to-clients.js", + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "ci-install.js", + "debug.js", + "env-api-key.js", + "environment.js", + "errors.js", + "file.js", + "gateway-session.js", + "headless-ui.js", + "interactive.js", + "local-dev.js", + "mint-failure.js", + "non-interactive.js", + "playground.js", + "posthog.js", + "provisioning.js", + "setup-utils.js", + "start-tui.js", + "store.js", + "task-stream.js", + "telemetry.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "bounded-fs.js": { + "sources": [ + "src/utils/bounded-fs.ts" + ], + "imports": [ + "analytics.js", + "debug.js" + ] + }, + "ci-install.js": { + "sources": [ + "src/commands/basic-integration/ci-install.ts" + ], + "imports": [ + "bin.js", + "debug.js", + "errors.js", + "mint-failure.js", + "provisioning.js" + ] + }, + "debug.js": { + "sources": [ + "src/env.ts", + "src/lib/constants.ts", + "src/lib/headless-mode.ts", + "src/lib/health-checks/endpoints.ts", + "src/lib/health-checks/readiness.ts", + "src/lib/version.ts", + "src/ui/index.ts", + "src/ui/logging-ui.ts", + "src/ui/wizard-ui.ts", + "src/utils/debug.ts", + "src/utils/paths.ts" + ], + "imports": [ + "analytics.js", + "local-dev.js" + ] + }, + "defaults.js": { + "sources": [ + "src/steps/add-mcp-server-to-clients/defaults.ts", + "src/steps/add-mcp-server-to-clients/results.ts" + ], + "imports": [] + }, + "env-api-key.js": { + "sources": [ + "src/utils/env-api-key.ts" + ], + "imports": [ + "bounded-fs.js", + "rolldown-runtime.js" + ] + }, + "environment.js": { + "sources": [ + "src/utils/environment.ts" + ], + "imports": [ + "rolldown-runtime.js" + ] + }, + "errors.js": { + "sources": [ + "src/lib/agent/signals.ts", + "src/lib/errors/agent-map.ts", + "src/lib/errors/auth.ts", + "src/lib/errors/catalog.ts", + "src/lib/errors/codes.ts", + "src/lib/errors/detect-map.ts", + "src/lib/errors/emit.ts", + "src/lib/errors/run-failure.ts", + "src/lib/errors/sanitize.ts", + "src/lib/errors/skill-map.ts" + ], + "imports": [] + }, + "file.js": { + "sources": [ + "src/lib/task-stream/destinations/file.ts" + ], + "imports": [ + "debug.js" + ] + }, + "file-watcher.js": { + "sources": [ + "src/lib/file-watcher.ts" + ], + "imports": [ + "debug.js" + ] + }, + "gateway-session.js": { + "sources": [ + "src/lib/gateway-session.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "errors.js", + "wizard-abort.js" + ] + }, + "headless-ui.js": { + "sources": [ + "src/ui/headless-ui.ts" + ], + "imports": [ + "debug.js" + ] + }, + "interactive.js": { + "sources": [ + "src/commands/basic-integration/interactive.ts" + ], + "imports": [ + "bin.js", + "mint-failure.js" + ] + }, + "local-dev.js": { + "sources": [ + "src/lib/local-dev.ts" + ], + "imports": [] + }, + "mcp.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/mcp.ts" + ], + "imports": [ + "debug.js" + ] + }, + "mcp-prompt-streaming.js": { + "sources": [ + "src/lib/agent/mcp-prompt-streaming.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "gateway-session.js" + ] + }, + "mint-failure.js": { + "sources": [ + "src/lib/detection/project-scope.ts", + "src/lib/programs/agent-skill/content/index.tsx", + "src/lib/programs/agent-skill/index.ts", + "src/lib/programs/agent-skill/steps.ts", + "src/lib/programs/ai-observability/index.ts", + "src/lib/programs/audit/detect.ts", + "src/lib/programs/audit/index.ts", + "src/lib/programs/audit/seed.ts", + "src/lib/programs/error-tracking-upload-source-maps/content/index.tsx", + "src/lib/programs/error-tracking-upload-source-maps/detect.ts", + "src/lib/programs/error-tracking-upload-source-maps/index.ts", + "src/lib/programs/error-tracking-upload-source-maps/prompt.ts", + "src/lib/programs/error-tracking-upload-source-maps/steps.ts", + "src/lib/programs/error-tracking/content/index.tsx", + "src/lib/programs/error-tracking/content/tips.ts", + "src/lib/programs/error-tracking/detect-agentic.ts", + "src/lib/programs/error-tracking/index.ts", + "src/lib/programs/events-audit/index.ts", + "src/lib/programs/events-audit/seed.ts", + "src/lib/programs/events-audit/steps.ts", + "src/lib/programs/mcp-analytics/index.ts", + "src/lib/programs/mcp/index.ts", + "src/lib/programs/metrics/index.ts", + "src/lib/programs/migration/content/free-tier.tsx", + "src/lib/programs/migration/content/index.tsx", + "src/lib/programs/migration/content/pricing-structure.tsx", + "src/lib/programs/migration/content/vendor-stack.tsx", + "src/lib/programs/migration/index.ts", + "src/lib/programs/migration/steps.ts", + "src/lib/programs/posthog-doctor/fetch.ts", + "src/lib/programs/posthog-doctor/index.ts", + "src/lib/programs/posthog-doctor/kind-metadata.ts", + "src/lib/programs/posthog-doctor/steps.ts", + "src/lib/programs/posthog-doctor/types.ts", + "src/lib/programs/posthog-integration/content/data-flow.tsx", + "src/lib/programs/posthog-integration/content/funnel.tsx", + "src/lib/programs/posthog-integration/content/index.tsx", + "src/lib/programs/posthog-integration/content/line-chart.tsx", + "src/lib/programs/posthog-integration/content/product-suite.tsx", + "src/lib/programs/posthog-integration/handoff.ts", + "src/lib/programs/posthog-integration/index.ts", + "src/lib/programs/posthog-integration/steps.ts", + "src/lib/programs/program-registry.ts", + "src/lib/programs/replay-vision/index.ts", + "src/lib/programs/revenue-analytics/detect.ts", + "src/lib/programs/revenue-analytics/index.ts", + "src/lib/programs/revenue-analytics/steps.ts", + "src/lib/programs/self-driving/content/index.tsx", + "src/lib/programs/self-driving/content/pipeline-diagram.tsx", + "src/lib/programs/self-driving/content/pricing.ts", + "src/lib/programs/self-driving/content/tips.ts", + "src/lib/programs/self-driving/detect-agentic.ts", + "src/lib/programs/self-driving/detect.ts", + "src/lib/programs/self-driving/index.ts", + "src/lib/programs/self-driving/prompt.ts", + "src/lib/programs/self-driving/step-keys.ts", + "src/lib/programs/self-driving/steps.ts", + "src/lib/programs/shared/health-check-step.ts", + "src/lib/programs/shared/posthog-cli-preinstall.ts", + "src/lib/programs/slack/index.ts", + "src/lib/programs/warehouse-source/index.ts", + "src/lib/programs/warehouse-source/steps.ts", + "src/lib/programs/web-analytics-doctor/detect.ts", + "src/lib/programs/web-analytics-doctor/index.ts", + "src/lib/programs/web-analytics-doctor/steps.ts", + "src/steps/install-cli-steering/index.ts", + "src/ui/mint-failure.ts", + "src/ui/tui/components/StatusPeekTrigger.tsx", + "src/ui/tui/primitives/TextBlock.tsx", + "src/ui/tui/primitives/content-types.ts", + "src/ui/tui/primitives/layout-helpers.ts", + "src/ui/tui/primitives/text-helpers.ts", + "src/ui/tui/styles.ts" + ], + "imports": [ + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "debug.js", + "errors.js", + "package-json.js", + "provisioning.js", + "queue-tools.js", + "registry.js", + "setup-utils.js", + "steps.js", + "telemetry.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "non-interactive.js": { + "sources": [ + "src/commands/basic-integration/non-interactive.ts" + ], + "imports": [ + "debug.js", + "errors.js" + ] + }, + "orchestrator-tools.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/orchestrator-tools.ts" + ], + "imports": [ + "analytics.js", + "queue-tools.js" + ] + }, + "package-json.js": { + "sources": [ + "src/utils/package-json.ts" + ], + "imports": [ + "rolldown-runtime.js" + ] + }, + "package-manager.js": { + "sources": [ + "src/frameworks/python/utils.ts", + "src/lib/detection/package-manager.ts" + ], + "imports": [ + "setup-utils.js" + ] + }, + "pi.js": { + "sources": [ + "src/lib/agent/aio-capture.ts", + "src/lib/agent/runner/harness/pi/completion.ts", + "src/lib/agent/runner/harness/pi/index.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "errors.js", + "gateway-session.js", + "mcp.js", + "security.js", + "subagent.js", + "task.js", + "tasks.js", + "tools.js", + "yara-hooks.js" + ] + }, + "playground.js": { + "sources": [ + "src/commands/basic-integration/playground.ts", + "src/ui/tui/playground/PlaygroundApp.tsx", + "src/ui/tui/playground/demos/AiOptInDemo.tsx", + "src/ui/tui/playground/demos/AskModalDemo.tsx", + "src/ui/tui/playground/demos/AuditChecksDemo.tsx", + "src/ui/tui/playground/demos/DoctorReportDemo.tsx", + "src/ui/tui/playground/demos/EndScreensDemo.tsx", + "src/ui/tui/playground/demos/HealthCheckDemo.tsx", + "src/ui/tui/playground/demos/InputDemo.tsx", + "src/ui/tui/playground/demos/KeyboardHintsDemo.tsx", + "src/ui/tui/playground/demos/LayoutDemo.tsx", + "src/ui/tui/playground/demos/LearnDeckDemo.tsx", + "src/ui/tui/playground/demos/LogDemo.tsx", + "src/ui/tui/playground/demos/McpDemo.tsx", + "src/ui/tui/playground/demos/McpSuggestedPromptsDemo.tsx", + "src/ui/tui/playground/demos/ModalDemo.tsx", + "src/ui/tui/playground/demos/ProgressDemo.tsx", + "src/ui/tui/playground/demos/RunScreenDemo.tsx", + "src/ui/tui/playground/demos/ViewportGuardDemo.tsx", + "src/ui/tui/playground/demos/WelcomeDemo.tsx", + "src/ui/tui/playground/start-playground.ts" + ], + "imports": [ + "bin.js", + "debug.js", + "mint-failure.js", + "setup-utils.js", + "store.js", + "terminal.js" + ] + }, + "posthog.js": { + "sources": [ + "src/lib/task-stream/destinations/posthog.ts" + ], + "imports": [ + "debug.js" + ] + }, + "provisioning.js": { + "sources": [ + "src/utils/provisioning.ts", + "src/utils/urls.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "rolldown-runtime.js" + ] + }, + "queue-tools.js": { + "sources": [ + "src/lib/agent/runner/sequence/orchestrator/queue-tools.ts", + "src/lib/agent/runner/sequence/orchestrator/queue.ts", + "src/lib/agent/runner/switchboard/models.ts", + "src/utils/atomic-ledger.ts" + ], + "imports": [ + "analytics.js", + "debug.js" + ] + }, + "registry.js": { + "sources": [ + "src/frameworks/android/android-wizard-agent.ts", + "src/frameworks/android/utils.ts", + "src/frameworks/angular/angular-wizard-agent.ts", + "src/frameworks/angular/utils.ts", + "src/frameworks/astro/astro-wizard-agent.ts", + "src/frameworks/astro/utils.ts", + "src/frameworks/django/django-wizard-agent.ts", + "src/frameworks/django/utils.ts", + "src/frameworks/elixir/elixir-wizard-agent.ts", + "src/frameworks/fastapi/fastapi-wizard-agent.ts", + "src/frameworks/fastapi/utils.ts", + "src/frameworks/flask/flask-wizard-agent.ts", + "src/frameworks/flask/utils.ts", + "src/frameworks/flutter/flutter-wizard-agent.ts", + "src/frameworks/go/go-wizard-agent.ts", + "src/frameworks/java/java-wizard-agent.ts", + "src/frameworks/javascript-node/javascript-node-wizard-agent.ts", + "src/frameworks/javascript-web/javascript-web-wizard-agent.ts", + "src/frameworks/javascript-web/utils.ts", + "src/frameworks/kmp/kmp-wizard-agent.ts", + "src/frameworks/laravel/laravel-wizard-agent.ts", + "src/frameworks/laravel/utils.ts", + "src/frameworks/nextjs/nextjs-wizard-agent.ts", + "src/frameworks/nextjs/utils.ts", + "src/frameworks/nuxt/nuxt-wizard-agent.ts", + "src/frameworks/python/python-wizard-agent.ts", + "src/frameworks/rails/rails-wizard-agent.ts", + "src/frameworks/rails/utils.ts", + "src/frameworks/react-native/react-native-wizard-agent.ts", + "src/frameworks/react-native/utils.ts", + "src/frameworks/react-router/react-router-wizard-agent.ts", + "src/frameworks/react-router/utils.ts", + "src/frameworks/ruby/ruby-wizard-agent.ts", + "src/frameworks/ruby/utils.ts", + "src/frameworks/rust/rust-wizard-agent.ts", + "src/frameworks/svelte/svelte-wizard-agent.ts", + "src/frameworks/swift/swift-wizard-agent.ts", + "src/frameworks/swift/utils.ts", + "src/frameworks/tanstack-router/tanstack-router-wizard-agent.ts", + "src/frameworks/tanstack-router/utils.ts", + "src/frameworks/tanstack-start/tanstack-start-wizard-agent.ts", + "src/frameworks/tanstack-start/utils.ts", + "src/frameworks/vue/vue-wizard-agent.ts", + "src/lib/framework-config.ts", + "src/lib/registry.ts" + ], + "imports": [ + "bounded-fs.js", + "debug.js", + "package-json.js", + "package-manager.js", + "rolldown-runtime.js", + "setup-utils.js" + ] + }, + "rolldown-runtime.js": { + "sources": [], + "imports": [] + }, + "security.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/security.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "yara-hooks.js" + ] + }, + "setup-utils.js": { + "sources": [ + "src/lib/host-resolution.ts", + "src/lib/oauth/program-scopes.ts", + "src/utils/oauth-errors.ts", + "src/utils/oauth.ts", + "src/utils/package-manager.ts", + "src/utils/project-resolution.ts", + "src/utils/semver.ts", + "src/utils/setup-utils.ts" + ], + "imports": [ + "analytics.js", + "api.js", + "bounded-fs.js", + "debug.js", + "local-dev.js", + "provisioning.js", + "rolldown-runtime.js", + "telemetry.js", + "wizard-abort.js" + ] + }, + "start-tui.js": { + "sources": [ + "src/lib/programs/error-tracking-upload-source-maps/detect-agentic.ts", + "src/lib/wizard-spellbook.ts", + "src/steps/add-mcp-server-to-clients/browser-client.ts", + "src/steps/add-mcp-server-to-clients/login-client.ts", + "src/ui/tui/App.tsx", + "src/ui/tui/components/PrivacyPanel.tsx", + "src/ui/tui/exit-line.ts", + "src/ui/tui/hooks/useGithubConnection.ts", + "src/ui/tui/ink-ui.ts", + "src/ui/tui/posthog-integration-intro.ts", + "src/ui/tui/screen-registry.tsx", + "src/ui/tui/screens/AgentSkillIntroScreen.tsx", + "src/ui/tui/screens/AiObservabilityIntroScreen.tsx", + "src/ui/tui/screens/AuthErrorScreen.tsx", + "src/ui/tui/screens/AuthScreen.tsx", + "src/ui/tui/screens/ErrorTrackingDetectScreen.tsx", + "src/ui/tui/screens/ErrorTrackingIntroScreen.tsx", + "src/ui/tui/screens/ExitScreen.tsx", + "src/ui/tui/screens/IntroScreenLayout.tsx", + "src/ui/tui/screens/KeepSkillsScreen.tsx", + "src/ui/tui/screens/ManagedSettingsScreen.tsx", + "src/ui/tui/screens/ManualAuthCodeScreen.tsx", + "src/ui/tui/screens/MetricsIntroScreen.tsx", + "src/ui/tui/screens/MigrationIntroScreen.tsx", + "src/ui/tui/screens/MintFailureScreen.tsx", + "src/ui/tui/screens/PortConflictScreen.tsx", + "src/ui/tui/screens/PostHogIntegrationIntroScreen.tsx", + "src/ui/tui/screens/RevenueIntroScreen.tsx", + "src/ui/tui/screens/RunScreen.tsx", + "src/ui/tui/screens/SelfDrivingGitHubScreen.tsx", + "src/ui/tui/screens/SelfDrivingHandoffScreen.tsx", + "src/ui/tui/screens/SelfDrivingIntegrationCheckScreen.tsx", + "src/ui/tui/screens/SelfDrivingIntegrationDetectScreen.tsx", + "src/ui/tui/screens/SelfDrivingIntroScreen.tsx", + "src/ui/tui/screens/SessionTimeoutScreen.tsx", + "src/ui/tui/screens/SettingsOverrideScreen.tsx", + "src/ui/tui/screens/SetupScreen.tsx", + "src/ui/tui/screens/SourceMapsDetectScreen.tsx", + "src/ui/tui/screens/SourceMapsIntroScreen.tsx", + "src/ui/tui/screens/SourceMapsOutroScreen.tsx", + "src/ui/tui/screens/TaskNoticeScreen.tsx", + "src/ui/tui/screens/WarehouseIntroScreen.tsx", + "src/ui/tui/screens/WizardAskScreen.tsx", + "src/ui/tui/screens/audit/AuditAreaPane.tsx", + "src/ui/tui/screens/audit/AuditChecksOutroSection.tsx", + "src/ui/tui/screens/audit/AuditIntroScreen.tsx", + "src/ui/tui/screens/audit/AuditOutroScreen.tsx", + "src/ui/tui/screens/audit/AuditRunScreen.tsx", + "src/ui/tui/screens/audit/PendingChecksList.tsx", + "src/ui/tui/screens/audit/slides/events-audit/createDashboard.tsx", + "src/ui/tui/screens/audit/slides/events-audit/detectSdk.tsx", + "src/ui/tui/screens/audit/slides/events-audit/enrichSites.tsx", + "src/ui/tui/screens/audit/slides/events-audit/index.ts", + "src/ui/tui/screens/audit/slides/events-audit/queryVolume.tsx", + "src/ui/tui/screens/audit/slides/events-audit/scanSites.tsx", + "src/ui/tui/screens/audit/slides/events-audit/uploadNotebook.tsx", + "src/ui/tui/screens/audit/slides/events-audit/writeReport.tsx", + "src/ui/tui/screens/doctor/DoctorIntroScreen.tsx", + "src/ui/tui/screens/doctor/DoctorReportScreen.tsx", + "src/ui/tui/screens/health/HealthCheckScreen.tsx", + "src/ui/tui/services/coding-agent-launcher.ts", + "src/ui/tui/services/mcp-installer.ts", + "src/ui/tui/services/mcp-suggested-prompts-services.ts", + "src/ui/tui/start-tui.ts", + "src/utils/clipboard.ts" + ], + "imports": [ + "add-mcp-server-to-clients.js", + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "bin.js", + "bounded-fs.js", + "debug.js", + "defaults.js", + "errors.js", + "mcp-prompt-streaming.js", + "mint-failure.js", + "provisioning.js", + "registry.js", + "setup-utils.js", + "store.js", + "telemetry.js", + "terminal.js", + "wizard-abort.js", + "wizard-session.js", + "yara-hooks.js" + ] + }, + "steps.js": { + "sources": [ + "src/steps/upload-environment-variables/EnvironmentProvider.ts", + "src/steps/upload-environment-variables/index.ts", + "src/steps/upload-environment-variables/providers/vercel.ts" + ], + "imports": [ + "analytics.js", + "debug.js", + "telemetry.js" + ] + }, + "store.js": { + "sources": [ + "src/lib/programs/ai-opt-in-gate.ts", + "src/lib/programs/program-step.ts", + "src/ui/tui/router.ts", + "src/ui/tui/screen-sequences.ts", + "src/ui/tui/store.ts" + ], + "imports": [ + "agent-runner.js", + "analytics.js", + "debug.js", + "mint-failure.js", + "rolldown-runtime.js", + "wizard-session.js" + ] + }, + "subagent.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/subagent.ts" + ], + "imports": [ + "debug.js" + ] + }, + "task.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/task.ts" + ], + "imports": [ + "agent-interface.js", + "agent-prompt-loader.js", + "analytics.js", + "debug.js", + "errors.js", + "gateway-session.js", + "mcp.js", + "orchestrator-tools.js", + "pi.js", + "queue-tools.js", + "security.js", + "tools.js", + "yara-hooks.js" + ] + }, + "task-stream.js": { + "sources": [ + "src/lib/task-stream/audit-areas.ts", + "src/lib/task-stream/event-plan-watcher.ts", + "src/lib/task-stream/task-stream-push.ts" + ], + "imports": [ + "debug.js", + "errors.js", + "file-watcher.js", + "file.js", + "posthog.js" + ] + }, + "tasks.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/tasks.ts" + ], + "imports": [ + "debug.js" + ] + }, + "telemetry.js": { + "sources": [ + "src/telemetry.ts", + "src/utils/links.ts" + ], + "imports": [ + "analytics.js" + ] + }, + "terminal.js": { + "sources": [ + "src/lib/mcp-project-profile.ts", + "src/lib/mcp-role-prompts.copy.json", + "src/lib/mcp-role-prompts.ts", + "src/lib/mcp-seed-events.ts", + "src/ui/tui/components/LearnCard.tsx", + "src/ui/tui/components/PhaseVisuals.tsx", + "src/ui/tui/components/ServiceHealthList.tsx", + "src/ui/tui/components/TipsCard.tsx", + "src/ui/tui/components/TitleBar.tsx", + "src/ui/tui/components/TokenCostHud.tsx", + "src/ui/tui/components/visualizer/CrateStack.tsx", + "src/ui/tui/components/visualizer/DashboardGrid.tsx", + "src/ui/tui/components/visualizer/DiffCascade.tsx", + "src/ui/tui/components/visualizer/LibraryShelf.tsx", + "src/ui/tui/components/visualizer/MatrixRain.tsx", + "src/ui/tui/components/visualizer/Tumblers.tsx", + "src/ui/tui/components/visualizer/grid.ts", + "src/ui/tui/components/visualizer/palette.ts", + "src/ui/tui/components/visualizer/panel.tsx", + "src/ui/tui/hooks/useDismissOnAnyKey.ts", + "src/ui/tui/hooks/useTick.ts", + "src/ui/tui/primitives/CardLayout.tsx", + "src/ui/tui/primitives/ConfirmationInput.tsx", + "src/ui/tui/primitives/ContentSequencer.tsx", + "src/ui/tui/primitives/DissolveTransition.tsx", + "src/ui/tui/primitives/Divider.tsx", + "src/ui/tui/primitives/EventPlanViewer.tsx", + "src/ui/tui/primitives/GroupedPickerMenu.tsx", + "src/ui/tui/primitives/HNViewer.tsx", + "src/ui/tui/primitives/KeyboardHintsBar.tsx", + "src/ui/tui/primitives/LinesBlock.tsx", + "src/ui/tui/primitives/LinkText.tsx", + "src/ui/tui/primitives/LoadingBox.tsx", + "src/ui/tui/primitives/LogViewer.tsx", + "src/ui/tui/primitives/ModalOverlay.tsx", + "src/ui/tui/primitives/NodeBlock.tsx", + "src/ui/tui/primitives/ProgressList.tsx", + "src/ui/tui/primitives/ScreenContainer.tsx", + "src/ui/tui/primitives/ScreenErrorBoundary.tsx", + "src/ui/tui/primitives/SplitView.tsx", + "src/ui/tui/primitives/TabContainer.tsx", + "src/ui/tui/primitives/ViewportTooSmall.tsx", + "src/ui/tui/primitives/link-helpers.ts", + "src/ui/tui/screens/AiOptInRequiredScreen.tsx", + "src/ui/tui/screens/McpScreen.tsx", + "src/ui/tui/screens/McpSuggestedPromptsScreen.tsx", + "src/ui/tui/screens/OutroScreen.tsx", + "src/ui/tui/screens/SkillSourceInfo.tsx", + "src/ui/tui/screens/SlackConnectScreen.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/AreaHeaderRow.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/AuditChecksViewer.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/CheckRow.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/DetailRow.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/Footer.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/Header.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/Legend.tsx", + "src/ui/tui/screens/audit/AuditChecksViewer/layout.ts", + "src/ui/tui/screens/audit/AuditChecksViewer/sort.ts", + "src/ui/tui/screens/audit/slides/eventCapture.tsx", + "src/ui/tui/screens/audit/slides/identification.tsx", + "src/ui/tui/screens/audit/slides/index.ts", + "src/ui/tui/screens/audit/slides/installation.tsx", + "src/ui/tui/screens/audit/slides/liveData.tsx", + "src/ui/tui/screens/audit/slides/shared.tsx", + "src/ui/tui/screens/audit/slides/uploadNotebook.tsx", + "src/ui/tui/screens/audit/slides/writeReport.tsx", + "src/ui/tui/screens/doctor/IssueTable.tsx", + "src/ui/tui/terminal.ts" + ], + "imports": [ + "agent-interface.js", + "agent-runner.js", + "analytics.js", + "api.js", + "bin.js", + "debug.js", + "defaults.js", + "mint-failure.js", + "setup-utils.js", + "store.js", + "telemetry.js", + "yara-hooks.js" + ] + }, + "tools.js": { + "sources": [ + "src/lib/agent/runner/harness/pi/tools.ts" + ], + "imports": [ + "agent-interface.js", + "analytics.js", + "debug.js", + "package-manager.js", + "pi.js", + "queue-tools.js", + "yara-hooks.js" + ] + }, + "wizard-abort.js": { + "sources": [], + "imports": [ + "wizard-abort.js" + ] + }, + "wizard-session.js": { + "sources": [ + "src/lib/wizard-session.ts" + ], + "imports": [ + "local-dev.js" + ] + }, + "yara-hooks.js": { + "sources": [], + "imports": [ + "yara-hooks.js" + ] + } +} diff --git a/src/__tests__/architecture/import-boundaries.test.ts b/src/__tests__/architecture/import-boundaries.test.ts new file mode 100644 index 000000000..7fa8942bb --- /dev/null +++ b/src/__tests__/architecture/import-boundaries.test.ts @@ -0,0 +1,413 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +export type Surface = 'env' | 'store' | 'agent' | 'tui' | 'cli' | 'harness'; + +const HERE = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(HERE, '../../..'); + +const SURFACE_RULES: ReadonlyArray boolean]> = + [ + ['env', (p) => p === 'src/env.ts'], + [ + 'agent', + (p) => + p.startsWith('src/lib/agent/') || + p.startsWith('src/lib/middleware/') || + p === 'src/lib/gateway-session.ts' || + p === 'src/lib/yara-hooks.ts' || + p === 'src/lib/yara-policy.ts' || + p === 'src/lib/wizard-tools/mcp.ts', + ], + [ + 'tui', + (p) => + p.startsWith('src/ui/tui/') || + p === 'src/ui/logging-ui.ts' || + p === 'src/ui/headless-ui.ts' || + p === 'src/commands/factories/family-picker.tsx' || + /^src\/lib\/programs\/[^/]+\/content\//.test(p) || + /^src\/lib\/programs\/[^/]+\/tips\.ts$/.test(p), + ], + [ + 'cli', + (p) => + p === 'bin.ts' || + p === 'src/wizard.ts' || + p === 'src/telemetry.ts' || + p.startsWith('src/commands/') || + p.startsWith('src/lib/runners/'), + ], + [ + 'harness', + (p) => p.startsWith('e2e-harness/') || p.startsWith('scripts/'), + ], + ]; + +export function classifySurface(relPath: string): Surface { + const p = relPath.split(path.sep).join('/'); + for (const [surface, matches] of SURFACE_RULES) { + if (matches(p)) return surface; + } + return 'store'; +} + +export const ALLOWED_IMPORTS: Record< + Exclude, + readonly Surface[] +> = { + env: [], + store: ['env', 'store'], + agent: ['env', 'store', 'agent'], + tui: ['env', 'store', 'tui'], + cli: ['env', 'store', 'agent', 'tui', 'cli'], +}; + +const TUI_ONLY_PACKAGES = ['ink', 'react', '@inkjs/ui', 'ink-testing-library']; + +const SKIP_DIRS = new Set([ + '__tests__', + '__mocks__', + '__fixtures__', + '__snapshots__', + 'node_modules', + 'dist', + 'coverage', +]); + +const SPECIFIER_PATTERNS = [ + /import\s+(?:type\s+)?[^'"]*?from\s*['"]([^'"]+)['"]/g, + /import\s*['"]([^'"]+)['"]/g, + /export\s+(?:type\s+)?[^'"]*?from\s*['"]([^'"]+)['"]/g, + /import\(\s*['"]([^'"]+)['"]\s*\)/g, + /require\(\s*['"]([^'"]+)['"]\s*\)/g, +]; + +const REGEX_PRECEDERS = new Set([ + '\n', + '(', + ')', + ',', + '=', + ':', + '[', + '!', + '&', + '|', + '?', + '{', + '}', + ';', + '+', + '-', + '*', + '%', + '^', + '~', + '<', + '>', +]); + +function endOfString(source: string, start: number, quote: string): number { + let i = start + 1; + while (i < source.length) { + if (source[i] === '\\') { + i += 2; + continue; + } + if (source[i] === quote) return i + 1; + i++; + } + return source.length; +} + +function endOfRegex(source: string, start: number): number { + let i = start + 1; + let inClass = false; + while (i < source.length) { + const c = source[i]; + if (c === '\\') { + i += 2; + continue; + } + if (c === '\n') return start + 1; + if (c === '[') inClass = true; + else if (c === ']') inClass = false; + else if (c === '/' && !inClass) return i + 1; + i++; + } + return source.length; +} + +function stripComments(source: string): string { + let out = ''; + let prev = '\n'; + let i = 0; + while (i < source.length) { + const c = source[i]; + const next = source[i + 1]; + if (c === '/' && next === '/') { + while (i < source.length && source[i] !== '\n') i++; + continue; + } + if (c === '/' && next === '*') { + i += 2; + while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) + i++; + i += 2; + out += ' '; + continue; + } + if (c === '"' || c === "'" || c === '`') { + const end = endOfString(source, i, c); + out += source.slice(i, end); + prev = c; + i = end; + continue; + } + if (c === '/' && REGEX_PRECEDERS.has(prev)) { + const end = endOfRegex(source, i); + out += source.slice(i, end); + prev = '/'; + i = end; + continue; + } + out += c; + if (c === '\n' || !/\s/.test(c)) prev = c; + i++; + } + return out; +} + +function toRepoRelative(abs: string): string { + return path.relative(REPO_ROOT, abs).split(path.sep).join('/'); +} + +function isFile(abs: string): boolean { + return fs.statSync(abs, { throwIfNoEntry: false })?.isFile() ?? false; +} + +function collectFiles(absDir: string, into: string[]): void { + for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) { + const abs = path.join(absDir, entry.name); + if (entry.isDirectory()) { + if (!SKIP_DIRS.has(entry.name)) collectFiles(abs, into); + continue; + } + if (!entry.isFile()) continue; + if (!/\.tsx?$/.test(entry.name)) continue; + if (/\.d\.ts$/.test(entry.name) || /\.test\.tsx?$/.test(entry.name)) + continue; + into.push(toRepoRelative(abs)); + } +} + +function loadAliases(): ReadonlyArray { + const tsconfig = JSON.parse( + fs.readFileSync(path.join(REPO_ROOT, 'tsconfig.build.json'), 'utf8'), + ) as { compilerOptions?: { paths?: Record } }; + return Object.entries(tsconfig.compilerOptions?.paths ?? {}).map( + ([pattern, targets]) => [pattern, targets[0]] as const, + ); +} + +function aliasTarget( + spec: string, + aliases: ReadonlyArray, +): string | null { + for (const [pattern, target] of aliases) { + if (pattern.endsWith('*')) { + const prefix = pattern.slice(0, -1); + if (spec.startsWith(prefix)) { + return path.resolve( + REPO_ROOT, + target.slice(0, -1) + spec.slice(prefix.length), + ); + } + } else if (spec === pattern) { + return path.resolve(REPO_ROOT, target); + } + } + return null; +} + +function probe(base: string): string | null { + const candidates: string[] = []; + if (base.endsWith('.js')) { + const stem = base.slice(0, -3); + candidates.push(`${stem}.ts`, `${stem}.tsx`); + } + candidates.push( + `${base}.ts`, + `${base}.tsx`, + path.join(base, 'index.ts'), + path.join(base, 'index.tsx'), + base, + ); + return candidates.find(isFile) ?? null; +} + +function specifiersIn(text: string): string[] { + const found = new Set(); + for (const pattern of SPECIFIER_PATTERNS) { + pattern.lastIndex = 0; + let match = pattern.exec(text); + while (match !== null) { + found.add(match[1]); + match = pattern.exec(text); + } + } + return [...found]; +} + +type Analysis = { + files: string[]; + edges: string[]; + violations: ReadonlyArray<{ key: string; rule: string }>; + unresolved: string[]; +}; + +function analyze(): Analysis { + const files: string[] = ['bin.ts']; + collectFiles(path.join(REPO_ROOT, 'src'), files); + files.sort(); + + const aliases = loadAliases(); + const edges = new Set(); + const violations = new Map(); + const unresolved = new Set(); + + for (const file of files) { + const text = stripComments( + fs.readFileSync(path.join(REPO_ROOT, file), 'utf8'), + ); + const from = classifySurface(file); + const allowed = from === 'harness' ? [] : ALLOWED_IMPORTS[from]; + + for (const spec of specifiersIn(text)) { + const base = spec.startsWith('.') + ? path.resolve(REPO_ROOT, path.dirname(file), spec) + : aliasTarget(spec, aliases); + + if (base === null) { + const tuiOnly = + TUI_ONLY_PACKAGES.includes(spec) || spec.startsWith('react/'); + if (tuiOnly && from !== 'tui') { + violations.set(`${file} -> pkg:${spec}`, 'ink-outside-tui'); + } + continue; + } + + const resolved = probe(base); + if (resolved === null) { + unresolved.add(`${file} -> ${spec}`); + continue; + } + + const target = toRepoRelative(resolved); + const key = `${file} -> ${target}`; + edges.add(key); + + const to = classifySurface(target); + if (to === 'harness') violations.set(key, 'harness'); + else if (!allowed.includes(to)) + violations.set(key, `matrix:${from}->${to}`); + } + } + + return { + files, + edges: [...edges].sort(), + violations: [...violations] + .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + .map(([key, rule]) => ({ key, rule })), + unresolved: [...unresolved].sort(), + }; +} + +const analysis = analyze(); + +const known = ( + JSON.parse( + fs.readFileSync(path.join(HERE, 'known-violations.json'), 'utf8'), + ) as { violations: string[] } +).violations; + +if (process.env.PRINT_VIOLATIONS) { + const byRule = new Map(); + const byImporter = new Map(); + for (const { key, rule } of analysis.violations) { + byRule.set(rule, (byRule.get(rule) ?? 0) + 1); + const file = key.split(' -> ')[0]; + byImporter.set(file, (byImporter.get(file) ?? 0) + 1); + } + const lines = [ + `files scanned: ${analysis.files.length}`, + `edges: ${analysis.edges.length}`, + `violations: ${analysis.violations.length}`, + `unresolved: ${analysis.unresolved.length}`, + ...analysis.unresolved.map((u) => ` unresolved: ${u}`), + ...[...byRule] + .sort((a, b) => b[1] - a[1]) + .map(([rule, count]) => ` ${rule}: ${count}`), + 'top importers:', + ...[...byImporter] + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([file, count]) => ` ${file}: ${count}`), + ]; + process.stderr.write(`${lines.join('\n')}\n`); + process.stderr.write( + `${JSON.stringify( + { violations: analysis.violations.map((v) => v.key) }, + null, + 2, + )}\n`, + ); +} + +describe('import boundaries', () => { + it('resolves every internal specifier', () => { + expect(analysis.unresolved).toEqual([]); + }); + + it('introduces no violation outside known-violations.json', () => { + const knownSet = new Set(known); + const added = analysis.violations + .filter(({ key }) => !knownSet.has(key)) + .map(({ key, rule }) => `${key} [${rule}]`); + expect(added).toEqual([]); + }); + + it('keeps known-violations.json free of stale entries', () => { + const current = new Set(analysis.violations.map((v) => v.key)); + const stale = known.filter((key) => !current.has(key)); + expect( + stale, + 'stale entries, delete them from known-violations.json', + ).toEqual([]); + }); +}); + +describe('surface classification', () => { + it('maps representative paths to their surface', () => { + expect(classifySurface('src/env.ts')).toBe('env'); + expect(classifySurface('src/utils/analytics.ts')).toBe('store'); + expect(classifySurface('src/lib/agent/agent-runner.ts')).toBe('agent'); + expect(classifySurface('src/ui/tui/App.tsx')).toBe('tui'); + expect(classifySurface('bin.ts')).toBe('cli'); + expect(classifySurface('e2e-harness/e2e-profile.ts')).toBe('harness'); + expect(classifySurface('src/lib/wizard-tools/mcp.ts')).toBe('agent'); + expect(classifySurface('src/lib/wizard-tools/tools.ts')).toBe('store'); + expect(classifySurface('src/commands/factories/family-picker.tsx')).toBe( + 'tui', + ); + expect( + classifySurface('src/lib/programs/posthog-integration/content/index.tsx'), + ).toBe('tui'); + expect( + classifySurface('src/lib/programs/posthog-integration/index.ts'), + ).toBe('store'); + }); +}); diff --git a/src/__tests__/architecture/known-violations.json b/src/__tests__/architecture/known-violations.json new file mode 100644 index 000000000..d523b2a73 --- /dev/null +++ b/src/__tests__/architecture/known-violations.json @@ -0,0 +1,76 @@ +{ + "violations": [ + "src/commands/factories/family-picker.tsx -> src/commands/command.ts", + "src/env.ts -> src/lib/headless-mode.ts", + "src/lib/agent/mcp-prompt-streaming.ts -> src/ui/tui/services/mcp-suggested-prompts-services.ts", + "src/lib/detection/agentic.ts -> src/lib/agent/agent-interface.ts", + "src/lib/detection/project-scope.ts -> src/lib/agent/runner/shared/authenticate.ts", + "src/lib/errors/agent-map.ts -> src/lib/agent/signals.ts", + "src/lib/programs/agent-skill/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/agent-skill/index.ts -> src/lib/programs/agent-skill/content/index.tsx", + "src/lib/programs/ai-observability/index.ts -> src/lib/programs/agent-skill/content/index.tsx", + "src/lib/programs/audit/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/audit/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/dispatch-family.ts -> src/commands/command.ts", + "src/lib/programs/dispatch-family.ts -> src/commands/factories/shared.ts", + "src/lib/programs/error-tracking-upload-source-maps/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/error-tracking-upload-source-maps/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/error-tracking-upload-source-maps/index.ts -> src/lib/programs/error-tracking-upload-source-maps/content/index.tsx", + "src/lib/programs/error-tracking-upload-source-maps/prompt.ts -> src/lib/agent/agent-interface.ts", + "src/lib/programs/error-tracking/index.ts -> src/lib/agent/runner/shared/types.ts", + "src/lib/programs/error-tracking/index.ts -> src/lib/programs/error-tracking/content/index.tsx", + "src/lib/programs/error-tracking/index.ts -> src/lib/programs/error-tracking/content/tips.ts", + "src/lib/programs/events-audit/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/mcp-analytics/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/metrics/index.ts -> src/lib/programs/agent-skill/content/index.tsx", + "src/lib/programs/migration/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/migration/index.ts -> src/lib/programs/migration/content/index.tsx", + "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/agent-interface.ts", + "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/posthog-integration/index.ts -> src/lib/agent/runner/shared/bootstrap.ts", + "src/lib/programs/posthog-integration/index.ts -> src/lib/programs/posthog-integration/content/index.tsx", + "src/lib/programs/program-registry.ts -> src/lib/programs/agent-skill/content/index.tsx", + "src/lib/programs/program-step.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/program-step.ts -> src/ui/tui/components/TipsCard.tsx", + "src/lib/programs/program-step.ts -> src/ui/tui/primitives/index.ts", + "src/lib/programs/program-step.ts -> src/ui/tui/store.ts", + "src/lib/programs/replay-vision/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/revenue-analytics/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/revenue-analytics/index.ts -> src/lib/programs/revenue-analytics/content/index.tsx", + "src/lib/programs/self-driving/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/self-driving/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/self-driving/index.ts -> src/lib/programs/self-driving/content/index.tsx", + "src/lib/programs/self-driving/index.ts -> src/lib/programs/self-driving/content/pricing.ts", + "src/lib/programs/self-driving/index.ts -> src/lib/programs/self-driving/content/tips.ts", + "src/lib/programs/self-driving/prompt.ts -> src/lib/agent/agent-interface.ts", + "src/lib/programs/self-driving/prompt.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/warehouse-source/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/warehouse-source/index.ts -> src/lib/agent/agent-runner.ts", + "src/lib/programs/warehouse-source/index.ts -> src/lib/programs/warehouse-source/content/index.tsx", + "src/lib/programs/web-analytics-doctor/detect.ts -> src/lib/agent/agent-runner.ts", + "src/lib/task-stream/event-plan-watcher.ts -> src/ui/tui/store.ts", + "src/lib/task-stream/task-stream-push.ts -> src/ui/tui/store.ts", + "src/lib/wizard-session.ts -> src/lib/agent/claude-settings.ts", + "src/lib/wizard-tools/index.ts -> src/lib/wizard-tools/mcp.ts", + "src/lib/wizard-tools/tools.ts -> src/lib/yara-hooks.ts", + "src/steps/add-mcp-server-to-clients/index.ts -> src/telemetry.ts", + "src/steps/add-or-update-environment-variables.ts -> src/telemetry.ts", + "src/steps/run-prettier.ts -> src/telemetry.ts", + "src/steps/upload-environment-variables/index.ts -> src/telemetry.ts", + "src/ui/index.ts -> src/ui/logging-ui.ts", + "src/ui/logging-ui.ts -> src/lib/agent/claude-settings.ts", + "src/ui/tui/components/PhaseVisuals.tsx -> src/lib/agent/agent-phase.ts", + "src/ui/tui/components/TokenCostHud.tsx -> src/lib/agent/token-pricing.ts", + "src/ui/tui/exit-line.ts -> src/lib/agent/token-pricing.ts", + "src/ui/tui/ink-ui.ts -> src/lib/agent/claude-settings.ts", + "src/ui/tui/playground/demos/RunScreenDemo.tsx -> src/lib/agent/agent-phase.ts", + "src/ui/tui/screens/ManagedSettingsScreen.tsx -> src/lib/agent/claude-settings.ts", + "src/ui/tui/services/mcp-suggested-prompts-services.ts -> src/lib/agent/mcp-prompt-streaming.ts", + "src/ui/tui/store.ts -> src/lib/agent/claude-settings.ts", + "src/ui/tui/store.ts -> src/lib/agent/token-pricing.ts", + "src/ui/wizard-ui.ts -> src/lib/agent/claude-settings.ts", + "src/utils/package-manager.ts -> src/telemetry.ts", + "src/utils/setup-utils.ts -> src/telemetry.ts", + "src/utils/wizard-abort.ts -> src/ui/logging-ui.ts" + ] +} diff --git a/src/lib/programs/__tests__/__snapshots__/flow-traces.test.ts.snap b/src/lib/programs/__tests__/__snapshots__/flow-traces.test.ts.snap new file mode 100644 index 000000000..cc4a39428 --- /dev/null +++ b/src/lib/programs/__tests__/__snapshots__/flow-traces.test.ts.snap @@ -0,0 +1,1104 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`flow traces per program > agent-skill (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "agent-skill-intro", + "program": "agent-skill", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "agent-skill", + }, + { + "event": "screen run", + "from": "auth", + "program": "agent-skill", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "agent-skill", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "agent-skill", + }, + { + "event": "screen outro", + "from": "run", + "program": "agent-skill", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "agent-skill", + }, + ], + "program": "agent-skill", + "screens": [ + "agent-skill-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > ai-observability (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "ai-observability-intro", + "program": "ai-observability", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "ai-observability", + }, + { + "event": "screen run", + "from": "auth", + "program": "ai-observability", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "ai-observability", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "ai-observability", + }, + { + "event": "screen outro", + "from": "run", + "program": "ai-observability", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "ai-observability", + }, + ], + "program": "ai-observability", + "screens": [ + "ai-observability-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > audit (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "audit-intro", + "program": "audit", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "audit", + }, + { + "event": "screen audit-run", + "from": "auth", + "program": "audit", + }, + { + "event": "screen ai-opt-in", + "from": "audit-run", + "program": "audit", + }, + { + "event": "screen audit-run", + "from": "ai-opt-in", + "program": "audit", + }, + { + "event": "screen audit-outro", + "from": "audit-run", + "program": "audit", + }, + { + "event": "screen keep-skills", + "from": "audit-outro", + "program": "audit", + }, + ], + "program": "audit", + "screens": [ + "audit-intro", + "health-check", + "auth", + "ai-opt-in", + "audit-run", + "audit-outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > error-tracking (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "error-tracking-intro", + "program": "error-tracking", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "error-tracking", + }, + { + "event": "screen run", + "from": "auth", + "program": "error-tracking", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "error-tracking", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "error-tracking", + }, + { + "event": "screen outro", + "from": "run", + "program": "error-tracking", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "error-tracking", + }, + ], + "program": "error-tracking", + "screens": [ + "error-tracking-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > error-tracking-upload-source-maps (node) 1`] = ` +{ + "events": [ + { + "event": "screen auth", + "from": "source-maps-intro", + "program": "error-tracking-upload-source-maps", + }, + { + "event": "screen source-maps-detect", + "from": "auth", + "program": "error-tracking-upload-source-maps", + }, + { + "event": "screen ai-opt-in", + "from": "source-maps-detect", + "program": "error-tracking-upload-source-maps", + }, + { + "event": "screen source-maps-detect", + "from": "ai-opt-in", + "program": "error-tracking-upload-source-maps", + }, + { + "event": "screen run", + "from": "source-maps-detect", + "program": "error-tracking-upload-source-maps", + }, + { + "event": "screen source-maps-outro", + "from": "run", + "program": "error-tracking-upload-source-maps", + }, + { + "event": "screen keep-skills", + "from": "source-maps-outro", + "program": "error-tracking-upload-source-maps", + }, + ], + "program": "error-tracking-upload-source-maps", + "screens": [ + "source-maps-intro", + "auth", + "ai-opt-in", + "source-maps-detect", + "run", + "source-maps-outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > events-audit (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "audit-intro", + "program": "events-audit", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "events-audit", + }, + { + "event": "screen audit-run", + "from": "auth", + "program": "events-audit", + }, + { + "event": "screen ai-opt-in", + "from": "audit-run", + "program": "events-audit", + }, + { + "event": "screen audit-run", + "from": "ai-opt-in", + "program": "events-audit", + }, + { + "event": "screen mcp", + "from": "audit-run", + "program": "events-audit", + }, + { + "event": "screen audit-outro", + "from": "mcp", + "program": "events-audit", + }, + { + "event": "screen keep-skills", + "from": "audit-outro", + "program": "events-audit", + }, + ], + "program": "events-audit", + "screens": [ + "audit-intro", + "health-check", + "auth", + "ai-opt-in", + "audit-run", + "mcp", + "audit-outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > mcp-add (node) 1`] = ` +{ + "events": [ + { + "event": "screen exit", + "from": "mcp-add", + "program": "mcp-add", + }, + ], + "program": "mcp-add", + "screens": [ + "mcp-add", + "exit", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > mcp-analytics (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "agent-skill-intro", + "program": "mcp-analytics", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "mcp-analytics", + }, + { + "event": "screen run", + "from": "auth", + "program": "mcp-analytics", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "mcp-analytics", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "mcp-analytics", + }, + { + "event": "screen outro", + "from": "run", + "program": "mcp-analytics", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "mcp-analytics", + }, + ], + "program": "mcp-analytics", + "screens": [ + "agent-skill-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > mcp-remove (node) 1`] = ` +{ + "events": [ + { + "event": "screen exit", + "from": "mcp-remove", + "program": "mcp-remove", + }, + ], + "program": "mcp-remove", + "screens": [ + "mcp-remove", + "exit", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > mcp-tutorial (node) 1`] = ` +{ + "events": [ + { + "event": "screen slack-connect", + "from": "mcp-suggested-prompts", + "program": "mcp-tutorial", + }, + { + "event": "screen exit", + "from": "slack-connect", + "program": "mcp-tutorial", + }, + ], + "program": "mcp-tutorial", + "screens": [ + "mcp-suggested-prompts", + "slack-connect", + "exit", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > metrics (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "metrics-intro", + "program": "metrics", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "metrics", + }, + { + "event": "screen run", + "from": "auth", + "program": "metrics", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "metrics", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "metrics", + }, + { + "event": "screen outro", + "from": "run", + "program": "metrics", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "metrics", + }, + ], + "program": "metrics", + "screens": [ + "metrics-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > migration (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "migration-intro", + "program": "migration", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "migration", + }, + { + "event": "screen run", + "from": "auth", + "program": "migration", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "migration", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "migration", + }, + { + "event": "screen outro", + "from": "run", + "program": "migration", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "migration", + }, + ], + "program": "migration", + "screens": [ + "migration-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > posthog-doctor (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "doctor-intro", + "program": "posthog-doctor", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "posthog-doctor", + }, + { + "event": "screen doctor-report", + "from": "auth", + "program": "posthog-doctor", + }, + { + "event": "screen outro", + "from": "doctor-report", + "program": "posthog-doctor", + }, + { + "event": "screen exit", + "from": "outro", + "program": "posthog-doctor", + }, + ], + "program": "posthog-doctor", + "screens": [ + "doctor-intro", + "health-check", + "auth", + "doctor-report", + "outro", + "exit", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > posthog-integration (nextjs, with a setup question) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "intro", + "program": "posthog-integration", + }, + { + "event": "screen setup", + "from": "health-check", + "program": "posthog-integration", + }, + { + "event": "screen auth", + "from": "setup", + "program": "posthog-integration", + }, + { + "event": "screen run", + "from": "auth", + "program": "posthog-integration", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "posthog-integration", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "posthog-integration", + }, + { + "event": "screen outro", + "from": "run", + "program": "posthog-integration", + }, + { + "event": "screen mcp", + "from": "outro", + "program": "posthog-integration", + }, + { + "event": "screen slack-connect", + "from": "mcp", + "program": "posthog-integration", + }, + { + "event": "screen keep-skills", + "from": "slack-connect", + "program": "posthog-integration", + }, + ], + "program": "posthog-integration", + "screens": [ + "intro", + "health-check", + "setup", + "auth", + "ai-opt-in", + "run", + "outro", + "mcp", + "slack-connect", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > posthog-integration (no framework detected) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "intro", + "program": "posthog-integration", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "posthog-integration", + }, + { + "event": "screen run", + "from": "auth", + "program": "posthog-integration", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "posthog-integration", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "posthog-integration", + }, + { + "event": "screen outro", + "from": "run", + "program": "posthog-integration", + }, + { + "event": "screen mcp", + "from": "outro", + "program": "posthog-integration", + }, + { + "event": "screen slack-connect", + "from": "mcp", + "program": "posthog-integration", + }, + { + "event": "screen keep-skills", + "from": "slack-connect", + "program": "posthog-integration", + }, + ], + "program": "posthog-integration", + "screens": [ + "intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "mcp", + "slack-connect", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > posthog-integration (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "intro", + "program": "posthog-integration", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "posthog-integration", + }, + { + "event": "screen run", + "from": "auth", + "program": "posthog-integration", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "posthog-integration", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "posthog-integration", + }, + { + "event": "screen outro", + "from": "run", + "program": "posthog-integration", + }, + { + "event": "screen mcp", + "from": "outro", + "program": "posthog-integration", + }, + { + "event": "screen slack-connect", + "from": "mcp", + "program": "posthog-integration", + }, + { + "event": "screen keep-skills", + "from": "slack-connect", + "program": "posthog-integration", + }, + ], + "program": "posthog-integration", + "screens": [ + "intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "mcp", + "slack-connect", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > replay-vision (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "agent-skill-intro", + "program": "replay-vision", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "replay-vision", + }, + { + "event": "screen run", + "from": "auth", + "program": "replay-vision", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "replay-vision", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "replay-vision", + }, + { + "event": "screen outro", + "from": "run", + "program": "replay-vision", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "replay-vision", + }, + ], + "program": "replay-vision", + "screens": [ + "agent-skill-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > revenue-analytics-setup (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "revenue-intro", + "program": "revenue-analytics-setup", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "revenue-analytics-setup", + }, + { + "event": "screen run", + "from": "auth", + "program": "revenue-analytics-setup", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "revenue-analytics-setup", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "revenue-analytics-setup", + }, + { + "event": "screen outro", + "from": "run", + "program": "revenue-analytics-setup", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "revenue-analytics-setup", + }, + ], + "program": "revenue-analytics-setup", + "screens": [ + "revenue-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > self-driving (node) 1`] = ` +{ + "events": [ + { + "event": "screen self-driving-integration-check", + "from": "self-driving-intro", + "program": "self-driving", + }, + { + "event": "screen health-check", + "from": "self-driving-integration-check", + "program": "self-driving", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "self-driving", + }, + { + "event": "screen run", + "from": "auth", + "program": "self-driving", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "self-driving", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "self-driving", + }, + { + "event": "screen self-driving-handoff", + "from": "run", + "program": "self-driving", + }, + { + "event": "screen self-driving-github", + "from": "self-driving-handoff", + "program": "self-driving", + }, + { + "event": "screen run", + "from": "self-driving-github", + "program": "self-driving", + }, + { + "event": "screen outro", + "from": "run", + "program": "self-driving", + }, + { + "event": "screen exit", + "from": "outro", + "program": "self-driving", + }, + ], + "program": "self-driving", + "screens": [ + "self-driving-intro", + "self-driving-integration-check", + "health-check", + "auth", + "ai-opt-in", + "run", + "self-driving-handoff", + "self-driving-github", + "run", + "outro", + "exit", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > slack (node) 1`] = ` +{ + "events": [ + { + "event": "screen exit", + "from": "slack-connect", + "program": "slack", + }, + ], + "program": "slack", + "screens": [ + "slack-connect", + "exit", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > warehouse-source (node) 1`] = ` +{ + "events": [ + { + "event": "screen auth", + "from": "warehouse-intro", + "program": "warehouse-source", + }, + { + "event": "screen run", + "from": "auth", + "program": "warehouse-source", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "warehouse-source", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "warehouse-source", + }, + { + "event": "screen outro", + "from": "run", + "program": "warehouse-source", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "warehouse-source", + }, + ], + "program": "warehouse-source", + "screens": [ + "warehouse-intro", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`flow traces per program > web-analytics-doctor (node) 1`] = ` +{ + "events": [ + { + "event": "screen health-check", + "from": "agent-skill-intro", + "program": "web-analytics-doctor", + }, + { + "event": "screen auth", + "from": "health-check", + "program": "web-analytics-doctor", + }, + { + "event": "screen run", + "from": "auth", + "program": "web-analytics-doctor", + }, + { + "event": "screen ai-opt-in", + "from": "run", + "program": "web-analytics-doctor", + }, + { + "event": "screen run", + "from": "ai-opt-in", + "program": "web-analytics-doctor", + }, + { + "event": "screen outro", + "from": "run", + "program": "web-analytics-doctor", + }, + { + "event": "screen keep-skills", + "from": "outro", + "program": "web-analytics-doctor", + }, + ], + "program": "web-analytics-doctor", + "screens": [ + "agent-skill-intro", + "health-check", + "auth", + "ai-opt-in", + "run", + "outro", + "keep-skills", + ], + "stoppedOn": null, +} +`; + +exports[`headless walk analytics > audit: run phases without a TUI 1`] = ` +{ + "events": [], + "program": "audit", + "screen": "audit-intro", +} +`; + +exports[`headless walk analytics > posthog-integration: run phases without a TUI 1`] = ` +{ + "events": [], + "program": "posthog-integration", + "screen": "intro", +} +`; diff --git a/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap b/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap new file mode 100644 index 000000000..764f5263a --- /dev/null +++ b/src/lib/programs/__tests__/__snapshots__/post-auth-gates.test.ts.snap @@ -0,0 +1,28 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`post-auth gate ids per program > match the golden 1`] = ` +{ + "agent-skill": [], + "ai-observability": [], + "audit": [], + "error-tracking": [], + "error-tracking-upload-source-maps": [ + "detect", + ], + "events-audit": [], + "mcp-add": [], + "mcp-analytics": [], + "mcp-remove": [], + "mcp-tutorial": [], + "metrics": [], + "migration": [], + "posthog-doctor": [], + "posthog-integration": [], + "replay-vision": [], + "revenue-analytics-setup": [], + "self-driving": [], + "slack": [], + "warehouse-source": [], + "web-analytics-doctor": [], +} +`; diff --git a/src/lib/programs/__tests__/flow-traces.test.ts b/src/lib/programs/__tests__/flow-traces.test.ts new file mode 100644 index 000000000..acf23d064 --- /dev/null +++ b/src/lib/programs/__tests__/flow-traces.test.ts @@ -0,0 +1,231 @@ +/** + * Golden screen sequences and `screen ` analytics per program, produced + * by walking each program's steps through the store with a generic advance per + * screen. Baseline for the surface split: must stay byte identical. + */ +import { WizardStore, ScreenId, RunPhase, McpOutcome } from '@ui/tui/store'; +import { InkUI } from '@ui/tui/ink-ui'; +import { setUI } from '@ui/index'; +import { + buildSession, + OutroKind, + type WizardSession, +} from '@lib/wizard-session'; +import { Integration } from '@lib/constants'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { HostResolution } from '@lib/host-resolution'; +import { WizardReadiness } from '@lib/health-checks/readiness'; +import { analytics } from '@utils/analytics'; +import { + PROGRAM_REGISTRY, + getProgramConfig, + type ProgramId, +} from '../program-registry'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '../self-driving/detect'; +import { ERROR_TRACKING_PROJECT_PATH_KEY } from '../error-tracking/detect-agentic'; +import { SOURCE_MAPS_CONTEXT_KEYS } from '../error-tracking-upload-source-maps/detect'; + +vi.mock('@utils/analytics', () => ({ + analytics: { + capture: vi.fn(), + wizardCapture: vi.fn(), + setTag: vi.fn(), + captureException: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }, + sessionProperties: vi.fn(() => ({})), +})); + +const wizardCapture = analytics.wizardCapture as Mock; + +interface ScreenEvent { + event: string; + from: unknown; + program: unknown; +} + +function screenEvents(): ScreenEvent[] { + return wizardCapture.mock.calls + .filter(([name]) => typeof name === 'string' && name.startsWith('screen ')) + .map(([name, props]) => ({ + event: name as string, + from: (props as Record)?.from_screen, + program: (props as Record)?.program_id, + })); +} + +const NODE = FRAMEWORK_REGISTRY[Integration.javascriptNode]; + +function createStore(program: ProgramId, integration: Integration | null) { + const store = new WizardStore(program); + setUI(new InkUI(store)); + const session = buildSession({ installDir: '/app', ci: false }); + if (integration) { + session.integration = integration; + session.frameworkConfig = FRAMEWORK_REGISTRY[integration]; + } + store.session = session; + return store; +} + +const approved = (ok: boolean) => + ({ + organization: { is_ai_data_processing_approved: ok }, + } as unknown as WizardSession['apiUser']); + +/** Commit what a user, the runner, or the agent would commit on this screen. */ +function advance(store: WizardStore, screen: string): boolean { + const s = store.session; + if (screen === ScreenId.Intro || screen.endsWith('-intro')) { + store.completeSetup(); + return true; + } + switch (screen) { + case ScreenId.HealthCheck: + store.setReadinessResult({ + decision: WizardReadiness.Yes, + health: {} as never, + reasons: [], + }); + return true; + case ScreenId.Setup: { + const questions = s.frameworkConfig?.metadata.setup?.questions ?? []; + for (const q of questions) { + if (!(q.key in s.frameworkContext)) { + store.setFrameworkContext(q.key, q.options[0].value); + } + } + return true; + } + case ScreenId.Auth: + store.setCredentials({ + accessToken: 'phx_test', + projectApiKey: 'phc_test', + host: HostResolution.fromApiHost('https://us.posthog.com'), + projectId: 1, + }); + store.setApiUser(approved(false)); + return true; + case ScreenId.AiOptIn: + store.setApiUser(approved(true)); + return true; + case ScreenId.Run: + case ScreenId.AuditRun: { + const steps = getProgramConfig(store.router.activeProgram).steps; + const runStep = steps.find( + (st) => + st.screenId === screen && + (!st.show || st.show(s)) && + (!st.isComplete || !st.isComplete(s)), + ); + if (runStep?.run) { + store.completeRunStep(runStep.id); + } else { + store.setRunPhase(RunPhase.Running); + store.setRunPhase(RunPhase.Completed); + } + return true; + } + case ScreenId.Outro: + case ScreenId.AuditOutro: + case ScreenId.SourceMapsOutro: + store.setOutroDismissed(); + return true; + case ScreenId.DoctorReport: + store.setOutroData({ kind: OutroKind.Success, message: 'done' }); + return true; + case ScreenId.Mcp: + case ScreenId.McpAdd: + case ScreenId.McpRemove: + store.setMcpComplete(McpOutcome.Skipped); + return true; + case ScreenId.McpSuggestedPrompts: + store.setMcpSuggestedPromptsDismissed(); + return true; + case ScreenId.SlackConnect: + store.setSlackStepDismissed(); + return true; + case ScreenId.KeepSkills: + store.setSkillsComplete(true); + return true; + case ScreenId.SelfDrivingIntegrationCheck: + store.setIntegrate(true); + return true; + case ScreenId.SelfDrivingIntegrationDetect: + store.setFrameworkContext(SELF_DRIVING_INTEGRATE_PATH_KEY, '.'); + store.setFrameworkConfig(Integration.javascriptNode, NODE); + return true; + case ScreenId.SelfDrivingHandoff: + store.confirmSelfDrivingHandoff(); + return true; + case ScreenId.SelfDrivingGithub: + store.setGithubConnected(true); + return true; + case ScreenId.ErrorTrackingDetect: + store.setFrameworkContext(ERROR_TRACKING_PROJECT_PATH_KEY, '.'); + store.setFrameworkConfig(Integration.javascriptNode, NODE); + return true; + case ScreenId.SourceMapsDetect: + store.setFrameworkContext( + SOURCE_MAPS_CONTEXT_KEYS.selectedVariant, + 'node', + ); + store.setFrameworkContext(SOURCE_MAPS_CONTEXT_KEYS.selectedPath, '.'); + return true; + default: + return false; + } +} + +function trace(program: ProgramId, integration: Integration | null) { + wizardCapture.mockClear(); + const store = createStore(program, integration); + const screens: string[] = []; + let stoppedOn: string | null = null; + for (let guard = 0; guard < 40; guard++) { + const screen = store.router.resolve(store.session); + screens.push(screen); + if (screen === ScreenId.Exit) break; + if (!advance(store, screen)) { + stoppedOn = screen; + break; + } + if (store.session.skillsComplete) break; + } + return { program, screens, stoppedOn, events: screenEvents() }; +} + +describe('flow traces per program', () => { + for (const config of PROGRAM_REGISTRY) { + it(`${config.id} (node)`, () => { + expect(trace(config.id, Integration.javascriptNode)).toMatchSnapshot(); + }); + } + + it('posthog-integration (nextjs, with a setup question)', () => { + expect(trace('posthog-integration', Integration.nextjs)).toMatchSnapshot(); + }); + + it('posthog-integration (no framework detected)', () => { + expect(trace('posthog-integration', null)).toMatchSnapshot(); + }); +}); + +describe('headless walk analytics', () => { + for (const program of ['posthog-integration', 'audit'] as ProgramId[]) { + it(`${program}: run phases without a TUI`, () => { + wizardCapture.mockClear(); + const store = new WizardStore(program); + setUI(new InkUI(store)); + store.session = buildSession({ installDir: '/app', ci: true }); + store.setRunPhase(RunPhase.Running); + store.setOutroData({ kind: OutroKind.Success, message: 'done' }); + store.setRunPhase(RunPhase.Completed); + expect({ + program, + screen: store.router.resolve(store.session), + events: screenEvents(), + }).toMatchSnapshot(); + }); + } +}); diff --git a/src/lib/programs/__tests__/post-auth-gates.test.ts b/src/lib/programs/__tests__/post-auth-gates.test.ts new file mode 100644 index 000000000..01ad25efd --- /dev/null +++ b/src/lib/programs/__tests__/post-auth-gates.test.ts @@ -0,0 +1,26 @@ +/** + * Golden of the post-auth gate ids the agent runner awaits per program + * (the walk at runner/shared/bootstrap.ts between the `auth` and `run` steps). + */ +import { PROGRAM_REGISTRY } from '../program-registry'; + +function legacyPostAuthGateIds( + steps: (typeof PROGRAM_REGISTRY)[number]['steps'], +): string[] { + const authIndex = steps.findIndex((s) => s.screenId === 'auth'); + const runIndex = steps.findIndex((s) => s.screenId === 'run'); + if (authIndex === -1 || runIndex <= authIndex) return []; + return steps + .slice(authIndex + 1, runIndex) + .filter((s) => s.gate) + .map((s) => s.id); +} + +describe('post-auth gate ids per program', () => { + it('match the golden', () => { + const gates = Object.fromEntries( + PROGRAM_REGISTRY.map((c) => [c.id, legacyPostAuthGateIds(c.steps)]), + ); + expect(gates).toMatchSnapshot(); + }); +}); diff --git a/src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-120x40.txt new file mode 100644 index 000000000..a507eb1ba --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's run the unknown skill. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/agent-skill-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-120x40.txt new file mode 100644 index 000000000..dee366aea --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's instrument your LLM calls with PostHog AI Observability. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/ai-observability-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-120x40.txt new file mode 100644 index 000000000..9cb0e0106 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + PostHog Setup Wizard + + ⚠ PostHog AI services are disabled for your organization + + The wizard uses Anthropic Claude. To proceed, enable "Enable PostHog + features that use third-party AI services" in your organization + settings. + + http://localhost:8010/project/42/settings/organization-details#organization-ai-consent + + + [O] Open settings in browser + [S] Show how to use your own AI + [R] Retry (after the toggle is enabled) + [E] Exit + + + + + + + + + + + + + + + + + + + + O open settings S show skill R retry E exit diff --git a/src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/ai-opt-in-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/audit-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/audit-intro-120x40.txt new file mode 100644 index 000000000..8132aed5d --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/audit-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's review your existing PostHog setup for best practices. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/audit-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/audit-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/audit-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/audit-outro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/audit-outro-120x40.txt new file mode 100644 index 000000000..9727f363f --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/audit-outro-120x40.txt @@ -0,0 +1,17 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ✔ PostHog is set up! + + Report saved to: + /app/posthog-setup-report.md + A markdown file in your project folder. Open it in any editor to read the full audit. + + Items audited: + 14 checks · 0 errors · 0 warnings · 0 suggestions + • No issues found. + + Learn more: https://posthog.com/docs + + Press any key to continue diff --git a/src/ui/tui/__tests__/__snapshots__/frames/audit-outro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/audit-outro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/audit-outro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/audit-run-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/audit-run-120x40.txt new file mode 100644 index 000000000..12c5a9ca5 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/audit-run-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Verifying installation Checks + + ┌──────────────────────────────────────────────────────┐ ⠋ Installation (0/4) + │ app boot │ ◌ PostHog SDK installed + │ ▼ posthog.init(...) once │ ◌ SDK version up to date + │ │ │ ◌ Initialization is correct + │ ▼ posthog.capture('pageview') │ ◌ One initialization per runtime + │ posthog.capture('signup') │ + │ posthog.capture('purchase') │ Identification (0/4) + └──────────────────────────────────────────────────────┘ ◌ Stable distinct_id (not session UUID) + ◌ identify() called before captures / flag evals + PostHog releases frequent SDK updates to fix bugs and ◌ Same distinct_id across client and server + add new features. We're checking your project's SDK ◌ reset() called on logout / account switch + version and making sure it's up to date. + Event Capture (0/3) + We're also checking that your SDK is initialized ◌ Event names are static and consistent + correctly and in the right part of your app's lifecycle. ◌ Captures route through a reverse proxy + ◌ Key activation events captured + This ensures you won't miss any autocaptured events. + Live Data (0/1) + [O] Learn more ◌ Open findings in PostHog + + Write report (0/1) + ◌ Create posthog-audit-report.md + + Upload notebook (0/1) + ◌ Write the report into a PostHog notebook + + + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ◆ Reviewing autocapture coverage + + Status Audit plan Tail logs HN + + ←→ switch tab s toggle status diff --git a/src/ui/tui/__tests__/__snapshots__/frames/audit-run-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/audit-run-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/audit-run-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/auth-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/auth-120x40.txt new file mode 100644 index 000000000..694cc94f1 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/auth-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + PostHog Setup Wizard + + Privacy & data + • Source files are read by Claude for AI context + • .env* and secrets stay on your machine + • Press [I] for full privacy & usage info + + ⠋ Waiting for authentication... + + If the browser didn't open, copy and paste this URL: + + https://us.posthog.com/login?next=/oauth/authorize + + Press [C] to copy the link · on a remote machine or devbox? Press [P] to paste the callback URL. + + + + + + + + + + + + + + + + + + + + + P paste auth code C copy link I privacy & data diff --git a/src/ui/tui/__tests__/__snapshots__/frames/auth-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/auth-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/auth-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/auth-error-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/auth-error-120x40.txt new file mode 100644 index 000000000..5629b373b --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/auth-error-120x40.txt @@ -0,0 +1,18 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ✘ Authentication error + + The Wizard couldn't connect to the PostHog LLM Gateway. Claude Code settings on this machine override the Wizard's + credentials. + + • /app/.claude/settings.json sets ANTHROPIC_BASE_URL + + Remove those keys from the file(s) above, or log out of Claude Code, then re-run the Wizard: + + claude auth logout + + Verbose log: /tmp/posthog-wizard.log + + Press any key to exit diff --git a/src/ui/tui/__tests__/__snapshots__/frames/auth-error-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/auth-error-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/auth-error-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-120x40.txt new file mode 100644 index 000000000..0ec73cc4e --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + PostHog Doctor + Scan your project configuration for issues that may need attention. + + The wizard will: + • Sign you in to PostHog + • Fetch active health issues for your project + • Show you what needs to be resolved, with docs links + + ▸ Continue + Cancel + + + + + + + + + + + + + + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/doctor-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/doctor-report-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/doctor-report-120x40.txt new file mode 100644 index 000000000..8b2c637da --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/doctor-report-120x40.txt @@ -0,0 +1,16 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + PostHog Doctor Report + Project 42 • https://us.posthog.com + + 2 active issues: 1 critical, 1 warning + + Critical (1) + ◼ Ingestion is delayed https://posthog.com/docs/support/troubleshooting + + Warning (1) + ◼ SDK version is out of date https://posthog.com/docs/libraries + + ▸ Continue diff --git a/src/ui/tui/__tests__/__snapshots__/frames/doctor-report-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/doctor-report-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/doctor-report-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-120x40.txt new file mode 100644 index 000000000..66d32065c --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-120x40.txt @@ -0,0 +1,9 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Detecting your project... + + ⠋ Scanning the repo for frameworks and PostHog SDKs... + + Starting up the detection agent… diff --git a/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-detect-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-120x40.txt new file mode 100644 index 000000000..545a8bb2b --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's make uncaught errors reach PostHog with readable stack traces. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/error-tracking-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/exit-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/exit-120x40.txt new file mode 100644 index 000000000..48456b7b9 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/exit-120x40.txt @@ -0,0 +1,3 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide diff --git a/src/ui/tui/__tests__/__snapshots__/frames/exit-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/exit-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/exit-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/health-check-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/health-check-120x40.txt new file mode 100644 index 000000000..c8a2f4974 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/health-check-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + ╭──────────────────────────────────────────────────────────────────────╮ + │ │ + │ Ongoing service disruptions │ + │ │ + │ ◼ Down ◼ Degraded ◼ No connection │ + │ │ + │ ◼ Skills download │ + │ │ + │ The Wizard can't download the skills it needs — neither GitHub │ + │ Releases nor PostHog's mirror is reachable right now. │ + │ │ + │ ──────────────────────────────────────────────────────────────── │ + │ │ + │ │ + │ ▸ Exit [Esc] │ + │ │ + ╰──────────────────────────────────────────────────────────────────────╯ + + + + + + + + + + ←→ switch esc cancel enter confirm diff --git a/src/ui/tui/__tests__/__snapshots__/frames/health-check-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/health-check-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/health-check-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/intro-120x40.txt new file mode 100644 index 000000000..724976efa --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + Review what data is shared in "Privacy & data." + .env* values stay on your machine. + + Let's do two hours of work in eight minutes. + + Directory ✔ /app + Framework ✔ Next.js (detected) + + ▸ Continue + Change framework + More info + Privacy & data + Cancel + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/keep-skills-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/keep-skills-120x40.txt new file mode 100644 index 000000000..56e20ea52 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/keep-skills-120x40.txt @@ -0,0 +1,7 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Keep the skills? + + Checking installed skills... diff --git a/src/ui/tui/__tests__/__snapshots__/frames/keep-skills-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/keep-skills-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/keep-skills-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/managed-settings-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/managed-settings-120x40.txt new file mode 100644 index 000000000..0d95de9a7 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/managed-settings-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + ╭──────────────────────────────────────────────────────────────────────╮ + │ │ + │ ⚠ Settings conflict │ + │ │ + │ These Claude Code settings override credentials and prevent the │ + │ Wizard from reaching the PostHog LLM Gateway. │ + │ │ + │ Organization-managed settings │ + │ /Library/Application Support/ClaudeCode/managed-settings.json │ + │ • ANTHROPIC_BASE_URL │ + │ │ + │ Remove these keys (or run "claude auth logout"). Managed files │ + │ are root-owned — ask your IT administrator. │ + │ │ + │ ──────────────────────────────────────────────────────────────── │ + │ │ + │ Fix the file(s) above, then re-run the Wizard. │ + │ │ + │ ▸ Exit [Esc] │ + │ │ + ╰──────────────────────────────────────────────────────────────────────╯ + + + + + + + + ←→ switch esc cancel enter confirm diff --git a/src/ui/tui/__tests__/__snapshots__/frames/managed-settings-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/managed-settings-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/managed-settings-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-120x40.txt new file mode 100644 index 000000000..41a786da6 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-120x40.txt @@ -0,0 +1,11 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ◆ Paste authorization code + + After authorizing, paste the callback URL it lands on — or just the code from it — here: + + http://localhost:8239/callback?code=… or the code + + ENTER submit · ESC cancel diff --git a/src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/manual-auth-code-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-120x40.txt new file mode 100644 index 000000000..782547112 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-120x40.txt @@ -0,0 +1,15 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Install the MCP so you can chat to your data + + • Ask your agent: "List my feature flags" — and it does. + • Run SQL, build dashboards, ship flags, all from your IDE. + • No copy-pasting tokens or context. Your agent has the keys. + + Detected: Claude Code, Cursor + + Install the PostHog MCP server and plugin? + + ▸ Install No thanks diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-add-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-add-120x40.txt new file mode 100644 index 000000000..782547112 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-add-120x40.txt @@ -0,0 +1,15 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Install the MCP so you can chat to your data + + • Ask your agent: "List my feature flags" — and it does. + • Run SQL, build dashboards, ship flags, all from your IDE. + • No copy-pasting tokens or context. Your agent has the keys. + + Detected: Claude Code, Cursor + + Install the PostHog MCP server and plugin? + + ▸ Install No thanks diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-add-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-add-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-add-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-120x40.txt new file mode 100644 index 000000000..e725c3cc8 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-120x40.txt @@ -0,0 +1,11 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Remove the PostHog MCP + + Detected: Claude Code, Cursor + + Remove the PostHog MCP server and plugin? + + ▸ Remove No thanks diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-remove-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-120x40.txt new file mode 100644 index 000000000..89e26e46e --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + PostHog MCP + + With MCP your agent works directly with the PostHog platform. You can prompt it to: + + ◆ Build dashboards + ◆ Run SQL queries + ◆ Deploy feature flags + ◆ Debug exceptions and errors + ◆ And lots more... + + Want a live demo using real data from your project? + ▸ Start MCP tutorial + Exit + + + + + + + + + + + + + + + + + + + + + ↑↓ navigate p pick new prompt esc exit enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mcp-suggested-prompts-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-120x40.txt new file mode 100644 index 000000000..ac846908d --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's instrument your service with PostHog application metrics. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/metrics-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/migration-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/migration-intro-120x40.txt new file mode 100644 index 000000000..70e454685 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/migration-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's migrate this project to PostHog. + + Directory ✔ /app + + ▸ Continue + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/migration-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/migration-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/migration-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mint-failure-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/mint-failure-120x40.txt new file mode 100644 index 000000000..8a6356d08 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mint-failure-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + + + ██ The Wizard's a little busy 🦔 + + The wizard can still share spells. Grab a skill and let your agent take over. + + ▸ Save skill + Open in Claude Code + Open in Codex + Report this issue + Exit + + + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/mint-failure-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/mint-failure-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/mint-failure-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/outro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/outro-120x40.txt new file mode 100644 index 000000000..acc954cc4 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/outro-120x40.txt @@ -0,0 +1,20 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ✔ PostHog is set up! + + Events will start flowing once you run the app. + + Check ./posthog-setup-report.md for details + + What the agent did: + • Added posthog-js + • Wired the provider + + Learn more: https://posthog.com/docs?utm_source=wizard&utm_medium=cli&utm_content=outro-docs + + Note: This wizard uses an LLM agent to analyze and modify your project. Please review the changes made. + How did this work for you? Drop us a line: wizard@posthog.com + + Press any key to continue diff --git a/src/ui/tui/__tests__/__snapshots__/frames/outro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/outro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/outro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/port-conflict-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/port-conflict-120x40.txt new file mode 100644 index 000000000..8de35ce48 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/port-conflict-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + ╭──────────────────────────────────────────────────────────────────────╮ + │ │ + │ OAuth ports in use │ + │ │ + │ The wizard needs a local port for OAuth. We tried these ports │ + │ which are all in use: │ + │ │ + │ Port 8239 │ + │ Port 8238 │ + │ Port 8240 │ + │ Port 8237 │ + │ Port 8236 │ + │ Port 8235 │ + │ │ + │ Please free one of these ports and retry. │ + │ │ + │ ──────────────────────────────────────────────────────────────── │ + │ │ + │ Retry after freeing ports? │ + │ │ + │ ▸ Retry [Enter] Exit [Esc] │ + │ │ + ╰──────────────────────────────────────────────────────────────────────╯ + + + + + + + ←→ switch esc cancel enter confirm diff --git a/src/ui/tui/__tests__/__snapshots__/frames/port-conflict-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/port-conflict-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/port-conflict-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-120x40.txt new file mode 100644 index 000000000..18b366381 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's create revenue analytics with Stripe and PostHog. + + Link purchases to product data. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/revenue-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/run-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/run-120x40.txt new file mode 100644 index 000000000..4fa1590f9 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/run-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Learn Tasks + + ◼ Install posthog-js + ▶ Wiring the provider + ◻ Capture a test event + + ⠋ Progress: 1/3 completed + + + + + + + + + + + + + + + + + + + + + + + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ◆ Editing app/layout.tsx + + Status Event plan Tail logs Visualizer HN + + ←→ switch tab s toggle status diff --git a/src/ui/tui/__tests__/__snapshots__/frames/run-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/run-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/run-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-120x40.txt new file mode 100644 index 000000000..dac2e9023 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + Connect GitHub + + Self-driving needs GitHub access to research findings in your code and open fixes, so setup cannot finish without it. + + ◆ Grant it the repos you want Self-driving to work with — include this project's repo so it can also watch its issues. + + Install it: https://us.posthog.com/api/environments/42/integrations/authorize?kind=github + + ▸ Open GitHub App install + I can't connect right now + + + + + + + + + + + + + + + + + + + + + + + + ↑↓ navigate esc end setup enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-github-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-120x40.txt new file mode 100644 index 000000000..5fba8e669 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ✔ PostHog is installed + + Now let's make your product self-driving. + + Next, the agent connects GitHub, turns on signal sources, and tunes the scouts that watch your product data. + + About 10 more minutes, and it needs your input a few times. Keep this terminal open. + + ▸ Set up Self-driving [Enter] + + You can find your PostHog integration report at ./posthog-setup-report.md + + + + + + + + + + + + + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-handoff-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-120x40.txt new file mode 100644 index 000000000..402292cfb --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + No PostHog integration found + + This will kick off an agent to explore your project and find existing PostHog integrations. Self-driving reads PostHog + data, so we'll set that up first: it gives your signal sources something to watch. To do that we need to connect to + PostHog — do you already have an account? + + ▸ Yes, log me in (opens PostHog to authorize) + No, create one for me (we'll email you a login link) + + [I] Privacy & data + + + + + + + + + + + + + + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-check-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-120x40.txt new file mode 100644 index 000000000..ad8b12c10 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-120x40.txt @@ -0,0 +1,9 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Detecting your project. Stick around, this will take a few seconds... + + ⠋ Scanning the repo for frameworks and PostHog SDKs... + + Starting up the detection agent… diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-integration-detect-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-120x40.txt new file mode 100644 index 000000000..47914a5eb --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use .env* file contents will not leave your machine.lf-driving. + + + Let's set up PostHog Self-driving. + + PostHog watches how people really use your product, finds issues, and proposes fixes. + + An agent wAbout 10 minutes, with a few questions along the way.our scouts. + Agents charge a flat $15 per pull request they ship. + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/self-driving-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/session-timeout-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/session-timeout-120x40.txt new file mode 100644 index 000000000..ad1add63d --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/session-timeout-120x40.txt @@ -0,0 +1,11 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ✘ Login timed out + + The OAuth link timed out after 30 minutes. + + Re-run the wizard to get a fresh link and try again. + + Press any key to exit diff --git a/src/ui/tui/__tests__/__snapshots__/frames/session-timeout-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/session-timeout-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/session-timeout-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/settings-override-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/settings-override-120x40.txt new file mode 100644 index 000000000..67fa2fb3e --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/settings-override-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + ╭──────────────────────────────────────────────────────────────╮ + │ │ + │ ⚠ Settings conflict │ + │ │ + │ Your settings file at /app/.claude/settings.json sets: │ + │ • ANTHROPIC_BASE_URL │ + │ • apiKeyHelper │ + │ │ + │ These settings override credentials and prevent the │ + │ Wizard from reaching the PostHog LLM Gateway. We can │ + │ back up the file and continue. │ + │ │ + │ ──────────────────────────────────────────────────────── │ + │ │ + │ Back up to .wizard-backup and continue? │ + │ │ + │ ▸ Backup & continue [Enter] Exit [Esc] │ + │ │ + ╰──────────────────────────────────────────────────────────────╯ + + + + + + + + + ←→ switch esc cancel enter confirm diff --git a/src/ui/tui/__tests__/__snapshots__/frames/settings-override-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/settings-override-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/settings-override-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/setup-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/setup-120x40.txt new file mode 100644 index 000000000..59b1eb3f8 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/setup-120x40.txt @@ -0,0 +1,10 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Project Setup + Configuring Next.js integration + + Which Next.js router are you using? + ▸ App Router + Pages Router diff --git a/src/ui/tui/__tests__/__snapshots__/frames/setup-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/setup-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/setup-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/slack-connect-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/slack-connect-120x40.txt new file mode 100644 index 000000000..4f524734d --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/slack-connect-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + @PostHog in Slack + + Ask about your product data, debug issues, and generate PRs without leaving the thread. + + ◆ Tag @PostHog with a bug, edit, or a feature idea. It will spin up a sandboxed environment, plan, edit files, run + tests, and open a draft PR. + + ◆ Tag @PostHog with any data question. It's the same SQL-writing, statistically-minded assistant as PostHog AI, but it + responds where you send work memes. + + Connect it: + https://app.posthog.com/integrations/slack?utm_source=wizard&utm_medium=cli&utm_content=slack-connect-setup + Learn more: https://posthog.com/slack?utm_source=wizard&utm_medium=cli&utm_content=slack-connect-learn-more + + ▸ Open Slack setup + Skip / Continue + + + + + + + + + + + + + + + + + + ↑↓ navigate esc skip enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/slack-connect-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/slack-connect-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/slack-connect-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-120x40.txt new file mode 100644 index 000000000..66d32065c --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-120x40.txt @@ -0,0 +1,9 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + Detecting your project... + + ⠋ Scanning the repo for frameworks and PostHog SDKs... + + Starting up the detection agent… diff --git a/src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-detect-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-120x40.txt new file mode 100644 index 000000000..c65253abf --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + The Wizard will run an agent to detect your project's + framework(s), download the relevant docs, and implement + source-map uploads for you. + + Ready? + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-120x40.txt new file mode 100644 index 000000000..e899fff37 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-120x40.txt @@ -0,0 +1,24 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + ✔ PostHog is set up! + + What the wizard did: + • Installed the packages needed for source map upload. + • Wrote the PostHog upload credentials to your .env file. + + How uploads work now: + • Every build now uploads source maps to PostHog automatically — no extra command to remember. + • Run your app from the built output. Source maps only resolve for errors thrown by the build that was uploaded. + • In CI, the build job needs the same upload credentials. If the wizard wired your pipeline, add the referenced + secrets to your CI provider (e.g. GitHub repo secrets) before your next deploy. + + Details in /app/posthog-setup-report.md + + Learn more: https://posthog.com/docs + + Note: This wizard uses an LLM agent to analyze and modify your project. Please review the changes made. + How did this work for you? Drop us a line: wizard@posthog.com + + Press any key to continue diff --git a/src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/source-maps-outro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/task-notice-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/task-notice-120x40.txt new file mode 100644 index 000000000..367dd053c --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/task-notice-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + ╭──────────────────────────────────────────────────────────────────────────╮ + │ │ + │ Connect your data sources │ + │ │ + │ We detected warehouse sources in this project. │ + │ │ + │ Postgres, Stripe │ + │ │ + │ ──────────────────────────────────────────────────────────────────── │ + │ │ + │ Connect these during setup? │ + │ │ + │ ▸ Continue [Enter] Skip [Esc] │ + │ │ + ╰──────────────────────────────────────────────────────────────────────────╯ + + + + + + + + + + + ←→ switch esc cancel enter confirm diff --git a/src/ui/tui/__tests__/__snapshots__/frames/task-notice-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/task-notice-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/task-notice-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-120x40.txt new file mode 100644 index 000000000..4d35faa52 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + ███ PostHog Wizard 🦔 + + We'll use AI to analyze your project and complete work. + .env* file contents will not leave your machine. + + Let's connect your data to PostHog's data warehouse. + + Directory ✔ /app + + ▸ Continue + More info + Privacy & data + Cancel + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/warehouse-intro-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-120x40.txt b/src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-120x40.txt new file mode 100644 index 000000000..a176b216c --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-120x40.txt @@ -0,0 +1,39 @@ + PostHog Wizard v0.0.0-test Feedback: wizard@posthog.com + + Cost (running): $0.00 · no agent turns yet Ctrl+T to hide + + + + + + + + + + + + + ╭──────────────────────────────────────────────────────────────────────╮ + │ │ + │ ◆ integration-nextjs │ + │ │ + │ Which router does this app use? │ + │ │ + │ ▸ App Router │ + │ Pages Router │ + │ │ + │ ESC skip │ + │ │ + ╰──────────────────────────────────────────────────────────────────────╯ + + + + + + + + + + + + ↑↓ navigate enter select diff --git a/src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-60x15.txt b/src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-60x15.txt new file mode 100644 index 000000000..cebaf5c47 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/frames/wizard-ask-60x15.txt @@ -0,0 +1,9 @@ + + + + + + The Wizard needs more room to display required text. + Please make this terminal bigger. + + Currently 60×15, needs at least 80×28. diff --git a/src/ui/tui/__tests__/__snapshots__/keyboard-equivalence.test.tsx.snap b/src/ui/tui/__tests__/__snapshots__/keyboard-equivalence.test.tsx.snap new file mode 100644 index 000000000..c9b7a24f5 --- /dev/null +++ b/src/ui/tui/__tests__/__snapshots__/keyboard-equivalence.test.tsx.snap @@ -0,0 +1,241 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`keyboard commit vs control action commit > audit-outro: any key vs dismiss_outro 1`] = ` +{ + "action": { + "outroDismissed": true, + "screen": "keep-skills", + }, + "equal": false, + "keyboard": { + "outroDismissed": true, + "screen": "keep-skills", + "skillsComplete": true, + }, +} +`; + +exports[`keyboard commit vs control action commit > intro: enter on Continue vs confirm_setup 1`] = ` +{ + "action": { + "screen": "health-check", + "setupConfirmed": true, + }, + "equal": false, + "keyboard": { + "scanConsent": "granted", + "screen": "health-check", + "setupConfirmed": true, + "warehouseSourcesReported": true, + }, +} +`; + +exports[`keyboard commit vs control action commit > keep-skills: mount with no skills dir vs keep_skills 1`] = ` +{ + "action": { + "skillsComplete": true, + }, + "equal": false, + "keyboard": {}, +} +`; + +exports[`keyboard commit vs control action commit > manual-auth-code: escape vs dismiss_auth_code 1`] = ` +{ + "action": { + "overlay": false, + "screen": "auth", + }, + "equal": true, + "keyboard": { + "overlay": false, + "screen": "auth", + }, +} +`; + +exports[`keyboard commit vs control action commit > manual-auth-code: paste code and enter vs submit_auth_code 1`] = ` +{ + "action": { + "overlay": false, + "screen": "auth", + }, + "equal": true, + "keyboard": { + "overlay": false, + "screen": "auth", + }, +} +`; + +exports[`keyboard commit vs control action commit > mcp: decline install vs set_mcp_outcome skipped 1`] = ` +{ + "action": { + "mcpComplete": true, + "mcpOutcome": "skipped", + "screen": "slack-connect", + }, + "equal": false, + "keyboard": { + "mcpComplete": true, + "mcpOutcome": "skipped", + "screen": "slack-connect", + "slackConnected": false, + }, +} +`; + +exports[`keyboard commit vs control action commit > outro: any key vs dismiss_outro 1`] = ` +{ + "action": { + "outroDismissed": true, + "screen": "mcp", + }, + "equal": true, + "keyboard": { + "outroDismissed": true, + "screen": "mcp", + }, +} +`; + +exports[`keyboard commit vs control action commit > port-conflict: enter vs resolve_port_conflict 1`] = ` +{ + "action": { + "overlay": false, + "portConflictProcess": null, + "screen": "auth", + }, + "equal": true, + "keyboard": { + "overlay": false, + "portConflictProcess": null, + "screen": "auth", + }, +} +`; + +exports[`keyboard commit vs control action commit > self-driving-handoff: enter vs confirm_self_driving_handoff 1`] = ` +{ + "action": { + "screen": "self-driving-github", + "selfDrivingHandoffConfirmed": true, + }, + "equal": true, + "keyboard": { + "screen": "self-driving-github", + "selfDrivingHandoffConfirmed": true, + }, +} +`; + +exports[`keyboard commit vs control action commit > self-driving-integration-check: log me in vs set_integrate true 1`] = ` +{ + "action": { + "integrate": true, + "screen": "health-check", + }, + "equal": true, + "keyboard": { + "integrate": true, + "screen": "health-check", + }, +} +`; + +exports[`keyboard commit vs control action commit > setup: enter on first router option vs choose 1`] = ` +{ + "action": { + "frameworkContext": { + "router": "app-router", + }, + "screen": "auth", + }, + "equal": true, + "keyboard": { + "frameworkContext": { + "router": "app-router", + }, + "screen": "auth", + }, +} +`; + +exports[`keyboard commit vs control action commit > slack-connect: skip vs dismiss_slack 1`] = ` +{ + "action": { + "screen": "keep-skills", + "slackStepDismissed": true, + }, + "equal": false, + "keyboard": { + "screen": "keep-skills", + "skillsComplete": true, + "slackStepDismissed": true, + }, +} +`; + +exports[`keyboard commit vs control action commit > source-maps-outro: any key vs dismiss_outro 1`] = ` +{ + "action": { + "outroDismissed": true, + "screen": "keep-skills", + }, + "equal": false, + "keyboard": { + "outroDismissed": true, + "screen": "keep-skills", + "skillsComplete": true, + }, +} +`; + +exports[`keyboard commit vs control action commit > task-notice: enter vs resolve_notice keep 1`] = ` +{ + "action": { + "overlay": false, + "screen": "intro", + "taskNotice": null, + }, + "equal": true, + "keyboard": { + "overlay": false, + "screen": "intro", + "taskNotice": null, + }, +} +`; + +exports[`keyboard commit vs control action commit > task-notice: escape vs resolve_notice decline 1`] = ` +{ + "action": { + "overlay": false, + "screen": "intro", + "taskNotice": null, + }, + "equal": true, + "keyboard": { + "overlay": false, + "screen": "intro", + "taskNotice": null, + }, +} +`; + +exports[`keyboard commit vs control action commit > wizard-ask: type answer and enter vs answer_question 1`] = ` +{ + "action": { + "overlay": false, + "pendingQuestion": null, + "screen": "intro", + }, + "equal": true, + "keyboard": { + "overlay": false, + "pendingQuestion": null, + "screen": "intro", + }, +} +`; diff --git a/src/ui/tui/__tests__/frames.test.tsx b/src/ui/tui/__tests__/frames.test.tsx new file mode 100644 index 000000000..bd7f5ca91 --- /dev/null +++ b/src/ui/tui/__tests__/frames.test.tsx @@ -0,0 +1,587 @@ +// Behaviour baseline: every ScreenId and Overlay through real Ink at two sizes. +// Every fixture arranges session state until the router resolves to its target, +// so each frame carries the full ScreenContainer chrome. No screen needs a +// bare, router-bypassing render. + +import { vi, it, expect, describe, beforeAll, afterEach } from 'vitest'; +import { cleanup } from 'ink-testing-library'; + +vi.mock('ink', () => + vi.importActual('../../../../node_modules/ink/build/index.js'), +); + +const { pending } = vi.hoisted(() => ({ + pending: () => new Promise(() => undefined), +})); + +vi.mock('opn', () => ({ default: vi.fn(pending) })); +vi.mock('@utils/analytics', () => ({ + analytics: { + capture: vi.fn(), + wizardCapture: vi.fn(), + captureException: vi.fn(), + setTag: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }, + sessionProperties: vi.fn(() => ({})), +})); +vi.mock('@utils/clipboard', () => ({ + copyToClipboard: vi.fn().mockResolvedValue(true), + openInBrowser: vi.fn().mockResolvedValue(true), + browserOpenCommands: vi.fn(() => []), +})); +vi.mock('@utils/links', async (actual) => ({ + ...(await actual>()), + openTrackedLink: vi.fn(), +})); +vi.mock('@utils/debug', async (actual) => ({ + ...(await actual>()), + getLogFilePath: () => '/tmp/posthog-wizard.log', + logToFile: vi.fn(), + debug: vi.fn(), +})); +vi.mock('@utils/setup-utils', async (actual) => ({ + ...(await actual>()), + getOrAskForProjectData: vi.fn(pending), +})); +vi.mock('@lib/api', async (actual) => ({ + ...(await actual>()), + fetchUserData: vi.fn(pending), + fetchSlackConnected: vi.fn(pending), + fetchGithubConnected: vi.fn(pending), +})); +vi.mock('@lib/wizard-tools', async (actual) => ({ + ...(await actual>()), + fetchSkillMenu: vi.fn(pending), + downloadSkill: vi.fn(pending), +})); +vi.mock('@ui/tui/hooks/useGithubConnection', () => ({ + useGithubConnection: () => undefined, + fetchLoginUrl: vi.fn().mockResolvedValue(null), +})); +vi.mock('@lib/programs/self-driving/detect-agentic', async (actual) => ({ + ...(await actual>()), + detectSelfDrivingIntegrationProjects: vi.fn(pending), +})); +vi.mock('@lib/programs/error-tracking/detect-agentic', async (actual) => ({ + ...(await actual>()), + detectErrorTrackingProjects: vi.fn(pending), +})); +vi.mock( + '@lib/programs/error-tracking-upload-source-maps/detect-agentic', + async (actual) => ({ + ...(await actual>()), + detectSourceMapsProjects: vi.fn(pending), + }), +); +vi.mock('@lib/programs/posthog-doctor/fetch', () => ({ + fetchHealthIssues: vi.fn().mockResolvedValue([ + { + id: 'issue-1', + kind: 'ingestion_lag', + severity: 'critical', + status: 'active', + dismissed: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + { + id: 'issue-2', + kind: 'sdk_outdated', + severity: 'warning', + status: 'active', + dismissed: false, + created_at: '2026-01-01T00:00:00Z', + updated_at: '2026-01-01T00:00:00Z', + }, + ]), +})); + +import { WizardStore, TaskStatus, type ScreenName } from '@ui/tui/store'; +import { InkUI } from '@ui/tui/ink-ui'; +import { setUI } from '@ui/index'; +import { ScreenId, Overlay } from '@ui/tui/router'; +import { createServices, type ScreenServices } from '@ui/tui/screen-registry'; +import { + buildSession, + OutroKind, + RunPhase, + McpOutcome, +} from '@lib/wizard-session'; +import { HostResolution } from '@lib/host-resolution'; +import { Integration } from '@lib/constants'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import type { FrameworkConfig } from '@lib/framework-config'; +import { Program, type ProgramId } from '@lib/programs/program-registry'; +import { + WizardReadiness, + type WizardReadinessResult, +} from '@lib/health-checks/readiness'; +import { ServiceHealthStatus } from '@lib/health-checks/types'; +import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/detect'; +import { AUDIT_CHECKS_KEY } from '@lib/programs/audit/types'; +import { AUDIT_SEED_CHECKS } from '@lib/programs/audit/seed'; +import type { McpInstaller } from '@ui/tui/services/mcp-installer'; +import type { McpSuggestedPromptsServices } from '@ui/tui/services/mcp-suggested-prompts-services'; +import { + renderScreen, + screenShell, + type TerminalSize, +} from './helpers/render-screen.no-jest'; + +const SIZES: TerminalSize[] = [ + { columns: 120, rows: 40 }, + { columns: 60, rows: 15 }, +]; + +const CREDENTIALS = { + accessToken: 'phx_test_token', + projectApiKey: 'phc_test_key', + host: HostResolution.fromApiHost('https://us.posthog.com'), + projectId: 42, +}; + +const HEALTHY: WizardReadinessResult = { + decision: WizardReadiness.Yes, + health: { skillsOrigin: { status: ServiceHealthStatus.Healthy } }, + reasons: [], +}; + +const OUTAGE: WizardReadinessResult = { + decision: WizardReadiness.No, + health: { skillsOrigin: { status: ServiceHealthStatus.Down } }, + reasons: ['Skill downloads are unavailable.'], +}; + +const SUCCESS_OUTRO = { + kind: OutroKind.Success, + message: 'PostHog is set up!', + body: 'Events will start flowing once you run the app.', + changes: ['Added posthog-js', 'Wired the provider'], + reportFile: 'posthog-setup-report.md', + docsUrl: 'https://posthog.com/docs', +}; + +/** Next.js config with its setup question stubbed so detection never touches disk. */ +function staticFrameworkConfig(): FrameworkConfig { + const base = FRAMEWORK_REGISTRY[Integration.nextjs]; + const setup = base.metadata.setup; + if (!setup) return base; + return { + ...base, + metadata: { + ...base.metadata, + setup: { + ...setup, + questions: setup.questions.map((q) => ({ + ...q, + detect: () => Promise.resolve(null), + })), + }, + }, + }; +} + +const fakeInstaller: McpInstaller = { + detectClients: () => + Promise.resolve([ + { name: 'Claude Code', supportsPlugin: true, pluginBundlesMcp: false }, + { name: 'Cursor', supportsPlugin: false, pluginBundlesMcp: false }, + ]), + install: pending, + remove: pending, + installPlugins: pending, +}; + +const inertPromptsServices = { + performLogin: pending, + runPromptStreaming: () => ({ + [Symbol.asyncIterator]: () => ({ next: pending }), + }), + probeProjectData: pending, + seedDemoEvents: pending, +} as unknown as McpSuggestedPromptsServices; + +function makeStore(program: ProgramId): WizardStore { + const store = new WizardStore(program); + setUI(new InkUI(store)); + store.version = '0.0.0-test'; + store.session = buildSession({ installDir: '/app' }); + return store; +} + +function makeServices(store: WizardStore): ScreenServices { + return { + ...createServices(store), + mcpInstaller: fakeInstaller, + mcpSuggestedPromptsServices: inertPromptsServices, + }; +} + +function authed(store: WizardStore): void { + store.completeSetup(); + store.setReadinessResult(HEALTHY); + store.setCredentials(CREDENTIALS); +} + +function ranSuccessfully(store: WizardStore): void { + store.setRunPhase(RunPhase.Completed); + store.setOutroData(SUCCESS_OUTRO); +} + +interface Fixture { + program: ProgramId; + arrange?: (store: WizardStore) => void; +} + +const FIXTURES: Record = { + // ── Overlays ─────────────────────────────────────────────────── + [Overlay.SettingsOverride]: { + program: Program.PostHogIntegration, + arrange: (s) => + void s.showSettingsOverride( + [ + { + source: 'project', + path: '/app/.claude/settings.json', + keys: ['ANTHROPIC_BASE_URL', 'apiKeyHelper'], + writable: true, + }, + ], + () => true, + ), + }, + [Overlay.ManagedSettings]: { + program: Program.PostHogIntegration, + arrange: (s) => + void s.showSettingsOverride( + [ + { + source: 'managed', + path: '/Library/Application Support/ClaudeCode/managed-settings.json', + keys: ['ANTHROPIC_BASE_URL'], + writable: false, + }, + ], + () => true, + ), + }, + [Overlay.PortConflict]: { + program: Program.PostHogIntegration, + arrange: (s) => + void s.showPortConflict({ + command: 'node', + pid: '4242', + port: 8010, + user: 'wizard', + }), + }, + [Overlay.TaskNotice]: { + program: Program.PostHogIntegration, + arrange: (s) => + void s.showTaskNotice({ + title: 'Connect your data sources', + body: ['We detected warehouse sources in this project.'], + items: ['Postgres', 'Stripe'], + confirmLabel: 'Continue [Enter]', + cancelLabel: 'Skip [Esc]', + prompt: 'Connect these during setup?', + }), + }, + [Overlay.ManualAuthCode]: { + program: Program.PostHogIntegration, + arrange: (s) => { + s.completeSetup(); + s.setReadinessResult(HEALTHY); + s.showManualAuthCode(); + }, + }, + [Overlay.AuthError]: { + program: Program.PostHogIntegration, + arrange: (s) => + s.showAuthError({ + hasSettingsConflict: true, + conflicts: [ + { + source: 'project', + path: '/app/.claude/settings.json', + keys: ['ANTHROPIC_BASE_URL'], + writable: true, + }, + ], + credentialPlaces: ['ANTHROPIC_API_KEY in your shell profile'], + logFilePath: '/tmp/posthog-wizard.log', + }), + }, + [Overlay.SessionTimeout]: { + program: Program.PostHogIntegration, + arrange: (s) => s.showSessionTimeout(), + }, + [Overlay.WizardAsk]: { + program: Program.PostHogIntegration, + arrange: (s) => + void s.requestQuestion({ + id: 'q1', + source: 'integration-nextjs', + questions: [ + { + id: 'router', + prompt: 'Which router does this app use?', + kind: 'single', + options: [ + { label: 'App Router', value: 'app' }, + { label: 'Pages Router', value: 'pages' }, + ], + }, + ], + }), + }, + + // ── Program screens ──────────────────────────────────────────── + [ScreenId.Intro]: { + program: Program.PostHogIntegration, + arrange: (s) => { + s.setFrameworkConfig(Integration.nextjs, staticFrameworkConfig()); + s.setDetectedFramework('Next.js'); + s.setSkillId('nextjs'); + s.setDetectionComplete(); + }, + }, + [ScreenId.RevenueIntro]: { program: Program.RevenueAnalyticsSetup }, + [ScreenId.WarehouseIntro]: { program: Program.WarehouseSource }, + [ScreenId.SourceMapsIntro]: { + program: Program.ErrorTrackingUploadSourceMaps, + }, + [ScreenId.SourceMapsDetect]: { + program: Program.ErrorTrackingUploadSourceMaps, + arrange: (s) => { + s.completeSetup(); + s.setCredentials(CREDENTIALS); + }, + }, + [ScreenId.SourceMapsOutro]: { + program: Program.ErrorTrackingUploadSourceMaps, + arrange: (s) => { + s.completeSetup(); + s.setCredentials(CREDENTIALS); + s.setFrameworkContext(SOURCE_MAPS_CONTEXT_KEYS.selectedVariant, 'node'); + s.setFrameworkContext(SOURCE_MAPS_CONTEXT_KEYS.selectedPath, '.'); + ranSuccessfully(s); + }, + }, + [ScreenId.MigrationIntro]: { program: Program.Migration }, + [ScreenId.AgentSkillIntro]: { program: Program.AgentSkill }, + [ScreenId.AiObservabilityIntro]: { program: Program.AiObservability }, + [ScreenId.MetricsIntro]: { program: Program.Metrics }, + [ScreenId.ErrorTrackingIntro]: { program: Program.ErrorTracking }, + [ScreenId.ErrorTrackingDetect]: { + program: Program.ErrorTracking, + arrange: authed, + }, + [ScreenId.SelfDrivingIntro]: { program: Program.SelfDriving }, + [ScreenId.SelfDrivingIntegrationCheck]: { + program: Program.SelfDriving, + arrange: (s) => s.completeSetup(), + }, + [ScreenId.SelfDrivingIntegrationDetect]: { + program: Program.SelfDriving, + arrange: (s) => { + s.setIntegrate(true); + authed(s); + }, + }, + [ScreenId.SelfDrivingHandoff]: { + program: Program.SelfDriving, + arrange: (s) => { + s.setIntegrate(true); + authed(s); + s.setFrameworkConfig(Integration.nextjs, staticFrameworkConfig()); + s.completeRunStep('integrate-run'); + }, + }, + [ScreenId.SelfDrivingGithub]: { + program: Program.SelfDriving, + arrange: (s) => { + s.setIntegrate(true); + authed(s); + s.setFrameworkConfig(Integration.nextjs, staticFrameworkConfig()); + s.completeRunStep('integrate-run'); + s.confirmSelfDrivingHandoff(); + s.setGithubConnected(false); + }, + }, + [ScreenId.AuditIntro]: { program: Program.Audit }, + [ScreenId.AuditRun]: { + program: Program.Audit, + arrange: (s) => { + authed(s); + s.setFrameworkContext(AUDIT_CHECKS_KEY, AUDIT_SEED_CHECKS); + s.pushStatus('Reviewing autocapture coverage'); + }, + }, + [ScreenId.AuditOutro]: { + program: Program.Audit, + arrange: (s) => { + authed(s); + s.setFrameworkContext(AUDIT_CHECKS_KEY, AUDIT_SEED_CHECKS); + ranSuccessfully(s); + }, + }, + [ScreenId.HealthCheck]: { + program: Program.PostHogIntegration, + arrange: (s) => { + s.completeSetup(); + s.setReadinessResult(OUTAGE); + }, + }, + [ScreenId.DoctorIntro]: { program: Program.PosthogDoctor }, + [ScreenId.DoctorReport]: { + program: Program.PosthogDoctor, + arrange: authed, + }, + [ScreenId.Setup]: { + program: Program.PostHogIntegration, + arrange: (s) => { + s.setFrameworkConfig(Integration.nextjs, staticFrameworkConfig()); + s.completeSetup(); + s.setReadinessResult(HEALTHY); + }, + }, + [ScreenId.Auth]: { + program: Program.PostHogIntegration, + arrange: (s) => { + s.completeSetup(); + s.setReadinessResult(HEALTHY); + s.setLoginUrl('https://us.posthog.com/login?next=/oauth/authorize'); + }, + }, + [ScreenId.AiOptIn]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + s.setApiUser(apiUser(false)); + }, + }, + [ScreenId.Run]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + s.setRunPhase(RunPhase.Running); + s.syncTodos([ + { content: 'Install posthog-js', status: TaskStatus.Completed }, + { + content: 'Wire the provider', + status: TaskStatus.InProgress, + activeForm: 'Wiring the provider', + }, + { content: 'Capture a test event', status: TaskStatus.Pending }, + ]); + s.setEventPlan([ + { name: 'signup_completed', description: 'A new account was created' }, + ]); + s.pushStatus('Editing app/layout.tsx'); + }, + }, + [ScreenId.Mcp]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + ranSuccessfully(s); + s.setOutroDismissed(); + }, + }, + [ScreenId.McpSuggestedPrompts]: { program: Program.McpTutorial }, + [ScreenId.SlackConnect]: { + program: Program.SlackConnect, + arrange: (s) => { + s.setCredentials(CREDENTIALS); + s.setSlackConnected(false); + }, + }, + [ScreenId.KeepSkills]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + ranSuccessfully(s); + s.setOutroDismissed(); + s.setMcpComplete(McpOutcome.Skipped); + s.setSlackStepDismissed(); + }, + }, + [ScreenId.Outro]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + ranSuccessfully(s); + s.setDashboardUrl('https://us.posthog.com/project/42/dashboard/7'); + }, + }, + [ScreenId.MintFailure]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + s.setRunPhase(RunPhase.Error); + s.setOutroData({ + kind: OutroKind.Error, + message: 'The agent run failed', + }); + }, + }, + [ScreenId.Exit]: { + program: Program.PostHogIntegration, + arrange: (s) => { + authed(s); + s.setRunPhase(RunPhase.Error); + s.setOutroData({ + kind: OutroKind.Error, + message: 'The agent run failed', + }); + s.setMintHandoff('exit'); + }, + }, + [ScreenId.McpAdd]: { program: Program.McpAdd }, + [ScreenId.McpRemove]: { program: Program.McpRemove }, +}; + +/** Only the org's AI consent and membership level drive the gate screen. */ +function apiUser(approved: boolean): WizardStore['session']['apiUser'] { + return { + distinct_id: 'user-1', + team: { id: 42, organization: '00000000-0000-0000-0000-000000000000' }, + organization: { + id: '00000000-0000-0000-0000-000000000000', + name: 'Hedgehogs Inc', + membership_level: 8, + is_ai_data_processing_approved: approved, + }, + } as unknown as WizardStore['session']['apiUser']; +} + +beforeAll(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-01-01T00:00:00Z')); + vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + vi.spyOn(Math, 'random').mockReturnValue(0.42); +}); + +afterEach(cleanup); + +describe.each(Object.entries(FIXTURES))('%s', (name, fixture) => { + it.each(SIZES)(`at $columns x $rows`, async (size) => { + const screen = name as ScreenName; + const store = makeStore(fixture.program); + fixture.arrange?.(store); + expect(store.currentScreen).toBe(screen); + + const { frame } = await renderScreen( + store, + screenShell(store, makeServices(store)), + size, + ); + await expect(frame).toMatchFileSnapshot(snapshotPath(screen, size)); + }); +}); + +function snapshotPath(screen: ScreenName, size: TerminalSize): string { + return `__snapshots__/frames/${screen}-${size.columns}x${size.rows}.txt`; +} diff --git a/src/ui/tui/__tests__/helpers/render-screen.no-jest.tsx b/src/ui/tui/__tests__/helpers/render-screen.no-jest.tsx new file mode 100644 index 000000000..60e16196b --- /dev/null +++ b/src/ui/tui/__tests__/helpers/render-screen.no-jest.tsx @@ -0,0 +1,60 @@ +import { render } from 'ink-testing-library'; +import type { ReactNode } from 'react'; +import { vi } from 'vitest'; +import { ScreenContainer } from '@ui/tui/primitives/ScreenContainer'; +import { createScreens, type ScreenServices } from '@ui/tui/screen-registry'; +import type { WizardStore } from '@ui/tui/store'; + +export interface TerminalSize { + columns: number; + rows: number; +} + +export interface RenderedScreen { + app: ReturnType; + frame: string; +} + +// OSC 8 hyperlinks (LinkText) as well as the usual CSI colour/cursor codes. +// eslint-disable-next-line no-control-regex +const OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g; +// eslint-disable-next-line no-control-regex +const CSI_RE = /[\x1b\x9b][[()#;?]*[0-9;]*[A-Za-z]/g; + +export function toFrameText(raw: string | undefined): string { + return `${(raw ?? '').replace(OSC_RE, '').replace(CSI_RE, '')}\n` + .split('\n') + .map((line) => line.replace(/[ \t]+$/, '')) + .join('\n') + .replace(/\n+$/, '\n'); +} + +export async function flushInk(ms = 50): Promise { + await vi.advanceTimersByTimeAsync(ms); +} + +/** The shell App.tsx builds: router-resolved screen inside the full chrome. */ +export function screenShell( + store: WizardStore, + services: ScreenServices, +): ReactNode { + return ( + + ); +} + +export async function renderScreen( + store: WizardStore, + element: ReactNode, + { columns, rows }: TerminalSize, +): Promise { + const app = render(<>{element}); + const dimensions = { columns, rows, isTTY: true }; + for (const [key, value] of Object.entries(dimensions)) { + Object.defineProperty(app.stdout, key, { value, configurable: true }); + } + app.stdout.emit('resize'); + store.emitChange(); + await flushInk(); + return { app, frame: toFrameText(app.lastFrame()) }; +} diff --git a/src/ui/tui/__tests__/keyboard-equivalence.test.tsx b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx new file mode 100644 index 000000000..330523f58 --- /dev/null +++ b/src/ui/tui/__tests__/keyboard-equivalence.test.tsx @@ -0,0 +1,457 @@ +/** + * Baseline: for each screen a controller can act on, drive the real screen by + * keyboard on one store and apply the control action on another, then golden + * both session diffs. Pairs whose diffs differ today are recorded, not hidden. + */ +import { vi, describe, it, expect, afterEach, beforeAll } from 'vitest'; +import { render, cleanup } from 'ink-testing-library'; +import { + WizardStore, + Program, + ScreenId, + Overlay, + RunPhase, + McpOutcome, + type ProgramId, +} from '../store'; +import { InkUI } from '../ink-ui'; +import { setUI } from '@ui/index'; +import { + buildSession, + OutroKind, + type WizardSession, +} from '@lib/wizard-session'; +import { Integration } from '@lib/constants'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { HostResolution } from '@lib/host-resolution'; +import { WizardReadiness } from '@lib/health-checks/readiness'; +import { SOURCE_MAPS_CONTEXT_KEYS } from '@lib/programs/error-tracking-upload-source-maps/detect'; +import { SELF_DRIVING_INTEGRATE_PATH_KEY } from '@lib/programs/self-driving/detect'; +import { ScreenContainer } from '../primitives/ScreenContainer'; +import { + createScreens, + createServices, + type ScreenServices, +} from '../screen-registry'; +import { ACTION_REGISTRY } from '@e2e-harness/action-registry'; + +vi.mock('ink', () => + vi.importActual('../../../../node_modules/ink/build/index.js'), +); +vi.mock('@utils/analytics', () => ({ + analytics: { + capture: vi.fn(), + wizardCapture: vi.fn(), + setTag: vi.fn(), + captureException: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }, + sessionProperties: vi.fn(() => ({})), +})); +vi.mock('@utils/links', async (importOriginal) => ({ + ...(await importOriginal()), + openTrackedLink: vi.fn(), +})); +vi.mock('@utils/clipboard', async (importOriginal) => ({ + ...(await importOriginal()), + copyToClipboard: vi.fn().mockResolvedValue(false), + openInBrowser: vi.fn().mockResolvedValue(false), +})); +vi.mock('opn', () => ({ default: vi.fn() })); +vi.mock('@lib/api', async (importOriginal) => ({ + ...(await importOriginal()), + fetchSlackConnected: vi.fn().mockResolvedValue(false), + fetchUserData: vi.fn(() => new Promise(() => undefined)), +})); +vi.mock('@lib/wizard-tools', async (importOriginal) => ({ + ...(await importOriginal()), + fetchSkillMenu: vi.fn(() => new Promise(() => undefined)), +})); +vi.mock('@utils/setup-utils', async (importOriginal) => ({ + ...(await importOriginal()), + getOrAskForProjectData: vi.fn(() => new Promise(() => undefined)), +})); +vi.mock('@utils/wizard-abort', async (importOriginal) => ({ + ...(await importOriginal()), + wizardAbort: vi.fn().mockResolvedValue(undefined), +})); + +const ENTER = '\r'; +const ESC = '\u001B'; +const DOWN = '\u001B[B'; +const tick = (ms = 40) => new Promise((r) => setTimeout(r, ms)); + +const clean = { + decision: WizardReadiness.Yes, + health: {} as never, + reasons: [] as string[], +}; +const approved = (ok: boolean) => + ({ + organization: { is_ai_data_processing_approved: ok }, + } as unknown as WizardSession['apiUser']); + +const confirmed = (s: WizardStore) => { + s.completeSetup(); + s.setReadinessResult(clean); +}; +const authed = (s: WizardStore) => { + s.setCredentials({ + accessToken: 'phx_test', + projectApiKey: 'phc_test', + host: HostResolution.fromApiHost('https://us.posthog.com'), + projectId: 1, + }); + s.setApiUser(approved(true)); +}; +const ran = (s: WizardStore) => { + s.setRunPhase(RunPhase.Running); + s.setOutroData({ kind: OutroKind.Success, message: 'done' }); + s.setRunPhase(RunPhase.Completed); +}; + +const fakeInstaller = { + detectClients: () => + Promise.resolve([ + { name: 'Cursor', supportsPlugin: false }, + { name: 'Claude Code', supportsPlugin: true }, + ]), + install: () => Promise.resolve([]), +} as unknown as ScreenServices['mcpInstaller']; + +interface Pair { + name: string; + program: ProgramId; + integration?: Integration; + screen: string; + arrange: (s: WizardStore) => void; + keys: string[]; + action: string; + params?: Record; +} + +const nextjsRouterFirst = () => { + const setup = FRAMEWORK_REGISTRY[Integration.nextjs].metadata.setup; + if (!setup) throw new Error('nextjs setup questions missing'); + return setup.questions[0].options[0].value; +}; + +const PAIRS: Pair[] = [ + { + name: 'intro: enter on Continue vs confirm_setup', + program: Program.PostHogIntegration, + screen: ScreenId.Intro, + arrange: () => undefined, + keys: [ENTER], + action: 'confirm_setup', + }, + { + name: 'setup: enter on first router option vs choose', + program: Program.PostHogIntegration, + integration: Integration.nextjs, + screen: ScreenId.Setup, + arrange: confirmed, + keys: [ENTER], + action: 'choose', + params: { key: 'router', value: nextjsRouterFirst() }, + }, + { + name: 'outro: any key vs dismiss_outro', + program: Program.PostHogIntegration, + screen: ScreenId.Outro, + arrange: (s) => { + confirmed(s); + authed(s); + ran(s); + }, + keys: [ENTER], + action: 'dismiss_outro', + }, + { + name: 'mcp: decline install vs set_mcp_outcome skipped', + program: Program.PostHogIntegration, + screen: ScreenId.Mcp, + arrange: (s) => { + confirmed(s); + authed(s); + ran(s); + s.setOutroDismissed(); + }, + keys: [ESC], + action: 'set_mcp_outcome', + params: { outcome: 'skipped' }, + }, + { + name: 'slack-connect: skip vs dismiss_slack', + program: Program.PostHogIntegration, + screen: ScreenId.SlackConnect, + arrange: (s) => { + confirmed(s); + authed(s); + ran(s); + s.setOutroDismissed(); + s.setMcpComplete(McpOutcome.Skipped); + }, + keys: [DOWN, ENTER], + action: 'dismiss_slack', + }, + { + name: 'keep-skills: mount with no skills dir vs keep_skills', + program: Program.PostHogIntegration, + screen: ScreenId.KeepSkills, + arrange: (s) => { + confirmed(s); + authed(s); + ran(s); + s.setOutroDismissed(); + s.setMcpComplete(McpOutcome.Skipped); + s.setSlackStepDismissed(); + }, + keys: [], + action: 'keep_skills', + params: { kept: true }, + }, + { + name: 'audit-outro: any key vs dismiss_outro', + program: Program.Audit, + screen: ScreenId.AuditOutro, + arrange: (s) => { + confirmed(s); + authed(s); + ran(s); + }, + keys: [ENTER], + action: 'dismiss_outro', + }, + { + name: 'source-maps-outro: any key vs dismiss_outro', + program: Program.ErrorTrackingUploadSourceMaps, + screen: ScreenId.SourceMapsOutro, + arrange: (s) => { + s.completeSetup(); + authed(s); + s.setFrameworkContext(SOURCE_MAPS_CONTEXT_KEYS.selectedVariant, 'node'); + s.setFrameworkContext(SOURCE_MAPS_CONTEXT_KEYS.selectedPath, '.'); + ran(s); + }, + keys: [ENTER], + action: 'dismiss_outro', + }, + { + name: 'self-driving-integration-check: log me in vs set_integrate true', + program: Program.SelfDriving, + screen: ScreenId.SelfDrivingIntegrationCheck, + arrange: (s) => { + s.setFrameworkContext('postHogPresent', false); + s.completeSetup(); + }, + keys: [ENTER], + action: 'set_integrate', + params: { integrate: true }, + }, + { + name: 'self-driving-handoff: enter vs confirm_self_driving_handoff', + program: Program.SelfDriving, + screen: ScreenId.SelfDrivingHandoff, + arrange: (s) => { + s.setFrameworkContext('postHogPresent', false); + s.completeSetup(); + s.setIntegrate(true); + s.setReadinessResult(clean); + authed(s); + s.setFrameworkContext(SELF_DRIVING_INTEGRATE_PATH_KEY, '.'); + s.setFrameworkConfig( + Integration.javascriptNode, + FRAMEWORK_REGISTRY[Integration.javascriptNode], + ); + s.completeRunStep('integrate-run'); + }, + keys: [ENTER], + action: 'confirm_self_driving_handoff', + }, + { + name: 'wizard-ask: type answer and enter vs answer_question', + program: Program.PostHogIntegration, + screen: Overlay.WizardAsk, + arrange: (s) => { + void s.requestQuestion({ + id: 'ask-1', + source: 'test', + questions: [{ id: 'name', prompt: 'Name?', kind: 'text' }], + }); + }, + keys: ['yes', ENTER], + action: 'answer_question', + params: { answers: { name: 'yes' } }, + }, + { + name: 'task-notice: enter vs resolve_notice keep', + program: Program.PostHogIntegration, + screen: Overlay.TaskNotice, + arrange: (s) => { + void s.showTaskNotice({ + title: 'Optional step', + body: ['Body'], + prompt: 'Run it?', + confirmLabel: 'Yes', + cancelLabel: 'No', + }); + }, + keys: [ENTER], + action: 'resolve_notice', + params: { keep: true }, + }, + { + name: 'task-notice: escape vs resolve_notice decline', + program: Program.PostHogIntegration, + screen: Overlay.TaskNotice, + arrange: (s) => { + void s.showTaskNotice({ + title: 'Optional step', + body: ['Body'], + prompt: 'Run it?', + confirmLabel: 'Yes', + cancelLabel: 'No', + }); + }, + keys: [ESC], + action: 'resolve_notice', + params: { keep: false }, + }, + { + name: 'port-conflict: enter vs resolve_port_conflict', + program: Program.PostHogIntegration, + screen: Overlay.PortConflict, + arrange: (s) => { + confirmed(s); + void s.showPortConflict({ + command: 'node', + pid: '1', + port: 8000, + user: 'me', + }); + }, + keys: [ENTER], + action: 'resolve_port_conflict', + }, + { + name: 'manual-auth-code: paste code and enter vs submit_auth_code', + program: Program.PostHogIntegration, + screen: Overlay.ManualAuthCode, + arrange: (s) => { + confirmed(s); + s.showManualAuthCode(); + }, + keys: ['https://app.test/callback?code=abc123', ENTER], + action: 'submit_auth_code', + params: { code: 'abc123' }, + }, + { + name: 'manual-auth-code: escape vs dismiss_auth_code', + program: Program.PostHogIntegration, + screen: Overlay.ManualAuthCode, + arrange: (s) => { + confirmed(s); + s.showManualAuthCode(); + }, + keys: [ESC], + action: 'dismiss_auth_code', + }, +]; + +function makeStore(pair: Pair): WizardStore { + const store = new WizardStore(pair.program); + store.version = '0.0.0-test'; + setUI(new InkUI(store)); + const session = buildSession({ installDir: '/app', ci: false }); + const integration = pair.integration ?? Integration.javascriptNode; + session.integration = integration; + session.frameworkConfig = FRAMEWORK_REGISTRY[integration]; + store.session = session; + store.setDetectionComplete(); + pair.arrange(store); + return store; +} + +interface Snap { + screen: string; + overlay: boolean; + session: Record; +} + +function snap(store: WizardStore): Snap { + const session: Record = {}; + for (const [k, v] of Object.entries(store.session)) { + session[k] = k === 'frameworkConfig' ? (v ? '[config]' : null) : v; + } + return { + screen: store.currentScreen, + overlay: store.router.hasOverlay, + session, + }; +} + +function diff(before: Snap, after: Snap): Record { + const out: Record = {}; + if (before.screen !== after.screen) out.screen = after.screen; + if (before.overlay !== after.overlay) out.overlay = after.overlay; + for (const key of Object.keys(after.session)) { + if ( + JSON.stringify(before.session[key]) !== JSON.stringify(after.session[key]) + ) { + out[key] = after.session[key]; + } + } + return out; +} + +async function driveKeyboard(pair: Pair): Promise> { + const store = makeStore(pair); + const services = { ...createServices(store), mcpInstaller: fakeInstaller }; + const screens = createScreens(store, services); + const { stdin, unmount } = render( + , + ); + await tick(60); + expect(store.currentScreen).toBe(pair.screen); + const before = snap(store); + for (const key of pair.keys) { + stdin.write(key); + await tick(); + } + await tick(60); + const after = snap(store); + unmount(); + return diff(before, after); +} + +function applyAction(pair: Pair): Record { + const store = makeStore(pair); + expect(store.currentScreen).toBe(pair.screen); + const before = snap(store); + const action = ACTION_REGISTRY[pair.screen as ScreenId]?.find( + (a) => a.id === pair.action, + ); + if (!action) throw new Error(`no action ${pair.action} on ${pair.screen}`); + action.apply(store, pair.params ?? {}); + return diff(before, snap(store)); +} + +describe('keyboard commit vs control action commit', () => { + beforeAll(() => { + vi.spyOn(process, 'exit').mockImplementation(() => undefined as never); + }); + afterEach(() => cleanup()); + + for (const pair of PAIRS) { + it(pair.name, async () => { + const keyboard = await driveKeyboard(pair); + const action = applyAction(pair); + expect({ + keyboard, + action, + equal: JSON.stringify(keyboard) === JSON.stringify(action), + }).toMatchSnapshot(); + }); + } +}); diff --git a/src/ui/tui/__tests__/store-invariants.test.ts b/src/ui/tui/__tests__/store-invariants.test.ts new file mode 100644 index 000000000..8977738e7 --- /dev/null +++ b/src/ui/tui/__tests__/store-invariants.test.ts @@ -0,0 +1,873 @@ +/** + * Behaviour baseline for WizardStore + WizardRouter, taken before a refactor. + * Every expectation here pins what the code does today, exceptions included. + */ + +import { + WizardStore, + TaskStatus, + Program, + type ProgramId, + ScreenId, + Overlay, + RunPhase, + McpOutcome, + type ScreenName, +} from '@ui/tui/store'; +import { + buildSession, + AdditionalFeature, + DiscoveredFeature, + OutroKind, + type AskAnswers, + type PendingQuestion, + type TaskNotice, + type WizardSession, +} from '@lib/wizard-session'; +import { EXPANDED_COUNT } from '@ui/tui/constants'; +import { PROGRAM_SEQUENCES } from '@ui/tui/screen-sequences'; +import { WizardReadiness } from '@lib/health-checks/readiness'; +import { HostResolution } from '@lib/host-resolution'; +import { Integration } from '@lib/constants'; +import { FRAMEWORK_REGISTRY } from '@lib/registry'; +import { analytics } from '@utils/analytics'; +import { PROGRAM_REGISTRY } from '@lib/programs/program-registry'; +import type { SettingsConflict } from '@lib/agent/claude-settings'; + +vi.mock('../../../utils/analytics.js', () => ({ + analytics: { + capture: vi.fn(), + wizardCapture: vi.fn(), + captureException: vi.fn(), + setTag: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + }, + sessionProperties: vi.fn(() => ({})), +})); + +vi.mock('../../../lib/health-checks/readiness.js', () => ({ + evaluateWizardReadiness: vi.fn().mockResolvedValue({ + decision: 'yes', + health: {}, + reasons: [], + }), + WizardReadiness: { + Yes: 'yes', + No: 'no', + YesWithWarnings: 'yes-with-warnings', + }, + SERVICE_LABELS: {}, + // Generated signup sessions reach the branch that reads this config. + SIGNUP_WIZARD_READINESS_CONFIG: {}, + getBlockingServiceKeys: vi.fn(() => []), +})); + +const wizardCaptureMock = analytics.wizardCapture as Mock; +const setTagMock = analytics.setTag as Mock; + +const CREDENTIALS = { + accessToken: 'tok', + projectApiKey: 'pk', + host: HostResolution.fromApiHost('https://app.posthog.com'), + projectId: 1, +}; + +const CLEAN_READINESS = { + decision: WizardReadiness.Yes, + health: {} as never, + reasons: [], +}; + +const SETTINGS_CONFLICT: SettingsConflict = { + source: 'project', + path: '/app/.claude/settings.json', + keys: ['ANTHROPIC_BASE_URL'], + writable: true, +}; + +const TASK_NOTICE: TaskNotice = { + title: 'Connect your sources', + body: ['body'], + confirmLabel: 'Continue', + cancelLabel: 'Skip', + prompt: 'Connect these?', +}; + +const PENDING_QUESTION: PendingQuestion = { + id: 'ask-1', + questions: [{ id: 'a', prompt: 'p', kind: 'text' }], + source: 'test-skill', +}; + +const ANSWERS: AskAnswers = { a: 'yes' }; + +const aiUser = (approved: boolean): WizardSession['apiUser'] => + ({ organization: { is_ai_data_processing_approved: approved } } as never); + +function createStore(program?: ProgramId): WizardStore { + return new WizardStore(program); +} + +async function flushMicrotasks(): Promise { + await Promise.resolve(); + await Promise.resolve(); +} + +function tracked(promise: Promise): { resolved: boolean } { + const state = { resolved: false }; + void promise.then(() => { + state.resolved = true; + }); + return state; +} + +function countEmissions(store: WizardStore, act: () => void): number { + let count = 0; + const unsubscribe = store.subscribe(() => { + count += 1; + }); + act(); + unsubscribe(); + return count; +} + +interface MutationCase { + name: string; + /** Untracked setup — emissions it fires are excluded from the count. */ + prepare?: (store: WizardStore) => void; + invoke: (store: WizardStore) => void; + emits: number; +} + +const MUTATIONS: MutationCase[] = [ + { + name: 'setCurrentStage', + invoke: (s) => s.setCurrentStage('stage'), + emits: 1, + }, + { + name: 'toggleStatusExpanded', + invoke: (s) => s.toggleStatusExpanded(), + emits: 1, + }, + { + name: 'setStatusExpanded', + invoke: (s) => s.setStatusExpanded(true), + emits: 1, + }, + { name: 'completeSetup', invoke: (s) => s.completeSetup(), emits: 1 }, + { name: 'grantSharing', invoke: (s) => s.grantSharing(), emits: 1 }, + { name: 'declineSharing', invoke: (s) => s.declineSharing(), emits: 1 }, + { + name: 'setRunPhase', + invoke: (s) => s.setRunPhase(RunPhase.Running), + emits: 1, + }, + { + name: 'setCredentials', + invoke: (s) => s.setCredentials(CREDENTIALS), + emits: 1, + }, + { + name: 'setAccessToken', + invoke: (s) => s.setAccessToken(CREDENTIALS), + emits: 1, + }, + { + name: 'setRoleAtOrganization', + invoke: (s) => s.setRoleAtOrganization('engineer'), + emits: 1, + }, + { name: 'setApiUser', invoke: (s) => s.setApiUser(aiUser(true)), emits: 1 }, + { + name: 'setFrameworkConfig', + invoke: (s) => + s.setFrameworkConfig( + Integration.javascriptNode, + FRAMEWORK_REGISTRY[Integration.javascriptNode], + ), + emits: 1, + }, + { + name: 'setDetectionComplete', + invoke: (s) => s.setDetectionComplete(), + emits: 1, + }, + { + name: 'setDetectedFramework', + invoke: (s) => s.setDetectedFramework('Node.js'), + emits: 1, + }, + { + name: 'setPosthogSdkDetected', + invoke: (s) => s.setPosthogSdkDetected(true), + emits: 1, + }, + { + name: 'setSpellbook', + invoke: (s) => s.setSpellbook({ path: '/app/skill', skillsIncluded: true }), + emits: 1, + }, + { + name: 'setMintHandoff', + invoke: (s) => s.setMintHandoff('continue'), + emits: 1, + }, + { + name: 'setSkillId', + invoke: (s) => s.setSkillId('integration-nextjs'), + emits: 1, + }, + { + name: 'setUnsupportedVersion', + invoke: (s) => + s.setUnsupportedVersion({ current: '1', minimum: '2', docsUrl: 'u' }), + emits: 1, + }, + { + name: 'setLoginUrl', + invoke: (s) => s.setLoginUrl('http://localhost:8010'), + emits: 1, + }, + { + name: 'setAuthorizeUrl', + invoke: (s) => s.setAuthorizeUrl('https://app.posthog.com'), + emits: 1, + }, + { + name: 'setReadinessResult', + invoke: (s) => s.setReadinessResult(CLEAN_READINESS), + emits: 1, + }, + { name: 'dismissOutage', invoke: (s) => s.dismissOutage(), emits: 1 }, + { + name: 'showSettingsOverride', + invoke: (s) => void s.showSettingsOverride([SETTINGS_CONFLICT], () => true), + emits: 1, + }, + { + name: 'showPortConflict', + invoke: (s) => + void s.showPortConflict({ + command: 'node', + pid: '1', + port: 8010, + user: 'u', + }), + emits: 1, + }, + { + name: 'resolvePortConflict', + prepare: (s) => + void s.showPortConflict({ + command: 'node', + pid: '1', + port: 8010, + user: 'u', + }), + invoke: (s) => s.resolvePortConflict(), + emits: 1, + }, + { + name: 'showTaskNotice', + invoke: (s) => void s.showTaskNotice(TASK_NOTICE), + emits: 1, + }, + { + name: 'resolveTaskNotice', + prepare: (s) => void s.showTaskNotice(TASK_NOTICE), + invoke: (s) => s.resolveTaskNotice(true), + emits: 1, + }, + // No overlay push — the modal is opened separately by showManualAuthCode. + { + name: 'waitForManualAuthCode', + invoke: (s) => void s.waitForManualAuthCode(), + emits: 0, + }, + { + name: 'showManualAuthCode', + invoke: (s) => s.showManualAuthCode(), + emits: 1, + }, + { + name: 'dismissManualAuthCode', + prepare: (s) => s.showManualAuthCode(), + invoke: (s) => s.dismissManualAuthCode(), + emits: 1, + }, + { + name: 'submitManualAuthCode', + prepare: (s) => s.showManualAuthCode(), + invoke: (s) => s.submitManualAuthCode('code'), + emits: 1, + }, + { + name: 'requestQuestion', + invoke: (s) => void s.requestQuestion(PENDING_QUESTION), + emits: 1, + }, + { + name: 'resolvePendingQuestion', + prepare: (s) => void s.requestQuestion(PENDING_QUESTION), + invoke: (s) => s.resolvePendingQuestion(ANSWERS), + emits: 1, + }, + { + name: 'cancelPendingQuestion', + prepare: (s) => void s.requestQuestion(PENDING_QUESTION), + invoke: (s) => s.cancelPendingQuestion(), + emits: 1, + }, + { + name: 'backupAndFixSettingsOverride', + prepare: (s) => + void s.showSettingsOverride([SETTINGS_CONFLICT], () => true), + invoke: (s) => void s.backupAndFixSettingsOverride(), + emits: 1, + }, + { + name: 'showAuthError', + invoke: (s) => + s.showAuthError({ hasSettingsConflict: false, logFilePath: '/tmp/log' }), + emits: 1, + }, + { + name: 'showSessionTimeout', + invoke: (s) => s.showSessionTimeout(), + emits: 1, + }, + { + name: 'addDiscoveredFeature', + invoke: (s) => s.addDiscoveredFeature(DiscoveredFeature.Stripe), + emits: 1, + }, + { + name: 'enableFeature', + invoke: (s) => s.enableFeature(AdditionalFeature.LLM), + emits: 1, + }, + { + name: 'setMcpComplete', + invoke: (s) => s.setMcpComplete(McpOutcome.Installed, ['claude'], 'all'), + emits: 1, + }, + { + name: 'setSkillsComplete', + invoke: (s) => s.setSkillsComplete(true), + emits: 1, + }, + { + name: 'setMcpSuggestedPromptsDismissed', + invoke: (s) => s.setMcpSuggestedPromptsDismissed(), + emits: 1, + }, + { + name: 'setSlackStepDismissed', + invoke: (s) => s.setSlackStepDismissed(), + emits: 1, + }, + { + name: 'setSlackConnected', + invoke: (s) => s.setSlackConnected(true), + emits: 1, + }, + { + name: 'setGithubConnected', + invoke: (s) => s.setGithubConnected(true), + emits: 1, + }, + { + name: 'declineGithub', + invoke: (s) => + s.declineGithub({ kind: OutroKind.Cancel, message: 'declined' }), + emits: 1, + }, + { + name: 'setIntegrate', + invoke: (s) => s.setIntegrate(true, { via: 'screen' }), + emits: 1, + }, + { + name: 'chooseProvisionAccount', + invoke: (s) => s.chooseProvisionAccount('a@b.com', 'us'), + emits: 1, + }, + { + name: 'confirmSelfDrivingHandoff', + invoke: (s) => s.confirmSelfDrivingHandoff(), + emits: 1, + }, + { + name: 'completeRunStep', + invoke: (s) => s.completeRunStep('integrate-run'), + emits: 1, + }, + { name: 'setOutroDismissed', invoke: (s) => s.setOutroDismissed(), emits: 1 }, + { + name: 'setOutroData', + invoke: (s) => s.setOutroData({ kind: OutroKind.Success, message: 'done' }), + emits: 1, + }, + { + name: 'setDashboardUrl', + invoke: (s) => s.setDashboardUrl('https://d'), + emits: 1, + }, + { + name: 'setNotebookUrl', + invoke: (s) => s.setNotebookUrl('https://n'), + emits: 1, + }, + { + name: 'setFrameworkContext', + invoke: (s) => s.setFrameworkContext('k', 'v'), + emits: 1, + }, + { + name: 'switchProgram', + invoke: (s) => s.switchProgram(Program.Metrics), + emits: 1, + }, + { + name: 'pushOverlay', + invoke: (s) => s.pushOverlay(Overlay.WizardAsk), + emits: 1, + }, + { + name: 'popOverlay', + prepare: (s) => s.pushOverlay(Overlay.WizardAsk), + invoke: (s) => s.popOverlay(), + emits: 1, + }, + { name: 'pushStatus', invoke: (s) => s.pushStatus('working'), emits: 1 }, + { name: 'toggleTokenHud', invoke: (s) => s.toggleTokenHud(), emits: 1 }, + { + name: 'addTokenUsage', + invoke: (s) => + s.addTokenUsage({ + inputTokens: 10, + outputTokens: 5, + cacheReadTokens: 0, + cacheCreationTokens: 0, + cacheCreation5m: 0, + cacheCreation1h: 0, + }), + emits: 1, + }, + { + name: 'setFinalTokenCostUsd', + invoke: (s) => s.setFinalTokenCostUsd(1.25), + emits: 1, + }, + { + name: 'setTasks', + invoke: (s) => + s.setTasks([{ label: 'a', status: TaskStatus.Pending, done: false }]), + emits: 1, + }, + { + name: 'updateTask', + prepare: (s) => + s.setTasks([{ label: 'a', status: TaskStatus.Pending, done: false }]), + invoke: (s) => s.updateTask(0, true), + emits: 1, + }, + { + name: 'setEventPlan', + invoke: (s) => s.setEventPlan([{ name: 'signed_up', description: 'd' }]), + emits: 1, + }, + { + name: 'setHandoffText', + invoke: (s) => s.setHandoffText('handoff'), + emits: 1, + }, + // Render-only cursor: the learn card drives its own re-render. + { + name: 'setLearnCardBlockIdx', + invoke: (s) => s.setLearnCardBlockIdx(2), + emits: 0, + }, + { + name: 'setLearnCardComplete', + invoke: (s) => s.setLearnCardComplete(), + emits: 1, + }, + { + name: 'syncTodos', + invoke: (s) => s.syncTodos([{ content: 'a', status: 'pending' }]), + emits: 1, + }, +]; + +/** Read-only or notification-plumbing methods, excluded by the task brief. */ +const NON_MUTATING = [ + 'subscribe', + 'getSnapshot', + 'getVersion', + 'runInitHooks', + 'runReadyHooks', + 'getGate', + 'waitUntil', + 'onEnterScreen', + 'emitChange', +]; + +describe('store invariants', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('one notification per mutation', () => { + it('enumerates every public method on the class', () => { + const methods = Object.getOwnPropertyNames(WizardStore.prototype).filter( + (name) => { + const descriptor = Object.getOwnPropertyDescriptor( + WizardStore.prototype, + name, + ); + return ( + typeof descriptor?.value === 'function' && + name !== 'constructor' && + !name.startsWith('_') + ); + }, + ); + const covered = new Set([ + ...MUTATIONS.map((c) => c.name), + ...NON_MUTATING, + ]); + + expect(methods.filter((name) => !covered.has(name))).toEqual([]); + expect([...covered].filter((name) => !methods.includes(name))).toEqual( + [], + ); + }); + + it.each(MUTATIONS)( + '$name notifies $emits time(s)', + ({ prepare, invoke, emits }) => { + const store = createStore(); + prepare?.(store); + expect(countEmissions(store, () => invoke(store))).toBe(emits); + }, + ); + + it('cancelPendingQuestion with no question pending notifies nothing', () => { + const store = createStore(); + expect(countEmissions(store, () => store.cancelPendingQuestion())).toBe( + 0, + ); + }); + + it('backupAndFixSettingsOverride with no pending fix notifies nothing', () => { + const store = createStore(); + expect( + countEmissions(store, () => void store.backupAndFixSettingsOverride()), + ).toBe(0); + }); + + it('updateTask on a missing index notifies nothing', () => { + const store = createStore(); + expect(countEmissions(store, () => store.updateTask(3, true))).toBe(0); + }); + + it('switchProgram to the active program notifies nothing', () => { + const store = createStore(); + expect( + countEmissions(store, () => + store.switchProgram(Program.PostHogIntegration), + ), + ).toBe(0); + }); + + it('setStatusExpanded to the current value notifies nothing', () => { + const store = createStore(); + expect(countEmissions(store, () => store.setStatusExpanded(false))).toBe( + 0, + ); + }); + + it('setCurrentStage with the same stage notifies nothing', () => { + const store = createStore(); + store.setCurrentStage('stage'); + expect(countEmissions(store, () => store.setCurrentStage('stage'))).toBe( + 0, + ); + }); + + it('setHandoffText with identical text notifies nothing', () => { + const store = createStore(); + store.setHandoffText('handoff'); + expect(countEmissions(store, () => store.setHandoffText('handoff'))).toBe( + 0, + ); + }); + }); + + describe('gates latch once', () => { + it('resolves when the predicate turns true and stays resolved after it turns false', async () => { + const store = createStore(); + const gate = tracked(store.getGate('intro')); + await flushMicrotasks(); + expect(gate.resolved).toBe(false); + + store.completeSetup(); + await flushMicrotasks(); + expect(gate.resolved).toBe(true); + + store.session = buildSession({}); + await flushMicrotasks(); + expect(store.session.setupConfirmed).toBe(false); + + const relatched = tracked(store.getGate('intro')); + await flushMicrotasks(); + expect(relatched.resolved).toBe(true); + }); + + it('returns an already resolved promise for an unknown step', async () => { + const store = createStore(); + const gate = tracked(store.getGate('nonexistent')); + await flushMicrotasks(); + expect(gate.resolved).toBe(true); + }); + + it('waitUntil resolves on the next commit that matches', async () => { + const store = createStore(); + const waiter = tracked(store.waitUntil((s) => s.credentials !== null)); + await flushMicrotasks(); + expect(waiter.resolved).toBe(false); + + store.setDetectionComplete(); + await flushMicrotasks(); + expect(waiter.resolved).toBe(false); + + store.setCredentials(CREDENTIALS); + await flushMicrotasks(); + expect(waiter.resolved).toBe(true); + }); + + it('waitUntil evaluates live, so an already true predicate resolves immediately', async () => { + const store = createStore(); + store.completeSetup(); + const waiter = tracked(store.waitUntil((s) => s.setupConfirmed)); + await flushMicrotasks(); + expect(waiter.resolved).toBe(true); + }); + }); + + describe('overlays are LIFO', () => { + it('unwinds in reverse order back to the program screen', () => { + const store = createStore(); + expect(store.router.hasOverlay).toBe(false); + + store.pushOverlay(Overlay.AuthError); + store.pushOverlay(Overlay.SessionTimeout); + expect(store.router.resolve(store.session)).toBe(Overlay.SessionTimeout); + expect(store.router.hasOverlay).toBe(true); + + store.popOverlay(); + expect(store.router.resolve(store.session)).toBe(Overlay.AuthError); + expect(store.router.hasOverlay).toBe(true); + + store.popOverlay(); + expect(store.router.hasOverlay).toBe(false); + expect(store.router.resolve(store.session)).toBe(ScreenId.Intro); + }); + + it('tracks the nav direction across emits and overlay moves', () => { + const store = createStore(); + expect(store.lastNavDirection).toBeNull(); + + store.emitChange(); + expect(store.lastNavDirection).toBe('push'); + + store.pushOverlay(Overlay.AuthError); + expect(store.lastNavDirection).toBe('push'); + + store.popOverlay(); + expect(store.lastNavDirection).toBe('pop'); + + store.emitChange(); + expect(store.lastNavDirection).toBe('push'); + }); + }); + + describe('status ring', () => { + it('skips consecutive duplicates but keeps a repeat that is not adjacent', () => { + const store = createStore(); + store.pushStatus('a'); + store.pushStatus('a'); + store.pushStatus('b'); + store.pushStatus('a'); + expect(store.statusMessages).toEqual(['a', 'b', 'a']); + }); + + it('caps at the expanded window, dropping the oldest', () => { + const store = createStore(); + const total = EXPANDED_COUNT + 3; + for (let i = 0; i < total; i++) store.pushStatus(`m${i}`); + + expect(store.statusMessages).toHaveLength(EXPANDED_COUNT); + expect(store.statusMessages[0]).toBe(`m${total - EXPANDED_COUNT}`); + expect(store.statusMessages[EXPANDED_COUNT - 1]).toBe(`m${total - 1}`); + }); + }); + + describe('screen resolution is total', () => { + const PROGRAM_IDS = PROGRAM_REGISTRY.map((config) => config.id); + const SCREEN_NAMES = new Set([ + ...Object.values(ScreenId), + ...Object.values(Overlay), + ]); + const SEED = 0x5eed; + const SESSION_COUNT = 200; + + function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; + } + + function randomSession(rand: () => number): WizardSession { + const pick = (values: readonly T[]): T => + values[Math.floor(rand() * values.length)]; + const flip = (): boolean => rand() < 0.5; + + const session = buildSession({ installDir: '/app', ci: flip() }); + session.setupConfirmed = flip(); + session.credentials = flip() ? CREDENTIALS : null; + session.apiUser = pick([null, aiUser(true), aiUser(false)]); + session.runPhase = pick(Object.values(RunPhase)); + session.outroDismissed = flip(); + session.outroData = flip() + ? { kind: OutroKind.Error, message: 'x' } + : null; + session.mintHandoff = pick([null, 'exit', 'continue'] as const); + session.mcpComplete = flip(); + session.mcpOutcome = pick([null, ...Object.values(McpOutcome)]); + session.slackStepDismissed = flip(); + session.skillsComplete = flip(); + session.integrate = pick([null, true, false]); + if (flip()) { + session.integration = Integration.javascriptNode; + session.frameworkConfig = + FRAMEWORK_REGISTRY[Integration.javascriptNode]; + } + session.selfDrivingHandoffConfirmed = flip(); + session.githubConnected = pick([null, true, false]); + session.githubDeclined = flip(); + session.readinessResult = flip() ? CLEAN_READINESS : null; + session.outageDismissed = flip(); + if (flip()) session.frameworkContext = { postHogPresent: flip() }; + session.completedRuns = flip() ? ['integrate-run'] : []; + session.detectionComplete = flip(); + session.signup = flip(); + return session; + } + + function resolveAll(program: ProgramId): ScreenName[] { + const store = createStore(program); + const rand = mulberry32(SEED); + const screens: ScreenName[] = []; + for (let i = 0; i < SESSION_COUNT; i++) { + screens.push(store.router.resolve(randomSession(rand))); + } + return screens; + } + + it.each(PROGRAM_IDS)( + 'resolves a known screen for every session in %s', + (program) => { + const screens = resolveAll(program); + expect(screens).toHaveLength(SESSION_COUNT); + expect(screens.filter((screen) => !SCREEN_NAMES.has(screen))).toEqual( + [], + ); + }, + ); + + it.each(PROGRAM_IDS)('%s sequence ends on the exit screen', (program) => { + const sequence = PROGRAM_SEQUENCES[program]; + expect(sequence[sequence.length - 1].id).toBe(ScreenId.Exit); + }); + + it('generates the same sessions from the same seed', () => { + expect(resolveAll(Program.PostHogIntegration)).toEqual( + resolveAll(Program.PostHogIntegration), + ); + }); + }); + + describe('transition analytics shape', () => { + function driveIntegrationFlow(): void { + const store = createStore(); + store.completeSetup(); + store.setReadinessResult(CLEAN_READINESS); + store.setCredentials(CREDENTIALS); + store.setRunPhase(RunPhase.Running); + store.setRunPhase(RunPhase.Completed); + store.setOutroDismissed(); + } + + it('captures one screen event per transition', () => { + driveIntegrationFlow(); + + const screenEvents = wizardCaptureMock.mock.calls + .filter(([event]) => String(event).startsWith('screen ')) + .map(([event, props]) => ({ + event, + from_screen: props.from_screen, + program_id: props.program_id, + })); + + expect(screenEvents).toMatchInlineSnapshot(` + [ + { + "event": "screen auth", + "from_screen": "health-check", + "program_id": "posthog-integration", + }, + { + "event": "screen run", + "from_screen": "auth", + "program_id": "posthog-integration", + }, + { + "event": "screen outro", + "from_screen": "run", + "program_id": "posthog-integration", + }, + { + "event": "screen mcp", + "from_screen": "outro", + "program_id": "posthog-integration", + }, + ] + `); + }); + + it('tags $screen_name on every transition', () => { + driveIntegrationFlow(); + + const screenTags = setTagMock.mock.calls + .filter(([key]) => key === '$screen_name') + .map(([, value]) => value); + + expect(screenTags).toMatchInlineSnapshot(` + [ + "health-check", + "auth", + "run", + "outro", + "mcp", + ] + `); + }); + }); +});