Merge branch 'v2/main' into v2/chore/2148-web-tab-smokes

This commit is contained in:
Cliff Hall 2026-08-26 23:36:39 -04:00 • committed by GitHub
commit 11a1bdf6a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 3008 additions and 1237 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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,
});
});

View file

@ -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;
}