| name | vitest-testing-guidelines |
| description | Use when adding or changing tests in this Vitest-based monorepo. |
Generated from ai/registry.json. Do not edit manually.
Canonical skill: ../../../ai/skills/vitest-testing-guidelines.md.
Referenced context:
../../../ai/rules/testing-rules.md
../../../ai/architecture/monorepo.md
../../../ai/architecture/client-app.md
../../../ai/examples/good-evaluator-test.md
../../../ai/examples/good-public-config-service.md
This file is compiled from canonical AI knowledge files. Edit canonical files under ai, then run npm run ai:sync.
Compiled AI Skill: vitest-testing-guidelines
Canonical Skill: ai/skills/vitest-testing-guidelines.md
Vitest Testing Guidelines
Use this skill when adding or changing tests in this Vitest-based monorepo.
Goal
Add focused tests that protect observable behavior and domain invariants without coupling to implementation details.
Read First
ai/rules/testing-rules.md
ai/architecture/monorepo.md
ai/architecture/client-app.md
ai/examples/good-evaluator-test.md
ai/examples/good-public-config-service.md
Workflow
- Identify the behavior being protected and the narrowest workspace test suite.
- Reuse nearby test setup patterns before adding helpers or dependencies.
- For
apps/client, place tests in the owning __tests__/ folder described by the client architecture and use existing Testing Library/React Query test helpers.
- For
apps/client API operations and hooks, test both successful mocked responses and mocked API error responses.
- For API service tests, mock only the Prisma methods and collaborators the test exercises.
- For evaluator and SDK tests, assert public behavior through package exports.
- For client coverage changes, run the coverage script and keep configured thresholds at 90% or higher unless the user explicitly narrows scope.
Expected Output
- Regression tests cover bug fixes.
- Client tests follow the current
__tests__/ folder layout instead of loose sibling *.test.ts(x) files.
- Client request functions and React Query hooks cover success, error, enabled state, and cache invalidation where applicable.
- Domain tests cover role gates, tenant boundaries, revision bumps, SDK key secrecy, config serialization, fallback behavior, or evaluation order when those paths change.
- No
.only remains.
Verification
- Run the targeted test file first when one exists.
- Run the affected workspace test command after the targeted test passes.
- Run
npm --workspace @capture-flag/client run test:coverage after changing client coverage configuration or broad client tests.
- State blockers clearly if tests cannot run because of missing services or dependencies.
Referenced Context
Reference: ai/rules/testing-rules.md
Testing Rules
Testing rules for this Vitest-based monorepo.
Always
- Use Vitest APIs such as
describe, it, expect, vi.fn, and vi.spyOn.
- Add regression tests next to changed behavior when fixing bugs.
- Test observable behavior and domain invariants instead of implementation details.
- Run a targeted test first when a specific test file exists.
- Run the affected workspace test command after targeted tests pass.
- Keep client tests in
__tests__/ folders next to the source area they cover, matching the Storybook stories/ grouping convention.
- Keep
apps/client/src/api request and hook tests in src/api/__tests__, using the existing fetch, React Query, and render helpers from src/test.
- Keep
apps/client/src/core tests under src/core/<category>/__tests__/<name>.test.ts.
- Keep shared client component tests under
src/components/__tests__ and shared member component tests under src/components/members/__tests__.
- Keep page tests under
src/pages/<PageName>/__tests__ or src/pages/<PageName>/<section>/__tests__ for colocated page internals.
- Use
src/__tests__ only for source files owned directly by apps/client/src, such as permissions.ts.
- For
apps/client code that calls the API, cover both successful responses and API error responses with mocked fetch/response behavior.
- Keep
apps/client coverage at 90% or higher for configured client coverage targets; aim for 100% on pure helpers and request functions.
- For API tests, mock Prisma and collaborators with plain objects containing only exercised
vi.fn() methods.
- For SDK and evaluator packages, prefer pure unit tests around evaluation order, fallback behavior, type handling, and request/cache boundaries.
Never
- Do not use Jest-only APIs or
jest.mock patterns.
- Do not leave
.only in tests.
- Do not introduce coverage targets unless a coverage script exists or the task explicitly asks for coverage.
- Do not place
apps/client tests beside source files as loose *.test.ts(x) files when a nearby __tests__/ folder is available.
- Do not add duplicate client test helpers when
src/test/render.tsx, src/test/api.ts, or src/test/pageApi.ts already cover the setup.
- Do not reach for a real database in API unit tests by default.
Verification
- API tests:
npm --workspace @capture-flag/api run test.
- Client tests:
npm --workspace @capture-flag/client run test.
- Client coverage:
npm --workspace @capture-flag/client run test:coverage.
- Shared tests:
npm --workspace @capture-flag/shared run test.
- Evaluator tests:
npm --workspace @capture-flag/evaluator run test.
- SDK tests:
npm --workspace @capture-flag/sdk-js run test.
- All workspace tests:
npm run test.
Reference: ai/architecture/monorepo.md
Monorepo Architecture
Capture Flag is a TypeScript npm workspaces monorepo.
Workspaces
apps/api: NestJS API, Prisma access, authentication, tenant services, public SDK config endpoint.
apps/client: Vite React app for platform UI.
packages/shared: shared package boundary for reusable cross-workspace code.
packages/evaluator: pure local evaluation engine.
packages/sdk-js: JavaScript SDK that fetches public config and evaluates locally.
packages/react: React provider and hook around the JavaScript SDK.
Root Commands
- Build all workspaces:
npm run build.
- Test all workspaces:
npm run test.
- Lint repository:
npm run lint.
- Sync AI routes:
npm run ai:sync.
- Check AI routes:
npm run ai:check.
Boundaries
- API-only code stays in
apps/api.
- Client-only code stays in
apps/client.
- SDK and evaluator code must not import server-only packages.
- Shared code belongs in
packages/* only when there is a real cross-workspace consumer.
- Product and contract docs in
docs/* are part of the source of truth for behavior.
Reference: ai/architecture/client-app.md
Client App Architecture
apps/client is a Vite React application for the Capture Flag platform UI.
Entry And Routing
src/main.tsx owns top-level providers.
src/router.tsx owns React Router route definitions.
- Route modules are lazy-loaded through the local
lazyRoute() helper and should expose named exports.
src/layouts contains route layout wrappers that render shared shells, navigation, headers, and nested <Outlet /> regions.
src/pages contains route-level screens. Multi-file route screens use folder modules with index.ts named exports; simple one-file screens may stay as direct page files.
src/components contains shared UI used by multiple pages or sections.
src/core contains context-independent client utilities and reusable hooks organized by category.
src/api contains client request functions, React Query hooks, operation barrels, domain barrels, and domain query keys.
src/routing contains route path and route context helpers shared by pages and layouts.
src/stories contains shared Storybook fixtures and API mocks, not component stories.
src/test contains shared Vitest and Testing Library helpers.
PlatformLayout owns the authenticated shell, navigation frame, sidebar/header state, logout flow, and nested route outlet.
- Selected organization, project, config, and environment state is derived by route helpers such as
useRouteContext, not stored in a mutable layout context.
Route Map
/: redirects to /organizations.
/login: GitHub login screen.
/account: authenticated user account details and display name editing.
/organizations and /organizations/:organizationId: organization selection and organization members.
/organizations/:organizationId/projects and /organizations/:organizationId/projects/:projectId: project selection and project members.
/organizations/:organizationId/projects/:projectId/environments: environments for the selected project.
/organizations/:organizationId/projects/:projectId/configs and /organizations/:organizationId/projects/:projectId/configs/:configId: configs and public Config JSON preview.
/organizations/:organizationId/projects/:projectId/configs/:configId/flags: feature flags and remote config values.
/organizations/:organizationId/projects/:projectId/configs/:configId/segments: reusable targeting segments.
/organizations/:organizationId/projects/:projectId/sdk-keys: SDK key lifecycle for project configs/environments, with selected config/environment in ?configId= and ?environmentId= when needed.
/organizations/:organizationId/audit-logs: organization/project audit log timeline, with selected project in ?projectId= when needed.
*: redirects to /.
Data Flow
- React Query owns server state such as authenticated user, organizations, projects, configs, environments, SDK keys, members, and feature flags.
- API operations live under
src/api/<domain>/<operation>.
- Request functions perform HTTP calls and contain no React imports.
- Query and mutation hooks are the UI-facing API.
- Query hooks may use
useQuery or useInfiniteQuery according to the API shape.
- Mutation hooks invalidate affected query keys.
- Mutations that affect derived server state may invalidate query keys from multiple API domains inside the mutation hook.
- API request and hook tests mock successful responses and API errors instead of reaching a real backend.
- Route params, search params, and server state are combined by
useRouteContext for selected resources and redirect-safe navigation paths.
- Permission gates in the client are UX only; API guards and services remain authoritative.
UI Composition
- Route components compose page sections and own screen-level flow.
- Repeated panels, forms, controls, lists, and empty states move into named components.
- React component files should stay at or below 400 lines by splitting real responsibilities into focused files.
- Short, fixed navigation or action sets should be rendered explicitly instead of through artificial arrays.
- Page-specific components stay colocated under the page folder until reused elsewhere.
- Layout-specific components stay colocated under
src/layouts/<LayoutName> until reused by another layout or page.
- Shared primitives live under
src/components and are imported directly through aliases such as @components/Button.
- Member management uses shared
components/members primitives with page-specific role options.
- Feature flag and segment page internals stay colocated under their page folders until reused.
- Storybook stories live in a
stories/ child folder beside the source area they cover.
- Route-level and cross-page grouping stories live in
src/pages/stories.
- Shared Storybook data and fetch mocks live in
src/stories and are reused by stories and page tests.
Shared Core Utilities
- Context-independent helpers and reusable client hooks live under
src/core/<category>/<name>.ts.
- Current core categories include
date, json, strings, validation, and hooks.
- Each core file exports one function or hook; import it from the direct file path such as
src/core/date/toDate.
- Do not add
index.ts barrels under src/core; multiple helpers require multiple explicit imports.
- Core tests live under
src/core/<category>/__tests__/<name>.test.ts next to the category they cover.
- Client tests live in
__tests__/ child folders beside the source area they cover, mirroring Storybook stories/ folders.
- Shared test setup and helpers live under
src/test and provide Testing Library render helpers, React Query providers, and fetch response mocks.
- Coverage is run with
npm --workspace @capture-flag/client run test:coverage and should stay at 90% or higher for configured client targets.
- Page, domain, API, or route-specific helpers stay colocated with their owning feature until they become context-independent reuse.
Client Test And Story Layout
- API request and hook tests live in
src/api/__tests__ and cover operation behavior through mocked fetch responses.
- Shared component tests live in
src/components/__tests__; member component tests live in src/components/members/__tests__.
- Page tests live in
src/pages/<PageName>/__tests__ when they cover one page folder.
- Page subsection tests live in
src/pages/<PageName>/<section>/__tests__ when they cover colocated page internals such as feature flags or segments.
- Root-level client tests live in
src/__tests__ only for source files owned directly by src, such as permissions.ts.
- Shared component stories live in
src/components/stories; member component stories live in src/components/members/stories.
- Layout stories live in
src/layouts/<LayoutName>/stories.
- Page and page subsection stories live in the owning page folder's
stories/ child folder.
- Cross-page route or panel grouping stories live in
src/pages/stories and use route parameters plus shared mock data to render realistic page states.
- Do not place component stories in
src/stories; that folder is reserved for shared Storybook data, routes, and API mocks.
Form Flow
- React Hook Form owns field state and submission.
- Zod schemas parse and validate form values.
- API mutation hooks submit normalized payloads and refresh server state.
Reference: ai/examples/good-evaluator-test.md
Good Evaluator Test
Source: packages/evaluator/test/index.spec.ts (sha256: 40f396c39736d1e39ecc8575adc4f3566a656b3665b3513c10b64d3d49018e81)
Why this is canonical:
- Builds small config fixtures around the public evaluator contract.
- Tests observable evaluation behavior instead of internals.
- Covers fallback, rule order, segment references, prerequisite flags, advanced operators, rollout determinism, JSON values, and type mismatch behavior.
Canonical evaluator test pattern from packages/evaluator/test/index.spec.ts.
function createFlag(overrides: Partial<CaptureFlagConfigFlag> = {}): CaptureFlagConfigFlag {
return {
defaultValue: false,
percentageAttribute: "identifier",
percentageOptions: [],
rules: [],
type: "boolean",
...overrides,
};
}
function createConfig(flag: CaptureFlagConfigFlag = createFlag()): CaptureFlagConfig {
return {
configKey: "default",
environment: "production",
flags: {
newCheckout: flag,
},
generatedAt: "2026-05-12T00:00:00.000Z",
projectKey: "ecommerce",
revision: 1,
schemaVersion: 1,
};
}
Tests build small config fixtures and assert behavior through the public evaluate function.
Behavior Focus
- Missing or invalid config returns fallback.
- Rules evaluate top-down.
- Segment references evaluate locally and invalid segment references do not match.
- Prerequisite flags evaluate locally and cycles do not match.
- Advanced operators cover arrays, dates, and SemVer precedence.
- All rule conditions must match.
- Percentage rollout is deterministic, including basis-point decimal percentages.
- JSON object and array values are evaluated through default, rule, and rollout behavior.
- Type mismatches return fallback.
Reference: ai/examples/good-public-config-service.md
Good Public Config Service
Source: apps/api/src/public-sdk/public-sdk.service.ts (sha256: 85e49bab9d48419a9c4333b902d45cd724ae62a4b946eea88831bc3a4d42007c)
Why this is canonical:
- Authenticates public config access through hashed SDK keys.
- Reads SDK key, state, and flag values in one transactional path.
- Emits config-scoped segments for local SDK evaluation.
- Preserves ETag and not-modified semantics for SDK cache behavior.
- Uses safe default cache headers for SDK-key URLs while allowing explicit deployment override.
- Records SDK key usage without making telemetry writes break valid config responses.
Canonical public config pattern from apps/api/src/public-sdk/public-sdk.service.ts.
const transactionResult = await this.prisma.$transaction(
async (tx) => {
const sdkKey = await this.sdkKeyAuth.findActiveSdkKey(tx, rawSdkKey);
const state = await tx.configEnvironmentState.findUnique({
where: {
configId_environmentId: {
configId: sdkKey.configId,
environmentId: sdkKey.environmentId,
},
},
});
},
{ isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead },
);
The public endpoint authenticates by hashed SDK key and reads config state in the same transaction as flag values.
ETag Pattern
if (this.cache.matchesIfNoneMatch(ifNoneMatch, state.etag)) {
return {
result: {
etag: state.etag,
cacheControl,
notModified: true,
},
sdkKeyId: sdkKey.id,
};
}
Not-modified responses still count as valid SDK config access for lastUsedAt.
Cache-Control And Usage Pattern
const cacheControl = this.cache.cacheControlHeader();
await this.sdkKeyUsage.recordUse(transactionResult.sdkKeyId);
The public endpoint defaults away from shared caching because the raw SDK key is in the URL path, and usage telemetry is best-effort after a valid config read.