Verification
Once the FSD slices fixed in architecture have actual code in them, something has to ask whether that code does what the user wrote down as a scenario. The base answers that question along two axes: static and behavioral.
| Tool | What it covers |
|---|---|
review-extension | Six static dimensions: FSD boundaries, MV3 forbidden patterns, adapter separation, dependency cycles, message-type consistency, lint and typecheck |
/test-extension | The behavioral side. Turns the scenarios in docs/design/extension-spec.md §2 into actual function calls |
The two look at the same code from different angles. Review asks “is the code clean?” and test asks “does the code do what the user described?” Passing only one of the two does not count as passing.
Where it sits
Section titled “Where it sits”implement-* (per-area builders) ↓/test-extension (spec → tests → run) ↓review-extension (six static dimensions) ↓build-and-packageTest runs before review on purpose. A static check can pass even when behavior is broken, and a green review on a behaviorally broken implementation tells you nothing useful. Catching the heavier signal first, then layering the static one on top, beats checking the same code twice in the wrong order.
Spec-driven, the boilerplate gap
Section titled “Spec-driven, the boilerplate gap”A typical boilerplate hands you a Vitest setup. You still have to write the test code yourself. /test-extension adds one step on top of that.
extension-spec.md §2 scenario (written by user) ↓/test-extension parses and maps it ↓scenario unit → Vitest test code (generated by AI) ↓co-located in src/{slice}/__tests__/The user only writes the scenario. A line like “On a GitHub issue page, extract title, body, and labels and save them to Notion as a new page.” gets broken down by the skill into a small set of verifiable units, each with a generated test function.
Scenario to test mapping
Section titled “Scenario to test mapping”The decomposition follows FSD slices.
| Scenario fragment | Test unit | Location |
|---|---|---|
| extract issue title, body, labels | extractPageContent | src/features/analyze-page/__tests__/ |
| save to Notion as new page | notionAdapter.send(payload) | src/shared/api/backend/__tests__/ |
| retry resends the same payload | retry logic | src/features/send-to-backend/__tests__/ |
Three test patterns are baked in.
- content or feature tests: in jsdom, set
document.body.innerHTML, call the extractor, then check fields on the returned object - adapter tests:
vi.stubGlobal('fetch', mock)intercepts fetch, the test asserts payload shape and headers - entities domain tests: pure units like discriminated-union validators, no external dependency
Test files land inside the slice in __tests__/, co-located. They bypass the FSD public API and import slice internals directly. Tests are part of the slice, not external consumers of it.
chrome.* mocks and jsdom
Section titled “chrome.* mocks and jsdom”Content-script tests need a DOM, so vitest.config.ts pins test.environment to jsdom. When background or popup tests call into chrome.storage or chrome.runtime, the minimal mock in vitest.setup.ts catches them.
global.chrome = { storage: { local: { get: vi.fn().mockResolvedValue({}), set: vi.fn().mockResolvedValue(undefined), }, }, runtime: { sendMessage: vi.fn(), onMessage: { addListener: vi.fn() } },} as anyThe mock starts thin on purpose. As tests accumulate, which chrome APIs you actually need becomes clear, and adding a line to setup.ts when a test demands it costs less than maintaining a fat fake from day one.
Reporting in scenario language
Section titled “Reporting in scenario language”Raw Vitest output never reaches the user as-is. The skill rewrites it scenario by scenario.
Total scenarios: 5 — 4 passed, 1 failed
✓ Page content extraction — title, body, and labels pulled correctly from a GitHub issue✓ Backend send (server-relay) — Bearer token + POST sent correctly✗ PageContent validation — empty title rejection: an empty title was accepted instead of being rejected File: src/entities/page-content/__tests__/model.test.ts:8 Fix at: model/validate.ts, isValidPageContentHiding the raw output is deliberate. For a non-developer or a domain PM, “this scenario passed and that scenario failed” lands closer to a decision than expected 'foo' to be 'bar' at line 32. The full log still lives in .claude/state/last-test.log for the moments when someone has to dig.
review-extension on the static side
Section titled “review-extension on the static side”The six static dimensions get one paragraph here rather than their own page. On top of lint and typecheck, review-extension checks for FSD boundary violations, MV3 forbidden patterns (such as inline code in executeScript or any eval reliance), adapter interface violations, missing branches in the message union, duplicate definitions of the same payload shape, and breaches of the domain rules in AI_AUTOMATION.md. Two consecutive changes_requested reviews flip the stage to blocked and trigger /recover-from-blocked. That flow is covered in Recovery slash commands.
Beginner pace
Section titled “Beginner pace”The same idea written at a beginner pace lives at the end of beginner chapter 04. Same principle: the user writes the scenario, the AI writes the verification code, the report comes back in scenario language.
Build-and-package comes next. How the FSD tree from architecture meets manifest.config.ts and turns into a dist/ Chrome can load, and what pnpm run build and pnpm run package each leave on disk.