* refactor(web): sharpen src/lib vs src/utils rule and move misfiled modules (#1776) Codify the empirical rule the web client already followed loosely — utils = pure functions that compute; lib = things that instantiate, adapt, or touch the environment — in AGENTS.md and clients/web/README.md, and move the modules that sat on the wrong side of it. Moves (source + co-located test): - lib/sandbox-csp -> utils (pure CSP-string construction; type-only import) - utils/browserTabVisibility -> lib (registers visibilitychange listeners) - utils/oauthResume -> lib (reads/writes sessionStorage) - utils/clearServerOAuthState -> lib (drives live InspectorClient / OAuthStorage) Borderline audits (kept in place): pendingReauth (pure type declarations), deepLink / correlateTransportErrors / mcpNetworkHeaders / oauthUx / oauthFlow / oauthNetworkPhase (pure transforms / type-only core imports) stay in utils; downloadFile stays whole in lib (dominant side is DOM side effects). Also relocate the lone src/test/lib/downloadFile.test.ts straggler beside its source at src/lib/downloadFile.test.ts, and document the test-placement rule (side-by-side by default; src/test/ only for core/ tests, the integration project, and shared scaffolding). Pure move — no behavior change; no build/coverage config keys off the boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * refactor(web): address review — fix stale paths, move customHeaders, one-way arrows (#1776) Responds to the @claude review on PR #1784: 1. Stale path references (fixed): update the four in-repo pointers left at pre-move paths — specification/v2_auth_mid_session.md (oauthResume, browserTabVisibility → lib/), and the two load-bearing sandbox security comments (sandbox_proxy.html, sandbox-controller.ts → src/utils/sandbox-csp). 2. lib/types/customHeaders (moved): it's pure (types + pure transforms), the rule's clearest counterexample. Moved lib/types/customHeaders.{ts,test.ts} → utils/ (no external importers; only its own test) and removed the undocumented lib/types/ subdir. Documented that utils/ is the home for pure shared domain types + their constructors — no separate types/ bucket. 3. One-way arrows (item 3): declare OAuthResumeAuthKind in utils/pendingReauth and re-export it from lib/oauthResume, so the only cross-directory arrow now points lib → utils. Codified that invariant in AGENTS.md. Wording nit: AGENTS.md + README now state diagnostic logging (console.warn/error) doesn't count as a side effect for the utils rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs: address review round 2 — stale customHeaders refs, rule wording (#1776) Doc-only follow-up to the round-2 @claude review on PR #1784 (no code changed): 1. Stale paths (same class as round-1 item 1): the customHeaders move left three references to the deleted lib/types/customHeaders.ts path in specification/ (v2_ux_interfaces_phase_0_4_inventory.md, v2_ux_interfaces_plan.md ×2) — updated to utils/customHeaders.ts. 2. customHeaders anchor: note in AGENTS.md that its shape is staged for ServerSettingsForm (per v2_ux_interfaces_plan.md), so its zero-importer state reads as intentional rather than rot; kept as the no-types/-bucket illustration and led the anchor list with load-bearing modules. 3. Type-only-import clause: split the conflated clause — a type-only import (pendingReauth) and re-exporting core's pure functions/constants (oauthUx/oauthFlow) are distinct reasons to stay in utils; what makes a module lib is wrapping core's stateful runtime. Fixed in both AGENTS.md and the web README. Cosmetic: re-padded the v2_auth_mid_session.md table row to column width. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs: address review round 3 — scope the "no types/ bucket" claim (#1776) Doc-only follow-up to the round-3 @claude review on PR #1784 (no code changed): 1. "no separate types/ bucket" overreached: clients/web/src/types/ does exist (navigation.ts + react-syntax-highlighter .d.ts ambient stubs wired via tsconfig.app.json paths). Scoped the claim in both AGENTS.md and the web README to "no types/ sub-bucket INSIDE lib/utils", and noted src/types/ is for ambient .d.ts stubs, not domain types. 2. README example list drifted from AGENTS.md's round-2 load-bearing anchors — realigned it (inspectorTabs/mcpNetworkHeaders, dropped the zero-importer customHeaders from the headline list). 3. Clarified v2_ux_interfaces_plan.md:185 so the v2 path reads as the copy's destination, not as the v1.5 source location. Follow-up filed as #1785 (board Todo): src/types/navigation.ts InspectorTab is dead and duplicates utils/inspectorTabs.ts — kept out of this pure-move PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs: address review round 4 — reconcile src/types/ claim with #1785 (#1776) Doc-only follow-up to the round-4 @claude review on PR #1784 (no code changed): The round-3 scope fix overshot: it asserted src/types/ holds "not domain types at all", but the dead navigation.ts InspectorTab is a plain-.ts domain type still living there, and specification/v2_ux_interfaces_plan.md:181-182 actively prescribes placing InspectorTab there — so AGENTS.md and that spec doc gave opposite guidance about the same directory. - AGENTS.md + web README: softened to "not the place for new domain types" and added a parenthetical that the one lingering domain type (dead navigation.ts) is tracked for removal in #1785; until then a pure domain type belongs in utils/, not src/types/. - v2_ux_interfaces_plan.md:181: marked the tab-enum placement guidance as superseded by #1785 (live type is InspectorTabId in utils/inspectorTabs.ts). Self-heals on merge of #1785. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs: address review round 5 — fix garbled #1785 clause, restructure utils rule (#1776) Doc-only follow-up to the round-5 @claude review on PR #1784 (no code changed): 1. Grammar (item 1): the round-4 #1785 clause read "superseding [#1785] will remove it" — a participle with no subject. Reworded to "the dead navigation.ts InspectorTab, which #1785 will remove". 2. Readability (item 2): the src/utils/ bullet had accreted into a single ~1,400-char blob across five review rounds. Promoted the carve-outs to scannable sub-bullets (Domain types / Diagnostic logging / Importing from core) and lifted the src/types/ note out to its own line, since it's a statement about a sibling directory, not about utils. AGENTS.md only; the README's parallel sentence was already clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 * docs+config: address review round 6 — correct the coverage-glob claim (#1776) Responds to the round-6 @claude review on PR #1784: 1. "no build/coverage glob" was factually wrong (should-fix): the web coverage include in clients/web/vite.config.ts DOES enumerate src/lib/** and src/utils/**. The conclusion (a move between them is coverage-neutral) survives, but the reason was wrong. Reworded both AGENTS.md and the web README to say nothing *enforces* the boundary — no path alias, and the include lists both dirs so a move is coverage-neutral — plus the important whitelist caveat: a module outside components/lib/utils/server falls out of the ≥90 gate entirely. 2. Removed a pre-existing duplicate glob in vite.config.ts (src/lib/**/*.{ts,tsx} appeared twice, lines 161 and 163). Inert (vitest de-dupes) but it sat three lines from the config the doc now describes. Nits: gave the README bullet the same round-5 sub-bullet restructure as AGENTS.md, and made its #1785 reference a real markdown link (bare #N doesn't autolink in rendered repo markdown). npm run ci green (config change re-verified through coverage + smokes + Storybook). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5 --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
495 lines
14 KiB
TypeScript
495 lines
14 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
import {
|
|
applyOAuthResumeUi,
|
|
buildTabUiSnapshot,
|
|
clearOAuthResumeSnapshot,
|
|
consumeOAuthResumeSnapshot,
|
|
oauthResumeInsufficientScopeMessage,
|
|
oauthResumeToastMessage,
|
|
OAUTH_PENDING_SERVER_KEY,
|
|
OAUTH_RESUME_KEY,
|
|
readOAuthResumeSnapshot,
|
|
restoreTabUiFromSnapshot,
|
|
writeOAuthResumeSnapshot,
|
|
type OAuthResumeSnapshot,
|
|
} from "./oauthResume.js";
|
|
import {
|
|
EMPTY_TOOLS_UI,
|
|
EMPTY_PROMPTS_UI,
|
|
EMPTY_RESOURCES_UI,
|
|
EMPTY_APPS_UI,
|
|
EMPTY_TASKS_UI,
|
|
EMPTY_LOGS_UI,
|
|
EMPTY_PROTOCOL_UI,
|
|
EMPTY_NETWORK_UI,
|
|
} from "../components/screens/screenUiState.js";
|
|
|
|
describe("oauthResume", () => {
|
|
const storage = new Map<string, string>();
|
|
|
|
beforeEach(() => {
|
|
storage.clear();
|
|
vi.stubGlobal("sessionStorage", {
|
|
getItem: (key: string) => storage.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
storage.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
storage.delete(key);
|
|
},
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("consumeOAuthResumeSnapshot reads once then clears storage", () => {
|
|
const snapshot: OAuthResumeSnapshot = {
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
};
|
|
writeOAuthResumeSnapshot(snapshot);
|
|
expect(consumeOAuthResumeSnapshot()).toEqual(snapshot);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
expect(consumeOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("clearOAuthResumeSnapshot removes pending redirect state (explicit disconnect)", () => {
|
|
writeOAuthResumeSnapshot({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
});
|
|
clearOAuthResumeSnapshot();
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
expect(consumeOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("round-trips OAuthResumeSnapshot", () => {
|
|
const snapshot: OAuthResumeSnapshot = {
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "step_up",
|
|
tabUi: {
|
|
Tools: {
|
|
...EMPTY_TOOLS_UI,
|
|
selectedToolName: "echo",
|
|
formValues: { message: "hi" },
|
|
},
|
|
},
|
|
remoteSessionId: "remote-abc",
|
|
};
|
|
writeOAuthResumeSnapshot(snapshot);
|
|
expect(readOAuthResumeSnapshot()).toEqual(snapshot);
|
|
expect(storage.get(OAUTH_PENDING_SERVER_KEY)).toBeUndefined();
|
|
clearOAuthResumeSnapshot();
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("builds and restores tab ui snapshots", () => {
|
|
const toolsUi = {
|
|
...EMPTY_TOOLS_UI,
|
|
selectedToolName: "get_temp",
|
|
formValues: { city: "NYC" },
|
|
};
|
|
const tabUi = buildTabUiSnapshot({
|
|
toolsUi,
|
|
promptsUi: EMPTY_PROMPTS_UI,
|
|
resourcesUi: EMPTY_RESOURCES_UI,
|
|
appsUi: EMPTY_APPS_UI,
|
|
tasksUi: EMPTY_TASKS_UI,
|
|
logsUi: EMPTY_LOGS_UI,
|
|
protocolUi: EMPTY_PROTOCOL_UI,
|
|
networkUi: EMPTY_NETWORK_UI,
|
|
});
|
|
const setToolsUi = vi.fn();
|
|
restoreTabUiFromSnapshot(tabUi, {
|
|
setToolsUi,
|
|
setPromptsUi: vi.fn(),
|
|
setResourcesUi: vi.fn(),
|
|
setAppsUi: vi.fn(),
|
|
setTasksUi: vi.fn(),
|
|
setLogsUi: vi.fn(),
|
|
setProtocolUi: vi.fn(),
|
|
setNetworkUi: vi.fn(),
|
|
});
|
|
expect(setToolsUi).toHaveBeenCalledWith(toolsUi);
|
|
});
|
|
|
|
it("falls back to legacy pending server key", () => {
|
|
storage.set(OAUTH_PENDING_SERVER_KEY, "legacy-srv");
|
|
const snapshot = readOAuthResumeSnapshot();
|
|
expect(snapshot?.serverId).toBe("legacy-srv");
|
|
expect(snapshot?.activeTab).toBe("Servers");
|
|
expect(snapshot?.authKind).toBe("reauth");
|
|
});
|
|
|
|
it("returns toast copy by auth kind", () => {
|
|
expect(oauthResumeToastMessage("step_up", { recoverySource: "tool" })).toBe(
|
|
"Step-up authorization succeeded. Retry your action.",
|
|
);
|
|
expect(oauthResumeToastMessage("step_up")).toBe(
|
|
"Step-up authorization succeeded.",
|
|
);
|
|
expect(oauthResumeToastMessage("reauth", { recoverySource: "tool" })).toBe(
|
|
"Authentication succeeded. Retry your action.",
|
|
);
|
|
expect(oauthResumeToastMessage("reauth")).toBe("Authentication succeeded.");
|
|
});
|
|
|
|
it("returns insufficient-scope message with tool context", () => {
|
|
expect(
|
|
oauthResumeInsufficientScopeMessage({
|
|
reason: "insufficient_scope",
|
|
requiredScopes: ["weather:read"],
|
|
context: { toolName: "get_temp" },
|
|
}),
|
|
).toMatch(/get_temp/);
|
|
});
|
|
|
|
it("round-trips authChallenge on step-up snapshot", () => {
|
|
const challenge = {
|
|
reason: "insufficient_scope" as const,
|
|
requiredScopes: ["weather:read"],
|
|
context: { toolName: "get_temp" },
|
|
};
|
|
const snapshot: OAuthResumeSnapshot = {
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "step_up",
|
|
tabUi: {},
|
|
authChallenge: challenge,
|
|
};
|
|
writeOAuthResumeSnapshot(snapshot);
|
|
expect(readOAuthResumeSnapshot()?.authChallenge).toEqual(challenge);
|
|
});
|
|
|
|
it("rejects snapshots with invalid tabUi keys", () => {
|
|
writeOAuthResumeSnapshot({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: { NotATab: {} },
|
|
} as OAuthResumeSnapshot);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("applyOAuthResumeUi restores tab ui, active tab, and clears in-flight panels", () => {
|
|
const toolsUi = {
|
|
...EMPTY_TOOLS_UI,
|
|
selectedToolName: "get_temp",
|
|
formValues: { city: "NYC" },
|
|
};
|
|
const snapshot: OAuthResumeSnapshot = {
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "step_up",
|
|
tabUi: { Tools: toolsUi },
|
|
};
|
|
const setToolsUi = vi.fn();
|
|
const setActiveTab = vi.fn();
|
|
const clearToolCallState = vi.fn();
|
|
const clearGetPromptState = vi.fn();
|
|
const clearReadResourceState = vi.fn();
|
|
|
|
applyOAuthResumeUi(snapshot, {
|
|
setToolsUi,
|
|
setPromptsUi: vi.fn(),
|
|
setResourcesUi: vi.fn(),
|
|
setAppsUi: vi.fn(),
|
|
setTasksUi: vi.fn(),
|
|
setLogsUi: vi.fn(),
|
|
setProtocolUi: vi.fn(),
|
|
setNetworkUi: vi.fn(),
|
|
setActiveTab,
|
|
clearToolCallState,
|
|
clearGetPromptState,
|
|
clearReadResourceState,
|
|
});
|
|
|
|
expect(setToolsUi).toHaveBeenCalledWith(toolsUi);
|
|
expect(setActiveTab).toHaveBeenCalledWith("Tools");
|
|
expect(clearToolCallState).toHaveBeenCalledOnce();
|
|
expect(clearGetPromptState).toHaveBeenCalledOnce();
|
|
expect(clearReadResourceState).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot returns undefined for a non-JSON string", () => {
|
|
storage.set(OAUTH_RESUME_KEY, "not-json{");
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot returns undefined when parsed JSON is null", () => {
|
|
storage.set(OAUTH_RESUME_KEY, "null");
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot rejects a wrong version", () => {
|
|
storage.set(
|
|
OAUTH_RESUME_KEY,
|
|
JSON.stringify({
|
|
version: 2,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
}),
|
|
);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot rejects a non-string serverId", () => {
|
|
storage.set(
|
|
OAUTH_RESUME_KEY,
|
|
JSON.stringify({
|
|
version: 1,
|
|
serverId: 42,
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
}),
|
|
);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot rejects an unknown authKind", () => {
|
|
storage.set(
|
|
OAUTH_RESUME_KEY,
|
|
JSON.stringify({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "bogus",
|
|
tabUi: {},
|
|
}),
|
|
);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot rejects a non-string activeTab", () => {
|
|
storage.set(
|
|
OAUTH_RESUME_KEY,
|
|
JSON.stringify({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: 7,
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
}),
|
|
);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot accepts a snapshot with tabUi absent", () => {
|
|
storage.set(
|
|
OAUTH_RESUME_KEY,
|
|
JSON.stringify({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
}),
|
|
);
|
|
const snapshot = readOAuthResumeSnapshot();
|
|
expect(snapshot?.serverId).toBe("srv-1");
|
|
expect(snapshot?.tabUi).toBeUndefined();
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot rejects non-object tabUi (string, array, null)", () => {
|
|
for (const tabUi of ['"nope"', "[]", "null"]) {
|
|
storage.set(
|
|
OAUTH_RESUME_KEY,
|
|
`{"version":1,"serverId":"srv-1","activeTab":"Tools","authKind":"reauth","tabUi":${tabUi}}`,
|
|
);
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
it("writeOAuthResumeSnapshot swallows setItem failures", () => {
|
|
vi.stubGlobal("sessionStorage", {
|
|
getItem: () => null,
|
|
setItem: () => {
|
|
throw new Error("quota exceeded");
|
|
},
|
|
removeItem: () => {},
|
|
});
|
|
expect(() =>
|
|
writeOAuthResumeSnapshot({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
}),
|
|
).not.toThrow();
|
|
});
|
|
|
|
it("clearOAuthResumeSnapshot swallows removeItem failures", () => {
|
|
vi.stubGlobal("sessionStorage", {
|
|
getItem: () => null,
|
|
setItem: () => {},
|
|
removeItem: () => {
|
|
throw new Error("blocked");
|
|
},
|
|
});
|
|
expect(() => clearOAuthResumeSnapshot()).not.toThrow();
|
|
});
|
|
|
|
it("legacy fallback returns undefined when the pending-key read throws", () => {
|
|
vi.stubGlobal("sessionStorage", {
|
|
getItem: (key: string) => {
|
|
if (key === OAUTH_PENDING_SERVER_KEY) {
|
|
throw new Error("blocked");
|
|
}
|
|
return null;
|
|
},
|
|
setItem: () => {},
|
|
removeItem: () => {},
|
|
});
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("legacy fallback returns undefined when no pending server is stored", () => {
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
describe("without a window global", () => {
|
|
beforeEach(() => {
|
|
vi.stubGlobal("window", undefined);
|
|
});
|
|
|
|
it("writeOAuthResumeSnapshot is a no-op", () => {
|
|
expect(() =>
|
|
writeOAuthResumeSnapshot({
|
|
version: 1,
|
|
serverId: "srv-1",
|
|
activeTab: "Tools",
|
|
authKind: "reauth",
|
|
tabUi: {},
|
|
}),
|
|
).not.toThrow();
|
|
expect(storage.size).toBe(0);
|
|
});
|
|
|
|
it("readOAuthResumeSnapshot returns undefined", () => {
|
|
expect(readOAuthResumeSnapshot()).toBeUndefined();
|
|
});
|
|
|
|
it("clearOAuthResumeSnapshot is a no-op", () => {
|
|
expect(() => clearOAuthResumeSnapshot()).not.toThrow();
|
|
});
|
|
});
|
|
|
|
it("restoreTabUiFromSnapshot returns early when tabUi is undefined", () => {
|
|
const setToolsUi = vi.fn();
|
|
restoreTabUiFromSnapshot(undefined, {
|
|
setToolsUi,
|
|
setPromptsUi: vi.fn(),
|
|
setResourcesUi: vi.fn(),
|
|
setAppsUi: vi.fn(),
|
|
setTasksUi: vi.fn(),
|
|
setLogsUi: vi.fn(),
|
|
setProtocolUi: vi.fn(),
|
|
setNetworkUi: vi.fn(),
|
|
});
|
|
expect(setToolsUi).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("restoreTabUiFromSnapshot skips keys that are not inspector tabs", () => {
|
|
const setters = {
|
|
setToolsUi: vi.fn(),
|
|
setPromptsUi: vi.fn(),
|
|
setResourcesUi: vi.fn(),
|
|
setAppsUi: vi.fn(),
|
|
setTasksUi: vi.fn(),
|
|
setLogsUi: vi.fn(),
|
|
setProtocolUi: vi.fn(),
|
|
setNetworkUi: vi.fn(),
|
|
};
|
|
restoreTabUiFromSnapshot(
|
|
{ NotATab: {} } as Record<string, unknown>,
|
|
setters,
|
|
);
|
|
for (const setter of Object.values(setters)) {
|
|
expect(setter).not.toHaveBeenCalled();
|
|
}
|
|
});
|
|
|
|
it("restoreTabUiFromSnapshot restores every tab with a present value", () => {
|
|
const setters = {
|
|
setToolsUi: vi.fn(),
|
|
setPromptsUi: vi.fn(),
|
|
setResourcesUi: vi.fn(),
|
|
setAppsUi: vi.fn(),
|
|
setTasksUi: vi.fn(),
|
|
setLogsUi: vi.fn(),
|
|
setProtocolUi: vi.fn(),
|
|
setNetworkUi: vi.fn(),
|
|
};
|
|
restoreTabUiFromSnapshot(
|
|
{
|
|
Tools: EMPTY_TOOLS_UI,
|
|
Prompts: EMPTY_PROMPTS_UI,
|
|
Resources: EMPTY_RESOURCES_UI,
|
|
Apps: EMPTY_APPS_UI,
|
|
Tasks: EMPTY_TASKS_UI,
|
|
Logs: EMPTY_LOGS_UI,
|
|
Protocol: EMPTY_PROTOCOL_UI,
|
|
Network: EMPTY_NETWORK_UI,
|
|
},
|
|
setters,
|
|
);
|
|
expect(setters.setToolsUi).toHaveBeenCalledWith(EMPTY_TOOLS_UI);
|
|
expect(setters.setPromptsUi).toHaveBeenCalledWith(EMPTY_PROMPTS_UI);
|
|
expect(setters.setResourcesUi).toHaveBeenCalledWith(EMPTY_RESOURCES_UI);
|
|
expect(setters.setAppsUi).toHaveBeenCalledWith(EMPTY_APPS_UI);
|
|
expect(setters.setTasksUi).toHaveBeenCalledWith(EMPTY_TASKS_UI);
|
|
expect(setters.setLogsUi).toHaveBeenCalledWith(EMPTY_LOGS_UI);
|
|
expect(setters.setProtocolUi).toHaveBeenCalledWith(EMPTY_PROTOCOL_UI);
|
|
expect(setters.setNetworkUi).toHaveBeenCalledWith(EMPTY_NETWORK_UI);
|
|
});
|
|
|
|
it("restoreTabUiFromSnapshot falls back to EMPTY state for undefined tab values", () => {
|
|
const setters = {
|
|
setToolsUi: vi.fn(),
|
|
setPromptsUi: vi.fn(),
|
|
setResourcesUi: vi.fn(),
|
|
setAppsUi: vi.fn(),
|
|
setTasksUi: vi.fn(),
|
|
setLogsUi: vi.fn(),
|
|
setProtocolUi: vi.fn(),
|
|
setNetworkUi: vi.fn(),
|
|
};
|
|
restoreTabUiFromSnapshot(
|
|
{
|
|
Tools: undefined,
|
|
Prompts: undefined,
|
|
Resources: undefined,
|
|
Apps: undefined,
|
|
Tasks: undefined,
|
|
Logs: undefined,
|
|
Protocol: undefined,
|
|
Network: undefined,
|
|
},
|
|
setters,
|
|
);
|
|
expect(setters.setToolsUi).toHaveBeenCalledWith(EMPTY_TOOLS_UI);
|
|
expect(setters.setPromptsUi).toHaveBeenCalledWith(EMPTY_PROMPTS_UI);
|
|
expect(setters.setResourcesUi).toHaveBeenCalledWith(EMPTY_RESOURCES_UI);
|
|
expect(setters.setAppsUi).toHaveBeenCalledWith(EMPTY_APPS_UI);
|
|
expect(setters.setTasksUi).toHaveBeenCalledWith(EMPTY_TASKS_UI);
|
|
expect(setters.setLogsUi).toHaveBeenCalledWith(EMPTY_LOGS_UI);
|
|
expect(setters.setProtocolUi).toHaveBeenCalledWith(EMPTY_PROTOCOL_UI);
|
|
expect(setters.setNetworkUi).toHaveBeenCalledWith(EMPTY_NETWORK_UI);
|
|
});
|
|
});
|