/* @vitest-environment jsdom */
import { render } from "lit" ;
import { describe, expect, it, vi } from "vitest" ;
import type { AppViewState } from "../app-view-state.ts" ;
import {
createModelCatalog,
createSessionsListResult,
DEFAULT_CHAT_MODEL_CATALOG,
} from "../chat-model.test-helpers.ts" ;
import type { GatewayBrowserClient } from "../gateway.ts" ;
import type { ModelCatalogEntry } from "../types.ts" ;
import { renderChatSessionSelect } from "./session-controls.ts" ;
function createChatHeaderState(
overrides: {
model?: string | null ;
modelProvider?: string | null ;
models?: ModelCatalogEntry[];
omitSessionFromList?: boolean ;
} = {},
): { state: AppViewState; request: ReturnType<typeof vi.fn> } {
let currentModel = overrides.model ?? null ;
let currentModelProvider = overrides.modelProvider ?? (currentModel ? "openai" : null );
const omitSessionFromList = overrides.omitSessionFromList ?? false ;
const catalog = overrides.models ?? createModelCatalog(...DEFAULT_CHAT_MODEL_CATALOG);
const request = vi.fn(async (method: string, params: Record<string, unknown>) => {
if (method === "sessions.patch" ) {
const nextModel = (params.model as string | null | undefined) ?? null ;
if (!nextModel) {
currentModel = null ;
currentModelProvider = null ;
} else {
const normalized = nextModel.trim();
const slashIndex = normalized.indexOf("/" );
if (slashIndex > 0 ) {
currentModelProvider = normalized.slice(0 , slashIndex);
currentModel = normalized.slice(slashIndex + 1 );
} else {
currentModel = normalized;
const matchingProviders = catalog
.filter((entry) => entry.id === normalized)
.map((entry) => entry.provider)
.filter(Boolean );
currentModelProvider =
matchingProviders.length === 1 ? matchingProviders[0 ] : currentModelProvider;
}
}
return { ok: true , key: "main" };
}
if (method === "chat.history" ) {
return { messages: [], thinkingLevel: null };
}
if (method === "sessions.list" ) {
return createSessionsListResult({
model: currentModel,
modelProvider: currentModelProvider,
omitSessionFromList,
});
}
if (method === "models.list" ) {
return { models: catalog };
}
if (method === "tools.effective" ) {
return {
agentId: "main" ,
profile: "coding" ,
groups: [],
};
}
throw new Error(`Unexpected request: ${method}`);
});
const state = {
sessionKey: "main" ,
connected: true ,
sessionsHideCron: true ,
sessionsResult: createSessionsListResult({
model: currentModel,
modelProvider: currentModelProvider,
omitSessionFromList,
}),
chatModelOverrides: {},
chatModelCatalog: catalog,
chatModelsLoading: false ,
client: { request } as unknown as GatewayBrowserClient,
settings: {
gatewayUrl: "" ,
token: "" ,
locale: "en" ,
sessionKey: "main" ,
lastActiveSessionKey: "main" ,
theme: "claw" ,
themeMode: "dark" ,
splitRatio: 0 .6 ,
navCollapsed: false ,
navGroupsCollapsed: {},
borderRadius: 50 ,
chatFocusMode: false ,
chatShowThinking: false ,
},
chatMessage: "" ,
chatStream: null ,
chatStreamStartedAt: null ,
chatRunId: null ,
chatQueue: [],
chatMessages: [],
chatLoading: false ,
chatThinkingLevel: null ,
lastError: null ,
chatAvatarUrl: null ,
basePath: "" ,
hello: null ,
agentsList: null ,
agentsPanel: "overview" ,
agentsSelectedId: null ,
toolsEffectiveLoading: false ,
toolsEffectiveLoadingKey: null ,
toolsEffectiveResultKey: null ,
toolsEffectiveError: null ,
toolsEffectiveResult: null ,
applySettings(next: AppViewState["settings" ]) {
state.settings = next;
},
loadAssistantIdentity: vi.fn(),
resetToolStream: vi.fn(),
resetChatScroll: vi.fn(),
} as unknown as AppViewState & {
client: GatewayBrowserClient;
settings: AppViewState["settings" ];
};
return { state, request };
}
function flushTasks() {
return new Promise<void >((resolve) => queueMicrotask(resolve));
}
describe("chat session controls" , () => {
it("patches the current session model from the chat header picker" , async () => {
vi.stubGlobal(
"fetch" ,
vi.fn().mockResolvedValue({
ok: false ,
} satisfies Partial<Response>),
);
const { state, request } = createChatHeaderState();
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("" );
modelSelect!.value = "openai/gpt-5-mini" ;
modelSelect!.dispatchEvent(new Event("change" , { bubbles: true }));
await flushTasks();
expect(request).toHaveBeenCalledWith("sessions.patch" , {
key: "main" ,
model: "openai/gpt-5-mini" ,
});
expect(request).not.toHaveBeenCalledWith("chat.history" , expect.anything());
expect(state.sessionsResult?.sessions[0 ]?.model).toBe("gpt-5-mini" );
expect(state.sessionsResult?.sessions[0 ]?.modelProvider).toBe("openai" );
vi.unstubAllGlobals();
});
it("reloads effective tools after a chat-header model switch for the active tools panel" , async () => {
vi.stubGlobal(
"fetch" ,
vi.fn().mockResolvedValue({
ok: false ,
} satisfies Partial<Response>),
);
const { state, request } = createChatHeaderState();
state.agentsPanel = "tools" ;
state.agentsSelectedId = "main" ;
state.toolsEffectiveResultKey = "main:main" ;
state.toolsEffectiveResult = {
agentId: "main" ,
profile: "coding" ,
groups: [],
};
const container = document.createElement("div" );
render(renderChatSessionSelect(state), container);
const modelSelect = container.querySelector<HTMLSelectElement>(
'select[data-chat-model-select="true"]' ,
);
expect(modelSelect).not.toBeNull();
modelSelect!.value = "openai/gpt-5-mini" ;
modelSelect!.dispatchEvent(new Event("change" , { bubbles: true }));
await flushTasks();
expect(request).toHaveBeenCalledWith("tools.effective" , {
agentId: "main" ,
sessionKey: "main" ,
});
expect(state.toolsEffectiveResultKey).toBe("main:main:model=openai/gpt-5-mini" );
vi.unstubAllGlobals();
});
it("clears the session model override back to the default model" , async () => {
vi.stubGlobal(
"fetch" ,
vi.fn().mockResolvedValue({
ok: false ,
} satisfies Partial<Response>),
);
const { state, request } = createChatHeaderState({ model: "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" );
modelSelect!.value = "" ;
modelSelect!.dispatchEvent(new Event("change" , { bubbles: true }));
await flushTasks();
expect(request).toHaveBeenCalledWith("sessions.patch" , {
key: "main" ,
model: null ,
});
expect(state.sessionsResult?.sessions[0 ]?.model).toBeUndefined();
vi.unstubAllGlobals();
});
it("disables the chat header model picker while a run is active" , () => {
const { state } = createChatHeaderState();
state.chatRunId = "run-123" ;
state.chatStream = "Working" ;
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?.disabled).toBe(true );
});
it("keeps the selected model visible when the active session is absent from sessions.list" , async () => {
vi.stubGlobal(
"fetch" ,
vi.fn().mockResolvedValue({
ok: false ,
} satisfies Partial<Response>),
);
const { state } = createChatHeaderState({ omitSessionFromList: true });
const container = document.createElement("div" );
render(renderChatSessionSelect(state), container);
const modelSelect = container.querySelector<HTMLSelectElement>(
'select[data-chat-model-select="true"]' ,
);
expect(modelSelect).not.toBeNull();
modelSelect!.value = "openai/gpt-5-mini" ;
modelSelect!.dispatchEvent(new Event("change" , { bubbles: true }));
await flushTasks();
render(renderChatSessionSelect(state), container);
const rerendered = container.querySelector<HTMLSelectElement>(
'select[data-chat-model-select="true"]' ,
);
expect(rerendered?.value).toBe("openai/gpt-5-mini" );
vi.unstubAllGlobals();
});
it("uses default thinking options when the active session is absent" , () => {
const { state } = createChatHeaderState({ omitSessionFromList: true });
state.sessionsResult = createSessionsListResult({
defaultsModel: "gpt-5.5" ,
defaultsProvider: "openai-codex" ,
defaultsThinkingLevels: [
{ id: "off" , label: "off" },
{ id: "adaptive" , label: "adaptive" },
{ id: "xhigh" , label: "xhigh" },
{ id: "max" , label: "maximum" },
],
omitSessionFromList: true ,
});
const container = document.createElement("div" );
render(renderChatSessionSelect(state), container);
const thinkingSelect = container.querySelector<HTMLSelectElement>(
'select[data-chat-thinking-select="true"]' ,
);
const options = [...(thinkingSelect?.options ?? [])].map((option) => option.value);
expect(options).toContain("adaptive" );
expect(options).toContain("xhigh" );
expect(options).toContain("max" );
expect(
[...(thinkingSelect?.options ?? [])]
.find((option) => option.value === "max" )
?.textContent?.trim(),
).toBe("maximum" );
});
});
Messung V0.5 in Prozent C=98 H=98 G=97
¤ Dauer der Verarbeitung: 0.5 Sekunden
¤
*© Formatika GbR, Deutschland