Skip to content

Flaky: ServerImportJsonModal "guards against a live edit made before the debounce re-validates" loses its race under load #2241

Description

@cliffhall

Found while running npm run local:gate on an unrelated branch (#2238 / #2217). Reproduces on a clean origin/v2/main tree, so it is not that branch's doing: two consecutive isolated runs of the file alone gave 1 failed | 15 passed then 16 passed.

The test

clients/web/src/components/groups/ServerImportJsonModal/ServerImportJsonModal.test.tsx:225guards against a live edit made before the debounce re-validates.

await pasteJson(npmJson);
await waitFor(() => expect(screen.getByRole("button", { name: "Add Server" })).toBeEnabled());
// Replace with invalid content; the button hasn't re-disabled yet (the
// debounce is still pending), so clicking exercises the submit-time guard.
await pasteJson("{not json");
fireEvent.click(screen.getByRole("button", { name: "Add Server" }));
expect(onAddServer).not.toHaveBeenCalled();
expect(await screen.findByText(/Fix the validation errors/)).toBeInTheDocument();

Why it is a race

The comment states the premise outright: the click must land while the debounce is still pending. Nothing enforces that. pasteJson is a real userEvent interaction against a real editor, so its duration is a function of machine load — and when it exceeds the debounce window, the re-validation has already run by the time the click is dispatched, the button is disabled, fireEvent.click on a disabled button is a no-op, and the submit-time guard never runs. onAddServer is legitimately not called (the first assertion still passes), so the failure surfaces one line later as:

expect(element).toBeInTheDocument()
element could not be found in the document
  at ServerImportJsonModal.test.tsx:245

A timing premise the test cannot control is a flake by construction, and this one fails open into a false negative shape: the message it looks for is exactly what a broken submit guard would also fail to produce, so a real regression and a lost race are indistinguishable in the output.

Suggested fix

Make the pending-debounce state deterministic rather than hoping for it — fake timers around the second paste so the debounce provably has not fired when the click is dispatched, or drive the component's validation state directly. Failing that, at minimum assert the precondition (the button is still enabled) before the click, so a lost race reports itself as a lost race instead of as a missing error message.

Impact

local:gate is a hard pre-push gate and CI runs the same suite, so this costs a full re-run (several minutes) whenever it lands. It was previously in scope for the closed #1596 (zero-flake requirement).

Filed from a session working on #2217.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv2Issues and PRs for v2

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions