mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
test: trim remaining hotspot tests
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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<string> {
|
||||
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;
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
isCronSessionKey,
|
||||
parseSessionKey,
|
||||
resolveAssistantAttachmentAuthToken,
|
||||
resolveSessionOptionGroups,
|
||||
resolveSessionDisplayName,
|
||||
switchChatSession,
|
||||
} from "./app-render.helpers.ts";
|
||||
@@ -46,6 +47,28 @@ function row(overrides: Partial<SessionRow> & { 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"] = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<HTMLSelectElement>(
|
||||
'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<HTMLSelectElement>(
|
||||
'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<HTMLSelectElement>(
|
||||
'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<HTMLSelectElement>("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<HTMLSelectElement>("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<HTMLSelectElement>("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<HTMLSelectElement>("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<HTMLSelectElement>("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<HTMLSelectElement>("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({
|
||||
|
||||
Reference in New Issue
Block a user