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
|
|
@ -225,7 +225,7 @@ describe("InspectorView", () => {
|
|||
|
||||
it("dispatches onToggleConnection with the server id when the card toggle is clicked", async () => {
|
||||
const onToggleConnection = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({ servers: [sampleServer], onToggleConnection })}
|
||||
|
|
@ -321,7 +321,7 @@ describe("InspectorView", () => {
|
|||
})}
|
||||
/>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const tabSelect = await screen.findByDisplayValue("Servers");
|
||||
await user.click(tabSelect);
|
||||
await user.click(await screen.findByText("Tools"));
|
||||
|
|
@ -514,7 +514,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("filters tools to apps and auto-launches a no-fields app on the Apps tab", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
// Plain (non-app) tool plus a tool with a malformed UI resource URI
|
||||
// exercise both branches of the appTools filter: the non-app drop and
|
||||
// the try/catch around `isAppTool` for malformed metadata.
|
||||
|
|
@ -630,7 +630,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("snaps activeTab back to Servers when the Apps tab disappears after a refresh", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const { rerender } = renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
|
|
@ -818,7 +818,7 @@ describe("InspectorView", () => {
|
|||
|
||||
it("dispatches onSetLogLevel through to the Logs screen", async () => {
|
||||
const onSetLogLevel = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
|
|
@ -849,7 +849,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("persists Logs sort direction to localStorage and restores it on remount", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const { unmount } = renderWithMantine(
|
||||
<InspectorView
|
||||
{...makeProps({
|
||||
|
|
@ -900,7 +900,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("falls back to newest-first when a corrupted sort value is stored", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
window.localStorage.setItem("inspector.sortDirection.history", "garbage");
|
||||
renderWithMantine(
|
||||
<InspectorView
|
||||
|
|
@ -923,7 +923,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("persists History list compact state to localStorage and restores it on remount", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const historyEntry = {
|
||||
id: "req-1",
|
||||
timestamp: new Date("2026-03-17T10:00:00Z"),
|
||||
|
|
@ -981,7 +981,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("falls back to collapsed when a corrupted compact value is stored", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
window.localStorage.setItem("inspector.listCompact.history", "garbage");
|
||||
const historyEntry = {
|
||||
id: "req-1",
|
||||
|
|
@ -1084,7 +1084,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("toggles the Servers list compact state from the list toggle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
renderWithMantine(
|
||||
<InspectorView {...makeProps({ servers: [sampleServer] })} />,
|
||||
);
|
||||
|
|
@ -1098,7 +1098,7 @@ describe("InspectorView", () => {
|
|||
});
|
||||
|
||||
it("toggles the Network list compact state from the list toggle", async () => {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const httpServer: ServerEntry = {
|
||||
id: "beta",
|
||||
name: "Beta",
|
||||
|
|
@ -1167,7 +1167,7 @@ describe("InspectorView", () => {
|
|||
// The indicator only mounts on the active screen, so each case connects,
|
||||
// navigates to the target tab, and asserts the "List updated" affordance.
|
||||
async function gotoTab(tab: string) {
|
||||
const user = userEvent.setup();
|
||||
const user = userEvent.setup({ delay: null });
|
||||
const tabSelect = await screen.findByDisplayValue("Servers");
|
||||
await user.click(tabSelect);
|
||||
await user.click(await screen.findByText(tab));
|
||||
|
|
|
|||
Loading…
Reference in a new issue