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:
parent
066676a241
commit
4bad110942
1 changed files with 193 additions and 161 deletions
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue