Skip to content

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.

ToolWhat it covers
review-extensionSix static dimensions: FSD boundaries, MV3 forbidden patterns, adapter separation, dependency cycles, message-type consistency, lint and typecheck
/test-extensionThe 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.

implement-* (per-area builders)
/test-extension (spec → tests → run)
review-extension (six static dimensions)
build-and-package

Test 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.

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.

The decomposition follows FSD slices.

Scenario fragmentTest unitLocation
extract issue title, body, labelsextractPageContentsrc/features/analyze-page/__tests__/
save to Notion as new pagenotionAdapter.send(payload)src/shared/api/backend/__tests__/
retry resends the same payloadretry logicsrc/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.

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.

vitest.setup.ts
global.chrome = {
storage: {
local: {
get: vi.fn().mockResolvedValue({}),
set: vi.fn().mockResolvedValue(undefined),
},
},
runtime: { sendMessage: vi.fn(), onMessage: { addListener: vi.fn() } },
} as any

The 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.

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, isValidPageContent

Hiding 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.

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.

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.