--- name: 'cli-e2e-test-authoring' description: 'Guide for adding new end-to-end tests for the Packmind CLI. This skill should be used when creating new test specs in the `apps/cli-e2e-tests/` directory that exercise CLI commands against a real binary and API.' --- # CLI E2E Test Authoring ## Overview Add end-to-end tests that exercise the Packmind CLI binary (`dist/apps/cli/main.cjs`) in realistic conditions. Tests run the actual CLI as a child process and, when authentication is needed, interact with a running API via HTTP gateways. ## Test File Location All spec files live in `apps/cli-e2e-tests/src/` and follow the naming convention `.spec.ts`. ## Choosing a Test Wrapper Two wrappers are available depending on whether the command requires authentication. ### Unauthenticated commands — `describeWithTempSpace` Provides an isolated temporary directory. No API required. ```typescript import { describeWithTempSpace, runCli } from './helpers'; describeWithTempSpace('my-command without auth', (getContext) => { let result: Awaited>; beforeEach(async () => { const { testDir } = await getContext(); result = await runCli('my-command', { cwd: testDir }); }); it('exits with code 1', () => { expect(result.returnCode).toBe(1); }); it('shows an error message', () => { expect(result.stdout).toContain('No credentials found'); }); }); ``` ### Authenticated commands — `describeWithUserSignedUp` Extends `describeWithTempSpace` by creating a real user account, signing in, and generating an API key. Requires a running API at `http://localhost:4200`. ```typescript import { describeWithUserSignedUp, runCli } from './helpers'; describeWithUserSignedUp('my-command with auth', (getContext) => { let result: Awaited>; beforeEach(async () => { const { apiKey, testDir } = await getContext(); result = await runCli('my-command', { apiKey, cwd: testDir }); }); it('succeeds', () => { expect(result.returnCode).toBe(0); }); it('displays expected output', () => { expect(result.stdout).toContain('Expected text'); }); }); ``` The context object from `describeWithUserSignedUp` provides: | Field | Description | |----------------|--------------------------------------------------| | `testDir` | Isolated temporary directory | | `apiKey` | Valid API key for the created user | | `user` | User object (email, etc.) | | `organization` | Organization the user belongs to | | `spaceId` | Global space ID for the organization | | `gateway` | Authenticated gateway to call the API directly | ## Setting Up Test Preconditions ### Git repository Commands that interact with git (e.g. `diff`, `install`) require a git repo. Call `setupGitRepo` in `beforeEach`: ```typescript import { setupGitRepo } from './helpers'; beforeEach(async () => { const { testDir } = await getContext(); await setupGitRepo(testDir); }); ``` ### Creating API resources Use `gateway` from the context to seed data before running the CLI: ```typescript beforeEach(async () => { const { gateway, spaceId, testDir } = await getContext(); await setupGitRepo(testDir); await gateway.commands.create({ spaceId, /* ... */ }); await gateway.packages.create({ spaceId, /* ... */ }); }); ``` ### File manipulation Use file helpers to create or modify files in the test directory: ```typescript import { readFile, updateFile, fileExists } from './helpers'; const content = readFile('path/to/file.md', testDir); updateFile('path/to/file.md', 'new content', testDir); const exists = fileExists('path/to/file.md', testDir); ``` ## Assertion Patterns Split assertions into individual `it` blocks. Store the CLI result in a block-scoped `let` variable populated by `beforeEach`: ```typescript let result: Awaited>; beforeEach(async () => { result = await runCli('some-command', { apiKey, cwd: testDir }); }); it('succeeds', () => { expect(result.returnCode).toBe(0); }); it('displays the summary', () => { expect(result.stdout).toContain('1 change submitted'); }); ``` ## Nesting Describes for Multi-Step Scenarios For commands that require chained operations (e.g. install then modify then diff), nest `describe` blocks. Each level adds its own `beforeEach` that builds on the parent context: ```typescript describeWithUserSignedUp('diff command', (getContext) => { beforeEach(async () => { // setup: git repo + seed data + install }); describe('when a change is submitted', () => { beforeEach(async () => { // modify file + run diff --submit }); it('succeeds', () => { /* ... */ }); describe('when running diff again', () => { beforeEach(async () => { // run diff without --submit }); it('excludes the already-submitted change', () => { /* ... */ }); }); }); }); ``` ## Feature Flags Before writing the test, ask the user: **"Is the feature under test gated by a feature flag?"** If yes, the test must create a user whose email is at `@packmind.com` so the flag is enabled. This is controlled by the `underFeatureFlag` fixture option. Add `.use({ underFeatureFlag: true })` immediately after the test wrapper import, before any `describe` or `it` blocks: ```typescript // When using testWithApi: testWithApi.use({ underFeatureFlag: true }); // When using testWithUserSignedUp: testWithUserSignedUp.use({ underFeatureFlag: true }); ``` Without this option, the fixture creates a user at `@example.com`, which does not have feature flags enabled, and the feature under test would not be accessible. ## Adding a New Gateway When the CLI command under test requires seeding a new type of API resource: 1. Add the interface method to `helpers/IPackmindGateway.ts`. All exposed methods must be typed with `Gateway` or `PublicGateway` (imported from `@packmind/types`) 2. Create `helpers/gateways/NewResourceGateway.ts` implementing the interface 3. Add a lazy getter to `helpers/gateways/PackmindGateway.ts` (reset on `initializeWithApiKey`) 4. Re-export from `helpers/index.ts`