1307 lines
46 KiB
TypeScript
1307 lines
46 KiB
TypeScript
import {
|
|
act,
|
|
fireEvent,
|
|
render,
|
|
screen,
|
|
waitFor,
|
|
within,
|
|
} from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type {
|
|
Message,
|
|
Model,
|
|
QuickPaneSettings,
|
|
RuntimeSnapshot,
|
|
UiState,
|
|
} from "../workspace/types";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
invoke: vi.fn(),
|
|
listen: vi.fn(),
|
|
onFocusChanged: vi.fn(),
|
|
startDragging: vi.fn(),
|
|
focusHandler: undefined as
|
|
| ((event: { payload: boolean }) => void)
|
|
| undefined,
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/api/core", () => ({ invoke: mocks.invoke }));
|
|
vi.mock("@tauri-apps/api/event", () => ({ listen: mocks.listen }));
|
|
vi.mock("@tauri-apps/api/window", () => ({
|
|
getCurrentWindow: () => ({
|
|
onFocusChanged: mocks.onFocusChanged,
|
|
startDragging: mocks.startDragging,
|
|
}),
|
|
}));
|
|
|
|
import QuickPane from "./QuickPane";
|
|
|
|
const settings: QuickPaneSettings = {
|
|
workspace: "/repo",
|
|
provider: "provider",
|
|
model: "quick/model",
|
|
followUpProvider: "provider",
|
|
followUpModel: "full/model",
|
|
instructions: "Stay read-only.",
|
|
maximumToolCalls: 6,
|
|
};
|
|
const uiState: UiState = {
|
|
version: 1,
|
|
directoryOrder: [],
|
|
runtimeOrderByDirectory: {},
|
|
drafts: {},
|
|
scroll: {},
|
|
lastSeen: {},
|
|
quickPane: settings,
|
|
};
|
|
const defaultModels: Model[] = [
|
|
{ provider: "provider", id: "quick/model", name: "Quick Model" },
|
|
];
|
|
|
|
function runtimeSnapshot(
|
|
models: Model[],
|
|
answer = "",
|
|
isStreaming = false,
|
|
runtime: RuntimeSnapshot["runtime"] = {
|
|
runtimeId: "unused",
|
|
worktreePath: "/repo",
|
|
state: "idle",
|
|
label: "Quick",
|
|
attention: false,
|
|
queueCount: 0,
|
|
lastActivity: "now",
|
|
openedAt: "now",
|
|
},
|
|
): RuntimeSnapshot {
|
|
return {
|
|
bridgeInstanceId: "bridge",
|
|
latestSeq: 1,
|
|
runtime,
|
|
models: { data: { models } },
|
|
state: { data: { isStreaming } },
|
|
transcript: answer
|
|
? { data: { messages: [{ role: "assistant", content: answer }] } }
|
|
: undefined,
|
|
extensions: [],
|
|
};
|
|
}
|
|
|
|
function snapshotWithTranscript(
|
|
messages: Message[],
|
|
isStreaming = false,
|
|
runtime: RuntimeSnapshot["runtime"] = runtimeSnapshot(defaultModels).runtime,
|
|
) {
|
|
const snapshot = runtimeSnapshot(defaultModels, "", isStreaming, runtime);
|
|
snapshot.transcript = { data: { messages } };
|
|
return snapshot;
|
|
}
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (value: T) => void;
|
|
let reject!: (reason?: unknown) => void;
|
|
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
resolve = resolvePromise;
|
|
reject = rejectPromise;
|
|
});
|
|
return { promise, resolve, reject };
|
|
}
|
|
|
|
function bridgeMock(
|
|
catalogs: Model[][],
|
|
activeSnapshot?: (
|
|
index: number,
|
|
) => RuntimeSnapshot | Promise<RuntimeSnapshot>,
|
|
) {
|
|
let runtimeCount = 0;
|
|
const submitted = new Set<string>();
|
|
const submittedPrompts = new Map<string, string>();
|
|
mocks.invoke.mockImplementation(
|
|
(
|
|
command: string,
|
|
payload?: { runtimeId?: string; agentId?: string; message?: string },
|
|
) => {
|
|
if (command === "load_ui_state") return Promise.resolve(uiState);
|
|
if (command === "create_quick_runtime") {
|
|
runtimeCount += 1;
|
|
return Promise.resolve({
|
|
runtime: {
|
|
agentId: `agent-${runtimeCount}`,
|
|
runtimeId: `runtime-${runtimeCount}`,
|
|
},
|
|
});
|
|
}
|
|
if (command === "submit_prompt") {
|
|
if (payload?.agentId) {
|
|
submitted.add(payload.agentId);
|
|
if (payload.message)
|
|
submittedPrompts.set(payload.agentId, payload.message);
|
|
}
|
|
return Promise.resolve(undefined);
|
|
}
|
|
if (command === "get_session_runtime_snapshot") {
|
|
const segments = payload?.runtimeId?.split("-") ?? [];
|
|
const index = Number(segments[segments.length - 1] ?? 1) - 1;
|
|
if (submitted.has(`agent-${index + 1}`) && activeSnapshot)
|
|
return Promise.resolve(activeSnapshot(index)).then((snapshot) => {
|
|
const messages = snapshot.transcript?.data?.messages;
|
|
const prompt = submittedPrompts.get(`agent-${index + 1}`);
|
|
if (
|
|
prompt &&
|
|
messages?.every((message) => message.role === "assistant")
|
|
)
|
|
messages.unshift({ role: "user", content: prompt });
|
|
return snapshot;
|
|
});
|
|
return Promise.resolve(
|
|
runtimeSnapshot(
|
|
catalogs[index] ?? catalogs[catalogs.length - 1] ?? [],
|
|
),
|
|
);
|
|
}
|
|
return Promise.resolve(undefined);
|
|
},
|
|
);
|
|
return { runtimeCount: () => runtimeCount };
|
|
}
|
|
|
|
function deferredCatalogBridge() {
|
|
const firstSnapshot = deferred<RuntimeSnapshot>();
|
|
const staleModels: Model[] = [
|
|
{ provider: "provider", id: "stale/model", name: "Stale" },
|
|
];
|
|
const freshModels: Model[] = [
|
|
{ provider: "provider", id: "fresh/model", name: "Fresh" },
|
|
];
|
|
let runtimeCount = 0;
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: { runtimeId?: string }) => {
|
|
if (command === "load_ui_state") return Promise.resolve(uiState);
|
|
if (command === "create_quick_runtime") {
|
|
runtimeCount += 1;
|
|
return Promise.resolve({
|
|
runtime: {
|
|
agentId: `agent-${runtimeCount}`,
|
|
runtimeId: `runtime-${runtimeCount}`,
|
|
},
|
|
});
|
|
}
|
|
if (command === "get_session_runtime_snapshot") {
|
|
if (payload?.runtimeId === "runtime-1") return firstSnapshot.promise;
|
|
return Promise.resolve(runtimeSnapshot(freshModels));
|
|
}
|
|
return Promise.resolve(undefined);
|
|
},
|
|
);
|
|
return {
|
|
firstSnapshot,
|
|
staleModels,
|
|
runtimeCount: () => runtimeCount,
|
|
};
|
|
}
|
|
|
|
async function preparedTrigger() {
|
|
return screen.findByRole("button", { name: /provider ·/i });
|
|
}
|
|
|
|
async function ask(text = "Inspect") {
|
|
const user = userEvent.setup();
|
|
const input = screen.getByRole("textbox", { name: "Request" });
|
|
await user.type(input, text);
|
|
await user.click(screen.getByRole("button", { name: "Send request" }));
|
|
}
|
|
|
|
describe("QuickPane", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.clearAllMocks();
|
|
mocks.focusHandler = undefined;
|
|
mocks.startDragging.mockResolvedValue(undefined);
|
|
mocks.listen.mockResolvedValue(vi.fn());
|
|
mocks.onFocusChanged.mockImplementation((handler) => {
|
|
mocks.focusHandler = handler;
|
|
return Promise.resolve(vi.fn());
|
|
});
|
|
});
|
|
|
|
it("renders toolbar before composer before exchange", async () => {
|
|
bridgeMock([defaultModels]);
|
|
const { container } = render(<QuickPane />);
|
|
await preparedTrigger();
|
|
const pane = container.querySelector(".quick-pane");
|
|
expect(
|
|
Array.from(pane?.children ?? []).map((element) => element.className),
|
|
).toEqual([
|
|
"quick-pane-drag-strip",
|
|
"quick-pane-toolbar",
|
|
"quick-pane-composer",
|
|
"quick-pane-exchange",
|
|
]);
|
|
});
|
|
|
|
it("starts native dragging only from the strip above toolbar controls", async () => {
|
|
bridgeMock([defaultModels]);
|
|
const { container } = render(<QuickPane />);
|
|
await preparedTrigger();
|
|
fireEvent.pointerDown(container.querySelector(".quick-pane-drag-strip")!);
|
|
expect(mocks.startDragging).toHaveBeenCalledTimes(1);
|
|
fireEvent.pointerDown(screen.getByRole("button", { name: "Close Quick Pane" }));
|
|
expect(mocks.startDragging).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("derives thinking choices from selected model metadata and falls back to Off", async () => {
|
|
const thinkingModels: Model[] = [
|
|
{
|
|
provider: "provider",
|
|
id: "quick/model",
|
|
name: "Quick Model",
|
|
thinkingLevelMap: { off: "off", high: "high" },
|
|
},
|
|
];
|
|
bridgeMock([thinkingModels], () => runtimeSnapshot(thinkingModels, "", true));
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
const thinking = screen.getByRole("combobox", { name: "Thinking" });
|
|
expect(within(thinking).getAllByRole("option").map((option) => option.textContent)).toEqual([
|
|
"Off",
|
|
"High",
|
|
]);
|
|
await userEvent.selectOptions(thinking, "high");
|
|
await ask("Inspect");
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith("set_thinking_level", {
|
|
agentId: "agent-1",
|
|
level: "high",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps Abort active-only and Renew idle after an exchange", async () => {
|
|
let streaming = true;
|
|
const tracker = bridgeMock([defaultModels, defaultModels], () =>
|
|
runtimeSnapshot(defaultModels, "Partial answer", streaming),
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
expect(screen.queryByRole("button", { name: "Abort" })).toBeNull();
|
|
expect(screen.queryByRole("button", { name: "Renew" })).toBeNull();
|
|
await ask("Inspect");
|
|
expect(await screen.findByRole("button", { name: "Abort" })).toBeEnabled();
|
|
await userEvent.click(screen.getByRole("button", { name: "Abort" }));
|
|
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
|
|
expect(mocks.invoke).toHaveBeenCalledWith("abort", { agentId: "agent-1" });
|
|
await waitFor(() =>
|
|
expect(screen.getByRole("button", { name: "Send request" })).toBeEnabled(),
|
|
);
|
|
|
|
streaming = false;
|
|
await ask("Again");
|
|
expect(await screen.findByRole("button", { name: "Renew" })).toBeEnabled();
|
|
});
|
|
|
|
it("reports Abort failures without replacing active chat", async () => {
|
|
bridgeMock([defaultModels], () =>
|
|
runtimeSnapshot(defaultModels, "Partial answer", true),
|
|
);
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
mocks.invoke.mockImplementation((command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "abort") return Promise.reject(new Error("abort unavailable"));
|
|
return baseImplementation?.(command, payload);
|
|
});
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Inspect");
|
|
await userEvent.click(await screen.findByRole("button", { name: "Abort" }));
|
|
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
"Could not abort quick request: Error: abort unavailable",
|
|
);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
expect(screen.getByRole("button", { name: "Abort" })).toBeEnabled();
|
|
});
|
|
|
|
it("keeps downward model menu inside the scroll-contained pane", async () => {
|
|
const user = userEvent.setup();
|
|
bridgeMock([defaultModels]);
|
|
const { container } = render(<QuickPane />);
|
|
const trigger = await preparedTrigger();
|
|
await user.click(trigger);
|
|
const menu = screen.getByRole("dialog", { name: "Choose model" });
|
|
const value = trigger.querySelector("span");
|
|
expect(value).toHaveTextContent("provider · Quick Model");
|
|
expect(trigger).toHaveClass("model-trigger");
|
|
expect(container.querySelector(".model-control-down .model-menu")).toBe(
|
|
menu,
|
|
);
|
|
expect(menu).toHaveClass("model-menu");
|
|
expect(
|
|
screen.getByRole("listbox", { name: "Available models" }),
|
|
).toHaveClass("model-results");
|
|
});
|
|
|
|
it("closes an open selector on first Escape and dismisses pane on second", async () => {
|
|
const user = userEvent.setup();
|
|
bridgeMock([defaultModels]);
|
|
render(<QuickPane />);
|
|
const trigger = await preparedTrigger();
|
|
await user.click(trigger);
|
|
await waitFor(() => expect(screen.getByRole("searchbox")).toHaveFocus());
|
|
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
expect(screen.queryByRole("dialog", { name: "Choose model" })).toBeNull();
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("dismiss_quick_pane");
|
|
await waitFor(() => expect(trigger).toHaveFocus());
|
|
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane"),
|
|
);
|
|
});
|
|
|
|
it("allows only one submission start while commands are pending", async () => {
|
|
const pendingModel = deferred<unknown>();
|
|
const tracker = bridgeMock([defaultModels]);
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "set_model") return pendingModel.promise;
|
|
return baseImplementation?.(command, payload);
|
|
},
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
const input = screen.getByRole("textbox", { name: "Request" });
|
|
fireEvent.change(input, { target: { value: "Inspect once" } });
|
|
const send = screen.getByRole("button", { name: "Send request" });
|
|
fireEvent.click(send);
|
|
fireEvent.click(send);
|
|
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(
|
|
mocks.invoke.mock.calls.filter(([command]) => command === "set_model"),
|
|
).toHaveLength(1);
|
|
expect(input).toHaveAttribute("readonly");
|
|
await act(async () => pendingModel.resolve(undefined));
|
|
});
|
|
|
|
it("keeps live assistant region stable through completion", async () => {
|
|
const pendingSnapshot = deferred<RuntimeSnapshot>();
|
|
bridgeMock([defaultModels], () => pendingSnapshot.promise);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Inspect this");
|
|
|
|
const response = screen.getByRole("region", { name: "Assistant response" });
|
|
expect(response).toHaveAttribute("aria-live", "polite");
|
|
expect(response).toHaveAttribute("aria-busy", "true");
|
|
expect(response.querySelector(".quick-pane-activity")).toBeInTheDocument();
|
|
|
|
await act(async () =>
|
|
pendingSnapshot.resolve(
|
|
runtimeSnapshot(defaultModels, "Final answer", false),
|
|
),
|
|
);
|
|
expect(await screen.findByText("Final answer")).toBeInTheDocument();
|
|
expect(screen.getByRole("region", { name: "Assistant response" })).toBe(
|
|
response,
|
|
);
|
|
expect(response).toHaveAttribute("aria-busy", "false");
|
|
expect(
|
|
within(response).getByText("Response complete."),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("clears only submitted draft and restores focus after completion", async () => {
|
|
let current = runtimeSnapshot(defaultModels, "Partial answer", true);
|
|
bridgeMock([defaultModels], () => current);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
const input = screen.getByRole("textbox", { name: "Request" });
|
|
await ask("Inspect this");
|
|
expect(await screen.findByText("Partial answer")).toBeInTheDocument();
|
|
expect(input).toHaveValue("Inspect this");
|
|
expect(input).toHaveAttribute("readonly");
|
|
|
|
current = runtimeSnapshot(defaultModels, "Final answer", false);
|
|
expect(
|
|
await screen.findByText("Final answer", {}, { timeout: 2_000 }),
|
|
).toBeInTheDocument();
|
|
await waitFor(() => expect(input).toHaveValue(""));
|
|
expect(input).not.toHaveAttribute("readonly");
|
|
expect(input).toHaveFocus();
|
|
await userEvent.type(input, "Next draft");
|
|
await new Promise((resolve) => window.setTimeout(resolve, 850));
|
|
expect(input).toHaveValue("Next draft");
|
|
});
|
|
|
|
it("shows manual continuation after a completed normal answer and hands off transcript", async () => {
|
|
bridgeMock([defaultModels], () =>
|
|
snapshotWithTranscript([
|
|
{ role: "user", content: "Inspect" },
|
|
{ role: "assistant", content: "Completed answer" },
|
|
]),
|
|
);
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "create_session_runtime")
|
|
return Promise.resolve({
|
|
runtime: { agentId: "agent-full", runtimeId: "runtime-full" },
|
|
});
|
|
return baseImplementation?.(command, payload);
|
|
},
|
|
);
|
|
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Inspect");
|
|
const continueButton = await screen.findByRole("button", {
|
|
name: "Continue in full session",
|
|
});
|
|
expect(continueButton.closest(".quick-pane-assistant-footer")).not.toBeNull();
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith(
|
|
"create_session_runtime",
|
|
expect.anything(),
|
|
);
|
|
|
|
await userEvent.click(continueButton);
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
|
|
agentId: "agent-full",
|
|
message:
|
|
"Inspect\n\nQuick Pane handoff: Continue this chat in a full session.\n\nQuick Pane transcript:\nUser: Inspect\n\nAssistant: Completed answer",
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("keeps unlimited follow-ups on one runtime and renders snapshot messages in order", async () => {
|
|
let turn = 0;
|
|
const tracker = bridgeMock([defaultModels], () => {
|
|
const messages: Message[] = [
|
|
{ role: "user", content: "**First request**" },
|
|
{
|
|
role: "assistant",
|
|
content: [
|
|
{ type: "thinking", thinking: "private reasoning" },
|
|
{ type: "text", text: "First answer" },
|
|
{ type: "toolCall", name: "read" },
|
|
],
|
|
},
|
|
{ role: "toolResult", content: "private tool output" },
|
|
];
|
|
if (turn > 1)
|
|
messages.push(
|
|
{ role: "user", content: "Second request" },
|
|
{ role: "assistant", content: "## Second answer" },
|
|
);
|
|
return snapshotWithTranscript(messages);
|
|
});
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "submit_prompt") turn += 1;
|
|
return baseImplementation?.(command, payload);
|
|
},
|
|
);
|
|
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
const input = screen.getByRole("textbox", { name: "Request" });
|
|
await ask("First");
|
|
expect(await screen.findByText("First answer")).toBeInTheDocument();
|
|
await waitFor(() => expect(input).toHaveValue(""));
|
|
await ask("Second");
|
|
expect(
|
|
await screen.findByRole("heading", { name: "Second answer" }),
|
|
).toBeInTheDocument();
|
|
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(
|
|
mocks.invoke.mock.calls.filter(
|
|
([command]) => command === "submit_prompt",
|
|
),
|
|
).toEqual([
|
|
["submit_prompt", expect.objectContaining({ agentId: "agent-1" })],
|
|
["submit_prompt", expect.objectContaining({ agentId: "agent-1" })],
|
|
]);
|
|
expect(screen.queryByText("private reasoning")).toBeNull();
|
|
expect(screen.queryByText("private tool output")).toBeNull();
|
|
expect(
|
|
screen.queryByRole("combobox", { name: "Answer history" }),
|
|
).toBeNull();
|
|
const cards = screen
|
|
.getByRole("region", { name: "Assistant response" })
|
|
.querySelectorAll("article");
|
|
expect(Array.from(cards).map((card) => card.textContent)).toEqual([
|
|
"YouFirst request",
|
|
"AssistantFirst answer",
|
|
"YouSecond request",
|
|
"AssistantSecond answerContinue in full session",
|
|
]);
|
|
});
|
|
|
|
it("does not use prior-turn answer after follow-up runtime failure", async () => {
|
|
const submittedPrompts: string[] = [];
|
|
bridgeMock([defaultModels], () => {
|
|
const messages: Message[] = [
|
|
{ role: "user", content: submittedPrompts[0] ?? "" },
|
|
{ role: "assistant", content: "First answer" },
|
|
];
|
|
if (submittedPrompts[1])
|
|
messages.push({ role: "user", content: submittedPrompts[1] });
|
|
return snapshotWithTranscript(
|
|
messages,
|
|
false,
|
|
submittedPrompts[1]
|
|
? {
|
|
...runtimeSnapshot(defaultModels).runtime,
|
|
error: { message: "Second runtime failed." },
|
|
}
|
|
: undefined,
|
|
);
|
|
});
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "create_session_runtime")
|
|
return Promise.resolve({
|
|
runtime: { agentId: "agent-full", runtimeId: "runtime-full" },
|
|
});
|
|
if (command === "submit_prompt" && payload?.agentId === "agent-1")
|
|
submittedPrompts.push(String(payload.message));
|
|
return baseImplementation?.(command, payload);
|
|
},
|
|
);
|
|
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("First");
|
|
expect(await screen.findByText("First answer")).toBeInTheDocument();
|
|
await waitFor(() =>
|
|
expect(screen.getByRole("textbox", { name: "Request" })).toHaveValue(""),
|
|
);
|
|
|
|
await ask("Second");
|
|
expect(
|
|
await screen.findByText("Second runtime failed."),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("First answer")).toBeInTheDocument();
|
|
await userEvent.click(screen.getByRole("button", { name: "Continue" }));
|
|
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith("create_session_runtime", {
|
|
worktreePath: "/repo",
|
|
}),
|
|
);
|
|
expect(mocks.invoke).toHaveBeenCalledWith("submit_prompt", {
|
|
agentId: "agent-full",
|
|
message:
|
|
"Second\n\nQuick Pane handoff: Quick runtime failed. Continue in a full session if needed.\n\nQuick Pane transcript:\nUser: First\n\nAssistant: First answer\n\nUser: Second",
|
|
});
|
|
});
|
|
|
|
it("maps the exact submitted payload to the entered multiline request despite delimiter collisions", async () => {
|
|
const supplementalInstructions =
|
|
"Stay read-only.\n\nUser request: supplemental collision.";
|
|
const enteredRequest =
|
|
"First line\n\nUser request: request collision.\nThird **bold** line";
|
|
const collisionState: UiState = {
|
|
...uiState,
|
|
quickPane: { ...settings, instructions: supplementalInstructions },
|
|
};
|
|
let reflectedPrompt = "";
|
|
bridgeMock([defaultModels], () =>
|
|
snapshotWithTranscript([
|
|
{ role: "user", content: "Normal earlier user text" },
|
|
{
|
|
role: "user",
|
|
content: "[Quick Pane advisory mode: unknown reflected prompt]",
|
|
},
|
|
{ role: "user", content: reflectedPrompt },
|
|
{ role: "assistant", content: "Done" },
|
|
]),
|
|
);
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "load_ui_state") return Promise.resolve(collisionState);
|
|
if (command === "submit_prompt")
|
|
reflectedPrompt = String(payload?.message ?? "");
|
|
return baseImplementation?.(command, payload);
|
|
},
|
|
);
|
|
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
|
|
target: { value: enteredRequest },
|
|
});
|
|
await userEvent.click(screen.getByRole("button", { name: "Send request" }));
|
|
|
|
const expectedPayload = `[Quick Pane advisory mode: use at most 6 read-only tool calls. Do not use subagents. Do not mutate files or system state. If request needs mutation, multi-step work, subagents, or more tool calls, start final response with [ESCALATE] and explain why.]\n${supplementalInstructions}\n\nUser request: ${enteredRequest}`;
|
|
await waitFor(() => expect(reflectedPrompt).toBe(expectedPayload));
|
|
await screen.findByText("Done");
|
|
const userArticles = document.querySelectorAll(".quick-pane-user-message");
|
|
expect(userArticles).toHaveLength(2);
|
|
expect(userArticles[0]).toHaveTextContent("YouNormal earlier user text");
|
|
expect(userArticles[1]).toHaveTextContent(
|
|
/YouFirst line\s+User request: request collision\.\s+Third bold line/,
|
|
);
|
|
expect(screen.queryByText(/unknown reflected prompt/)).toBeNull();
|
|
expect(userArticles[1]).not.toHaveTextContent("supplemental collision");
|
|
expect(userArticles[1]).not.toHaveTextContent("Quick Pane advisory mode");
|
|
});
|
|
|
|
it("keeps runtime errors and extension requests visible", async () => {
|
|
let failed = false;
|
|
bridgeMock([defaultModels], () => {
|
|
const snapshot = snapshotWithTranscript(
|
|
[
|
|
{ role: "user", content: "Inspect" },
|
|
{ role: "assistant", content: "Partial answer" },
|
|
],
|
|
true,
|
|
{
|
|
...runtimeSnapshot(defaultModels).runtime,
|
|
activeTool: "functions.read",
|
|
error: failed ? { message: "Runtime disconnected." } : undefined,
|
|
},
|
|
);
|
|
snapshot.extensions = [
|
|
{
|
|
id: "permission-1",
|
|
method: "confirm",
|
|
title: "Run guarded command",
|
|
message: "Approval needed",
|
|
},
|
|
];
|
|
return snapshot;
|
|
});
|
|
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Inspect");
|
|
expect(
|
|
await screen.findByRole("dialog", { name: "Review request" }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("Partial answer")).toBeInTheDocument();
|
|
|
|
failed = true;
|
|
expect(
|
|
await screen.findByText("Runtime disconnected.", {}, { timeout: 2_000 }),
|
|
).toBeInTheDocument();
|
|
expect(
|
|
screen.getByRole("dialog", { name: "Review request" }),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("Partial answer")).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders sanitized GFM Markdown and reports active tools", async () => {
|
|
let current = runtimeSnapshot(defaultModels, "", true, {
|
|
...runtimeSnapshot(defaultModels).runtime,
|
|
activeTool: "functions.grep",
|
|
});
|
|
bridgeMock([defaultModels], () => current);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Explain output");
|
|
expect(
|
|
await screen.findByText("Searching files", {
|
|
selector: ".quick-pane-activity > span:last-child",
|
|
}),
|
|
).toBeInTheDocument();
|
|
|
|
current = runtimeSnapshot(
|
|
defaultModels,
|
|
"## Result\n\n- item\n\n`const safe = true`\n\n[unsafe](javascript:alert(1))\n\n<img src=x onerror=alert(1)>",
|
|
false,
|
|
);
|
|
expect(
|
|
await screen.findByRole(
|
|
"heading",
|
|
{ name: "Result" },
|
|
{ timeout: 2_000 },
|
|
),
|
|
).toBeInTheDocument();
|
|
expect(screen.getByText("const safe = true")).toBeInTheDocument();
|
|
expect(screen.getByText("unsafe").closest("a")).toHaveAttribute("href", "");
|
|
expect(document.querySelector("img")).toBeNull();
|
|
});
|
|
|
|
it("suppresses escalation prefixes until content and completion are ready", async () => {
|
|
let current = runtimeSnapshot(defaultModels, "", true);
|
|
bridgeMock([defaultModels], () => current);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Change files");
|
|
const response = screen.getByRole("region", { name: "Assistant response" });
|
|
|
|
for (const prefix of ["[", "[ESCAL", "[ESCALATE]"]) {
|
|
current = runtimeSnapshot(defaultModels, prefix, true);
|
|
await new Promise((resolve) => window.setTimeout(resolve, 820));
|
|
expect(response).not.toHaveTextContent(prefix);
|
|
expect(screen.queryByText("Continue in full session")).toBeNull();
|
|
}
|
|
current = runtimeSnapshot(
|
|
defaultModels,
|
|
"[ESCALATE]Needs full session.",
|
|
true,
|
|
);
|
|
expect(
|
|
await within(response).findByText(
|
|
"Needs full session.",
|
|
{},
|
|
{ timeout: 2_000 },
|
|
),
|
|
).toBeInTheDocument();
|
|
expect(screen.queryByText("Continue in full session")).toBeNull();
|
|
|
|
current = runtimeSnapshot(
|
|
defaultModels,
|
|
"[ESCALATE]Needs full session.",
|
|
false,
|
|
);
|
|
expect(
|
|
await screen.findByText(
|
|
"Continue in full session",
|
|
{},
|
|
{ timeout: 2_000 },
|
|
),
|
|
).toBeInTheDocument();
|
|
expect(response).not.toHaveTextContent("[ESCALATE]");
|
|
}, 10_000);
|
|
|
|
it("does not render or retain a completed partial escalation marker", async () => {
|
|
let current = runtimeSnapshot(defaultModels, "", true);
|
|
bridgeMock([defaultModels], () => current);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Change files");
|
|
|
|
current = runtimeSnapshot(defaultModels, "[ESCAL", false);
|
|
await waitFor(
|
|
() =>
|
|
expect(
|
|
screen.getByRole("textbox", { name: "Request" }),
|
|
).not.toHaveAttribute("readonly"),
|
|
{ timeout: 2_000 },
|
|
);
|
|
const response = screen.getByRole("region", { name: "Assistant response" });
|
|
expect(response).not.toHaveTextContent("[ESCAL");
|
|
expect(
|
|
screen.queryByRole("combobox", { name: "Answer history" }),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("uses Enter to send and leaves Shift+Enter for a newline", async () => {
|
|
const tracker = bridgeMock([defaultModels], () =>
|
|
runtimeSnapshot(defaultModels, "", true),
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
const input = screen.getByRole("textbox", { name: "Request" });
|
|
fireEvent.change(input, { target: { value: "Inspect" } });
|
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: true });
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith(
|
|
"submit_prompt",
|
|
expect.anything(),
|
|
);
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith(
|
|
"submit_prompt",
|
|
expect.objectContaining({ agentId: "agent-1" }),
|
|
),
|
|
);
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
});
|
|
|
|
it("routes Close and pane-level Escape only through native dismissal", async () => {
|
|
bridgeMock([[]]);
|
|
render(<QuickPane />);
|
|
await screen.findByText("No models are currently available.");
|
|
await userEvent.click(
|
|
screen.getByRole("button", { name: "Close Quick Pane" }),
|
|
);
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
await waitFor(() =>
|
|
expect(
|
|
mocks.invoke.mock.calls.filter(
|
|
([command]) => command === "dismiss_quick_pane",
|
|
),
|
|
).toHaveLength(2),
|
|
);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
});
|
|
|
|
it("retries startup settings loading from the visible error action", async () => {
|
|
let loadAttempts = 0;
|
|
mocks.invoke.mockImplementation((command: string) => {
|
|
if (command === "load_ui_state") {
|
|
loadAttempts += 1;
|
|
return loadAttempts === 1
|
|
? Promise.reject(new Error("state unavailable"))
|
|
: Promise.resolve(uiState);
|
|
}
|
|
if (command === "create_quick_runtime")
|
|
return Promise.resolve({
|
|
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
|
|
});
|
|
if (command === "get_session_runtime_snapshot")
|
|
return Promise.resolve(runtimeSnapshot(defaultModels));
|
|
return Promise.resolve(undefined);
|
|
});
|
|
|
|
render(<QuickPane />);
|
|
const alert = await screen.findByRole("alert");
|
|
expect(alert).toHaveTextContent(
|
|
"Could not load settings: Error: state unavailable",
|
|
);
|
|
await userEvent.click(within(alert).getByRole("button", { name: "Retry" }));
|
|
|
|
expect(await preparedTrigger()).toBeInTheDocument();
|
|
expect(loadAttempts).toBe(2);
|
|
expect(screen.queryByText(/Could not load settings/)).toBeNull();
|
|
});
|
|
|
|
it("retries catalog preparation visibly after failed-runtime cleanup", async () => {
|
|
let attempts = 0;
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: { runtimeId?: string }) => {
|
|
if (command === "load_ui_state") return Promise.resolve(uiState);
|
|
if (command === "create_quick_runtime") {
|
|
attempts += 1;
|
|
return Promise.resolve({
|
|
runtime: {
|
|
agentId: `agent-${attempts}`,
|
|
runtimeId: `runtime-${attempts}`,
|
|
},
|
|
});
|
|
}
|
|
if (command === "get_session_runtime_snapshot") {
|
|
if (payload?.runtimeId === "runtime-1")
|
|
return Promise.reject(new Error("offline"));
|
|
return Promise.resolve(runtimeSnapshot(defaultModels));
|
|
}
|
|
return Promise.resolve(undefined);
|
|
},
|
|
);
|
|
render(<QuickPane />);
|
|
const alert = await screen.findByRole("alert");
|
|
expect(alert).toHaveTextContent(
|
|
"Could not prepare model catalog: Error: offline",
|
|
);
|
|
expect(within(alert).getByRole("button", { name: "Retry" })).toBeVisible();
|
|
expect(screen.getByRole("button", { name: /model/i })).toBeDisabled();
|
|
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
|
|
target: { value: "Blocked until ready" },
|
|
});
|
|
expect(screen.getByRole("button", { name: "Send request" })).toBeDisabled();
|
|
|
|
await userEvent.click(within(alert).getByRole("button", { name: "Retry" }));
|
|
expect(await preparedTrigger()).toBeInTheDocument();
|
|
expect(screen.queryByText(/Could not prepare model catalog/)).toBeNull();
|
|
const closeIndex = mocks.invoke.mock.calls.findIndex(
|
|
([command, payload]) =>
|
|
command === "close_quick_runtime" && payload?.runtimeId === "runtime-1",
|
|
);
|
|
const retryCreateIndex = mocks.invoke.mock.calls.findIndex(
|
|
([command], index) =>
|
|
command === "create_quick_runtime" && index > closeIndex,
|
|
);
|
|
expect(closeIndex).toBeGreaterThan(-1);
|
|
expect(retryCreateIndex).toBeGreaterThan(closeIndex);
|
|
});
|
|
});
|
|
|
|
describe("QuickPane fresh runtime catalog", () => {
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
vi.clearAllMocks();
|
|
mocks.listen.mockResolvedValue(vi.fn());
|
|
mocks.onFocusChanged.mockImplementation((handler) => {
|
|
mocks.focusHandler = handler;
|
|
return Promise.resolve(vi.fn());
|
|
});
|
|
});
|
|
|
|
it("uses snapshot catalog, removes stale model, and submits on that exact prepared runtime", async () => {
|
|
const tracker = bridgeMock([
|
|
[{ provider: "provider", id: "new/model", name: "New Model" }],
|
|
]);
|
|
render(<QuickPane />);
|
|
const trigger = await preparedTrigger();
|
|
expect(trigger).toHaveAccessibleName(/provider · New Model/i);
|
|
expect(
|
|
screen.getByText("Selected model unavailable; using provider/new/model."),
|
|
).toBeInTheDocument();
|
|
await ask();
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith("set_model", {
|
|
agentId: "agent-1",
|
|
provider: "provider",
|
|
modelId: "new/model",
|
|
}),
|
|
);
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("get_model_catalog");
|
|
});
|
|
|
|
it("preserves prepared runtime and catalog when focus changes during active request", async () => {
|
|
const tracker = bridgeMock([defaultModels]);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask();
|
|
await act(async () => mocks.focusHandler?.({ payload: false }));
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
});
|
|
|
|
it("preserves idle runtime, transcript, and catalog when hidden and reopened within three minutes", async () => {
|
|
let now = 1_000_000;
|
|
vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
const tracker = bridgeMock([defaultModels], () =>
|
|
snapshotWithTranscript([
|
|
{ role: "user", content: "Question" },
|
|
{ role: "assistant", content: "Remembered answer" },
|
|
]),
|
|
);
|
|
render(<QuickPane />);
|
|
const trigger = await preparedTrigger();
|
|
await ask("Question");
|
|
expect(await screen.findByText("Remembered answer")).toBeInTheDocument();
|
|
await waitFor(() =>
|
|
expect(
|
|
screen.getByRole("textbox", { name: "Request" }),
|
|
).not.toHaveAttribute("readonly"),
|
|
);
|
|
|
|
await userEvent.click(
|
|
screen.getByRole("button", { name: "Close Quick Pane" }),
|
|
);
|
|
await act(async () => mocks.focusHandler?.({ payload: false }));
|
|
now += 180_000;
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(trigger).toBeInTheDocument();
|
|
expect(screen.getByText("Remembered answer")).toBeInTheDocument();
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
action: "Close",
|
|
dismiss: async () => {
|
|
await userEvent.click(
|
|
screen.getByRole("button", { name: "Close Quick Pane" }),
|
|
);
|
|
},
|
|
},
|
|
{
|
|
action: "Escape",
|
|
dismiss: async () => {
|
|
fireEvent.keyDown(window, { key: "Escape" });
|
|
},
|
|
},
|
|
])(
|
|
"retains deferred catalog preparation across $action dismissal",
|
|
async ({ dismiss }) => {
|
|
const tracker = deferredCatalogBridge();
|
|
render(<QuickPane />);
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith(
|
|
"get_session_runtime_snapshot",
|
|
{ runtimeId: "runtime-1" },
|
|
),
|
|
);
|
|
|
|
await dismiss();
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith("dismiss_quick_pane"),
|
|
);
|
|
await act(async () => mocks.focusHandler?.({ payload: false }));
|
|
await act(async () =>
|
|
tracker.firstSnapshot.resolve(runtimeSnapshot(tracker.staleModels)),
|
|
);
|
|
expect(
|
|
await screen.findByRole("button", { name: /provider · Stale/i }),
|
|
).toBeInTheDocument();
|
|
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
},
|
|
);
|
|
|
|
it("retains deferred catalog preparation across idle focus loss", async () => {
|
|
const tracker = deferredCatalogBridge();
|
|
render(<QuickPane />);
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith(
|
|
"get_session_runtime_snapshot",
|
|
{ runtimeId: "runtime-1" },
|
|
),
|
|
);
|
|
|
|
await act(async () => mocks.focusHandler?.({ payload: false }));
|
|
await act(async () =>
|
|
tracker.firstSnapshot.resolve(runtimeSnapshot(tracker.staleModels)),
|
|
);
|
|
expect(
|
|
await screen.findByRole("button", { name: /provider · Stale/i }),
|
|
).toBeInTheDocument();
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("dismiss_quick_pane");
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
});
|
|
|
|
it("retries stale-created-runtime cleanup before stale chat renewal", async () => {
|
|
let now = 1_000_000;
|
|
vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
const firstSnapshot = deferred<RuntimeSnapshot>();
|
|
let runtimeCount = 0;
|
|
let firstCloseAttempts = 0;
|
|
const activeRuntimeIds = new Set<string>();
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: { runtimeId?: string }) => {
|
|
if (command === "load_ui_state") return Promise.resolve(uiState);
|
|
if (command === "create_quick_runtime") {
|
|
runtimeCount += 1;
|
|
const runtimeId = `runtime-${runtimeCount}`;
|
|
activeRuntimeIds.add(runtimeId);
|
|
return Promise.resolve({
|
|
runtime: { agentId: `agent-${runtimeCount}`, runtimeId },
|
|
});
|
|
}
|
|
if (command === "get_session_runtime_snapshot") {
|
|
if (payload?.runtimeId === "runtime-1") return firstSnapshot.promise;
|
|
return Promise.resolve(runtimeSnapshot(defaultModels));
|
|
}
|
|
if (command === "close_quick_runtime") {
|
|
if (payload?.runtimeId === "runtime-1") {
|
|
firstCloseAttempts += 1;
|
|
if (firstCloseAttempts === 1)
|
|
return Promise.reject(new Error("stale close unavailable"));
|
|
}
|
|
activeRuntimeIds.delete(payload?.runtimeId ?? "");
|
|
return Promise.resolve(undefined);
|
|
}
|
|
return Promise.resolve(undefined);
|
|
},
|
|
);
|
|
render(<QuickPane />);
|
|
await waitFor(() =>
|
|
expect(mocks.invoke).toHaveBeenCalledWith(
|
|
"get_session_runtime_snapshot",
|
|
{ runtimeId: "runtime-1" },
|
|
),
|
|
);
|
|
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
expect(await preparedTrigger()).toBeInTheDocument();
|
|
await act(async () =>
|
|
firstSnapshot.resolve(runtimeSnapshot(defaultModels)),
|
|
);
|
|
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
"Could not clean up stale quick runtime: Error: stale close unavailable",
|
|
);
|
|
expect(activeRuntimeIds).toEqual(new Set(["runtime-1", "runtime-2"]));
|
|
|
|
now += 180_001;
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
await waitFor(() => expect(runtimeCount).toBe(3));
|
|
expect(firstCloseAttempts).toBe(2);
|
|
expect(activeRuntimeIds).toEqual(new Set(["runtime-3"]));
|
|
expect(screen.queryByText(/stale close unavailable/)).toBeNull();
|
|
expect(
|
|
mocks.invoke.mock.calls.filter(
|
|
([command, payload]) =>
|
|
command === "close_quick_runtime" &&
|
|
payload?.runtimeId === "runtime-2",
|
|
),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("Ctrl+N resets an idle chat and retries cleanup before replacement", async () => {
|
|
const tracker = bridgeMock([defaultModels, defaultModels], () =>
|
|
runtimeSnapshot(defaultModels, "Old answer", false),
|
|
);
|
|
const baseImplementation = mocks.invoke.getMockImplementation();
|
|
let closeAttempts = 0;
|
|
mocks.invoke.mockImplementation(
|
|
(command: string, payload?: Record<string, unknown>) => {
|
|
if (command === "close_quick_runtime") {
|
|
closeAttempts += 1;
|
|
if (closeAttempts === 1)
|
|
return Promise.reject(new Error("close unavailable"));
|
|
}
|
|
return baseImplementation?.(command, payload);
|
|
},
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Old question");
|
|
expect(await screen.findByText("Old answer")).toBeInTheDocument();
|
|
await waitFor(() =>
|
|
expect(
|
|
screen.getByRole("textbox", { name: "Request" }),
|
|
).not.toHaveAttribute("readonly"),
|
|
);
|
|
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
|
|
target: { value: "draft" },
|
|
});
|
|
|
|
const event = new KeyboardEvent("keydown", {
|
|
key: "n",
|
|
ctrlKey: true,
|
|
cancelable: true,
|
|
});
|
|
window.dispatchEvent(event);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
|
|
expect(closeAttempts).toBe(2);
|
|
expect(screen.queryByText("Old answer")).toBeNull();
|
|
expect(screen.getByRole("textbox", { name: "Request" })).toHaveValue("");
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("abort", expect.anything());
|
|
});
|
|
|
|
it("Ctrl+N aborts active work before closing and preparing a fresh runtime", async () => {
|
|
const tracker = bridgeMock([defaultModels, defaultModels], () =>
|
|
runtimeSnapshot(defaultModels, "Partial", true),
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Active question");
|
|
expect(await screen.findByText("Partial")).toBeInTheDocument();
|
|
|
|
const event = new KeyboardEvent("keydown", {
|
|
key: "N",
|
|
ctrlKey: true,
|
|
cancelable: true,
|
|
});
|
|
window.dispatchEvent(event);
|
|
expect(event.defaultPrevented).toBe(true);
|
|
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
|
|
const abortIndex = mocks.invoke.mock.calls.findIndex(
|
|
([command]) => command === "abort",
|
|
);
|
|
const closeIndex = mocks.invoke.mock.calls.findIndex(
|
|
([command]) => command === "close_quick_runtime",
|
|
);
|
|
expect(mocks.invoke).toHaveBeenCalledWith("abort", { agentId: "agent-1" });
|
|
expect(abortIndex).toBeLessThan(closeIndex);
|
|
expect(screen.queryByText("Partial")).toBeNull();
|
|
});
|
|
|
|
it("renews a stale idle chat when reopened after three minutes", async () => {
|
|
let now = 1_000_000;
|
|
vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
const freshModels: Model[] = [
|
|
{ provider: "provider", id: "fresh/model", name: "Fresh" },
|
|
];
|
|
const tracker = bridgeMock([defaultModels, freshModels], () =>
|
|
snapshotWithTranscript([
|
|
{ role: "user", content: "Question" },
|
|
{ role: "assistant", content: "Expired answer" },
|
|
]),
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Question");
|
|
expect(await screen.findByText("Expired answer")).toBeInTheDocument();
|
|
await waitFor(() =>
|
|
expect(
|
|
screen.getByRole("textbox", { name: "Request" }),
|
|
).not.toHaveAttribute("readonly"),
|
|
);
|
|
|
|
await userEvent.click(
|
|
screen.getByRole("button", { name: "Close Quick Pane" }),
|
|
);
|
|
await act(async () => mocks.focusHandler?.({ payload: false }));
|
|
now += 180_001;
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
|
|
await waitFor(() => expect(tracker.runtimeCount()).toBe(2));
|
|
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
expect(screen.queryByText("Expired answer")).toBeNull();
|
|
expect(
|
|
await screen.findByRole("button", { name: /provider · Fresh/i }),
|
|
).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not renew or queue renewal while request is active", async () => {
|
|
let now = 1_000_000;
|
|
vi.spyOn(Date, "now").mockImplementation(() => now);
|
|
const tracker = bridgeMock([defaultModels], () =>
|
|
runtimeSnapshot(defaultModels, "Still working", true),
|
|
);
|
|
render(<QuickPane />);
|
|
await preparedTrigger();
|
|
await ask("Long request");
|
|
expect(await screen.findByText("Still working")).toBeInTheDocument();
|
|
|
|
now += 180_001;
|
|
await act(async () => mocks.focusHandler?.({ payload: true }));
|
|
expect(tracker.runtimeCount()).toBe(1);
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
});
|
|
|
|
it("blocks Send for empty snapshot catalog", async () => {
|
|
bridgeMock([[]]);
|
|
render(<QuickPane />);
|
|
expect(
|
|
await screen.findByText("No models are currently available."),
|
|
).toBeInTheDocument();
|
|
const input = screen.getByRole("textbox", { name: "Request" });
|
|
fireEvent.change(input, { target: { value: "Must not run" } });
|
|
expect(screen.getByRole("button", { name: "Send request" })).toBeDisabled();
|
|
fireEvent.keyDown(input, { key: "Enter" });
|
|
expect(mocks.invoke).not.toHaveBeenCalledWith(
|
|
"submit_prompt",
|
|
expect.anything(),
|
|
);
|
|
});
|
|
|
|
it("closes failed preparation runtime and blocks Send", async () => {
|
|
mocks.invoke.mockImplementation((command: string) => {
|
|
if (command === "load_ui_state") return Promise.resolve(uiState);
|
|
if (command === "create_quick_runtime")
|
|
return Promise.resolve({
|
|
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
|
|
});
|
|
if (command === "get_session_runtime_snapshot")
|
|
return Promise.reject(new Error("offline"));
|
|
return Promise.resolve(undefined);
|
|
});
|
|
render(<QuickPane />);
|
|
expect(await screen.findByRole("alert")).toHaveTextContent(
|
|
"Could not prepare model catalog: Error: offline",
|
|
);
|
|
expect(mocks.invoke).toHaveBeenCalledWith("close_quick_runtime", {
|
|
runtimeId: "runtime-1",
|
|
});
|
|
fireEvent.change(screen.getByRole("textbox", { name: "Request" }), {
|
|
target: { value: "Blocked" },
|
|
});
|
|
expect(screen.getByRole("button", { name: "Send request" })).toBeDisabled();
|
|
});
|
|
});
|