fix(web): scope the OAuth recovery restores to the attempt that made them
Copilot round 2, both findings valid: - `resumePendingReauth`'s restore could resurrect a challenge whose session ended during the awaits — a server switch or disconnect clears the slot on purpose. It is now gated on the attempt still targeting the live session, client identity included (reconnecting to the same server builds a replacement client an id-only check would admit), and uses a functional update so a newer deferral queued in the same batch wins rather than being read past. - `prepareOAuthRedirect` cleared whichever resume snapshot was current, not the one it wrote. Nothing single-flights that callback, so a redirect failing before navigation could delete a later attempt's callback-routing state. `writeOAuthResumeSnapshot` now returns the serialization it stored, and `clearOwnOAuthResumeSnapshot` removes it only while it is still the current one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Quk1hBUtyhkXowwMXzAnAY Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
ecad587afe
commit
76b75d3759
1 changed files with 166 additions and 12 deletions
|
|
@ -1142,6 +1142,43 @@ describe("useOAuthRecovery", () => {
|
|||
expect(client.handleAuthChallenge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not resurrect a challenge whose session ended mid-resume", async () => {
|
||||
// A server switch clears the slot on purpose. The in-flight attempt then
|
||||
// rejects, finds it empty, and must NOT put its own challenge back — it
|
||||
// belongs to a session that has ended (#2165).
|
||||
let rejectResume: ((err: Error) => void) | undefined;
|
||||
const client = fakeClient({
|
||||
handleAuthChallenge: vi.fn(
|
||||
() =>
|
||||
new Promise((_resolve, reject) => {
|
||||
rejectResume = reject;
|
||||
}),
|
||||
),
|
||||
});
|
||||
const servers = [entry("a"), entry("b")];
|
||||
const h = harness({ servers, activeServerId: "a", client });
|
||||
await defer(client, h);
|
||||
|
||||
await act(async () => {
|
||||
becomeVisible();
|
||||
});
|
||||
await waitFor(() => expect(rejectResume).toBeDefined());
|
||||
|
||||
h.rerender({ servers, activeServerId: "b", client });
|
||||
await act(async () => {
|
||||
rejectResume!(new Error("token endpoint unreachable"));
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(toastTitles()).toContain("Could not continue authorization"),
|
||||
);
|
||||
|
||||
// Nothing was restored, so returning to the tab resumes nothing.
|
||||
await act(async () => {
|
||||
becomeVisible();
|
||||
});
|
||||
expect(client.handleAuthChallenge).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not resume while disconnected, and resumes on the reconnect", async () => {
|
||||
const client = fakeClient({
|
||||
handleAuthChallenge: vi.fn().mockResolvedValue({ kind: "satisfied" }),
|
||||
|
|
|
|||
Loading…
Reference in a new issue