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
|
|
@ -11,6 +11,7 @@ import {
|
|||
readOAuthResumeSnapshot,
|
||||
restoreTabUiFromSnapshot,
|
||||
writeOAuthResumeSnapshot,
|
||||
clearOwnOAuthResumeSnapshot,
|
||||
type OAuthResumeSnapshot,
|
||||
} from "./oauthResume.js";
|
||||
import {
|
||||
|
|
@ -349,6 +350,43 @@ describe("oauthResume", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("clearOwnOAuthResumeSnapshot removes only the snapshot it was given", () => {
|
||||
const first = writeOAuthResumeSnapshot({
|
||||
version: 1,
|
||||
serverId: "a",
|
||||
activeTab: "tools",
|
||||
authKind: "reauth",
|
||||
tabUi: {},
|
||||
});
|
||||
// A later attempt replaces it before the first one's failure lands.
|
||||
writeOAuthResumeSnapshot({
|
||||
version: 1,
|
||||
serverId: "b",
|
||||
activeTab: "prompts",
|
||||
authKind: "reauth",
|
||||
tabUi: {},
|
||||
});
|
||||
|
||||
expect(clearOwnOAuthResumeSnapshot(first)).toBe(false);
|
||||
expect(readOAuthResumeSnapshot()?.serverId).toBe("b");
|
||||
|
||||
const second = window.sessionStorage.getItem(OAUTH_RESUME_KEY)!;
|
||||
expect(clearOwnOAuthResumeSnapshot(second)).toBe(true);
|
||||
expect(window.sessionStorage.getItem(OAUTH_RESUME_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it("clearOwnOAuthResumeSnapshot is a no-op without a token", () => {
|
||||
writeOAuthResumeSnapshot({
|
||||
version: 1,
|
||||
serverId: "a",
|
||||
activeTab: "tools",
|
||||
authKind: "reauth",
|
||||
tabUi: {},
|
||||
});
|
||||
expect(clearOwnOAuthResumeSnapshot(undefined)).toBe(false);
|
||||
expect(readOAuthResumeSnapshot()?.serverId).toBe("a");
|
||||
});
|
||||
|
||||
it("writeOAuthResumeSnapshot swallows setItem failures", () => {
|
||||
vi.stubGlobal("sessionStorage", {
|
||||
getItem: () => null,
|
||||
|
|
@ -368,6 +406,17 @@ describe("oauthResume", () => {
|
|||
).not.toThrow();
|
||||
});
|
||||
|
||||
it("clearOwnOAuthResumeSnapshot swallows storage failures", () => {
|
||||
vi.stubGlobal("sessionStorage", {
|
||||
getItem: () => {
|
||||
throw new Error("blocked");
|
||||
},
|
||||
setItem: () => {},
|
||||
removeItem: () => {},
|
||||
});
|
||||
expect(clearOwnOAuthResumeSnapshot("{}")).toBe(false);
|
||||
});
|
||||
|
||||
it("clearOAuthResumeSnapshot swallows removeItem failures", () => {
|
||||
vi.stubGlobal("sessionStorage", {
|
||||
getItem: () => null,
|
||||
|
|
@ -422,6 +471,10 @@ describe("oauthResume", () => {
|
|||
it("clearOAuthResumeSnapshot is a no-op", () => {
|
||||
expect(() => clearOAuthResumeSnapshot()).not.toThrow();
|
||||
});
|
||||
|
||||
it("clearOwnOAuthResumeSnapshot is a no-op", () => {
|
||||
expect(clearOwnOAuthResumeSnapshot("{}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("restoreTabUiFromSnapshot returns early when tabUi is undefined", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue