# or: stryker run
```

```
INFO ProjectReader Found 7 of 2947 file(s) to be mutated.
INFO Instrumenter Instrumented 7 source file(s) with 394 mutant(s)
INFO DryRunExecutor Initial test run succeeded. Ran 184 tests in 0 seconds.

Mutation testing  [====================] 100% | 394/394 Mutants tested
(35 survived, 0 timed out)

--------------|---------|----------|----------|----------|
File          |  % score | # killed | # survived | # no cov |
--------------|---------|----------|----------|----------|
All files     |   90.86 |      358 |         35 |        1 |
 backlinks.ts |   96.30 |       26 |          1 |        0 |
 callouts.ts  |   93.94 |       62 |          4 |        0 |
 graph.ts     |   91.55 |       65 |          6 |        0 |
 mentions.ts  |   91.30 |       63 |          5 |        1 |
 minimark.ts  |   82.61 |       76 |         16 |        0 |
 text.ts      |  100.00 |       34 |          0 |        0 |
 wikilinks.ts |   91.43 |       32 |          3 |        0 |
--------------|---------|----------|----------|----------|

INFO MutationTestExecutor Done in 36 seconds.
```

394 mutants tested across 7 files in 36 seconds. The report shows exactly which files have weak spots—`minimark.ts` at 82.61% needs attention, while `text.ts` is solid at 100%.

Stryker also generates an interactive HTML report where you can drill into each surviving mutant and see exactly what code change your tests failed to catch.

<Alert type="tip" title="Use Stryker When You Can">
  If your stack supports Stryker (standard Vitest in Node mode, Jest, Mocha), use it. Deterministic tooling in your CI pipeline beats manual approaches every time. The AI agent technique in this post is for when Stryker isn't an option.
</Alert>

---

## The Vitest Browser Mode Problem

But what if Stryker doesn't support your stack? Stryker doesn't work with Vitest's browser mode. Their instrumentation assumes Node.js execution, but browser mode runs tests in actual Chromium via Playwright.

My setup:
- **Framework**: Vitest 4 with `browser.enabled: true`
- **Provider**: Playwright (Chromium)
- **Test style**: Integration tests with real DOM

<InternalLink slug="vue3_testing_pyramid_vitest_browser_mode">My testing strategy</InternalLink> relies heavily on Vitest browser mode for realistic user flow testing. Stryker's mutation coverage reports? Not an option. And switching to Node-based testing would mean losing the browser-specific behavior I'm actually testing.

---

## AI Agents as Manual Mutation Testers

The mutation testing algorithm is simple enough that an AI coding agent can execute it manually. Claude Code can:

1. Read your source code
2. Apply mutations systematically
3. Run `pnpm test --run`
4. Record whether tests passed or failed
5. Restore the original code
6. Report surviving mutants with suggested fixes

I adapted a <InternalLink slug="claude-code-customization-guide-claudemd-skills-subagents">Claude Code skill</InternalLink> originally created by [Paul Hammond](https://www.linkedin.com/posts/paul-hammond-bb5b78251_mutation-testing-is-typically-expensive-but-activity-7414719212071473152-_xTm) that codifies this workflow.

```mermaid
flowchart TD
    subgraph Agent["AI Agent Workflow"]
        A[Read source file] --> B[Identify mutation targets]
        B --> C[Apply single mutation]
        C --> D[Run test suite]
        D --> E{Tests fail?}
        E -->|Yes| F[Mutant KILLED]
        E -->|No| G[Mutant SURVIVED]
        F --> H[Restore original code]
        G --> H
        H --> I{More mutations?}
        I -->|Yes| C
        I -->|No| J[Generate report]
    end

    subgraph Results["Report Output"]
        J --> K[Killed mutants: Tests caught the bug]
        J --> L[Survived mutants: Test gaps found]
        L --> M[Suggested fixes for each gap]
    end

    style G fill:#f96,stroke:#333
    style F fill:#6f9,stroke:#333
    style L fill:#f96,stroke:#333
    style K fill:#6f9,stroke:#333
```

### The Mutation Testing Skill

The skill defines mutation operators in priority order:

**Priority 1 - Boundaries** (most likely to survive):

| Original | Mutate To |
|----------|-----------|
| `<` | `<=` |
| `>` | `>=` |
| `<=` | `<` |
| `>=` | `>` |

**Priority 2 - Boolean Logic**:

| Original | Mutate To |
|----------|-----------|
| `&&` | `\|\|` |
| `\|\|` | `&&` |
| `!condition` | `condition` |

**Priority 3 - Return Values**:

| Original | Mutate To |
|----------|-----------|
| `return x` | `return null` |
| `return true` | `return false` |
| Early return | Remove it |

**Priority 4 - Statement Removal**:

| Original | Mutate To |
|----------|-----------|
| `array.push(x)` | Remove |
| `await save(x)` | Remove |
| `emit('event')` | Remove |

The agent applies each mutation one at a time, runs tests, records results, and restores the original code immediately.

---

## Real Example: Settings Feature

I ran this against my settings feature. The integration tests looked comprehensive—theme toggling, language switching, unit preferences. Code coverage would show high percentages.

**Results: 38% mutation score** (5 killed, 8 survived out of 13 mutations)

Here's what the AI agent found:

### Surviving Mutant #1: Volume Boundary Not Tested

```typescript
// Original (stores/settings.ts:65)
Math.min(Math.max(volume, 0.5), 1)

// Mutation: Change 0.5 to 0.4
Math.min(Math.max(volume, 0.4), 1)

// Result: Tests PASSED -> Mutant SURVIVED
```

My tests never verified the minimum volume constraint. A bug changing the minimum from 50% to 40% would ship undetected.

### Surviving Mutant #2: Theme DOM Class Not Verified

```typescript
// Original (composables/useTheme.ts:26)
newMode === 'dark'

// Mutation: Negate the condition
newMode !== 'dark'

// Result: Tests PASSED -> Mutant SURVIVED
```

My test checked that clicking the toggle changed the stored preference. It never verified that `document.documentElement.classList` actually received the `dark` class. The UI could break while tests pass.

### Surviving Mutant #3: Error Handling Path Untested

```typescript
// Original (stores/settings.ts:28)
if (error) return

// Mutation: Negate the condition
if (!error) return

// Result: Tests PASSED -> Mutant SURVIVED
```

No test exercised the error handling branch. A bug that inverted error handling would go unnoticed.

### The Fixes

The agent suggested specific tests for each surviving mutant:

```typescript
// Fix for Mutant #1: Boundary test
it('volume slider has minimum value constraint of 50%', async () => {
  const volumeSlider = page.getByTestId('timer-sound-volume-slider')
  await expect.poll(async () => {
    const el = await volumeSlider.element()
    return el.getAttribute('min')
  }).toBe('0.5')
})

// Fix for Mutant #2: DOM verification
it('adds dark class to html element when dark mode enabled', async () => {
  const themeToggle = page.getByTestId('theme-toggle')
  await userEvent.click(themeToggle)

  await expect.poll(() =>
    document.documentElement.classList.contains('dark')
  ).toBe(true)
})
```

---

## How to Set This Up

### Step 1: Create the Skill

Save this as `.claude/skills/mutation-testing/SKILL.md`:

<details>
<summary>Full Mutation Testing Skill (click to expand)</summary>

```markdown
---
name: mutation-testing
description: |
  Mutation testing patterns for verifying test effectiveness. Use when analyzing branch code
  to find weak or missing tests. Triggers: "mutation testing", "test effectiveness",
  "would tests catch this bug", "weak tests", "are my tests good enough", "surviving mutants".
---