Testing Workflow (ChatbotX)
The verification gate every change passes before it is "done". CI runs Types, Lint, and Tests on every PR and on push to main (.github/workflows/ci.yml), so a failure here blocks the merge — run the gate locally first rather than discovering it in CI.
The gate sequence (run in order, fix before advancing)
- Lint —
pnpm lint (Ultracite/Biome). Use pnpm fix to auto-fix, never hand-format.
- Types —
pnpm --filter <app|package> check-types for every workspace you touched.
- Test — run the affected package's Vitest suite.
- Coverage — keep the 80% threshold (
packages/vitest-config/src/node.ts). Do not set VITEST_SKIP_COVERAGE_THRESHOLDS to dodge it — that silently nulls all thresholds and hides under-coverage.
Where tests live
- App/package/integration-level tests (actions, routes, API behavior, cache, worker behavior, cross-boundary):
<workspace>/__tests__/ — e.g. apps/builder/__tests__, apps/worker/__tests__, packages/sdk/__tests__, integrations/messenger/__tests__.
- Narrow unit/component tests owned by one module: colocated
src/**/__tests__.
Test design (AAA + behavior names)
- Arrange–Act–Assert structure.
- Name by behavior:
test('throws ChannelError when sourceConversationId is missing'), not test('works').
- Test the boundary that matters: actions, routes, repositories, worker handlers, channel send/receive.
- Mock the database client for unit tests; use the repository/service layer, never
db directly.
Testing TanStack Query hooks
Mock @/lib/orpc/orpc (vi.hoisted + vi.mock, same pattern as
chat-store.test.ts) so the hook's underlying client.* call is a spy, then
wrap the component under test in a fresh QueryClientProvider per test
(new QueryClient({ defaultOptions: { queries: { retry: false } } })) so
query state doesn't leak between tests. See __tests__/use-ai-agents.test.tsx
for the reference: dedup across two readers, enabled guard on missing input,
error surfacing without throwing, and invalidation triggering a refetch.
What to test first (highest signal in this repo)
- New oRPC route / server action → its happy path + auth-scoping + one failure path.
- New service method → unit test in
packages/business/__tests__, mocking the repositories/services it calls — this is the layer that carries validation, cache invalidation, and events, so it's the highest-signal place to test new business logic (see .agents/rules/data-access.md).
- New repository method → query correctness incl.
workspaceId scoping.
- New worker consumer → success / error / retry / idempotency on re-run.
- New channel integration → webhook receive parse + outgoing send mapping.
- Public handler and private action converge on one service method → assert both call sites resolve to the same method call, with only the caller's scope differing. See
apps/builder/__tests__/contacts-crud-public-api.test.ts (public, unscoped) alongside apps/builder/__tests__/contacts-permissions.test.ts (private, scoped) — both exercise contactService.list/count, never a parallel implementation.
Stop condition
A change is verified only when lint + the touched packages' check-types + the affected tests all pass, with coverage at/above threshold (not skipped). If you cannot run a gate, say which and why — do not report "verified" on an unrun gate.
1---2name: testing-workflow3description: Use when adding or changing tests, or before considering a change done, in ChatbotX. Documents the real verification gate sequence (lint → types → test → coverage), where tests live, the Vitest setup, and the coverage threshold that must not be silently bypassed. Read before writing tests or claiming a task is verified.4---56# Testing Workflow (ChatbotX)78The verification gate every change passes before it is "done". CI runs Types, Lint, and Tests on every PR and on push to `main` (`.github/workflows/ci.yml`), so a failure here blocks the merge — run the gate locally first rather than discovering it in CI.910## The gate sequence (run in order, fix before advancing)11121. **Lint** — `pnpm lint` (Ultracite/Biome). Use `pnpm fix` to auto-fix, never hand-format.132. **Types** — `pnpm --filter <app|package> check-types` for every workspace you touched.143. **Test** — run the affected package's Vitest suite.154. **Coverage** — keep the 80% threshold (`packages/vitest-config/src/node.ts`). Do **not** set `VITEST_SKIP_COVERAGE_THRESHOLDS` to dodge it — that silently nulls all thresholds and hides under-coverage.1617## Where tests live1819- App/package/integration-level tests (actions, routes, API behavior, cache, worker behavior, cross-boundary): `<workspace>/__tests__/` — e.g. `apps/builder/__tests__`, `apps/worker/__tests__`, `packages/sdk/__tests__`, `integrations/messenger/__tests__`.20- Narrow unit/component tests owned by one module: colocated `src/**/__tests__`.2122## Test design (AAA + behavior names)2324- Arrange–Act–Assert structure.25- Name by behavior: `test('throws ChannelError when sourceConversationId is missing')`, not `test('works')`.26- Test the boundary that matters: actions, routes, repositories, worker handlers, channel send/receive.27- Mock the database client for unit tests; use the repository/service layer, never `db` directly.2829## Testing TanStack Query hooks3031Mock `@/lib/orpc/orpc` (`vi.hoisted` + `vi.mock`, same pattern as32`chat-store.test.ts`) so the hook's underlying `client.*` call is a spy, then33wrap the component under test in a fresh `QueryClientProvider` per test34(`new QueryClient({ defaultOptions: { queries: { retry: false } } })`) so35query state doesn't leak between tests. See `__tests__/use-ai-agents.test.tsx`36for the reference: dedup across two readers, `enabled` guard on missing input,37error surfacing without throwing, and invalidation triggering a refetch.3839## What to test first (highest signal in this repo)4041- New oRPC route / server action → its happy path + auth-scoping + one failure path.42- New service method → unit test in `packages/business/__tests__`, mocking the repositories/services it calls — this is the layer that carries validation, cache invalidation, and events, so it's the highest-signal place to test new business logic (see `.agents/rules/data-access.md`).43- New repository method → query correctness incl. `workspaceId` scoping.44- New worker consumer → success / error / retry / idempotency on re-run.45- New channel integration → webhook receive parse + outgoing send mapping.46- Public handler and private action converge on one service method → assert both call sites resolve to the same method call, with only the caller's scope differing. See `apps/builder/__tests__/contacts-crud-public-api.test.ts` (public, unscoped) alongside `apps/builder/__tests__/contacts-permissions.test.ts` (private, scoped) — both exercise `contactService.list`/`count`, never a parallel implementation.4748## Stop condition4950A change is verified only when lint + the touched packages' `check-types` + the affected tests all pass, with coverage at/above threshold (not skipped). If you cannot run a gate, say which and why — do not report "verified" on an unrun gate.