diff --git a/src/node-host/invoke-system-run.test.ts b/src/node-host/invoke-system-run.test.ts index 880dd26c1d8..36fdff4ce6c 100644 --- a/src/node-host/invoke-system-run.test.ts +++ b/src/node-host/invoke-system-run.test.ts @@ -920,7 +920,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { }); for (const runtime of ["bun", "deno", "tsx", "jiti"] as const) { - it(`denies approval-based execution when a ${runtime} script operand changes after approval`, async () => { + it(`validates approved ${runtime} script operand stability`, async () => { await withFakeRuntimeOnPath({ runtime, run: async () => { @@ -959,23 +959,15 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } finally { fs.rmSync(tmp, { recursive: true, force: true }); } - }, - }); - }); - - it(`keeps approved ${runtime} script execution working when the script is unchanged`, async () => { - await withFakeRuntimeOnPath({ - runtime, - run: async () => { - const tmp = fs.mkdtempSync( + const stableTmp = fs.mkdtempSync( path.join(os.tmpdir(), `openclaw-approval-${runtime}-script-stable-`), ); - const fixture = createRuntimeScriptOperandFixture({ tmp, runtime }); - fs.writeFileSync(fixture.scriptPath, fixture.initialBody); + const stableFixture = createRuntimeScriptOperandFixture({ tmp: stableTmp, runtime }); + fs.writeFileSync(stableFixture.scriptPath, stableFixture.initialBody); try { const prepared = buildSystemRunApprovalPlan({ - command: fixture.command, - cwd: tmp, + command: stableFixture.command, + cwd: stableTmp, }); expect(prepared.ok).toBe(true); if (!prepared.ok) { @@ -987,7 +979,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { command: prepared.plan.argv, rawCommand: prepared.plan.commandText, systemRunPlan: prepared.plan, - cwd: prepared.plan.cwd ?? tmp, + cwd: prepared.plan.cwd ?? stableTmp, approved: true, security: "full", ask: "off", @@ -996,7 +988,7 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { expect(runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(sendInvokeResult); } finally { - fs.rmSync(tmp, { recursive: true, force: true }); + fs.rmSync(stableTmp, { recursive: true, force: true }); } }, }); @@ -1310,32 +1302,33 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { }); }); - it.each([ - { - command: ["python3", "-c", "print('hi')"], - expected: "python3 -c requires explicit approval in strictInlineEval mode", - }, - { - command: ["awk", 'BEGIN{system("id")}', "/dev/null"], - expected: "awk inline program requires explicit approval in strictInlineEval mode", - }, - { - command: ["find", ".", "-exec", "id", "{}", ";"], - expected: "find -exec requires explicit approval in strictInlineEval mode", - }, - { - command: ["xargs", "id"], - expected: "xargs inline command requires explicit approval in strictInlineEval mode", - }, - { - command: ["make", "-f", "evil.mk"], - expected: "make -f requires explicit approval in strictInlineEval mode", - }, - { - command: ["sed", "s/.*/id/e", "/dev/null"], - expected: "sed inline program requires explicit approval in strictInlineEval mode", - }, - ] as const)("requires explicit approval for strict inline-eval carrier %j", async (testCase) => { + it("requires explicit approval for strict inline-eval carriers", async () => { + const cases = [ + { + command: ["python3", "-c", "print('hi')"], + expected: "python3 -c requires explicit approval in strictInlineEval mode", + }, + { + command: ["awk", 'BEGIN{system("id")}', "/dev/null"], + expected: "awk inline program requires explicit approval in strictInlineEval mode", + }, + { + command: ["find", ".", "-exec", "id", "{}", ";"], + expected: "find -exec requires explicit approval in strictInlineEval mode", + }, + { + command: ["xargs", "id"], + expected: "xargs inline command requires explicit approval in strictInlineEval mode", + }, + { + command: ["make", "-f", "evil.mk"], + expected: "make -f requires explicit approval in strictInlineEval mode", + }, + { + command: ["sed", "s/.*/id/e", "/dev/null"], + expected: "sed inline program requires explicit approval in strictInlineEval mode", + }, + ] as const; setRuntimeConfigSnapshot({ tools: { exec: { @@ -1344,22 +1337,24 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { }, }); try { - const { runCommand, sendInvokeResult, sendNodeEvent } = await runSystemInvoke({ - preferMacAppExecHost: false, - command: [...testCase.command], - security: "full", - ask: "off", - }); + for (const testCase of cases) { + const { runCommand, sendInvokeResult, sendNodeEvent } = await runSystemInvoke({ + preferMacAppExecHost: false, + command: [...testCase.command], + security: "full", + ask: "off", + }); - expect(runCommand).not.toHaveBeenCalled(); - expect(sendNodeEvent).toHaveBeenCalledWith( - expect.anything(), - "exec.denied", - expect.objectContaining({ reason: "approval-required" }), - ); - expectInvokeErrorMessage(sendInvokeResult, { - message: testCase.expected, - }); + expect(runCommand, testCase.command.join(" ")).not.toHaveBeenCalled(); + expect(sendNodeEvent, testCase.command.join(" ")).toHaveBeenCalledWith( + expect.anything(), + "exec.denied", + expect.objectContaining({ reason: "approval-required" }), + ); + expectInvokeErrorMessage(sendInvokeResult, { + message: testCase.expected, + }); + } } finally { clearRuntimeConfigSnapshot(); } @@ -1395,26 +1390,26 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } }); - it.each([ - { executable: "python3", args: ["-c", "print('hi')"] }, - { executable: "awk", args: ['BEGIN{system("id")}', "/dev/null"] }, - { executable: "find", args: [".", "-exec", "id", "{}", ";"] }, - { executable: "xargs", args: ["id"] }, - { executable: "sed", args: ["s/.*/id/e", "/dev/null"] }, - ] as const)( - "does not persist allow-always approvals for strict inline-eval carrier %j", - async (testCase) => { - setRuntimeConfigSnapshot({ - tools: { - exec: { - strictInlineEval: true, - }, + it("does not persist allow-always approvals for strict inline-eval carriers", async () => { + const cases = [ + { executable: "python3", args: ["-c", "print('hi')"] }, + { executable: "awk", args: ['BEGIN{system("id")}', "/dev/null"] }, + { executable: "find", args: [".", "-exec", "id", "{}", ";"] }, + { executable: "xargs", args: ["id"] }, + { executable: "sed", args: ["s/.*/id/e", "/dev/null"] }, + ] as const; + setRuntimeConfigSnapshot({ + tools: { + exec: { + strictInlineEval: true, }, - }); - try { - await withTempApprovalsHome({ - approvals: createAllowlistOnMissApprovals(), - run: async () => { + }, + }); + try { + await withTempApprovalsHome({ + approvals: createAllowlistOnMissApprovals(), + run: async () => { + for (const testCase of cases) { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-inline-eval-bin-")); try { const executablePath = createTempExecutable({ @@ -1437,13 +1432,13 @@ describe("handleSystemRunInvoke mac app exec host routing", () => { } finally { fs.rmSync(tempDir, { recursive: true, force: true }); } - }, - }); - } finally { - clearRuntimeConfigSnapshot(); - } - }, - ); + } + }, + }); + } finally { + clearRuntimeConfigSnapshot(); + } + }); it("persists benign awk allow-always approvals in strict inline-eval mode without reopening inline carriers", async () => { setRuntimeConfigSnapshot({ diff --git a/src/plugin-sdk/browser-maintenance.ts b/src/plugin-sdk/browser-maintenance.ts index 63acfc3c6d4..d430ea448eb 100644 --- a/src/plugin-sdk/browser-maintenance.ts +++ b/src/plugin-sdk/browser-maintenance.ts @@ -1,8 +1,3 @@ -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { generateSecureToken } from "../infra/secure-random.js"; -import { runExec } from "../process/exec.js"; import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-loader.js"; type CloseTrackedBrowserTabsParams = { @@ -44,6 +39,19 @@ export async function closeTrackedBrowserTabsForSessions( } export async function movePathToTrash(targetPath: string): Promise { + const [ + { default: fs }, + { default: os }, + { default: path }, + { generateSecureToken }, + { runExec }, + ] = await Promise.all([ + import("node:fs"), + import("node:os"), + import("node:path"), + import("../infra/secure-random.js"), + import("../process/exec.js"), + ]); try { await runExec("trash", [targetPath], { timeoutMs: 10_000 }); return targetPath; diff --git a/ui/src/ui/app-render.helpers.node.test.ts b/ui/src/ui/app-render.helpers.node.test.ts index bb0938378eb..cd0bb6b78ac 100644 --- a/ui/src/ui/app-render.helpers.node.test.ts +++ b/ui/src/ui/app-render.helpers.node.test.ts @@ -34,6 +34,7 @@ import { isCronSessionKey, parseSessionKey, resolveAssistantAttachmentAuthToken, + resolveSessionOptionGroups, resolveSessionDisplayName, switchChatSession, } from "./app-render.helpers.ts"; @@ -46,6 +47,28 @@ function row(overrides: Partial & { key: string }): SessionRow { return { kind: "direct", updatedAt: 0, ...overrides }; } +function labelsForSessionOptions(params: { + sessionKey: string; + sessions?: SessionRow[]; + agentsList?: AppViewState["agentsList"]; +}) { + const groups = resolveSessionOptionGroups( + { + sessionsHideCron: true, + agentsList: params.agentsList ?? null, + } as AppViewState, + params.sessionKey, + { + ts: 0, + path: "", + count: params.sessions?.length ?? 0, + defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null }, + sessions: params.sessions ?? [], + }, + ); + return groups.flatMap((group) => group.options.map((option) => option.label)); +} + /* ================================================================ * parseSessionKey – low-level key → type / fallback mapping * ================================================================ */ @@ -348,6 +371,94 @@ describe("isCronSessionKey", () => { }); }); +describe("resolveSessionOptionGroups", () => { + it("prefers grouped session labels over display names", () => { + const sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b"; + const labels = labelsForSessionOptions({ + sessionKey, + sessions: [ + row({ + key: sessionKey, + label: "cron-config-check", + displayName: "webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b", + }), + ], + }); + + expect(labels).toContain("Subagent: cron-config-check"); + expect(labels).not.toContain(sessionKey); + expect(labels).not.toContain( + "subagent:4f2146de-887b-4176-9abe-91140082959b · webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b", + ); + }); + + it("keeps scoped fallbacks for active grouped sessions without useful row metadata", () => { + const sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b"; + + expect(labelsForSessionOptions({ sessionKey })).toContain( + "subagent:4f2146de-887b-4176-9abe-91140082959b", + ); + expect( + labelsForSessionOptions({ + sessionKey, + sessions: [row({ key: sessionKey })], + }), + ).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b"); + }); + + it("disambiguates duplicate grouped labels with scoped suffixes", () => { + const labels = labelsForSessionOptions({ + sessionKey: "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b", + sessions: [ + row({ + key: "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b", + label: "cron-config-check", + }), + row({ + key: "agent:main:subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9", + label: "cron-config-check", + }), + ], + }); + + expect(labels).toContain( + "Subagent: cron-config-check · subagent:4f2146de-887b-4176-9abe-91140082959b", + ); + expect(labels).toContain( + "Subagent: cron-config-check · subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9", + ); + expect(labels).not.toContain("Subagent: cron-config-check"); + }); + + it("uses agent group labels to keep duplicate main sessions unique", () => { + const labels = labelsForSessionOptions({ + sessionKey: "agent:alpha:main", + agentsList: { + defaultId: "alpha", + mainKey: "agent:alpha:main", + scope: "all", + agents: [ + { id: "alpha", name: "Deep Chat" }, + { id: "beta", name: "Coding" }, + ], + }, + sessions: [ + row({ key: "agent:alpha:main" }), + row({ key: "agent:beta:main" }), + row({ + key: "agent:alpha:named-main", + label: "Deep Chat (alpha) / main", + }), + ], + }); + + expect(labels.filter((label) => label === "Deep Chat (alpha) / main")).toHaveLength(1); + expect(labels).toContain("Deep Chat (alpha) / main · named-main"); + expect(labels).toContain("Coding (beta) / main"); + expect(labels).not.toContain("main"); + }); +}); + describe("switchChatSession", () => { it("refreshes the chat avatar after clearing session-scoped state", async () => { const settings: AppViewState["settings"] = { diff --git a/ui/src/ui/chat-model-select-state.test.ts b/ui/src/ui/chat-model-select-state.test.ts index 9d947231f96..537c9a4f8aa 100644 --- a/ui/src/ui/chat-model-select-state.test.ts +++ b/ui/src/ui/chat-model-select-state.test.ts @@ -39,6 +39,34 @@ describe("chat-model-select-state", () => { expect(resolveChatModelOverrideValue(state)).toBe("openai/gpt-5-mini"); }); + it("normalizes cached bare overrides to the matching catalog option", () => { + const state = { + sessionKey: "main", + chatModelOverrides: { main: { kind: "raw", value: "gpt-5-mini" } }, + chatModelCatalog: createModelCatalog(...DEFAULT_CHAT_MODEL_CATALOG), + sessionsResult: createSessionsListResult({ model: null, modelProvider: null }), + } as const; + + const resolved = resolveChatModelSelectState(state); + expect(resolved.currentOverride).toBe("openai/gpt-5-mini"); + expect(resolved.options.map((option) => option.value)).toContain("openai/gpt-5-mini"); + expect(resolved.options.map((option) => option.value)).not.toContain("gpt-5-mini"); + }); + + it("prefers catalog provider matches over stale session providers", () => { + const state = { + sessionKey: "main", + chatModelOverrides: {}, + chatModelCatalog: createModelCatalog(DEEPSEEK_CHAT_MODEL), + sessionsResult: createSessionsListResult({ + model: "deepseek-chat", + modelProvider: "zai", + }), + }; + + expect(resolveChatModelSelectState(state).currentOverride).toBe("deepseek/deepseek-chat"); + }); + it("preserves already-qualified active-session models when the provider is stale and the catalog is empty", () => { const state = { sessionKey: "main", diff --git a/ui/src/ui/views/chat.test.ts b/ui/src/ui/views/chat.test.ts index 59f6b11d488..c764af37241 100644 --- a/ui/src/ui/views/chat.test.ts +++ b/ui/src/ui/views/chat.test.ts @@ -8,7 +8,6 @@ import type { AppViewState } from "../app-view-state.ts"; import { createModelCatalog, createSessionsListResult, - DEEPSEEK_CHAT_MODEL, DEFAULT_CHAT_MODEL_CATALOG, } from "../chat-model.test-helpers.ts"; import { resetAssistantAttachmentAvailabilityCacheForTest } from "../chat/grouped-render.ts"; @@ -947,277 +946,6 @@ describe("chat view", () => { vi.unstubAllGlobals(); }); - it("normalizes cached bare /model overrides to the matching catalog option", () => { - const { state } = createChatHeaderState(); - state.chatModelOverrides = { main: { kind: "raw", value: "gpt-5-mini" } }; - - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const modelSelect = container.querySelector( - 'select[data-chat-model-select="true"]', - ); - expect(modelSelect).not.toBeNull(); - expect(modelSelect?.value).toBe("openai/gpt-5-mini"); - - const optionValues = Array.from(modelSelect?.querySelectorAll("option") ?? []).map( - (option) => option.value, - ); - expect(optionValues).toContain("openai/gpt-5-mini"); - expect(optionValues).not.toContain("gpt-5-mini"); - }); - - it("prefers the catalog provider when the active session reports a stale provider", () => { - const { state } = createChatHeaderState({ - model: "deepseek-chat", - modelProvider: "zai", - models: createModelCatalog(DEEPSEEK_CHAT_MODEL), - }); - - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const modelSelect = container.querySelector( - 'select[data-chat-model-select="true"]', - ); - expect(modelSelect?.value).toBe("deepseek/deepseek-chat"); - }); - - it("falls back to the server-qualified session model when catalog lookup fails", () => { - const { state } = createChatHeaderState({ - model: "gpt-5-mini", - models: [], - }); - - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const modelSelect = container.querySelector( - 'select[data-chat-model-select="true"]', - ); - expect(modelSelect?.value).toBe("openai/gpt-5-mini"); - - const optionValues = Array.from(modelSelect?.querySelectorAll("option") ?? []).map( - (option) => option.value, - ); - expect(optionValues).toContain("openai/gpt-5-mini"); - expect(optionValues).not.toContain("gpt-5-mini"); - }); - - it("prefers the session label over displayName in the grouped chat session selector", () => { - const { state } = createChatHeaderState({ omitSessionFromList: true }); - state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b"; - state.settings.sessionKey = state.sessionKey; - state.sessionsResult = { - ts: 0, - path: "", - count: 1, - defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null }, - sessions: [ - { - key: state.sessionKey, - kind: "direct", - updatedAt: null, - label: "cron-config-check", - displayName: "webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b", - }, - ], - }; - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const [sessionSelect] = Array.from(container.querySelectorAll("select")); - const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) => - option.textContent?.trim(), - ); - - expect(labels).toContain("Subagent: cron-config-check"); - expect(labels).not.toContain(state.sessionKey); - expect(labels).not.toContain( - "subagent:4f2146de-887b-4176-9abe-91140082959b · webchat:g-agent-main-subagent-4f2146de-887b-4176-9abe-91140082959b", - ); - }); - - it("keeps a unique scoped fallback when the current grouped session is missing from sessions.list", () => { - const { state } = createChatHeaderState({ omitSessionFromList: true }); - state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b"; - state.settings.sessionKey = state.sessionKey; - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const [sessionSelect] = Array.from(container.querySelectorAll("select")); - const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) => - option.textContent?.trim(), - ); - - expect(labels).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b"); - expect(labels).not.toContain("Subagent:"); - }); - - it("keeps a unique scoped fallback when a grouped session row has no label or displayName", () => { - const { state } = createChatHeaderState({ omitSessionFromList: true }); - state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b"; - state.settings.sessionKey = state.sessionKey; - state.sessionsResult = { - ts: 0, - path: "", - count: 1, - defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null }, - sessions: [ - { - key: state.sessionKey, - kind: "direct", - updatedAt: null, - }, - ], - }; - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const [sessionSelect] = Array.from(container.querySelectorAll("select")); - const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) => - option.textContent?.trim(), - ); - - expect(labels).toContain("subagent:4f2146de-887b-4176-9abe-91140082959b"); - expect(labels).not.toContain("Subagent:"); - }); - - it("disambiguates duplicate grouped labels with the scoped key suffix", () => { - const { state } = createChatHeaderState({ omitSessionFromList: true }); - state.sessionKey = "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b"; - state.settings.sessionKey = state.sessionKey; - state.sessionsResult = { - ts: 0, - path: "", - count: 2, - defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null }, - sessions: [ - { - key: "agent:main:subagent:4f2146de-887b-4176-9abe-91140082959b", - kind: "direct", - updatedAt: null, - label: "cron-config-check", - }, - { - key: "agent:main:subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9", - kind: "direct", - updatedAt: null, - label: "cron-config-check", - }, - ], - }; - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const [sessionSelect] = Array.from(container.querySelectorAll("select")); - const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) => - option.textContent?.trim(), - ); - - expect(labels).toContain( - "Subagent: cron-config-check · subagent:4f2146de-887b-4176-9abe-91140082959b", - ); - expect(labels).toContain( - "Subagent: cron-config-check · subagent:6fb8b84b-c31f-410f-b7df-1553c82e43c9", - ); - expect(labels).not.toContain("Subagent: cron-config-check"); - }); - - it("prefixes duplicate agent session labels with the agent name", () => { - const { state } = createChatHeaderState({ omitSessionFromList: true }); - state.sessionKey = "agent:alpha:main"; - state.settings.sessionKey = state.sessionKey; - state.agentsList = { - defaultId: "alpha", - mainKey: "agent:alpha:main", - scope: "all", - agents: [ - { id: "alpha", name: "Deep Chat" }, - { id: "beta", name: "Coding" }, - ], - }; - state.sessionsResult = { - ts: 0, - path: "", - count: 2, - defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null }, - sessions: [ - { - key: "agent:alpha:main", - kind: "direct", - updatedAt: null, - }, - { - key: "agent:beta:main", - kind: "direct", - updatedAt: null, - }, - ], - }; - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const [sessionSelect] = Array.from(container.querySelectorAll("select")); - const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) => - option.textContent?.trim(), - ); - - expect(labels).toContain("Deep Chat (alpha) / main"); - expect(labels).toContain("Coding (beta) / main"); - expect(labels).not.toContain("main"); - }); - - it("keeps agent-prefixed labels unique when a custom label already matches the prefix", () => { - const { state } = createChatHeaderState({ omitSessionFromList: true }); - state.sessionKey = "agent:alpha:main"; - state.settings.sessionKey = state.sessionKey; - state.agentsList = { - defaultId: "alpha", - mainKey: "agent:alpha:main", - scope: "all", - agents: [ - { id: "alpha", name: "Deep Chat" }, - { id: "beta", name: "Coding" }, - ], - }; - state.sessionsResult = { - ts: 0, - path: "", - count: 3, - defaults: { modelProvider: "openai", model: "gpt-5", contextTokens: null }, - sessions: [ - { - key: "agent:alpha:main", - kind: "direct", - updatedAt: null, - }, - { - key: "agent:beta:main", - kind: "direct", - updatedAt: null, - }, - { - key: "agent:alpha:named-main", - kind: "direct", - updatedAt: null, - label: "Deep Chat (alpha) / main", - }, - ], - }; - const container = document.createElement("div"); - render(renderChatSessionSelect(state), container); - - const [sessionSelect] = Array.from(container.querySelectorAll("select")); - const labels = Array.from(sessionSelect?.querySelectorAll("option") ?? []).map((option) => - option.textContent?.trim(), - ); - - expect(labels.filter((label) => label === "Deep Chat (alpha) / main")).toHaveLength(1); - expect(labels).toContain("Deep Chat (alpha) / main · named-main"); - expect(labels).toContain("Coding (beta) / main"); - }); - it("keeps tool cards collapsed by default and expands them inline on demand", async () => { const container = document.createElement("div"); const props = createProps({