# Testing Guidance This document defines how to split test coverage between service tests and API integration tests so we avoid duplicated effort and keep runtime under control. ## Goals - Keep confidence high without duplicating the same business-rule matrix in multiple layers. - Keep API integration suites focused on HTTP contract and runtime wiring behaviour. - Keep business logic and edge-case matrices focused in service tests. ## Browser Test Principles Browser tests protect critical user journeys; they are not the default place to test business-rule permutations. ### Structure by Responsibility - Specs describe the user journey and expected outcome. - Fixtures provide authenticated contexts, reusable capabilities, and isolated test data. - UI drivers encapsulate repeated interactions and stable locators. - API helpers arrange prerequisites quickly through supported application APIs. - Runner and configuration code owns temporary storage, ports, server lifecycle, profiles, timeouts, and diagnostic artifacts. Keep browser-test code organised by those responsibilities. Do not mix environment orchestration, API setup, detailed selectors, and outcome assertions into each spec. ### Maintainability Rules 1. Keep specs readable. - A spec should read as a short user journey. - Do not expose API payloads, server orchestration, or detailed selectors in the scenario. 2. Use small workflow objects. - Introduce focused drivers such as `BoardPage` or `CardEditor` only where interactions are reused or complex. - Do not mirror the Vue component tree, create a universal base page, or build page-object inheritance hierarchies. - Keep meaningful outcome assertions visible in the spec. 3. Prefer semantic locators. - Prefer roles, labels, and visible text. - Use an existing stable semantic attribute where appropriate. - Add a test id only when no meaningful accessible selector exists. - Do not couple tests to styling classes, DOM ancestry, generated ids, or positional selectors. 4. Keep every test independent. - Tests must run individually and in any order. - Use fresh browser contexts and unique, readable test data. - Do not depend on state created by another test. - Use a disposable database for the run and remove the complete environment afterwards instead of relying on fragile per-test cleanup. 5. Use fixtures for capabilities. - Good fixtures provide capabilities such as an authenticated page, test board, second browser context, or API client. - Scenario-specific arrangements remain visible in the spec. - Avoid fixtures whose names hide an entire preconstructed scenario. 6. Wait for behaviour, never time. - Use Playwright's web-first assertions, URLs, API responses, and observable application state. - Do not use arbitrary sleeps to stabilise tests. 7. Maintain a strict smoke budget. - A test belongs in the GitHub smoke profile only when its failure materially undermines confidence in everyday BoardOil use. - Broader coverage belongs in the Jenkins regression profile. 8. Treat flakiness as a defect. - Begin with zero retries. - Do not hide flakes with blanket retries or larger global timeouts. - Diagnose failures with traces and fix the cause. - Any temporarily disabled browser test must reference a board bug. 9. Test at the lowest appropriate layer. - Business-rule matrices, API authorization permutations, and store edge cases remain in backend or Vitest suites. - Browser tests are reserved for browser behaviour and multi-layer composition such as focus/caret handling, cookies, routing, drag-and-drop, canvas, realtime, and critical user journeys. 10. Make failures readable. - Use concise `test.step` phases for longer journeys. - Retain actionable traces and screenshots on failure. - Prefer behavioural assertions over broad visual snapshots. ### Running the Browser Smoke Profile - Install Chromium once from `BoardOil.Web` with `npx playwright install chromium`. - Run the profile from `BoardOil.Web` with `npm run test:e2e:smoke`. - Name smoke specs `*.smoke.spec.ts`; the smoke command selects only that suffix. - Pass Playwright CLI options after `--` when narrowing or repeating a run. - The runner creates its own temporary SQLite database, image directory, API port, and frontend port, then removes the temporary environment. - Browser tests remain separate from `test-fast.mjs`, `test-full.mjs`, and the normal Vitest run. ### Running the Browser Regression Profile - Run every browser spec from `BoardOil.Web` with `npm run test:e2e:regression`. - Princess Posse Jenkins owns the scheduled regression run; GitHub Actions continues to run only the smoke profile. - Name broader or longer-running specs `*.regression.spec.ts` so their intended ownership is obvious. - Set `BOARDOIL_E2E_JUNIT_OUTPUT` when the caller needs a JUnit XML report; normal local and GitHub smoke runs retain the list reporter only. - Slow clean-build hosts may set `BOARDOIL_E2E_STARTUP_TIMEOUT_MS` to a positive integer; the default API/frontend readiness deadline remains 60000 ms. ### Running Browser Tests Against an External Runtime - Use `npm run test:e2e:smoke:external` for the smoke profile or `npm run test:e2e:regression:external` for the complete regression profile. - External mode requires `BOARDOIL_E2E_BASE_URL` to be an absolute HTTP(S) URL. It does not restore or start the API, start Vite, reserve local ports, create SQLite/image storage, or stop the supplied runtime. - Supply a fresh BoardOil data volume. The existing Playwright fixtures perform initial-admin setup and authenticate subsequent tests against that disposable runtime. - `BOARDOIL_E2E_JUNIT_OUTPUT`, Playwright traces/screenshots, command exit status, signal handling, and Playwright CLI arguments passed after `--` behave the same as managed-runtime runs. - The caller owns image selection, container lifecycle, isolation, cleanup, resource limits, and diagnostics. Princess Posse Jenkins invocation contract (run from `BoardOil.Web` after checking out the trusted source revision embedded in the selected image): ```sh BOARDOIL_E2E_BASE_URL=http://127.0.0.1:8080 \ BOARDOIL_E2E_JUNIT_OUTPUT=test-results/playwright/junit.xml \ npm run test:e2e:regression:external -- --project=chromium ``` ## Local Fast Loop Use repository scripts to keep local feedback fast and consistent: - `node scripts/test-fast.mjs` - default mode is changed-area detection from git diff - runs only impacted fast suites/checks (API, Dev orchestrator, Services, Web) - excludes slow API integration classes by design - supports suite overrides: `--api-only`, `--dev-only`, `--services-only`, `--web-only`, `--backend-only`, `--full` - supports output overrides: `--compact`, `--verbose` - `node scripts/test-full.mjs` - CI-like full local run (backend restore/build/tests + web check/test) - supports suite overrides: `--backend-only`, `--web-only` - supports output overrides: `--compact`, `--verbose` - supports `--profile-api` to print the ten slowest API test classes by cumulative test time Convenience wrappers: - `scripts/test-fast.sh`, `scripts/test-full.sh`, `scripts/test-fast.ps1`, and `scripts/test-full.ps1` delegate to the `.mjs` scripts. Recommended flow: 1. During implementation, run `node scripts/test-fast.mjs`. 2. If you changed only one area and want an explicit lane, use `node scripts/test-fast.mjs --api-only`, `--dev-only`, `--services-only`, `--web-only`, or `--backend-only`. 3. Before pushing risky backend/API auth/MCP/migration changes, run `node scripts/test-full.mjs --backend-only`. 4. Before pushing mixed backend+frontend changes, run full `node scripts/test-full.mjs`. 5. Avoid ad-hoc direct test commands during normal iteration; use the repository scripts so behavior stays consistent. These commands are validation tools, not mechanical source-control gates. When deciding whether to pause an explicitly requested commit or push, assess whether the tests that have not run would meaningfully exercise the actual changes. Raise a missing-validation concern only when that coverage could realistically reveal a defect; otherwise proceed with the requested source-control operation and report the validation that was performed. ## Test Output Modes The repository test scripts default to compact output when they detect an agent or CI environment (`CI`, `GITHUB_ACTIONS`, `CODEX_CI`, `CODEX_THREAD_ID`, or `CLAUDECODE`). Compact mode: - hides successful restore/build/check command output - prints concise pass summaries for xUnit and Vitest test runs - prints total and named build/test phase timings after a successful script run - replays full stdout/stderr for any failed command before reporting the failed suite - passes `--progress off --ansi off` to xUnit test applications - lets Vitest use its `agent` reporter with `silent: 'passed-only'` - suppresses Vite's successful build asset table in agent/CI runs while preserving warnings and errors Use `--verbose` or `BOARDOIL_TEST_OUTPUT=verbose` when investigating flaky tests, slow tests, or build output details. Use `--compact` or `BOARDOIL_TEST_OUTPUT=compact` to force low-noise output in a normal local shell. `npm test` and the Vite build wrapper in `BoardOil.Web` also detect these environment variables, so direct web test/build runs stay compact in agent/CI environments. Prefer the repository scripts for normal iteration because they also choose the right backend/frontend lanes. ## Ownership by Layer - Service tests (`BoardOil.Services.Tests`) own: - business rules and invariants - validation matrices and edge-case permutations - ordering/reassignment semantics - import/export/archive behaviour details - authorization decision logic at service boundary - API integration tests (`BoardOil.Api.Tests`) own: - endpoint contract shape (route, status code, envelope shape) - auth boundary behaviour (anonymous/forbidden/allowed) - model binding/serialization/middleware behaviour (CSRF, cookies, multipart binding) - API-only cross-cutting wiring (Swagger schema exposure, endpoint registration) ## API Integration Scope Rules For each endpoint family, target one test per endpoint concern, not one test per business-rule permutation. Keep in API integration: - one happy-path contract test per route family - one or more permission boundary tests where policy differs by actor - one validation mapping test per unique request-shape pattern - middleware/wiring tests that cannot be proven in service tests Move to service tests (or avoid adding in API integration): - additional permutations of the same validation rule - detailed state transition matrices already covered in services - repeated business-rule checks that only differ by data setup ## Duplication Check Before Adding Tests Before adding an API integration test: 1. Search `BoardOil.Services.Tests` for an equivalent business rule. 2. If service coverage exists, add API integration coverage only if we still need one of: - endpoint contract proof - permission boundary proof - middleware/binding proof 3. If none apply, add/extend service tests instead of API integration tests. ## Pruning Guidance When runtime becomes slow, prune in this order: 1. remove duplicated API integration business-rule permutations 2. keep API contract/auth/middleware representatives intact 3. keep migrations/realtime/MCP integration suites intact unless explicitly superseded ## Auth and MCP Policy Patterns For API auth coverage, prefer policy-level sentinels over endpoint-by-endpoint `401/403` assertions: - Keep a small set of representative auth boundary tests per policy shape (`authenticated`, `admin`, board membership variations). - Avoid duplicating anonymous/forbidden assertions on every endpoint when the route uses an already-covered policy. - Keep one endpoint metadata guard test that inspects mapped `/api/*` routes and fails if protected routes are missing authorization metadata. For security-behaviour suites, keep integration depth where transport/auth semantics are the behaviour: - Keep MCP bearer/path/auth-mode suites as integration tests. - Keep PAT scope behaviour and machine-token auth flow suites as integration tests. - Keep CSRF and internal API-key boundary checks as integration tests. ## Changed-Area Mapping `scripts/test-fast.mjs` maps changed paths to suites: - `BoardOil.Services/**` -> `BoardOil.Services.Tests` - `BoardOil.Api/**` or `BoardOil.Api.Tests/**` -> `BoardOil.Api.Tests` - `BoardOil.Dev/**` or `BoardOil.Dev.Tests/**` -> `BoardOil.Dev.Tests` - `BoardOil.Web/**` -> `npm run check` and `npm test` (in `BoardOil.Web`) - shared backend layers (`BoardOil.Contracts`, `BoardOil.Abstractions`, `BoardOil.Ef`, `BoardOil.Data.Abstractions`, `BoardOil.Mcp.Contracts`) -> API + Services tests - global tooling/workflow files (`BoardOil.slnx`, `Directory.Build.props`, `Directory.Packages.props`, `global.json`, `NuGet.config`, `.github/workflows/*`, `scripts/*`) -> API + Dev orchestrator + Services tests - unknown/non-code paths default to no tests in fast mode ## Naming Expectations Use names that make ownership clear: - Contract/middleware intent in API tests (for example `...ShouldReturnBadRequest`, `...ShouldReturnForbidden`, `...ShouldMark...`) - Rule/invariant intent in service tests (for example `...ShouldReassign...`, `...ShouldBeAtomic...`) ## Board Package Schema Compatibility - Treat currently supported board package schema versions as a compatibility set unless a story explicitly introduces versioned divergence. - When adding fields to board package payloads without a schema bump, add service tests that exercise those fields across all currently supported schema versions. - For BoardOil board package imports where schema `1` and `2` are intentionally equivalent, preserve regression coverage for both versions before introducing `3`.