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

@ -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));