fix: keep the pagination toggle on the last write across a server switch (#2095)

The displayed `paginatedLists` value falls back to the active `servers`
entry, and that list only advances when a `GET /api/servers` succeeds — so
once a list read fails it keeps describing disk as it was before the writes
made since. The optimistic override papered over that, but it was a single
app-wide slot cleared on every change of the *active* entry, so switching to
another server and back dropped it and the UI fell back to the stale entry.

Two things then disagreed, and the one the user could see was the wrong one:
the toggle read `off` while disk, the write tracker, and the client just
built from it all said `on`; and because the client skipped its connect-time
aggregate walk, the all-pages UI rendered an aggregate that was never
fetched, with no Load-next-page control to fill it.

Hold the override per server instead, in `usePaginatedListsOverride`, paired
with that server's entry as of when it was recorded and believed only while
the list still carries that same entry object — identity, not equality, so a
read reporting the value the override replaced supersedes it too. A server's
override then survives a switch away and back, and is still superseded by
the first successful read that rebuilds its entry.

The read cannot instead move to `lastPersistedSettings.resolve(id)`: that is
called during render, and it both reads refs and prunes superseded records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-25 20:20:57 -04:00
commit 7098da7473

View file

@ -555,6 +555,8 @@ vi.mock("./components/views/InspectorView/InspectorView", () => ({
switch-servers-tab
</button>
<button onClick={() => props.onToggleConnection("A")}>connect</button>
{/* A second target, so a test can drive an A -> B -> A switch (#2095). */}
<button onClick={() => props.onToggleConnection("B")}>connect-b</button>
<button onClick={() => props.onConnectionInfo()}>
open-connection-info
</button>
@ -3538,6 +3540,74 @@ describe("App paginated list pagination toggle (#1721)", () => {
};
expect(lastPush?.paginatedLists).toBe(true);
});
it("keeps the toggle on the last write after switching servers and back (#2095)", async () => {
// The override is what holds the display up while list reads are failing --
// the `servers` entry it would otherwise be read from is frozen at the last
// successful read, which this suite models exactly by mocking `useServers`
// to a fixed list.
//
// Held app-wide and cleared on every change of the *active* entry, it did
// not survive a plain server switch: A to B and back dropped it and the
// toggle fell back to that stale entry, reading `off` while disk, the
// tracker, and the client just built from it all said `on`. The lists were
// then rendered in all-pages mode showing an aggregate the client never
// fetched, with no Load-next-page control to fill them.
//
// Nothing here fails a list read explicitly: the fixed mock *is* that
// state, which is why one write is enough to reproduce it.
const user = userEvent.setup();
const previousUseServers = vi.mocked(useServers).getMockImplementation();
try {
vi.mocked(useServers).mockReturnValue({
servers: [
SERVER_A,
{ ...SERVER_A, id: "B", name: "Other" },
] as ServerEntry[],
loading: false,
error: undefined,
refresh: vi.fn().mockResolvedValue(undefined),
addServer: addServerSpy,
updateServer: updateServerSpy,
updateServerSettings: updateServerSettingsSpy,
removeServer: vi.fn(),
reorderServers: vi.fn(),
importSource: vi.fn().mockResolvedValue({ servers: {} }),
});
renderWithMantine(<App />);
await user.click(screen.getByText("connect"));
await waitFor(() => expect(clientInstances).toHaveLength(1));
await user.click(screen.getByText("paginated-on"));
await waitFor(() =>
expect(updateServerSettingsSpy).toHaveBeenCalledWith(
"A",
expect.objectContaining({ paginatedLists: true }),
),
);
await waitFor(() =>
expect(screen.getByTestId("tools-paginated")).toHaveTextContent("true"),
);
// Away to B, whose own entry has never been written and so reads false.
await user.click(screen.getByText("connect-b"));
await waitFor(() =>
expect(screen.getByTestId("active-server")).toHaveTextContent("B"),
);
expect(screen.getByTestId("tools-paginated")).toHaveTextContent("false");
// ...and back to A, which is still paginated on disk.
await user.click(screen.getByText("connect"));
await waitFor(() =>
expect(screen.getByTestId("active-server")).toHaveTextContent("A"),
);
expect(screen.getByTestId("tools-paginated")).toHaveTextContent("true");
} finally {
if (previousUseServers) {
vi.mocked(useServers).mockImplementation(previousUseServers);
}
}
});
});
// A post-write reload failure rejects *after* the write landed, so every piece