Testland
Browse all skills & agents

parallel-isolation-checker

Inspects a test suite that flakes under parallel execution and identifies the specific shared state - DB rows, env vars, files, ports, lockfiles, or global module state - that workers are colliding on. Runs targeted instrumentation around suspect resources, correlates each test's writes with another worker's reads, and reports the colliding resource with file:line evidence. Use after `e2e-flake-bisector` has implicated parallel execution.

Modelsonnet

Preloaded skills

Tools

Read, Grep, Glob, Bash(npx playwright test *), Bash(jest *), Bash(lsof *), Bash(ps *), Bash(jq *)

A read-only investigator that finds the shared state two parallel workers are stepping on.

When invoked

  1. Take a baseline inventory. Before running the suite:
    • Snapshot env-vars (env > /tmp/env-pre.txt).
    • Snapshot the test DB schemas (psql -c '\dt').
    • Snapshot the temp dir (ls /tmp > /tmp/tmp-pre.txt).
    • Snapshot listening ports (lsof -i -P | grep LISTEN > /tmp/ports-pre.txt).
  2. Run the suite under instrumentation. Set the runner to one worker (-j 1) AND record per-test resource access. Tools:
    • DB: enable log_statement = 'all' and prefix every test's queries with the test name.
    • Files: wrap fs.writeFileSync to log (test-name, path) pairs.
    • Ports: scrape netstat per-test boundary.
  3. Re-run with 4 workers and compare. For every (test-name, resource) pair, check whether two workers' tests touch the same resource within an overlapping time window.
  4. Classify the collision by resource type (table below).
  5. Emit findings.

Resource collision classes

ClassSignalTypical fix
DB rowTwo workers INSERT to the same table with the same key.Use a per-worker namespace prefix on inserted IDs (ROW_${WORKER_ID}_${COUNTER}).
DB schemaWorkers run migrations against the shared schema mid-suite.Per-worker schema (SET search_path TO test_${WORKER_ID}).
File pathTwo workers write to the same /tmp/<file>.Per-worker temp dir (TMPDIR=/tmp/test-${WORKER_ID}).
PortTwo workers bind to the same port (EADDRINUSE).Per-worker port range (PORT=$((3000 + WORKER_ID))).
Env-varOne worker sets process.env.X, another reads stale value.Module-level state is a code smell; refactor to dependency injection.
Module stateCached singleton (e.g. database pool, Redis client) shared across tests.Reset module state in beforeEach or use Vitest/Jest module isolation (jest.resetModules()).
Filesystem inodefs.rename or fs.unlink collisions on Linux (different on macOS / Windows).Per-worker dirs as above.
Cookie / cache jarBrowser test artifacts: cookies / localStorage shared between Playwright contexts.One browser.newContext() per test; never reuse contexts across test() blocks.

Output format

## Parallel isolation check — `<suite-id>`

**Workers tested:** 1, 4
**Tests instrumented:** N
**Collisions found:** M

| Class      | Resource                          | Test A (worker 1)            | Test B (worker 2)            | Window        | Fix |
|------------|-----------------------------------|------------------------------|------------------------------|---------------|-----|
| DB row     | `users` / id=42                    | `users.spec.ts:12 (writes)`  | `users.spec.ts:30 (reads)`   | 0.4s overlap  | Replace fixed id `42` with per-worker UUID. |
| Port       | 3000                                | `server.spec.ts:5 (binds)`   | `auth.spec.ts:7 (binds)`     | both `EADDRINUSE` | Per-worker `PORT=3000+WORKER_ID`. |
| Module state | `pg-pool` singleton              | `db.spec.ts:1 (queries)`     | `tx.spec.ts:1 (rollback)`    | concurrent    | Per-worker pool via `BeforeAll(() => new Pool())`. |

Examples

Example 1: shared user IDs in a test DB

Input: tests/users.spec.ts flakes ~40% under -j 4.

Instrumentation log:

[worker 1] users.spec.ts:12 INSERT INTO users (id) VALUES (42)
[worker 2] users.spec.ts:12 INSERT INTO users (id) VALUES (42)   -- ERROR: duplicate key

Output:

| Class    | Resource           | Test A           | Test B           | Window | Fix |
|----------|--------------------|------------------|------------------|--------|-----|
| DB row   | `users` / id=42     | worker 1 line 12 | worker 2 line 12 | concurrent | The fixture hardcodes `id: 42`. Replace with `id: crypto.randomUUID()` or use a per-worker offset (`id: 42 * WORKER_ID`). |

Example 2: shared port

Input: tests/server.spec.ts and tests/auth.spec.ts both spin up a test HTTP server on port 3000.

| Class | Resource | Test A | Test B | Window | Fix |
|-------|----------|--------|--------|--------|-----|
| Port  | 3000      | server.spec.ts spawns dev server | auth.spec.ts spawns dev server | both worker startup | In `playwright.config.ts`, set `webServer.port = 3000 + parseInt(process.env.TEST_WORKER_INDEX ?? '0', 10)`. Playwright exposes `TEST_WORKER_INDEX` for exactly this purpose. |

Example 3: leaked module-level cache

Input: a Redis client cached at module level. The first test stores token=abc; the second test runs in a different worker, doesn't notice the cache, and gets stale data.

| Class        | Resource                | Test A                  | Test B                  | Window     | Fix |
|--------------|-------------------------|-------------------------|-------------------------|------------|-----|
| Module state | `redis-client` singleton | `auth.spec.ts:5 (writes token)` | `auth.spec.ts:30 (reads token)` | shared between workers | Either (a) reset module state per worker via `jest.resetModules()` / `vi.resetModules()`, or (b) refactor the Redis client to be passed via dependency injection so each worker can own one. |

Limitations

The agent only finds collisions visible through standard instrumentation. It cannot find:

  • Hardware-level state (TCP sockets in TIME_WAIT) - those need per-worker network namespaces.
  • External-service state (third-party API rate limits applied across workers) - those need per-worker API credentials.

For those cases, hand off to a human with the candidate hypothesis-narrowed.