fix(ui): align control ui chat and config rendering
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import type { Tab } from "./navigation";
|
||||
import { setTabFromRoute } from "./app-settings";
|
||||
|
||||
type SettingsHost = Parameters<typeof import("./app-settings").setTabFromRoute>[0];
|
||||
type SettingsHost = Parameters<typeof setTabFromRoute>[0] & {
|
||||
logsPollInterval: number | null;
|
||||
debugPollInterval: number | null;
|
||||
};
|
||||
|
||||
const createHost = (tab: Tab): SettingsHost => ({
|
||||
settings: {
|
||||
@@ -30,62 +34,38 @@ const createHost = (tab: Tab): SettingsHost => ({
|
||||
basePath: "",
|
||||
themeMedia: null,
|
||||
themeMediaHandler: null,
|
||||
logsPollInterval: null,
|
||||
debugPollInterval: null,
|
||||
});
|
||||
|
||||
describe("setTabFromRoute", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
it("starts and stops log polling based on the tab", async () => {
|
||||
const startLogsPolling = vi.fn();
|
||||
const stopLogsPolling = vi.fn();
|
||||
const startDebugPolling = vi.fn();
|
||||
const stopDebugPolling = vi.fn();
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
vi.doMock("./app-polling", () => ({
|
||||
startLogsPolling,
|
||||
stopLogsPolling,
|
||||
startDebugPolling,
|
||||
stopDebugPolling,
|
||||
}));
|
||||
|
||||
const { setTabFromRoute } = await import("./app-settings");
|
||||
it("starts and stops log polling based on the tab", () => {
|
||||
const host = createHost("chat");
|
||||
|
||||
setTabFromRoute(host, "logs");
|
||||
expect(startLogsPolling).toHaveBeenCalledTimes(1);
|
||||
expect(stopLogsPolling).not.toHaveBeenCalled();
|
||||
expect(startDebugPolling).not.toHaveBeenCalled();
|
||||
expect(stopDebugPolling).toHaveBeenCalledTimes(1);
|
||||
expect(host.logsPollInterval).not.toBeNull();
|
||||
expect(host.debugPollInterval).toBeNull();
|
||||
|
||||
setTabFromRoute(host, "chat");
|
||||
expect(stopLogsPolling).toHaveBeenCalledTimes(1);
|
||||
expect(host.logsPollInterval).toBeNull();
|
||||
});
|
||||
|
||||
it("starts and stops debug polling based on the tab", async () => {
|
||||
const startLogsPolling = vi.fn();
|
||||
const stopLogsPolling = vi.fn();
|
||||
const startDebugPolling = vi.fn();
|
||||
const stopDebugPolling = vi.fn();
|
||||
|
||||
vi.doMock("./app-polling", () => ({
|
||||
startLogsPolling,
|
||||
stopLogsPolling,
|
||||
startDebugPolling,
|
||||
stopDebugPolling,
|
||||
}));
|
||||
|
||||
const { setTabFromRoute } = await import("./app-settings");
|
||||
it("starts and stops debug polling based on the tab", () => {
|
||||
const host = createHost("chat");
|
||||
|
||||
setTabFromRoute(host, "debug");
|
||||
expect(startDebugPolling).toHaveBeenCalledTimes(1);
|
||||
expect(stopDebugPolling).not.toHaveBeenCalled();
|
||||
expect(startLogsPolling).not.toHaveBeenCalled();
|
||||
expect(stopLogsPolling).toHaveBeenCalledTimes(1);
|
||||
expect(host.debugPollInterval).not.toBeNull();
|
||||
expect(host.logsPollInterval).toBeNull();
|
||||
|
||||
setTabFromRoute(host, "chat");
|
||||
expect(stopDebugPolling).toHaveBeenCalledTimes(1);
|
||||
expect(host.debugPollInterval).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,8 +46,13 @@ describe("chat markdown rendering", () => {
|
||||
|
||||
await app.updateComplete;
|
||||
|
||||
const toolCard = app.querySelector(".chat-tool-card") as HTMLElement | null;
|
||||
expect(toolCard).not.toBeNull();
|
||||
const toolCards = Array.from(
|
||||
app.querySelectorAll<HTMLElement>(".chat-tool-card"),
|
||||
);
|
||||
const toolCard = toolCards.find((card) =>
|
||||
card.querySelector(".chat-tool-card__preview, .chat-tool-card__inline"),
|
||||
);
|
||||
expect(toolCard).not.toBeUndefined();
|
||||
toolCard?.click();
|
||||
|
||||
await app.updateComplete;
|
||||
|
||||
@@ -26,17 +26,7 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
contentItems.some((item) => {
|
||||
const x = item as Record<string, unknown>;
|
||||
const t = String(x.type ?? "").toLowerCase();
|
||||
return (
|
||||
t === "toolcall" ||
|
||||
t === "tool_call" ||
|
||||
t === "tooluse" ||
|
||||
t === "tool_use" ||
|
||||
t === "toolresult" ||
|
||||
t === "tool_result" ||
|
||||
t === "tool_call" ||
|
||||
t === "tool_result" ||
|
||||
(typeof x.name === "string" && x.arguments != null)
|
||||
);
|
||||
return t === "toolresult" || t === "tool_result";
|
||||
});
|
||||
|
||||
const hasToolName =
|
||||
@@ -74,19 +64,19 @@ export function normalizeMessage(message: unknown): NormalizedMessage {
|
||||
*/
|
||||
export function normalizeRoleForGrouping(role: string): string {
|
||||
const lower = role.toLowerCase();
|
||||
// Preserve original casing when it's already a core role.
|
||||
if (role === "user" || role === "User") return role;
|
||||
if (role === "assistant") return "assistant";
|
||||
if (role === "system") return "system";
|
||||
// Keep tool-related roles distinct so the UI can style/toggle them.
|
||||
if (
|
||||
lower === "toolresult" ||
|
||||
lower === "tool_result" ||
|
||||
lower === "tool" ||
|
||||
lower === "function" ||
|
||||
lower === "toolresult"
|
||||
lower === "function"
|
||||
) {
|
||||
return "tool";
|
||||
}
|
||||
if (lower === "assistant") return "assistant";
|
||||
if (lower === "user") return "user";
|
||||
if (lower === "system") return "system";
|
||||
return role;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,20 +69,11 @@ describe("config form renderer", () => {
|
||||
"abc123",
|
||||
);
|
||||
|
||||
const select = container.querySelector("select") as HTMLSelectElement | null;
|
||||
const selects = Array.from(container.querySelectorAll("select"));
|
||||
const modeSelect = selects.find((el) =>
|
||||
Array.from(el.options).some((opt) => opt.textContent?.trim() === "token"),
|
||||
) as HTMLSelectElement | undefined;
|
||||
expect(modeSelect).not.toBeUndefined();
|
||||
if (!modeSelect) return;
|
||||
const tokenOption = Array.from(modeSelect.options).find(
|
||||
(opt) => opt.textContent?.trim() === "token",
|
||||
);
|
||||
expect(tokenOption).not.toBeUndefined();
|
||||
if (!tokenOption) return;
|
||||
modeSelect.value = tokenOption.value;
|
||||
modeSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
const tokenButton = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>(".cfg-segmented__btn"),
|
||||
).find((btn) => btn.textContent?.trim() === "token");
|
||||
expect(tokenButton).not.toBeUndefined();
|
||||
tokenButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["mode"], "token");
|
||||
|
||||
const checkbox = container.querySelector(
|
||||
@@ -110,16 +101,16 @@ describe("config form renderer", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const addButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Add",
|
||||
);
|
||||
const addButton = container.querySelector(
|
||||
".cfg-array__add",
|
||||
) as HTMLButtonElement | null;
|
||||
expect(addButton).not.toBeUndefined();
|
||||
addButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], ["+1", ""]);
|
||||
|
||||
const removeButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Remove",
|
||||
);
|
||||
const removeButton = container.querySelector(
|
||||
".cfg-array__item-remove",
|
||||
) as HTMLButtonElement | null;
|
||||
expect(removeButton).not.toBeUndefined();
|
||||
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["allowFrom"], []);
|
||||
@@ -140,19 +131,11 @@ describe("config form renderer", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const selects = Array.from(container.querySelectorAll("select"));
|
||||
const bindSelect = selects.find((el) =>
|
||||
Array.from(el.options).some((opt) => opt.textContent?.trim() === "tailnet"),
|
||||
) as HTMLSelectElement | undefined;
|
||||
expect(bindSelect).not.toBeUndefined();
|
||||
if (!bindSelect) return;
|
||||
const tailnetOption = Array.from(bindSelect.options).find(
|
||||
(opt) => opt.textContent?.trim() === "tailnet",
|
||||
);
|
||||
expect(tailnetOption).not.toBeUndefined();
|
||||
if (!tailnetOption) return;
|
||||
bindSelect.value = tailnetOption.value;
|
||||
bindSelect.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
const tailnetButton = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>(".cfg-segmented__btn"),
|
||||
).find((btn) => btn.textContent?.trim() === "tailnet");
|
||||
expect(tailnetButton).not.toBeUndefined();
|
||||
tailnetButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["bind"], "tailnet");
|
||||
});
|
||||
|
||||
@@ -182,9 +165,9 @@ describe("config form renderer", () => {
|
||||
container,
|
||||
);
|
||||
|
||||
const removeButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(btn) => btn.textContent?.trim() === "Remove",
|
||||
);
|
||||
const removeButton = container.querySelector(
|
||||
".cfg-map__item-remove",
|
||||
) as HTMLButtonElement | null;
|
||||
expect(removeButton).not.toBeUndefined();
|
||||
removeButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
expect(onPatch).toHaveBeenCalledWith(["slack"], {});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { guard } from "lit/directives/guard.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import type { SessionsListResult } from "../types";
|
||||
import type { ChatQueueItem } from "../ui-types";
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
normalizeMessage,
|
||||
normalizeRoleForGrouping,
|
||||
} from "../chat/message-normalizer";
|
||||
import { extractTextCached } from "../chat/message-extract";
|
||||
import {
|
||||
renderMessageGroup,
|
||||
renderReadingIndicatorGroup,
|
||||
@@ -115,56 +113,41 @@ export function renderChat(props: ChatProps) {
|
||||
|
||||
const splitRatio = props.splitRatio ?? 0.6;
|
||||
const sidebarOpen = Boolean(props.sidebarOpen && props.onCloseSidebar);
|
||||
const thread = guard(
|
||||
[
|
||||
props.loading,
|
||||
props.messages,
|
||||
props.toolMessages,
|
||||
props.stream,
|
||||
props.streamStartedAt,
|
||||
props.sessionKey,
|
||||
props.showThinking,
|
||||
reasoningLevel,
|
||||
props.assistantName,
|
||||
props.assistantAvatar,
|
||||
props.assistantAvatarUrl,
|
||||
],
|
||||
() => html`
|
||||
<div
|
||||
class="chat-thread"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
@scroll=${props.onChatScroll}
|
||||
>
|
||||
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
||||
${repeat(buildChatItems(props), (item) => item.key, (item) => {
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(assistantIdentity);
|
||||
}
|
||||
const thread = html`
|
||||
<div
|
||||
class="chat-thread"
|
||||
role="log"
|
||||
aria-live="polite"
|
||||
@scroll=${props.onChatScroll}
|
||||
>
|
||||
${props.loading ? html`<div class="muted">Loading chat…</div>` : nothing}
|
||||
${repeat(buildChatItems(props), (item) => item.key, (item) => {
|
||||
if (item.kind === "reading-indicator") {
|
||||
return renderReadingIndicatorGroup(assistantIdentity);
|
||||
}
|
||||
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
);
|
||||
}
|
||||
if (item.kind === "stream") {
|
||||
return renderStreamingGroup(
|
||||
item.text,
|
||||
item.startedAt,
|
||||
props.onOpenSidebar,
|
||||
assistantIdentity,
|
||||
);
|
||||
}
|
||||
|
||||
if (item.kind === "group") {
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
});
|
||||
}
|
||||
if (item.kind === "group") {
|
||||
return renderMessageGroup(item, {
|
||||
onOpenSidebar: props.onOpenSidebar,
|
||||
showReasoning,
|
||||
assistantName: props.assistantName,
|
||||
assistantAvatar: assistantIdentity.avatar,
|
||||
});
|
||||
}
|
||||
|
||||
return nothing;
|
||||
})}
|
||||
</div>
|
||||
`,
|
||||
);
|
||||
return nothing;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<section class="card chat">
|
||||
@@ -395,27 +378,6 @@ function messageKey(message: unknown, index: number): string {
|
||||
if (messageId) return `msg:${messageId}`;
|
||||
const timestamp = typeof m.timestamp === "number" ? m.timestamp : null;
|
||||
const role = typeof m.role === "string" ? m.role : "unknown";
|
||||
const fingerprint =
|
||||
extractTextCached(message) ??
|
||||
(typeof m.content === "string" ? m.content : null);
|
||||
const seed = fingerprint ?? safeJson(message) ?? String(index);
|
||||
const hash = fnv1a(seed);
|
||||
return timestamp ? `msg:${role}:${timestamp}:${hash}` : `msg:${role}:${hash}`;
|
||||
}
|
||||
|
||||
function safeJson(value: unknown): string | null {
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fnv1a(input: string): string {
|
||||
let hash = 0x811c9dc5;
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
hash ^= input.charCodeAt(i);
|
||||
hash = Math.imul(hash, 0x01000193);
|
||||
}
|
||||
return (hash >>> 0).toString(36);
|
||||
if (timestamp != null) return `msg:${role}:${timestamp}:${index}`;
|
||||
return `msg:${role}:${index}`;
|
||||
}
|
||||
|
||||
@@ -154,32 +154,24 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
const activeSection = props.activeSection;
|
||||
const activeSubsection = props.activeSubsection ?? null;
|
||||
|
||||
// Filter and sort entries
|
||||
let entries = Object.entries(properties);
|
||||
|
||||
// Filter by active section
|
||||
if (activeSection) {
|
||||
entries = entries.filter(([key]) => key === activeSection);
|
||||
}
|
||||
|
||||
// Filter by search
|
||||
if (searchQuery) {
|
||||
entries = entries.filter(([key, node]) => matchesSearch(key, node, searchQuery));
|
||||
}
|
||||
|
||||
// Sort by hint order, then alphabetically
|
||||
entries.sort((a, b) => {
|
||||
const entries = Object.entries(properties).sort((a, b) => {
|
||||
const orderA = hintForPath([a[0]], props.uiHints)?.order ?? 50;
|
||||
const orderB = hintForPath([b[0]], props.uiHints)?.order ?? 50;
|
||||
if (orderA !== orderB) return orderA - orderB;
|
||||
return a[0].localeCompare(b[0]);
|
||||
});
|
||||
|
||||
const filteredEntries = entries.filter(([key, node]) => {
|
||||
if (activeSection && key !== activeSection) return false;
|
||||
if (searchQuery && !matchesSearch(key, node, searchQuery)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
let subsectionContext:
|
||||
| { sectionKey: string; subsectionKey: string; schema: JsonSchema }
|
||||
| null = null;
|
||||
if (activeSection && activeSubsection && entries.length === 1) {
|
||||
const sectionSchema = entries[0]?.[1];
|
||||
if (activeSection && activeSubsection && filteredEntries.length === 1) {
|
||||
const sectionSchema = filteredEntries[0]?.[1];
|
||||
if (
|
||||
sectionSchema &&
|
||||
schemaType(sectionSchema) === "object" &&
|
||||
@@ -194,7 +186,7 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
if (filteredEntries.length === 0) {
|
||||
return html`
|
||||
<div class="config-empty">
|
||||
<div class="config-empty__icon">🔍</div>
|
||||
@@ -247,7 +239,7 @@ export function renderConfigForm(props: ConfigFormProps) {
|
||||
</section>
|
||||
`;
|
||||
})()
|
||||
: entries.map(([key, node]) => {
|
||||
: filteredEntries.map(([key, node]) => {
|
||||
const meta = SECTION_META[key] ?? {
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
description: node.description ?? "",
|
||||
|
||||
@@ -67,53 +67,37 @@ describe("config view", () => {
|
||||
expect(saveButton?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("applies MiniMax preset via onRawChange + onFormPatch", () => {
|
||||
it("switches mode via the sidebar toggle", () => {
|
||||
const container = document.createElement("div");
|
||||
const onRawChange = vi.fn();
|
||||
const onFormPatch = vi.fn();
|
||||
const onFormModeChange = vi.fn();
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
onRawChange,
|
||||
onFormPatch,
|
||||
onFormModeChange,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.includes("MiniMax M2.1"),
|
||||
b.textContent?.trim() === "Raw",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(btn).toBeTruthy();
|
||||
btn?.click();
|
||||
|
||||
expect(onRawChange).toHaveBeenCalled();
|
||||
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||
expect(raw).toContain("https://api.minimax.io/anthropic");
|
||||
expect(raw).toContain("anthropic-messages");
|
||||
expect(raw).toContain("minimax/MiniMax-M2.1");
|
||||
expect(raw).toContain("MINIMAX_API_KEY");
|
||||
|
||||
expect(onFormPatch).toHaveBeenCalledWith(
|
||||
["agents", "defaults", "model", "primary"],
|
||||
"minimax/MiniMax-M2.1",
|
||||
);
|
||||
expect(onFormModeChange).toHaveBeenCalledWith("raw");
|
||||
});
|
||||
|
||||
it("does not clobber existing MiniMax apiKey when applying preset", () => {
|
||||
it("switches sections from the sidebar", () => {
|
||||
const container = document.createElement("div");
|
||||
const onRawChange = vi.fn();
|
||||
const onSectionChange = vi.fn();
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
onRawChange,
|
||||
formValue: {
|
||||
models: {
|
||||
mode: "merge",
|
||||
providers: {
|
||||
minimax: {
|
||||
apiKey: "EXISTING_KEY",
|
||||
},
|
||||
},
|
||||
onSectionChange,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
gateway: { type: "object", properties: {} },
|
||||
agents: { type: "object", properties: {} },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -121,75 +105,31 @@ describe("config view", () => {
|
||||
);
|
||||
|
||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.includes("MiniMax M2.1"),
|
||||
b.textContent?.trim() === "Gateway",
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(btn).toBeTruthy();
|
||||
btn?.click();
|
||||
|
||||
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||
expect(raw).toContain("EXISTING_KEY");
|
||||
expect(onSectionChange).toHaveBeenCalledWith("gateway");
|
||||
});
|
||||
|
||||
it("applies Z.AI (GLM 4.7) preset", () => {
|
||||
it("wires search input to onSearchChange", () => {
|
||||
const container = document.createElement("div");
|
||||
const onRawChange = vi.fn();
|
||||
const onFormPatch = vi.fn();
|
||||
const onSearchChange = vi.fn();
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
onRawChange,
|
||||
onFormPatch,
|
||||
onSearchChange,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.includes("GLM 4.7"),
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(btn).toBeTruthy();
|
||||
btn?.click();
|
||||
|
||||
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||
expect(raw).toContain("zai/glm-4.7");
|
||||
expect(raw).toContain("ZAI_API_KEY");
|
||||
expect(onFormPatch).toHaveBeenCalledWith(
|
||||
["agents", "defaults", "model", "primary"],
|
||||
"zai/glm-4.7",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies Moonshot (Kimi) preset", () => {
|
||||
const container = document.createElement("div");
|
||||
const onRawChange = vi.fn();
|
||||
const onFormPatch = vi.fn();
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
onRawChange,
|
||||
onFormPatch,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const btn = Array.from(container.querySelectorAll("button")).find((b) =>
|
||||
b.textContent?.includes("Kimi"),
|
||||
) as HTMLButtonElement | undefined;
|
||||
expect(btn).toBeTruthy();
|
||||
btn?.click();
|
||||
|
||||
const raw = String(onRawChange.mock.calls.at(-1)?.[0] ?? "");
|
||||
expect(raw).toContain("https://api.moonshot.ai/v1");
|
||||
expect(raw).toContain("moonshot/kimi-k2-0905-preview");
|
||||
expect(raw).toContain("moonshot/kimi-k2-turbo-preview");
|
||||
expect(raw).toContain("moonshot/kimi-k2-thinking");
|
||||
expect(raw).toContain("moonshot/kimi-k2-thinking-turbo");
|
||||
expect(raw).toContain("Kimi K2 Turbo");
|
||||
expect(raw).toContain("Kimi K2 Thinking");
|
||||
expect(raw).toContain("Kimi K2 Thinking Turbo");
|
||||
expect(raw).toContain("MOONSHOT_API_KEY");
|
||||
expect(onFormPatch).toHaveBeenCalledWith(
|
||||
["agents", "defaults", "model", "primary"],
|
||||
"moonshot/kimi-k2-0905-preview",
|
||||
);
|
||||
const input = container.querySelector(
|
||||
".config-search__input",
|
||||
) as HTMLInputElement | null;
|
||||
expect(input).not.toBeNull();
|
||||
if (!input) return;
|
||||
input.value = "gateway";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
expect(onSearchChange).toHaveBeenCalledWith("gateway");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user