diff --git a/.gitignore b/.gitignore
index 7c3309ac20..dfdb8bae03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,5 +99,8 @@ opencode.json
/.playwright-mcp
/.devenv/mcp/
/opencode.json
+/.opencode/plans
+/.opencode/reports
+/.opencode/prompts
/.codex/
/tools/__pycache__
diff --git a/.opencode/skills/planner/SKILL.md b/.opencode/skills/planner/SKILL.md
index 7886dda85f..d1802652e2 100644
--- a/.opencode/skills/planner/SKILL.md
+++ b/.opencode/skills/planner/SKILL.md
@@ -1,6 +1,6 @@
---
name: planner
-description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to plans/YYYY-MM-DD-
.md only when the calling agent has write permission.
+description: Read-only planning and architecture analysis for Penpot — produce a structured implementation plan (Context, Affected modules, Approach, Risks, Testing). Always output to the user; additionally save to .opencode/plans/YYYY-MM-DD-.md.
---
# Planner
@@ -17,7 +17,7 @@ or modifies code.
names, and test strategy.
- The user asks "how would I implement X?" or "what's involved in fixing Y?".
- The user is about to start non-trivial work and wants a bite-sized task
- breakdown (DRY, YAGNI, TDD, frequent commits).
+ breakdown.
Do **not** use this skill to actually implement anything — it is read-only.
@@ -29,13 +29,8 @@ modify code.
You help users understand the codebase, design solutions, and create detailed
implementation plans that other agents or developers can execute. Document
-everything they need to know: which files to touch for each task, code, tests,
-docs they might need to check, and how to verify it. Give them the whole plan
-as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
-
-Assume the implementer is a skilled developer, but knows almost nothing about
-our toolset or problem domain. Assume they don't know good test design very
-well.
+everything they need to know: which files to touch for each task, code patterns,
+tests, and how to verify correctness. Apply DRY and KISS principles.
Do **not** suggest commit messages or commit names anywhere in your plans or
responses — committing is the developer's responsibility.
@@ -44,92 +39,233 @@ responses — committing is the developer's responsibility.
Before drafting any plan, work through the project's own guidance:
-1. Read `AGENTS.md` (root) for the project-level rules.
-2. Read `.serena/memories/critical-info.md` (or the equivalent entry point) to
- identify which modules are affected.
+1. Read `critical-info` (`.serena/memories/critical-info.md`) — the entry point
+ that describes the monorepo structure and module dependency graph.
+2. From `critical-info`, identify which modules your task affects.
3. Read each affected module's core memory, e.g. `mem:frontend/core`,
`mem:backend/core`, `mem:common/core`, `mem:exporter/core`,
`mem:render-wasm/core`. Follow `mem:` references deeper as needed.
-4. For frontend/backend work, check the relevant section's notes on lint,
- format, and test commands so the plan can include them.
+4. For each affected module, note its lint, format, and test commands so the
+ plan can include concrete verification steps.
Skipping this step is the #1 cause of incorrect or incomplete plans.
+## The Planning Process
+
+### Phase 1: Architecture Analysis
+
+1. Read the spec, requirements, or feature request.
+2. Analyze the codebase architecture and identify affected modules.
+3. Read project conventions (starting with `critical-info` and module core
+ memories) before drafting.
+4. Map dependencies between components (see the dependency graph in
+ `critical-info`).
+5. Identify risks, edge cases, performance implications, and breaking changes.
+
+### Phase 2: Task Breakdown
+
+Implementation order follows the monorepo's dependency graph:
+`frontend -> common`, `backend -> common`, `exporter -> common`,
+`frontend -> render-wasm`. Build shared foundations first, then layer
+consumers on top.
+
+#### Slice Vertically
+
+Instead of building all of common, then all of backend, then all of frontend —
+build one complete feature path at a time:
+
+```
+Task 1: common data types + schema ← foundation
+Task 2: backend RPC handler + persistence
+Task 3: frontend UI component + API integration
+```
+
+Each vertical slice delivers working, testable functionality.
+
+#### Write Tasks
+
+Each task follows this structure:
+
+```markdown
+## Task [N]: [Short descriptive title]
+
+**Description:** One paragraph explaining what this task accomplishes.
+
+**Acceptance criteria:**
+- [ ] [Specific, testable condition]
+- [ ] [Specific, testable condition]
+
+**Verification:**
+- [ ] Tests pass (module-specific test command)
+- [ ] Lint/formatter passes (module-specific check command)
+
+**Dependencies:** [Task numbers this depends on, or "None"]
+
+**Files likely touched:**
+- `path/to/file.clj`
+- `path/to/file_test.clj`
+```
+
+Replace "module-specific test command" with the actual commands for the module
+(e.g. `clojure -M:dev:test` for backend/common, `npx shadow-cljs compile test && npx karma start` for frontend,
+or the commands noted in the module's core memory).
+
+#### Estimate Scope
+
+| Size | Files | Scope |
+|------|-------|-------|
+| **XS** | 1 | Single function, config change, or schema tweak |
+| **S** | 1-2 | One handler or component method |
+| **M** | 3-5 | One vertical feature slice |
+| **L** | 5-8 | Multi-component feature |
+| **XL** | 8+ | **Too large — break it down further** |
+
+If a task is L or larger, break it into smaller tasks. Agents perform best on
+S and M tasks.
+
+**When to break a task down further:**
+- It would take more than one focused session
+- You cannot describe the acceptance criteria in 3 or fewer bullet points
+- It touches two or more independent subsystems
+- You find yourself writing "and" in the task title (a sign it is two tasks)
+
+#### Order and Checkpoints
+
+Arrange tasks so that:
+
+1. Dependencies are satisfied (build foundation first)
+2. Each task leaves the system in a working state
+3. Verification checkpoints occur after every 2-3 tasks
+4. High-risk tasks are early (fail fast)
+
+Add explicit checkpoints with the relevant module commands:
+
+```markdown
+## Checkpoint: After Tasks 1-3
+- [ ] All tests pass (module-specific command)
+- [ ] Lint/format passes (module-specific command)
+- [ ] Core flow works end-to-end
+- [ ] Review with human before proceeding
+```
+
## Requirements
- Analyze the codebase architecture and identify affected modules.
-- Read `AGENTS.md` and the memory system conventions before drafting.
+- Read project conventions before drafting (start with `critical-info` and
+ affected module core memories).
- Break down complex features or bugs into atomic, actionable steps.
- Propose solutions with clear rationale, trade-offs, and sequencing.
- Identify risks, edge cases, performance implications, and breaking changes.
- Apply DRY and KISS principles to the proposed implementation.
- Define a testing strategy aligned with each affected module's tooling.
+- Every task must have acceptance criteria and verification steps.
+- Checkpoints must exist between major phases.
## Constraints
- You are **analysis-only** — never create, edit, or delete source code.
-- The only file write you may attempt is the plan itself, and only when the
- calling agent has write permission (see "Plan Output"). If the write is
- denied, deliver the plan in the response and move on.
+- The only file write you may attempt is the plan itself, saved to
+ `.opencode/plans/`.
- You do **not** run builds, tests, linters, or any commands that modify state.
- You do **not** create git commits or interact with version control.
-- You do **not** execute shell commands beyond read-only searches (`rg`, `ls`,
- `find`, `cat`, `bat`).
+- You do **not** execute shell commands beyond read-only searches.
- Your output is a structured plan or analysis, ready for handoff to an
engineer agent or developer.
-## Plan Output
+## Output Format
The plan is always delivered in the response so the user sees it regardless
of which agent is running the skill.
-Persistence is a **separate, best-effort step** that only runs when the
-calling agent has `edit` write permission:
+Additionally, save the plan to:
-- **Has write permission** (e.g. `build`, `general`, `engineer`): in addition
- to the in-response plan, save the plan to:
+```
+.opencode/plans/YYYY-MM-DD-.md
+```
- ```
- plans/YYYY-MM-DD-.md
- ```
+Use today's date in the user's local timezone. The ``
+slug is lowercase, hyphen-separated, and a short summary of the task
+(e.g. `add-batch-get-profiles-for-file-comments`). Create the
+`.opencode/plans/` directory if it does not exist.
- Use today's date in the user's local timezone. The ``
- slug is lowercase, hyphen-separated, and a short summary of the task
- (e.g. `add-batch-get-profiles-for-file-comments`). Create the `plans/`
- directory if it does not exist.
+Always attempt the write. If the user explicitly provides a target file path,
+use that path instead of the default.
-- **No write permission** (e.g. the built-in `plan` agent, which denies
- `edit`): do not attempt to write the file — the write tool will be
- rejected. Just deliver the plan in the response. The user can copy it into
- `plans/...` manually if they want it persisted.
+### Plan Document Template
-If the user explicitly provides a target file path, use that path instead of
-the default `plans/YYYY-MM-DD-.md` (still subject to write permission).
+```markdown
+# Plan: [Feature/Project Name]
-How to detect write permission: try the write. If it is denied, treat the
-plan as response-only and proceed — do not retry, do not ask the user, and do
-not mention the failed write in the response.
+## Context
+[One paragraph: what is the problem or feature request? Why is it needed?]
-## Output Format
+## Affected Modules
+[Which modules of the monorepo are involved? Reference module paths and any
+`mem:` memories that were consulted.]
-Structure the plan as:
+## Architecture Decisions
+- [Key decision 1 and rationale]
+- [Key decision 2 and rationale]
-1. **Context** — What is the problem or feature request? Why is it needed?
-2. **Affected modules** — Which parts of the codebase are involved? Reference
- module paths and any `mem:` memories that were consulted.
-3. **Approach** — Step-by-step implementation plan with file paths, function
- names, and code shape where applicable. Group steps into atomic, ordered
- tasks.
-4. **Risks & considerations** — Edge cases, performance implications,
- breaking changes, migration concerns, security implications.
-5. **Testing strategy** — How to verify the implementation works correctly:
- which test commands to run per module, what cases to cover, manual
- verification steps, lint/format checks.
+## Risks & Considerations
+[Edge cases, performance implications, breaking changes, migration concerns,
+security implications.]
-Each step in **Approach** should be small enough to be reviewed and committed
-independently. Cite exact file paths (`path/to/file.ext:line` when useful) so
-the implementer can navigate directly.
+## Approach
+[Step-by-step implementation plan with file paths, function names, and code
+shape where applicable. Group steps into atomic, ordered tasks.]
+
+## Task List
+
+### Phase 1: Foundation
+- [ ] Task 1: ...
+- [ ] Task 2: ...
+
+### Checkpoint: Phase 1
+- [ ] Tests pass, lint/formatter clean (module-specific commands)
+
+### Phase 2: Core Features
+- [ ] Task 3: ...
+- [ ] Task 4: ...
+
+### Checkpoint: Phase 2
+- [ ] End-to-end flow works
+
+### Phase 3: Polish
+- [ ] Task 5: ...
+- [ ] Task 6: ...
+
+### Checkpoint: Complete
+- [ ] All acceptance criteria met
+- [ ] Ready for review
+
+## Testing Strategy
+[How to verify: which test commands to run per module, what cases to cover,
+manual verification steps, lint/format checks. Consult each module's core
+memory for the exact commands.]
+
+## Parallelization Opportunities
+- **Safe to parallelize:** Independent feature slices across separate
+ modules, tests for already-implemented features
+- **Must be sequential:** Shared common schema changes, database migrations
+- **Needs coordination:** Features that share a contract (define the contract
+ first, then parallelize)
+
+## Open Questions
+- [Question needing human input]
+```
When the plan is purely analytical (e.g. a code review or feasibility study
-with no implementation), skip the **Approach** section and lead with
-**Findings** instead, keeping the rest of the structure.
+with no implementation), skip the **Approach** and **Task List** sections and
+lead with **Findings** instead, keeping the rest of the structure.
+
+## Verification Checklist
+
+Before starting implementation, confirm:
+
+- [ ] Every task has acceptance criteria
+- [ ] Every task has a verification step
+- [ ] Task dependencies are identified and ordered correctly
+- [ ] No task touches more than ~5 files
+- [ ] Checkpoints exist between major phases
+- [ ] The human has reviewed and approved the plan
diff --git a/.serena/memories/backend/core.md b/.serena/memories/backend/core.md
index f416b78cf8..12a9bcda25 100644
--- a/.serena/memories/backend/core.md
+++ b/.serena/memories/backend/core.md
@@ -106,4 +106,4 @@ IMPORTANT: all CLI commands must be executed from the `backend/` subdirectory.
* **Coverage:** If code is added or modified in `src/`, corresponding tests in `test/backend_tests/` must be added or updated.
* **Isolated run:** `clojure -M:dev:test --focus backend-tests.my-ns-test` for a specific test namespace.
* **Regression run:** `clojure -M:dev:test` to ensure no regressions in related functional areas.
-* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:common/testing-principles`.
+* **Principles:** Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
diff --git a/.serena/memories/common/core.md b/.serena/memories/common/core.md
index 23fbe20198..6ae8f084e8 100644
--- a/.serena/memories/common/core.md
+++ b/.serena/memories/common/core.md
@@ -49,7 +49,7 @@ Components, variants, and debugging:
Text and tests:
- Shared text data conversion, DraftJS compatibility, modern text content, and derived position data: `mem:common/text-subtleties`.
- Common test commands, helper conventions, production-path test mutations, and runtime coverage choices: `mem:common/testing`.
-- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:common/testing-principles`.
+- Cross-cutting testing principles, anti-patterns, and verification checklist: `mem:testing`.
## Areas without focused memories
diff --git a/.serena/memories/common/testing-principles.md b/.serena/memories/common/testing-principles.md
deleted file mode 100644
index 860852393b..0000000000
--- a/.serena/memories/common/testing-principles.md
+++ /dev/null
@@ -1,47 +0,0 @@
-# Testing Principles (Cross-Cutting)
-
-Shared testing principles for all modules (backend, frontend, common, render-wasm, plugins). Module-specific commands and helpers are in each module's own testing memory.
-
-## Core Principles
-
-- **Test State, Not Interactions** — assert on outcomes, not method calls; survives refactoring
-- **DAMP over DRY** — tests are specifications; duplication OK if each test is self-contained and readable
-- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock; mock only at boundaries (network, filesystem, email)
-- **Arrange-Act-Assert** — every test: setup / action / verify
-- **One Assertion Per Concept** — each test verifies one behavior; split compound assertions
-- **Descriptive Test Names** — names read like specifications
-
-## Anti-Patterns
-
-- Testing implementation details → breaks on refactor
-- Flaky tests (timing, order-dependent) → erode trust
-- Mocking everything → tests pass, production breaks
-- No test isolation → pass individually, fail together
-- Testing framework code → waste of time
-- Snapshot abuse → nobody reviews, break on any change
-
-## Verification Checklist
-
-After completing any implementation:
-- Every new behavior has a corresponding test
-- All tests pass for touched modules
-- Bug fixes include a reproduction test that failed before the fix
-- Test names describe the behavior being verified
-- No tests were skipped or disabled
-
-## Red Flags
-
-- Writing code without corresponding tests
-- Tests that pass on the first run (may not be testing what you think)
-- Bug fixes without reproduction tests
-- Tests that test framework behavior instead of app behavior
-- Skipping tests to make the suite pass
-
-## Rationalizations
-
-- "I'll write tests after" → you won't; they'll test implementation not behavior
-- "Too simple to test" → simple gets complicated; test documents expected behavior
-- "Tests slow me down" → they speed up every future change
-- "I tested manually" → manual testing doesn't persist
-- "Code is self-explanatory" → tests ARE the specification
-- "Just a prototype" → prototypes become production; test debt accumulates
diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md
index 1027829bcf..d291c81bf9 100644
--- a/.serena/memories/critical-info.md
+++ b/.serena/memories/critical-info.md
@@ -6,6 +6,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- A section's top-level memory is `/core`. When a section is relevant, read the core memory
before focused memories.
- Edits/stale refs/duplication cleanup: `mem:memory-maintenance`.
+- Cross-cutting testing principles, TDD workflow, and anti-patterns: `mem:testing`.
# Development workflow
diff --git a/.serena/memories/exporter/core.md b/.serena/memories/exporter/core.md
index da61317d63..3bcf784f49 100644
--- a/.serena/memories/exporter/core.md
+++ b/.serena/memories/exporter/core.md
@@ -7,6 +7,7 @@
- Source: `exporter/src/`; config: `deps.edn`, `shadow-cljs.edn`, `package.json`; runtime helpers/assets: `vendor/`, `scripts/`.
- From `exporter/`: setup `./scripts/setup`; watch `pnpm run watch` or `pnpm run watch:app`; production build `pnpm run build`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
+- Cross-cutting testing principles and anti-patterns: `mem:testing`.
## HTTP and browser pool
diff --git a/.serena/memories/frontend/core.md b/.serena/memories/frontend/core.md
index b890de6770..5f06f8cfb0 100644
--- a/.serena/memories/frontend/core.md
+++ b/.serena/memories/frontend/core.md
@@ -52,7 +52,7 @@ Diagnostics and validation:
- Source-edit compile/hot-reload diagnostics: `mem:frontend/compile-diagnostics`.
- Runtime crash recovery: `mem:frontend/handling-crashes`.
- Tests and live verification: `mem:frontend/testing`.
-- Cross-cutting testing principles and anti-patterns: `mem:common/testing-principles`.
+- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Real pointer/keyboard gesture reproduction: `mem:frontend/playwright-gestures`.
## Areas without focused memories
diff --git a/.serena/memories/library/core.md b/.serena/memories/library/core.md
index dec9347c6f..7ff57c74b2 100644
--- a/.serena/memories/library/core.md
+++ b/.serena/memories/library/core.md
@@ -7,6 +7,7 @@
- Source: `library/src/`; tests: `library/test/`; experimentation/docs: `playground/`, `docs/`; config: `shadow-cljs.edn`, `deps.edn`, `package.json`.
- From `library/`: build `pnpm run build`; bundle helper `pnpm run build:bundle` or `./scripts/build`; tests `pnpm run test`; watch `pnpm run watch` / `pnpm run watch:test`; lint `pnpm run lint`; format check/fix `pnpm run check-fmt` / `pnpm run fmt`.
- When changing file-format construction or export behavior in `common/`, consider whether `@penpot/library` should be tested because it constructs Penpot files outside the app UI.
+- Cross-cutting testing principles and anti-patterns: `mem:testing`.
## JS API and builder state
diff --git a/.serena/memories/mcp/core.md b/.serena/memories/mcp/core.md
index 772f7a3949..7aeb784470 100644
--- a/.serena/memories/mcp/core.md
+++ b/.serena/memories/mcp/core.md
@@ -85,6 +85,7 @@ From the `mcp/` directory, run
* `pnpm run build` to test the build of all packages
* `pnpm run fmt` to apply the auto-formatter
+* Cross-cutting testing principles and anti-patterns: `mem:testing`.
## Devenv plugin/server wiring
diff --git a/.serena/memories/plugins/core.md b/.serena/memories/plugins/core.md
index ea51e5dad6..b042c34aa8 100644
--- a/.serena/memories/plugins/core.md
+++ b/.serena/memories/plugins/core.md
@@ -13,6 +13,7 @@
- From `plugins/`: install `pnpm -r install`; runtime dev server `pnpm run start` or `pnpm run start:app:runtime`; sample plugin `pnpm run start:plugin:`; build runtime `pnpm run build:runtime`; build plugins `pnpm run build:plugins`; lint `pnpm run lint`; format `pnpm run format:check` / `pnpm run format`; tests `pnpm run test`; e2e `pnpm run test:e2e`.
- If a change affects public Plugin API types or runtime, update `plugins/CHANGELOG.md`. Prefix type/signature entries with `**plugin-types:**`; runtime behavior entries with `**plugin-runtime:**`.
+- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- JS Plugin API behavior inside Penpot app: `mem:frontend/plugin-api-to-cljs-binding`; TS declarations are not runtime code; many API objects are CLJS proxies in `frontend/src/app/plugins/*.cljs`.
## Sandbox and global cleanup
diff --git a/.serena/memories/render-wasm/core.md b/.serena/memories/render-wasm/core.md
index 30734d2b0d..17345b7a7d 100644
--- a/.serena/memories/render-wasm/core.md
+++ b/.serena/memories/render-wasm/core.md
@@ -26,6 +26,7 @@ From `render-wasm/`:
- Build/copy frontend artifacts: `./build`.
- Watch rebuild: `./watch`.
- Rust tests: `./test` or `cargo test `.
+- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Lint: `./lint`.
- Format check: `cargo fmt --check`.
diff --git a/.serena/memories/testing.md b/.serena/memories/testing.md
new file mode 100644
index 0000000000..2f9f4c9131
--- /dev/null
+++ b/.serena/memories/testing.md
@@ -0,0 +1,149 @@
+# Testing
+
+## Overview
+
+Tests are proof that code works. Every behavior change needs a test.
+
+Testing in this monorepo varies by module. Each module has its own test
+commands, helpers, runner registration requirements, and conventions. This
+memory covers cross-cutting testing principles. For module-specific commands
+and helpers, consult:
+
+- `mem:common/testing` — CLJC unit tests (JVM + JS), test helpers, fixture
+ builders, production-path change helpers
+- `mem:frontend/testing` — CLJS unit tests, Playwright E2E integration tests,
+ live browser verification via nREPL
+- Backend — JVM `clojure.test` under `backend/test/`; see `mem:backend/core`
+
+## When to Use
+
+- Implementing new logic or behavior
+- Fixing any bug (reproduction test required)
+- Modifying existing functionality
+- Adding edge case handling
+
+**When NOT to use:** Pure configuration changes, documentation updates, or
+static content changes with no behavioral impact.
+
+## TDD: Recommended Workflow
+
+Write a failing test before writing the code that makes it pass. For bug fixes,
+reproduce the bug with a test before attempting a fix.
+
+When TDD isn't practical (exploratory work, tight coupling to unknown APIs),
+still write tests before considering the work complete.
+
+```
+ RED GREEN REFACTOR
+ Write a test Write minimal code Clean up the
+ that fails ──→ to make it pass ──→ implementation ──→ (repeat)
+ │ │ │
+ ▼ ▼ ▼
+ Test FAILS Test PASSES Tests still PASS
+```
+
+- **RED** — Write the test first. It must fail. A test that passes immediately
+ proves nothing.
+- **GREEN** — Write the minimum code to make the test pass. Don't over-engineer.
+- **REFACTOR** — With tests green, improve the code without changing behavior:
+ extract shared logic, improve naming, remove duplication. Run tests after
+ every step.
+
+## The Prove-It Pattern (Bug Fixes)
+
+When a bug is reported, **do not start by trying to fix it.** Start by writing
+a test that reproduces it:
+
+1. Write a test that demonstrates the bug
+2. Confirm the test FAILS (proving the bug exists)
+3. Implement the fix
+4. Confirm the test PASSES (proving the fix works)
+5. Run the full test suite for the module (no regressions)
+
+## Core Principles
+
+- **Test State, Not Interactions** — assert on outcomes, not method calls;
+ survives refactoring
+- **DAMP over DRY** — tests are specifications; duplication is OK if each test
+ is self-contained and readable. A test should tell a complete story without
+ requiring the reader to trace through shared helpers.
+- **Prefer Real Implementations** — hierarchy: Real > Fake > Stub > Mock;
+ mock only at boundaries (network, RPC, filesystem, email)
+- **Arrange-Act-Assert** — every test: setup / action / verify
+- **One Assertion Per Concept** — each test verifies one behavior; split
+ compound assertions
+- **Descriptive Test Names** — names read like specifications
+
+## Prefer Real Implementations Over Mocks
+
+Work down this list:
+
+1. **Real implementation** — Test the actual code with real collaborators.
+ Highest confidence.
+2. **Fake** — A simplified but functional in-memory implementation (e.g.
+ atom/dict-backed store instead of a real database).
+3. **Stub** — Returns canned data. Use when the collaborator's logic is
+ irrelevant.
+4. **Mock** — Last resort, only at boundaries. Use only when verifying
+ interaction with an external system that cannot be faked.
+
+**Rule of thumb:** If you can write a fake or use the real implementation, do
+that. If you find yourself asserting on call counts or invocation order, ask
+whether a fake would be clearer.
+
+## Fixtures over Manual Setup
+
+Use fixture/`beforeEach` mechanisms for shared setup and teardown. Each test
+should own its state so tests don't interfere with each other. Shorter-scope
+fixtures (`:each` / per-test) are preferred; longer-scope fixtures (`:once` /
+suite-level) are only for expensive, immutable shared setup.
+
+## Parametrized Tests
+
+Use your test framework's parametrize/table-driven mechanism to test multiple
+scenarios with a single test body. Keeps tests concise and surfaces all cases
+at a glance.
+
+## Test Pyramid
+
+```
+ ╱╲
+ ╱ ╲ E2E (few)
+ ╱ ╲ Full flows, real browser/server
+ ╱──────╲
+ ╱ ╲ Integration (some)
+ ╱ ╲ Cross-module, test DB
+ ╱────────────╲
+ ╱ ╲ Unit (most)
+ ╱ ╲ Pure logic, fast
+ ╱──────────────────╲
+```
+
+Prefer unit tests for pure logic. Reach for integration/E2E tests when covering
+RPC handlers, database queries, or full user flows. In the frontend, Playwright
+E2E tests should not be added unless explicitly requested.
+
+## Anti-Patterns
+
+| Anti-Pattern | Problem | Fix |
+|---|---|---|
+| Testing implementation details | Breaks on refactor | Test inputs/outputs |
+| Flaky tests (timing, order-dependent) | Erodes trust | Deterministic assertions, isolate state |
+| Mocking everything | Tests pass, production breaks | Prefer real implementations or fakes |
+| No test isolation | Pass individually, fail together | Per-test state fixtures |
+| Testing framework/platform code | Wastes time | Only test YOUR code |
+| Snapshot abuse | Nobody reviews, break on any change | Focused assertions |
+| Skipping tests to make suite pass | Hides real failures | Fix the test or fix the code |
+
+## Verification Checklist
+
+After completing any implementation:
+
+- [ ] Every new behavior has a corresponding test
+- [ ] All tests pass for touched modules
+- [ ] Bug fixes include a reproduction test that failed before the fix
+- [ ] Test names describe the behavior being verified
+- [ ] No tests were skipped or disabled
+- [ ] Lint/formatter passes for touched modules
+- [ ] New test files registered in the module's runner/entrypoint (see module
+ testing memory)