# Typescript Test Writing

> Use this skill when writing or modifying tests in the llxprt-code repository. Covers mandatory TDD, behavioral testing, bun:test conventions and file naming, mock hygiene (no mock theater), and what never to test. Distilled from dev-docs/RULES.md, which remains the source of truth.

- Skill: `vybestack/typescript-test-writing` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vybestack/typescript-test-writing`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vybestack/typescript-test-writing/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vybestack (https://skillmd.com/u/vybestack)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vybestack/typescript-test-writing

---


# Writing TypeScript Tests in llxprt-code

Distilled from dev-docs/RULES.md. When in doubt, read RULES.md in full — it is
the source of truth for development guidelines.

## Core principle: TDD is mandatory

Every line of production code must be written in response to a failing test.
No exceptions.

Red-Green-Refactor, followed strictly:

1. **RED**: Write a failing test for the next small behavior.
2. **GREEN**: Write ONLY enough code to make the test pass.
3. **REFACTOR**: Only if it improves clarity.
4. **COMMIT**: Feature + tests together; refactoring separately.

## Stack rules (non-negotiable)

- Bun + `bun:test` ONLY. Never create Vitest or Node test suites, and never add
  new `.js` files — everything is TypeScript run with bun.
- TypeScript strict mode: no `any` (use `unknown` with type guards), no type
  assertions (use type predicates), explicit return types.
- Import pattern (bun:test re-exports `vi`, `mock`, and `Mock`):

  ```typescript
  import {
    describe,
    it,
    expect,
    beforeEach,
    afterEach,
    vi,
    type Mock,
  } from 'bun:test';
  ```

- Prefer immutable data in tests and fixtures (`{ ...cart, items: [...cart.items, item] }`);
  never mutate shared fixtures between tests.

## Test behavior, not implementation

✅ Test:

- Public API behavior
- Input → Output transformations
- Edge cases and error conditions
- Integration between units
- Schema validation

❌ Never test:

- Implementation details
- Private methods
- Third-party libraries
- Mock interactions

❌ Never enshrine bugs as specification: do not write a passing test that asserts
incorrect behavior, even if that is what the code currently does. If you
discover a bug while writing tests: (1) file an issue or ask the user, (2) write
a failing test that asserts the CORRECT behavior, (3) fix the production code so
the test passes. A suite that encodes bugs as passing tests is worse than no
tests — it actively prevents future fixes.

## Test structure

- **describe**: feature/component name.
- **it**: specific behavior in plain English.
- **Arrange-Act-Assert**: clear sections; one behavior per test.
- **DRY setup**: never copy-paste identical beforeEach/afterEach boilerplate
  (e.g. temp-dir creation, cleanup) across describe blocks. Extract a shared
  helper that wires the lifecycle hooks (e.g. a `useTempDir()` helper that
  registers beforeEach/afterEach internally and returns a lazy accessor).
  Repeating 5+ lines of identical setup in N describe blocks is a maintenance
  hazard; one line of shared setup per describe block is the target.

## File conventions

- Tests are TypeScript, co-located with the code under test.
- `*.test.ts` is the dominant pattern in this repo; `*.spec.ts` and
  `__tests__/` directories are also in use — match the existing pattern of the
  neighboring tests before creating a new file.
- File names: kebab-case.
- Run a single file with `bun test path/to/file.test.ts`; the full suite is
  `npm run test`.

## Mock hygiene: no mock theater

**The fundamental rule: you cannot test a component by mocking that component.**

### Mock decision tree

```
Is it the component you're testing?
├─ Yes → [ERROR] NEVER MOCK IT
└─ No → Is it doing the core work being tested?
    ├─ Yes → [ERROR] DON'T MOCK IT
    └─ No → Is it infrastructure (FS, network, DB)?
        ├─ Yes → [OK] OK to mock
        └─ No → Is it completely unrelated to the test?
            ├─ Yes → [OK] OK to mock
            └─ No → WARNING: Probably shouldn't mock
```

### Prohibited mock patterns

1. **Self-mocking** — mocking the component under test. If you mock EmojiFilter
   to test EmojiFilter, you are testing the mock, not the component.
2. **Direct-value mock** — a mock that returns exactly the output the test
   expects. The test is worthless: it can never fail for the right reason.
3. **Mock verification** — asserting a mock was called
   (`expect(mockFn).toHaveBeenCalledWith(...)`) proves nothing about real code.
4. **Mirror-echo assertion** — configuring a stub with a literal and asserting
   that same literal back (`.mockReturnValue('JIT_MARKER')` …
   `expect(policy.marker).toBe('JIT_MARKER')`). Unless producing that literal
   IS the behavior under test, the test asserts its own configuration and can
   never fail for the right reason. Assert a derived property instead —
   ordering, precedence, counts, aggregation — with literals on the input side
   only.

Also watch for: the mock chain (A calls MockB calls MockC — no real code tested)
and mocks with complex implementations (if the mock has the logic, test the real
code instead).

### Allowed mock patterns

1. **Infrastructure mocking** — mock filesystem, network, databases (not
   business logic); instantiate the REAL component; assert real transformations
   through the real code paths.
2. **Irrelevant service mocking** — mock services unrelated to the behavior
   under test (auth, logging), while the component under test is real.
3. **Test data builders** — build input data; never mock behavior.

### The litmus test

After writing a test, all four answers must be the "good" one:

1. If I delete the real implementation, will this test fail? (If NO: worthless)
2. If I break the real implementation, will this test catch it? (If NO: worthless)
3. Am I testing my mock or my code? (If MOCK: worthless)
4. Could I replace the component with `return 'expected'` and pass? (If YES: worthless)

## Before submitting

- All tests pass (`npm run test`).
- No TypeScript errors (`npm run typecheck`).
- No linting warnings (`npm run lint`).
- No console.logs or debug code left behind.
- Every production line you touched is covered by behavior tests.
- Self-check files you touched with the AST test-audit scanner:
  `bun scripts/test-audit/scan.ts` — no new MOCK_MIRROR, ALWAYS_TRUE,
  SELF_CONFIRMING, or NO_ASSERT findings may appear on them. The scanner
  runs over the full corpus and writes `findings.tsv` + `file-stats.tsv` to
  the output dir (defaults to `tmp/test-audit`). To compare against the
  baseline, run it once on `main` with a custom output dir, then on your
  branch with a different output dir, and diff the TSVs:
  `bun scripts/test-audit/scan.ts tmp/scan-main` vs
  `bun scripts/test-audit/scan.ts tmp/scan-branch`, then
  `diff tmp/scan-main/findings.tsv tmp/scan-branch/findings.tsv`.
  Note: the scanner compares exact stable literals (not substrings) between
  mock configurations and assertion arguments. A MOCK_MIRROR finding on a
  test that verifies a transformation (e.g., asserting `'A
B'` where the
  stub returns `'A'` and `'B'` separately) is a false positive — the
  assertion literal does not exactly match any single mock literal. Inspect
  the exact positive equality that triggered the finding before dismissing
  it. Disjoint-source and leakage assertions (e.g., `not.toContain('X')`)
  are not mirror echoes and will not trigger MOCK_MIRROR.

