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
|
|
@ -38,7 +38,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("rejects an id containing illegal characters", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(<ServerConfigModal {...base()} />);
|
||||
await user.type(screen.getByLabelText(/Server ID/i), "bad id!");
|
||||
expect(
|
||||
|
|
@ -47,7 +47,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("flags duplicate ids against existingIds", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal {...base({ existingIds: ["alpha"] })} />,
|
||||
);
|
||||
|
|
@ -56,7 +56,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("calls onSubmit with a valid stdio config", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("requires a command for stdio submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("rejects malformed env lines", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -118,7 +118,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("submits an sse config with just the url (headers move to the settings form)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
|
|
@ -140,7 +140,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("submits a streamable-http config", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
|
|
@ -174,7 +174,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("rejects an empty URL on sse submission", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
|
|
@ -234,7 +234,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("shows the error message and stays open when onSubmit rejects", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = {
|
||||
...base(),
|
||||
onSubmit: vi.fn().mockRejectedValue(new Error("server full")),
|
||||
|
|
@ -252,7 +252,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("blocks submit and shows the id error message on bad-id submit click", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base({ existingIds: ["alpha"] });
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("supports editing the working directory field", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -283,7 +283,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("clears the Server ID field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
|
|
@ -299,7 +299,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("clears the Command field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
|
|
@ -315,7 +315,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("clears the URL field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
|
|
@ -331,7 +331,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("clears the Arguments field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
|
|
@ -351,7 +351,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("clears the Environment field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
|
|
@ -371,7 +371,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("clears the Working directory field via its Clear button", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ServerConfigModal
|
||||
{...base()}
|
||||
|
|
@ -391,7 +391,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("switches transport via the Transport select, revealing the URL field", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(<ServerConfigModal {...base()} />);
|
||||
|
||||
// Starts on stdio — Command is shown, URL is not.
|
||||
|
|
@ -411,7 +411,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("requires a server id before submitting", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -427,7 +427,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("rejects an env line whose '=' is at the start (no key)", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
|
||||
|
|
@ -442,7 +442,7 @@ describe("ServerConfigModal", () => {
|
|||
});
|
||||
|
||||
it("calls onClose when Cancel is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const props = base();
|
||||
renderWithMantine(<ServerConfigModal {...props} />);
|
||||
await user.click(screen.getByRole("button", { name: /Cancel/ }));
|
||||
|
|
|
|||
Loading…
Reference in a new issue