# Testing Patterns

## Test Coverage Goal

Tests should cover all paths in the corresponding `docs/command/*.md`:

- **Process Flow**: Each branch in the mermaid flowchart = one test case
- **Error Scenarios**: Each error code listed = one test case
- **Idempotent paths**: If flowchart shows "Already exists? → Return existing"

## Mock Database

Use `createKyselyMock` from `@tailor-platform/sdk/vitest`, typed with the namespace schema:

```typescript
import { createKyselyMock } from "@tailor-platform/sdk/vitest";
import type { Namespace } from "../generated/kysely-tailordb";

const mock = createKyselyMock<Namespace[keyof Namespace]>();

// Stage the rows upcoming queries return, in execution order
mock.enqueueResults([first], [second]);

// Commands run inside a transaction; queries take mock.db directly
const result = await mock.withTx((trx) => run(trx, input, ctx));
const queryResult = await run(mock.db, input);
```

Inspect recorded queries via `mock.selects` / `mock.inserts` / `mock.updates` / `mock.deletes` (each `{ kind, sql, parameters, node }`):

- Written values: `mock.inserts[0].insertValues()` (single row), `mock.inserts[0].insertRows()` (multi-row), `mock.updates[0].updateValues()`
- Query shape: assert on `mock.selects[0].sql` and `mock.selects[0].parameters`

## Injected Cross-Module Queries & Commands

Cross-module access goes through injected query/command functions, not `selectFrom` ([CQRS rule](commands.md#command-side-reads-cqrs-separation)). Mock that function directly instead of staging foreign rows via `enqueueResults`, and keep the stubs in `testing/moduleMocks.ts`. Each mock factory takes a fixture store and returns `vi.fn` stubs that mirror the real query's lookup semantics over those rows, deriving the row type from the query's return so fixtures stay strict:

```typescript
export type Company = NonNullable<
  Awaited<ReturnType<OrganizationQueries["getCompany"]>>["value"]["company"]
>;

export const mockOrganizationQueries = (store: { companies?: Company[] }) => ({
  getCompany: vi.fn<OrganizationQueries["getCompany"]>((_db, input) =>
    Promise.resolve(
      ok({ company: store.companies?.find((company) => company.id === input.id) ?? null }),
    ),
  ),
});

// in the command test
run(trx, input, ctx, mockOrganizationQueries({ companies: [baseCompany] }));
```

Command mocks are plain `vi.fn` spies — assert their inputs via `.mock.lastCall` and inject failures with `mockResolvedValueOnce(err(...))`.

## Custom Fields Passthrough

For commands with generic `CF`, add one test verifying custom fields reach the insert:

- Import from `./createX.generated` (the generated shell that wires permissions)
- Pass extra fields in the input: `await mock.withTx((trx) => createX(trx, { name: "Test", myField: "value" }, ctx))`
- Assert with `insertValues()`: `expect(mock.inserts[0].insertValues()).toEqual(expect.objectContaining({ myField: "value" }))`
- Index into `mock.inserts[n]` when multiple inserts occur (e.g., audit events)

## Test Case Documentation Sync

Command and query docs include a `## Test Cases` section listing each `it()` description. This list must stay in sync with the actual test file:

- Each `it("...")` in the test file must appear as `- ...` in the doc's `## Test Cases`
- Each item in `## Test Cases` must have a corresponding `it()` in the test file
- Run `erp-kit module sync-check` to verify alignment
- When adding a new test, add the `it()` description to the doc first, then write the test
- Test case descriptions should match the doc's Process Flow branches and Error Scenarios

## Fixtures (`testing/fixtures.ts`)

- Import `Schema` from `lib/types` (not `Namespace` from generated code)
- Pattern: `export const baseEntity = { ... } as const satisfies Entity<Schema>`
- Foreign-module entities `satisfies` the `moduleMocks.ts`-derived type instead (see Injected Cross-Module Queries & Commands)
- Fixed IDs for traceability: `"entity-1"`
- Consistent timestamp: `new Date("2024-01-01T00:00:00.000Z")`
- `updatedAt: null` for base fixtures
