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
|
|
@ -77,7 +77,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("shows the read error alert when error and a resource is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen
|
||||
readState={{ status: "error", error: "boom" }}
|
||||
|
|
@ -89,7 +89,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("falls back to default error when error message is missing", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen readState={{ status: "error" }} />,
|
||||
);
|
||||
|
|
@ -98,7 +98,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("shows the loading state when reading", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen readState={{ status: "pending" }} />,
|
||||
);
|
||||
|
|
@ -107,7 +107,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("renders the preview panel when readState has a result", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen
|
||||
readState={{
|
||||
|
|
@ -139,7 +139,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("toggles a section's open state through the lifted openSections handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(<ControlledResourcesScreen />);
|
||||
const urisHeader = screen.getByRole("button", { name: /URIs \(2\)/ });
|
||||
// Open by default (compact=false); clicking routes through ResourcesScreen's
|
||||
|
|
@ -150,7 +150,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("renders the template panel when a template is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(<ControlledResourcesScreen />);
|
||||
// Templates section is open by default; select the template to open its form.
|
||||
await user.click(screen.getByText("files"));
|
||||
|
|
@ -160,7 +160,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("hides the template panel once the user reads the resource", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onReadResource = vi.fn();
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen onReadResource={onReadResource} />,
|
||||
|
|
@ -176,7 +176,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("auto-reads when a resource is clicked in the sidebar", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onReadResource = vi.fn();
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen onReadResource={onReadResource} />,
|
||||
|
|
@ -186,7 +186,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("forwards refresh and subscribe events from the preview panel", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onReadResource = vi.fn();
|
||||
const onSubscribeResource = vi.fn();
|
||||
renderWithMantine(
|
||||
|
|
@ -209,7 +209,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("closing the preview returns to the originating template form", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onReadResource = vi.fn();
|
||||
const templates: ResourceTemplate[] = [
|
||||
{ uriTemplate: "file:///{path}", name: "files" },
|
||||
|
|
@ -251,7 +251,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("closing the preview from the error state returns to the template form", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const templates: ResourceTemplate[] = [
|
||||
{ uriTemplate: "demo://resource/dynamic/text/{id}", name: "Dynamic" },
|
||||
];
|
||||
|
|
@ -282,7 +282,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("closing the preview for a plain resource returns to the empty state", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen
|
||||
readState={{
|
||||
|
|
@ -300,7 +300,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("invokes onUnsubscribeResource when already subscribed", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onUnsubscribeResource = vi.fn();
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen
|
||||
|
|
@ -319,7 +319,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("routes sidebar search text through onUiChange", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onUiChange = vi.fn();
|
||||
renderWithMantine(<ControlledResourcesScreen onUiChange={onUiChange} />);
|
||||
await user.type(screen.getByPlaceholderText("Search..."), "y.txt");
|
||||
|
|
@ -329,7 +329,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("renders nothing in the preview when an ok state carries no result", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen
|
||||
// readResource is truthy (selected resource exists) but the ok
|
||||
|
|
@ -348,7 +348,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("threads onCompleteArgument with a ref/resource envelope from the template form", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const onCompleteArgument = vi
|
||||
.fn<
|
||||
(
|
||||
|
|
@ -379,7 +379,7 @@ describe("ResourcesScreen", () => {
|
|||
});
|
||||
|
||||
it("hides the Subscriptions section and Subscribe button when subscriptionsSupported is false", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<ControlledResourcesScreen
|
||||
subscriptionsSupported={false}
|
||||
|
|
|
|||
Loading…
Reference in a new issue