chore(web): extract App.tsx's OAuth recovery cluster into useOAuthRecovery (#2153)
Phase 2 step 2 of the App.tsx decomposition (#2129, under #2126). App.tsx goes from 4,267 to 3,178 lines; no behaviour change. The new hook owns the step-up slot and its retry operation, the re-auth banner, the deferred background-tab recovery, the redirect plumbing (webOAuthStorage, sessionStorageAdapter, onBeforeOAuthRedirect, prepareOAuthRedirect), the command-scoped recovery wrappers, the four OAuth effects, the abandoned-redirect check, the /oauth/callback completion, the Connection Info OAuth details, and the clear/finalize paths. All five once-only latches move with it unchanged. Three seams could not be a straight move: - useSessionRef no longer takes pendingStepUp/pendingReauth. They are this hook's own state, so App.tsx would have had to hold them purely to hand them back; instead the owner mirrors them into the ref in an effect of its own, following the shape activeServerName already had. - setupClientForServer is injected through a ref, because it reads onBeforeOAuthRedirect and sessionStorageAdapter out of this hook and so cannot be declared before it. The callback effect reads the ref before burning its one-shot latch. - The two clear-OAuth modal adapters and onReauthenticateFromBanner stay in App.tsx: they bind App-owned state, and the latter drives a reconnect, which step 3 owns. Dropping ~1,100 lines took App.tsx under the React Compiler's bail-out, so the compiler-backed react-hooks rules now analyse it and report nine pre-existing findings that were previously invisible. Each is suppressed with a narrow justified disable naming #2161, which is filed to resolve them; the one new finding (exhaustive-deps on setReAuthBanner) is fixed rather than suppressed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013hzwzS8UvBsr4yZm7o7sev Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
6c9c149406
commit
ed38a5e5fe
5 changed files with 2962 additions and 1237 deletions
File diff suppressed because it is too large
Load diff
1441
clients/web/src/hooks/useOAuthRecovery.test.tsx
Normal file
1441
clients/web/src/hooks/useOAuthRecovery.test.tsx
Normal file
File diff suppressed because it is too large
Load diff
1373
clients/web/src/hooks/useOAuthRecovery.ts
Normal file
1373
clients/web/src/hooks/useOAuthRecovery.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,8 +2,6 @@ import { describe, it, expect } from "vitest";
|
|||
import { InspectorClient } from "@inspector/core/mcp/index.js";
|
||||
import type { ServerEntry } from "@inspector/core/mcp/types.js";
|
||||
import { renderWithMantine } from "../test/renderWithMantine";
|
||||
import type { PendingReauth } from "../utils/pendingReauth";
|
||||
import type { PendingStepUp } from "../utils/stepUp";
|
||||
import {
|
||||
useSessionRef,
|
||||
type SessionRef,
|
||||
|
|
@ -27,27 +25,10 @@ const entry = (id: string, name = id): ServerEntry => ({
|
|||
const client = (): InspectorClient =>
|
||||
Object.create(InspectorClient.prototype) as InspectorClient;
|
||||
|
||||
const stepUp = (serverId: string): PendingStepUp => ({
|
||||
serverId,
|
||||
challenge: { reason: "unauthorized" },
|
||||
authorizationUrl: new URL("https://as.example/authorize"),
|
||||
source: "tool",
|
||||
});
|
||||
|
||||
const reauth = (serverId: string): PendingReauth => ({
|
||||
serverId,
|
||||
challenge: { reason: "unauthorized" },
|
||||
authorizationUrl: new URL("https://as.example/authorize"),
|
||||
authKind: "reauth",
|
||||
source: "tool",
|
||||
});
|
||||
|
||||
const values = (over: Partial<SessionValues> = {}): SessionValues => ({
|
||||
activeServerId: undefined,
|
||||
servers: [],
|
||||
inspectorClient: null,
|
||||
pendingStepUp: null,
|
||||
pendingReauth: null,
|
||||
...over,
|
||||
});
|
||||
|
||||
|
|
@ -84,6 +65,8 @@ describe("useSessionRef", () => {
|
|||
expect(snapshot.activeServerId).toBe("a");
|
||||
expect(snapshot.servers).toBe(servers);
|
||||
expect(snapshot.inspectorClient).toBe(c);
|
||||
// Seeded null and left alone: the two pending-OAuth slots are written by
|
||||
// their owner, `useOAuthRecovery`, not supplied to this hook (#2153).
|
||||
expect(snapshot.pendingStepUp).toBeNull();
|
||||
expect(snapshot.pendingReauth).toBeNull();
|
||||
});
|
||||
|
|
@ -99,34 +82,39 @@ describe("useSessionRef", () => {
|
|||
const h = harness(values());
|
||||
const servers = [entry("a"), entry("b")];
|
||||
const c = client();
|
||||
const up = stepUp("a");
|
||||
const re = reauth("b");
|
||||
h.update(
|
||||
values({
|
||||
activeServerId: "b",
|
||||
servers,
|
||||
inspectorClient: c,
|
||||
pendingStepUp: up,
|
||||
pendingReauth: re,
|
||||
}),
|
||||
);
|
||||
expect(h.ref().current).toMatchObject({
|
||||
activeServerId: "b",
|
||||
servers,
|
||||
inspectorClient: c,
|
||||
pendingStepUp: up,
|
||||
pendingReauth: re,
|
||||
});
|
||||
});
|
||||
|
||||
it("leaves the owner-written pending slots alone across renders", () => {
|
||||
const h = harness(values({ activeServerId: "a", servers: [entry("a")] }));
|
||||
// Stands in for `useOAuthRecovery`'s own mirror effect.
|
||||
h.ref().current.pendingStepUp = {
|
||||
serverId: "a",
|
||||
challenge: { reason: "unauthorized" },
|
||||
authorizationUrl: new URL("https://as.example/authorize"),
|
||||
source: "tool",
|
||||
};
|
||||
h.update(values({ activeServerId: "a", servers: [entry("a")] }));
|
||||
expect(h.ref().current.pendingStepUp?.serverId).toBe("a");
|
||||
});
|
||||
|
||||
it("clears a value that becomes null or undefined", () => {
|
||||
const h = harness(
|
||||
values({
|
||||
activeServerId: "a",
|
||||
servers: [entry("a")],
|
||||
inspectorClient: client(),
|
||||
pendingStepUp: stepUp("a"),
|
||||
pendingReauth: reauth("a"),
|
||||
}),
|
||||
);
|
||||
h.update(values());
|
||||
|
|
@ -134,8 +122,6 @@ describe("useSessionRef", () => {
|
|||
activeServerId: undefined,
|
||||
servers: [],
|
||||
inspectorClient: null,
|
||||
pendingStepUp: null,
|
||||
pendingReauth: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,12 +24,26 @@ export interface SessionSnapshot {
|
|||
* keeps the value the write replaced (#2089).
|
||||
*/
|
||||
inspectorClient: InspectorClient | null;
|
||||
/**
|
||||
* The two OAuth-recovery slots. Unlike the fields above they are **not**
|
||||
* supplied to this hook: they are owned by `useOAuthRecovery`, which mirrors
|
||||
* them in an effect of its own (#2153). Keeping them off `SessionValues`
|
||||
* is what lets that hook both own the state and read the ref — App.tsx
|
||||
* would otherwise have to hold the state purely to hand it back.
|
||||
*/
|
||||
pendingStepUp: PendingStepUp | null;
|
||||
pendingReauth: PendingReauth | null;
|
||||
}
|
||||
|
||||
/** What the caller supplies each render; `activeServerName` is derived. */
|
||||
export type SessionValues = Omit<SessionSnapshot, "activeServerName">;
|
||||
/**
|
||||
* What the caller supplies each render. `activeServerName` is derived, and the
|
||||
* two pending-OAuth slots are written by their owner — see
|
||||
* {@link SessionSnapshot}.
|
||||
*/
|
||||
export type SessionValues = Omit<
|
||||
SessionSnapshot,
|
||||
"activeServerName" | "pendingStepUp" | "pendingReauth"
|
||||
>;
|
||||
|
||||
/** A stable handle onto the latest {@link SessionSnapshot}. */
|
||||
export type SessionRef = RefObject<SessionSnapshot>;
|
||||
|
|
@ -60,6 +74,8 @@ export type SessionRef = RefObject<SessionSnapshot>;
|
|||
export function useSessionRef(values: SessionValues): SessionRef {
|
||||
const ref = useRef<SessionSnapshot>({
|
||||
activeServerName: undefined,
|
||||
pendingStepUp: null,
|
||||
pendingReauth: null,
|
||||
...values,
|
||||
});
|
||||
// Deliberately un-gated: this is a passive mirror, so re-assigning the same
|
||||
|
|
@ -73,8 +89,6 @@ export function useSessionRef(values: SessionValues): SessionRef {
|
|||
ref.current.activeServerId = values.activeServerId;
|
||||
ref.current.servers = values.servers;
|
||||
ref.current.inspectorClient = values.inspectorClient;
|
||||
ref.current.pendingStepUp = values.pendingStepUp;
|
||||
ref.current.pendingReauth = values.pendingReauth;
|
||||
});
|
||||
return ref;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue