test(web): eliminate timeout flakiness in the web test suite (closes #1596)

Root-cause and deterministically fix the recurring 5s-timeout flakes that
surfaced under v8-instrumented, concurrent load. All fixes are test-only; no
product source changed. No test was skipped or disabled, and the per-file
coverage gate (>=90 on all four dims) still holds.

Common root cause across the modal/form/screen suites: userEvent.setup()
schedules a real setTimeout between keystrokes. Under CPU contention those
yields balloon, so multi-field interactions blew past the 5s per-test
timeout. Fix: userEvent.setup({ delay: null }) removes the per-keystroke
real-timer dependence (typing dispatches synchronously), making the suites
load-independent.

Per file:
- ServerConfigModal / ServerImportConfigModal / ServerImportJsonModal /
  ResourcesScreen / InspectorView: userEvent.setup({ delay: null }).
- PromptArgumentsForm (completions): replaced wall-clock sleeps sized just
  past the 300ms completion debounce (await setTimeout(400)) — which race
  the debounce timer under load — with awaited conditions (findBy/waitFor)
  on the real rendered outcome, plus delay:null. Negative assertions now
  lean on the component's synchronous state/timer teardown instead of a
  timed window, so they are deterministic regardless of machine speed.
- inspectorClient integration "tracks stderr logs": the child's stderr is
  piped out-of-band from the tool's JSON-RPC response, so reading the log
  synchronously after callTool raced the stderr chunk. Wrapped the assertion
  in vi.waitFor so it polls until the line lands.

Verified: web validate + 6x test:coverage (4 sequential + 2 concurrent
under load) + standalone test:integration, all green (229 files / 3237
tests per coverage run; 799 integration tests), zero intermittent failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S3fTN8H3R8YV4yUGvZjYnX
This commit is contained in:
cliffhall 2026-07-01 21:11:11 -04:00
commit 4bad110942

View file

@ -1,4 +1,4 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import * as z from "zod/v4";
import { InspectorClient } from "@inspector/core/mcp/inspectorClient.js";
import {
@ -2452,11 +2452,21 @@ describe("InspectorClient", () => {
const writeToStderrTool = await getTool(client, "write_to_stderr");
await client.callTool(writeToStderrTool, { message: testMessage });
const logs = stderrLogState.getStderrLogs();
expect(Array.isArray(logs)).toBe(true);
const matching = logs.filter((l) => l.message.includes(testMessage));
expect(matching.length).toBeGreaterThan(0);
expect(matching[0]!.message).toContain(testMessage);
// The child's stderr is piped out-of-band from the tool's JSON-RPC
// response, so the "data" chunk carrying `testMessage` can still be in
// flight when `callTool` resolves. Reading the log synchronously here
// races that chunk (the historical flake). Poll until the line lands
// instead of asserting on a single sample.
await vi.waitFor(
() => {
const logs = stderrLogState.getStderrLogs();
expect(Array.isArray(logs)).toBe(true);
const matching = logs.filter((l) => l.message.includes(testMessage));
expect(matching.length).toBeGreaterThan(0);
expect(matching[0]!.message).toContain(testMessage);
},
{ timeout: 5000, interval: 25 },
);
stderrLogState.destroy();
});
});