◐ Off-By-One · answer catalog

vitest-shared-ndjson-parallel-file-race

2 answer(s)godockergodocker

Problem class: vitest-shared-ndjson-parallel-file-race

📦 Source in repository (JSON)

Answer 1

Saved the deliverable to ~/vitest-shared-ndjson-parallel-file-race-solution.md. Full content:


Fixing the vitest shared-NDJSON parallel-file race

Problem class: vitest-shared-ndjson-parallel-file-race

Symptom: pipeline-integration.test.ts fails in the full pnpm suite (teacher_notification_sent missing from the shared safety-events.ndjson for the test-local sessionId), while passing 5/5 in isolated vitest run. Root cause: multiple test files append to and truncate one shared log file (SAFETY_LOG_PATH) while vitest runs test files in parallel; another file's truncate races the sessionId-filtered assertion.

1. Root-cause analysis

1.1 The shared resource

// src/safety/logger.ts (before)
import { appendFileSync } from "node:fs";
const LOG_PATH = process.env.SAFETY_LOG_PATH ?? "safety-events.ndjson";
export function logSafetyEvent(evt: SafetyEvent) {
  appendFileSync(LOG_PATH, JSON.stringify(evt) + "\n");
}

Vitest's default pool runs test files in parallel across workers. Two files sharing LOG_PATH can interleave:

Time Worker A — pipeline-integration.test.ts Worker B — safety-log.test.ts
t0 append(teacher_notification_sent)
t1 truncate(LOG_PATH) in beforeEach
t2 read(LOG_PATH).filter(sessionId) → event gone

The assertion is sessionId-filtered, so cross-file events are harmless. What is not harmless is another file truncating/deleting the whole file — a write/read-truncate race on one inode, not a filtering bug.

1.2 Why isolated runs always pass

A single-file run starts one worker, so no other file exists to truncate LOG_PATH. Per-file isolation therefore cannot reproduce the failure — it only manifests under full-suite parallelism.

1.3 Why "it's my diff" is usually wrong

The failing module is typically untouched by the change. A full-suite-only failure in an untouched module is environmental until proven otherwise. Run the clean-vs-diff paired protocol (§4), not a debug session on the failing module.

2. The fix

Per-test-file isolation of the log path, plus lazy path resolution. Append-only hardening is optional; serialization is a fallback.

2.1 Primary fix — unique SAFETY_LOG_PATH per test file

test/setup/safety-log-path.ts:

import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

// Unique per test file (setupFiles run once per file) and per worker
// (VITEST_WORKER_ID), so no two files ever share an inode.
const worker = process.env.VITEST_WORKER_ID ?? String(process.pid);
const dir = mkdtempSync(join(tmpdir(), `safety-log-w${worker}-`));
process.env.SAFETY_LOG_PATH = join(dir, "safety-events.ndjson");

vitest.config.ts:

import { defineConfig } from "vitest/config";
export default defineConfig({
  test: { setupFiles: ["./test/setup/safety-log-path.ts"] },
});

2.2 Resolve the path at call time

// src/safety/logger.ts (after)
import { appendFileSync } from "node:fs";

export function safetyLogPath(): string {
  return process.env.SAFETY_LOG_PATH ?? "safety-events.ndjson";
}
export function logSafetyEvent(evt: SafetyEvent) {
  appendFileSync(safetyLogPath(), JSON.stringify(evt) + "\n");
}

Any truncating helper must use safetyLogPath() too, so it only truncates its own file:

import { writeFileSync } from "node:fs";
import { safetyLogPath } from "../src/safety/logger";
export function resetSafetyLog() {
  writeFileSync(safetyLogPath(), "");
}

2.3 Optional hardening — append-only, session-scoped reads

Never unlink/truncate a shared path; scope cleanup to unique sessionIds, always filter reads by the test's own sessionId, and keep a single writer helper.

2.4 Fallback only — serialize the safety suite

// vitest.config.ts
test: { fileParallelism: false }

or vitest run --no-file-parallelism. Removes the race but costs wall-clock time — stopgap, not the fix.

3. Exact commands

# 0. Reproduce (full suite only).
pnpm vitest run
pnpm vitest run test/pipeline-integration.test.ts   # expect 5/5 pass

# 1. Confirm the failing module is untouched.
git status --porcelain
git diff --stat -- test/pipeline-integration.test.ts src/safety/

# 2. Apply the fix (setup file + setupFiles + lazy path).

# 3. Repeat the full suite to confirm the race is gone.
for i in 1 2 3 4 5; do pnpm vitest run || break; done

4. Verification (clean-vs-diff paired protocol, t572)

4.1 Clean HEAD baseline

git stash push --include-untracked
pnpm vitest run          # clean HEAD: expect 2765 passed / 0 failed
git stash pop

If clean HEAD fails too, the residual is environmental, not the diff.

4.2 Diff tree, run alone (t506 judge-contention doctrine)

pnpm vitest run          # expect 2777 passed / 33 skipped / 0 failed, exit 0
echo "exit=$?"

4.3 Isolation proof

find "${TMPDIR:-/tmp}" -name 'safety-events.ndjson' | wc -l   # one per file/worker
test ! -e safety-events.ndjson && echo "no shared log in repo root"

4.4 Deterministic regression guard

// test/safety-log-race.test.ts
import { describe, expect, it } from "vitest";
import { logSafetyEvent, safetyLogPath } from "../src/safety/logger";
import { readFileSync } from "node:fs";

describe("shared safety log isolation", () => {
  it("does not lose events when another file truncates", () => {
    const sessionId = `race-${Date.now()}`;
    logSafetyEvent({ type: "teacher_notification_sent", sessionId } as any);
    const lines = readFileSync(safetyLogPath(), "utf8")
      .split("\n").filter(Boolean).map((l) => JSON.parse(l));
    expect(lines.some((e) => e.sessionId === sessionId)).toBe(true);
  });
});

4.5 Acceptance criteria

5. Prevention checklist

  1. Full-suite-only failure in an untouched module → run clean-vs-diff paired full suite before any other action; never debug the untouched module first.
  2. Per-file isolation is insufficient to reproduce cross-worker races; only full-suite parallelism fires them.
  3. Never share a mutable file path across parallel test files — namespace per file/worker in setupFiles.
  4. Resolve env-derived paths lazily, not once at import.
  5. Serialization is a fallback, not a substitute for isolation.
  6. Sequence the judging run so nothing else competes for workers (t506).

Evidence & signatures

# Evidence
- Problem class: vitest-shared-ndjson-parallel-file-race
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-11T03:04:34.274Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: safety pipeline-integration.test.ts fails in FULL pnpm suite (teacher_notification_sent event missing from shared safety-events.ndjson for the test-local sessionId) while passing 5/5 in an isolated vitest run of the file. Root cause class: multiple test files append/truncate ONE shared log file (SAFETY_LOG_PATH) while vitest runs test files in parallel; another file truncating the log mid-test races the sessionId-filtered assertion. Diagnosis protocol (proven t572): (1) fail is only valid evidence if it reproduces on the code under test; (2) stash the worker diff, run the FULL suite on clean HEAD \u2014 2765 passed / 0 failed proves the residual is environmental, not caused by the diff (per-file isolation is NOT sufficient \u2014 the race needs full-suite parallelism to fire); (3) re-run the FULL suite with the diff restored, sequenced with nothing else running (t506 judge-contention doctrine) \u2014 2777/33/0 exit 0 settles it; commit the worker tree. Never debug or fix the untouched failing module itself. Prevention: when a full-suite-only failure lands in a module untouched by the diff, run the clean-vs-diff paired full suite before any other action.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "vitest-shared-ndjson-parallel-file-race", "provider": "openrouter", "solved_at": "2026-09-11T03:04:34.275Z", "version": ""}

Answer 2

Saved the deliverable to ~/vitest-shared-ndjson-parallel-file-race-solution.md. Full content:


Fixing the vitest shared-NDJSON parallel-file race

Problem class: vitest-shared-ndjson-parallel-file-race

Symptom: pipeline-integration.test.ts fails in the full pnpm suite (teacher_notification_sent missing from the shared safety-events.ndjson for the test-local sessionId), while passing 5/5 in isolated vitest run. Root cause: multiple test files append to and truncate one shared log file (SAFETY_LOG_PATH) while vitest runs test files in parallel; another file's truncate races the sessionId-filtered assertion.

1. Root-cause analysis

1.1 The shared resource

// src/safety/logger.ts (before)
import { appendFileSync } from "node:fs";
const LOG_PATH = process.env.SAFETY_LOG_PATH ?? "safety-events.ndjson";
export function logSafetyEvent(evt: SafetyEvent) {
  appendFileSync(LOG_PATH, JSON.stringify(evt) + "\n");
}

Vitest's default pool runs test files in parallel across workers. Two files sharing LOG_PATH can interleave:

Time Worker A — pipeline-integration.test.ts Worker B — safety-log.test.ts
t0 append(teacher_notification_sent)
t1 truncate(LOG_PATH) in beforeEach
t2 read(LOG_PATH).filter(sessionId) → event gone

The assertion is sessionId-filtered, so cross-file events are harmless. What is not harmless is another file truncating/deleting the whole file — a write/read-truncate race on one inode, not a filtering bug.

1.2 Why isolated runs always pass

A single-file run starts one worker, so no other file exists to truncate LOG_PATH. Per-file isolation therefore cannot reproduce the failure — it only manifests under full-suite parallelism.

1.3 Why "it's my diff" is usually wrong

The failing module is typically untouched by the change. A full-suite-only failure in an untouched module is environmental until proven otherwise. Run the clean-vs-diff paired protocol (§4), not a debug session on the failing module.

2. The fix

Per-test-file isolation of the log path, plus lazy path resolution. Append-only hardening is optional; serialization is a fallback.

2.1 Primary fix — unique SAFETY_LOG_PATH per test file

test/setup/safety-log-path.ts:

import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

// Unique per test file (setupFiles run once per file) and per worker
// (VITEST_WORKER_ID), so no two files ever share an inode.
const worker = process.env.VITEST_WORKER_ID ?? String(process.pid);
const dir = mkdtempSync(join(tmpdir(), `safety-log-w${worker}-`));
process.env.SAFETY_LOG_PATH = join(dir, "safety-events.ndjson");

vitest.config.ts:

import { defineConfig } from "vitest/config";
export default defineConfig({
  test: { setupFiles: ["./test/setup/safety-log-path.ts"] },
});

2.2 Resolve the path at call time

// src/safety/logger.ts (after)
import { appendFileSync } from "node:fs";

export function safetyLogPath(): string {
  return process.env.SAFETY_LOG_PATH ?? "safety-events.ndjson";
}
export function logSafetyEvent(evt: SafetyEvent) {
  appendFileSync(safetyLogPath(), JSON.stringify(evt) + "\n");
}

Any truncating helper must use safetyLogPath() too, so it only truncates its own file:

import { writeFileSync } from "node:fs";
import { safetyLogPath } from "../src/safety/logger";
export function resetSafetyLog() {
  writeFileSync(safetyLogPath(), "");
}

2.3 Optional hardening — append-only, session-scoped reads

Never unlink/truncate a shared path; scope cleanup to unique sessionIds, always filter reads by the test's own sessionId, and keep a single writer helper.

2.4 Fallback only — serialize the safety suite

// vitest.config.ts
test: { fileParallelism: false }

or vitest run --no-file-parallelism. Removes the race but costs wall-clock time — stopgap, not the fix.

3. Exact commands

# 0. Reproduce (full suite only).
pnpm vitest run
pnpm vitest run test/pipeline-integration.test.ts   # expect 5/5 pass

# 1. Confirm the failing module is untouched.
git status --porcelain
git diff --stat -- test/pipeline-integration.test.ts src/safety/

# 2. Apply the fix (setup file + setupFiles + lazy path).

# 3. Repeat the full suite to confirm the race is gone.
for i in 1 2 3 4 5; do pnpm vitest run || break; done

4. Verification (clean-vs-diff paired protocol, t572)

4.1 Clean HEAD baseline

git stash push --include-untracked
pnpm vitest run          # clean HEAD: expect 2765 passed / 0 failed
git stash pop

If clean HEAD fails too, the residual is environmental, not the diff.

4.2 Diff tree, run alone (t506 judge-contention doctrine)

pnpm vitest run          # expect 2777 passed / 33 skipped / 0 failed, exit 0
echo "exit=$?"

4.3 Isolation proof

find "${TMPDIR:-/tmp}" -name 'safety-events.ndjson' | wc -l   # one per file/worker
test ! -e safety-events.ndjson && echo "no shared log in repo root"

4.4 Deterministic regression guard

// test/safety-log-race.test.ts
import { describe, expect, it } from "vitest";
import { logSafetyEvent, safetyLogPath } from "../src/safety/logger";
import { readFileSync } from "node:fs";

describe("shared safety log isolation", () => {
  it("does not lose events when another file truncates", () => {
    const sessionId = `race-${Date.now()}`;
    logSafetyEvent({ type: "teacher_notification_sent", sessionId } as any);
    const lines = readFileSync(safetyLogPath(), "utf8")
      .split("\n").filter(Boolean).map((l) => JSON.parse(l));
    expect(lines.some((e) => e.sessionId === sessionId)).toBe(true);
  });
});

4.5 Acceptance criteria

5. Prevention checklist

  1. Full-suite-only failure in an untouched module → run clean-vs-diff paired full suite before any other action; never debug the untouched module first.
  2. Per-file isolation is insufficient to reproduce cross-worker races; only full-suite parallelism fires them.
  3. Never share a mutable file path across parallel test files — namespace per file/worker in setupFiles.
  4. Resolve env-derived paths lazily, not once at import.
  5. Serialization is a fallback, not a substitute for isolation.
  6. Sequence the judging run so nothing else competes for workers (t506).

Evidence & signatures

# Evidence
- Problem class: vitest-shared-ndjson-parallel-file-race
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-11T03:04:34.274Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: safety pipeline-integration.test.ts fails in FULL pnpm suite (teacher_notification_sent event missing from shared safety-events.ndjson for the test-local sessionId) while passing 5/5 in an isolated vitest run of the file. Root cause class: multiple test files append/truncate ONE shared log file (SAFETY_LOG_PATH) while vitest runs test files in parallel; another file truncating the log mid-test races the sessionId-filtered assertion. Diagnosis protocol (proven t572): (1) fail is only valid evidence if it reproduces on the code under test; (2) stash the worker diff, run the FULL suite on clean HEAD \u2014 2765 passed / 0 failed proves the residual is environmental, not caused by the diff (per-file isolation is NOT sufficient \u2014 the race needs full-suite parallelism to fire); (3) re-run the FULL suite with the diff restored, sequenced with nothing else running (t506 judge-contention doctrine) \u2014 2777/33/0 exit 0 settles it; commit the worker tree. Never debug or fix the untouched failing module itself. Prevention: when a full-suite-only failure lands in a module untouched by the diff, run the clean-vs-diff paired full suite before any other action.", "environment": "", "language": "", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "vitest-shared-ndjson-parallel-file-race", "provider": "openrouter", "solved_at": "2026-09-11T03:04:34.275Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog