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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 36 additions & 14 deletions .claude/skills/wizard-testing/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
---
name: testing
description: Testing guidelines and conventions for the Confidence Wizard CLI project
version: '0.1'
description: >
Load before writing, modifying, or adding any test file (unit,
integration, or e2e). Covers testing philosophy, conventions, shared
test scaffolds (__tests__/shared/), test framework structure
(__tests__/e2e/testing-framework/, __tests__/ui/testing-framework/),
and the named-key press() API for e2e tests.
version: '0.2'
---

# Testing Guidelines
Expand Down Expand Up @@ -65,13 +70,27 @@ Tests live in `__tests__/` mirroring the `src/` directory structure:

```
__tests__/
shared/ # Utilities shared between e2e and integration tests
key-map.ts # Named terminal key mapping (Enter, ArrowDown, etc.)
auth.ts # JWT builders (buildTestJwt, buildExpiredJwt, buildAuthState)
project-scaffold.ts # Project directory factory (react, empty, react-statsig, etc.)
e2e/ # End-to-end tests (node-pty)
testing-framework/
terminal/ # PTY infrastructure (TerminalSession, screen buffer, ANSI strip)
mocks/ # Mock HTTP server + mock IDE binaries (binaries/ subdirectory)
navigation.ts # Screen navigation shortcuts
session-factory.ts # createSession() factory
utils.ts # simulateAuthCallback, readInvocation
*.e2e.ts # E2E test files
ui/ # Integration tests (ink-testing-library)
testing-framework/
ink/ # Ink rendering (renderScreen, renderApp, act)
mocks/ # Mock child process (createFakeChild, mockNextSpawn)
async.ts # delay, waitFor
screens/ # Screen test files
commands/
ui/
lib/
frameworks/
e2e/ # End-to-end tests (node-pty)
helpers/ # TerminalSession, mock server, navigation utils
*.e2e.ts # E2E test files
```

Unit/integration tests are colocated as `src/**/__tests__/**/*.test.{ts,tsx}`.
Expand All @@ -97,22 +116,25 @@ E2E tests use a dedicated vitest config (`vitest.config.e2e.ts`) with:
- No MSW setup (HTTP is mocked via a real local server)
- Global setup in `__tests__/e2e/global-setup.ts`

### Helpers (`__tests__/e2e/helpers/`)
### Testing Framework (`__tests__/e2e/testing-framework/`)

- **`createSession(opts?)`** — spawns the CLI in a pty with an isolated temp project dir. Pass `{ project: 'empty' }` for an empty project (no `package.json`). Returns a `TerminalSession` with `[Symbol.dispose]`.
- **`TerminalSession`** — wraps node-pty. Key methods: `waitForText(text)` (polls accumulated buffer), `sendKey(key)`, `waitForExit()`, `screen` (full ANSI-stripped output).
- **`simulateAuthCallback()`** — hits the CLI's local OAuth callback server to simulate browser auth.
- **`navigateToPlugins/ConnectTools/Onboarding(session)`** — navigation shortcuts that advance through earlier screens.
- **Mock HTTP server** — started in global setup, mimics all Confidence APIs (auth, MCP, skills, telemetry). The CLI's API URLs are configurable via env vars (e.g. `CONFIDENCE_AUTH_URL`), which the global setup points at the local server.
- **Mock `claude` binary** — placed on PATH, handles `mcp` subcommands and `--print` onboarding by emitting stream-json events.
- **`createSession(opts?)`** (`session-factory.ts`) — spawns the CLI in a pty with an isolated temp project dir. Pass `{ project: 'empty' }` for an empty project (no `package.json`). Returns a `TerminalSession` with `[Symbol.dispose]`.
- **`TerminalSession`** (`terminal/session.ts`) — wraps node-pty. Key methods: `press(key)` (named keys like `'Enter'`, `'ArrowDown'`), `pressRepeat(key, count)`, `waitForText(text)`, `waitForPattern(regex)`, `waitForExit()`, `checkpoint()`, `snapshot()`, `screen` (full ANSI-stripped output).
- **`simulateAuthCallback()`** (`utils.ts`) — hits the CLI's local OAuth callback server to simulate browser auth.
- **`navigateToPlugins/ConnectTools/Onboarding(session)`** (`navigation.ts`) — navigation shortcuts that advance through earlier screens.
- **Mock HTTP server** (`mocks/server.ts`) — started in global setup, mimics all Confidence APIs (auth, MCP, skills, telemetry). The CLI's API URLs are configurable via env vars (e.g. `CONFIDENCE_AUTH_URL`), which the global setup points at the local server.
- **Mock IDE binaries** (`mocks/binaries/`) — `claude`, `cursor`, `codex` mock scripts placed on PATH, handle subcommands and `--print` onboarding by emitting stream-json events.
- **Shared utilities** (`__tests__/shared/`) — `key-map.ts` (key escape sequences), `auth.ts` (JWT builders), `project-scaffold.ts` (temp project directory factory). Shared with integration tests.

### Writing E2E Tests

- **File naming**: `*.e2e.ts` (not `.test.ts`)
- **One concern per file**: group related scenarios (e.g. `skip-plugins.e2e.ts` covers all skip-plugin variations).
- **Use `createSession()` per test** — each call creates a fresh project dir for full isolation. No shared state between tests.
- **Use `using`** for automatic cleanup: `using session = createSession()`.
- **Use named keys** with `session.press('Enter')`, `session.press('ArrowDown')`, `session.pressRepeat('ArrowDown', 3)` — not raw escape code constants.
- **Assert positively** — the accumulated buffer contains ALL output ever rendered (including text from previous screens). Prefer `waitForText('expected')` over `not.toContain('unexpected')`.
- **Use `checkpoint()`** between screens to scope `waitForText` and `snapshot()` to the current screen, avoiding false positives from earlier output.
- **Use navigation helpers** to skip past earlier screens when testing later ones (e.g. `navigateToOnboarding(session)` advances through Welcome, SystemCheck, Auth, Plugins, and ConnectTools).
- **Add comments** before each interaction block to identify the screen and the intent of the action (e.g. `// Welcome`, `// Select "Skip for now"`, `// Done — no IDE set, only Exit option`).

Expand Down Expand Up @@ -195,7 +217,7 @@ describe('resolve method internals', () => {

- One assertion concern per test — multiple `expect` calls are fine if they assert the same behavior.
- No snapshot tests unless explicitly requested.
- **Prefer `createProjectDir()` for setting up project context** (framework, dependencies, project structure) in TUI screen tests. Pass dependencies to control framework detection (e.g., `createProjectDir({ react: '^19.0.0' })` for React, `createProjectDir({ express: '^4.0.0' })` for Node.js, `createProjectDir(null)` for an empty project). Only pre-build a `WizardStore` directly when the test needs store state that `createProjectDir` cannot provide (e.g., a framework already set from an earlier screen).
- **Prefer `createProjectDir()` for setting up project context** (framework, dependencies, project structure) in TUI screen tests. Use scaffold types to control framework detection (e.g., `createProjectDir('react')`, `createProjectDir('empty')`, `createProjectDir('react-statsig')`). The function lives in `__tests__/shared/project-scaffold.ts` and is shared between integration and e2e tests. Only pre-build a `WizardStore` directly when the test needs store state that `createProjectDir` cannot provide (e.g., a framework already set from an earlier screen).
- **Prefer `using` for disposable resources.** When a helper returns an object with `[Symbol.dispose]` (e.g., `createProjectDir()`, `renderScreen()`, `renderApp()`), declare it with `using` inside each test rather than sharing it via `beforeAll`/`afterAll`. This keeps each test self-contained and guarantees cleanup even if the test throws.

```ts
Expand Down
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["posthog", "statsig"]
}
18 changes: 9 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,18 @@ The stable `node-pty` release (v1.1.0) doesn't ship prebuilt binaries for Node.j
- All commits must follow Conventional Commits. The `commit-msg` hook enforces this via commitlint.
- Run `pnpm qa` before pushing to ensure CI will pass.
- When writing or modifying code, always use the `wizard-architecture` skill first to load the project's architecture and coding conventions.
- When writing or changing tests, always use the `wizard-testing` skill first to load the project's testing guidelines and conventions.
- When writing, modifying, or adding any test file (unit, integration, or e2e), always use the `wizard-testing` skill first to load the project's testing guidelines, conventions, and test framework structure.
- When making commits or working with the CI/release pipeline, use the `wizard-development-harness` skill for guidelines.

## Skills (Mandatory)

Before making any changes, agents MUST load the relevant skill(s) from `.claude/skills/`. These skills contain the authoritative guidelines for this project — architecture constraints, coding conventions, testing philosophy, and development harness rules. Skipping them leads to guideline violations.

| Skill | When to load | Key rules |
| ---------------------------- | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wizard-architecture` | Any code change | Path aliases for cross-domain imports, dependency direction, dry-run separation, initialization hooks, TypeScript style (`type` over `interface`, `satisfies never` in switch defaults, object params for 4+ args), module exports |
| `wizard-testing` | Any test change | Observable behavior only, AAA pattern, `sut` naming, `using` for disposables, `waitFor` over `delay`, MSW for HTTP mocks, `describe` blocks use consumer-perspective naming (`when…`/`given…`) |
| `wizard-ink-tui` | Any TUI/screen change | Ink rendering model, `@inkjs/ui` over standalone packages, `Colors`/`Icons`/`HAlign`/`VAlign` from `styles.ts`, named functions in `useEffect` |
| `wizard-integrations` | IDE integration changes | Strategy pattern, self-contained IDE subdirs, adding new IDEs, MCP/chat/plugin flows |
| `wizard-development-harness` | Commits, CI, releases | Conventional Commits, `pnpm qa` before push, pre-commit hooks, release-please |
| `wizard-workflows` | Workflow changes | Hash-pinned actions with version comments, minimal permissions, per-secret references |
| Skill | When to load | Key rules |
| ---------------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `wizard-architecture` | Any code change | Path aliases for cross-domain imports, dependency direction, dry-run separation, initialization hooks, TypeScript style (`type` over `interface`, `satisfies never` in switch defaults, object params for 4+ args), module exports |
| `wizard-testing` | Any test change or addition | Observable behavior only, AAA pattern, `sut` naming, `using` for disposables, `waitFor` over `delay`, MSW for HTTP mocks, shared test scaffolds (`__tests__/shared/`), `press('Enter')` for e2e keys |
| `wizard-ink-tui` | Any TUI/screen change | Ink rendering model, `@inkjs/ui` over standalone packages, `Colors`/`Icons`/`HAlign`/`VAlign` from `styles.ts`, named functions in `useEffect` |
| `wizard-integrations` | IDE integration changes | Strategy pattern, self-contained IDE subdirs, adding new IDEs, MCP/chat/plugin flows |
| `wizard-development-harness` | Commits, CI, releases | Conventional Commits, `pnpm qa` before push, pre-commit hooks, release-please |
| `wizard-workflows` | Workflow changes | Hash-pinned actions with version comments, minimal permissions, per-secret references |
4 changes: 2 additions & 2 deletions __tests__/e2e/__snapshots__/stale-auth.e2e.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ exports[`when auth token is stale > allows signing in after failed refresh and p


──────────────────────────────────────────────────────────────────────────────────────────────────
Which agent tool are you using?
Which CLI agent would you like to use?

❯ Claude Code
Cursor
Expand Down Expand Up @@ -99,7 +99,7 @@ exports[`when auth token is stale > refreshes and authenticates when choosing ex


──────────────────────────────────────────────────────────────────────────────────────────────────
Which agent tool are you using?
Which CLI agent would you like to use?

❯ Claude Code
Cursor
Expand Down
24 changes: 11 additions & 13 deletions __tests__/e2e/chat-launch.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,19 @@ import {
createSession,
navigateToOnboarding,
navigateToPlugins,
ENTER,
ARROW_DOWN,
CHAT_PROMPT_FILE,
} from './helpers/index.js';
} from './testing-framework/index.js';

describe('when the user starts chat after onboarding', () => {
it('includes code changes and report file in the prompt', async () => {
using session = createSession();

await navigateToOnboarding(session);
await session.sendKey(ENTER);
await session.press('Enter');
await session.waitForText('onboarding complete', { timeout: 30_000 });

await session.waitForText('Continue work with Claude Code');
await session.sendKey(ENTER);
await session.press('Enter');

const exitCode = await session.waitForExit();
expect(exitCode).toBe(0);
Expand All @@ -34,11 +32,11 @@ describe('when the user starts chat after onboarding', () => {
using session = createSession();

await navigateToOnboarding(session);
await session.sendKey(ARROW_DOWN);
await session.sendKey(ENTER);
await session.press('ArrowDown');
await session.press('Enter');

await session.waitForText('Continue work with Claude Code');
await session.sendKey(ENTER);
await session.press('Enter');

const exitCode = await session.waitForExit();
expect(exitCode).toBe(0);
Expand All @@ -53,23 +51,23 @@ describe('when the user starts chat after onboarding', () => {
using session = createSession();

await navigateToPlugins(session);
await session.sendKey(ENTER);
await session.press('Enter');

// Skip connecting tools
await session.waitForText('Connect Confidence tools?');
await session.sendKeyRepeat(ARROW_DOWN, 3);
await session.sendKey(ENTER);
await session.pressRepeat('ArrowDown', 3);
await session.press('Enter');
await session.waitForText('Skipped');

// Onboard — wait for options to render before pressing Enter
await session.waitForText('Start onboarding?');
await session.waitForText('Skip for now');
await session.sendKey(ENTER);
await session.press('Enter');
await session.waitForText('onboarding complete', { timeout: 30_000 });

// Chat
await session.waitForText('Continue work with Claude Code');
await session.sendKey(ENTER);
await session.press('Enter');

const exitCode = await session.waitForExit();
expect(exitCode).toBe(0);
Expand Down
6 changes: 3 additions & 3 deletions __tests__/e2e/empty-project.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createSession, ENTER } from './helpers/index.js';
import { createSession } from './testing-framework/index.js';

describe('when the project is empty', () => {
it('shows "Select framework" instead of "Start setup" on the Welcome screen', async () => {
Expand All @@ -16,12 +16,12 @@ describe('when the project is empty', () => {

// Welcome — no framework detected
await session.waitForText('Select framework');
await session.sendKey(ENTER);
await session.press('Enter');

// SelectFramework
await session.waitForText('Select Framework');
await session.waitForText("Select your project's framework or language:");
await session.sendKey(ENTER);
await session.press('Enter');

// Back to Welcome — now with framework set
await session.waitForText('Start setup');
Expand Down
4 changes: 2 additions & 2 deletions __tests__/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { startMockServer, type MockServer } from './helpers/mock-server.js';
import { createMockBinDir } from './helpers/mock-binaries.js';
import { startMockServer, type MockServer } from './testing-framework/mocks/server.js';
import { createMockBinDir } from './testing-framework/mocks/binaries/index.js';

let mockServer: MockServer;
let tempBase: string;
Expand Down
Loading