fix(ui): ignore router source responses from a superseded session

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yuneng 2026-09-22 01:10:01 +00:00
parent 5cf2abfc7c
commit 7a258aa231
3 changed files with 36 additions and 7 deletions

View file

@ -151,9 +151,17 @@ const Fallbacks: React.FC<FallbacksProps> = ({ accessToken, userRole, userID })
}
setRouterSettings(router_settings);
});
let cancelled = false;
getRouterSettingsCall(accessToken)
.then((data) => setSourcesState({ sessionKey, sources: data.source ?? {}, failed: false }))
.catch(() => setSourcesState({ sessionKey, sources: null, failed: true }));
.then((data) => {
if (!cancelled) setSourcesState({ sessionKey, sources: data.source ?? {}, failed: false });
})
.catch(() => {
if (!cancelled) setSourcesState({ sessionKey, sources: null, failed: true });
});
return () => {
cancelled = true;
};
}, [accessToken, userRole, userID, sessionKey]);
const handleDeleteClick = (fallbackEntry: FallbackEntry) => {

View file

@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fireEvent, renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
import { act, fireEvent, renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
import userEvent from "@testing-library/user-event";
import RouterSettings from "./index";
@ -208,10 +208,24 @@ describe("RouterSettings", () => {
const { rerender } = renderWithProviders(<RouterSettings {...defaultProps} />);
expect(await screen.findByRole("button", { name: /save changes/i })).toBeInTheDocument();
vi.mocked(getRouterSettingsCall).mockReturnValue(new Promise(() => {}));
rerender(<RouterSettings {...defaultProps} accessToken="other-token" />);
let resolveStale: (
value: typeof mockRouterSettingsResponse & { source: Record<string, string> },
) => void = () => {};
vi.mocked(getRouterSettingsCall).mockReturnValueOnce(
new Promise((resolve) => {
resolveStale = resolve;
}),
);
rerender(<RouterSettings {...defaultProps} accessToken="stale-token" />);
expect(screen.queryByRole("button", { name: /save changes/i })).not.toBeInTheDocument();
vi.mocked(getRouterSettingsCall).mockResolvedValueOnce({ ...mockRouterSettingsResponse, source: {} });
rerender(<RouterSettings {...defaultProps} accessToken="current-token" />);
expect(await screen.findByRole("button", { name: /save changes/i })).toBeInTheDocument();
resolveStale({ ...mockRouterSettingsResponse, source: {} });
await act(async () => {});
expect(screen.getByRole("button", { name: /save changes/i })).toBeInTheDocument();
});
it("shows an error instead of a blank page when the source request fails", async () => {

View file

@ -48,8 +48,10 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
selectedStrategy: initialStrategy,
}));
});
let cancelled = false;
getRouterSettingsCall(accessToken)
.then((data) => {
if (cancelled) return;
setSourcesState({ sessionKey, sources: data.source ?? {}, failed: false });
if (data.fields) {
// Build metadata map for easy lookup
@ -85,7 +87,12 @@ const RouterSettings: React.FC<RouterSettingsProps> = ({ accessToken, userRole,
}
}
})
.catch(() => setSourcesState({ sessionKey, sources: null, failed: true }));
.catch(() => {
if (!cancelled) setSourcesState({ sessionKey, sources: null, failed: true });
});
return () => {
cancelled = true;
};
}, [accessToken, userRole, userID, sessionKey]);
const handleSaveChanges = async () => {