fix(quick-pane): preserve lifecycle and answer history

This commit is contained in:
2026-08-25 23:08:45 +02:00
parent 1b0b34fad8
commit 4fc9c60ade
8 changed files with 506 additions and 65 deletions
+191 -2
View File
@@ -1,4 +1,11 @@
import { act, render, screen, waitFor } from "@testing-library/react";
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 { QuickPaneSettings, UiState } from "../workspace/types";
@@ -47,7 +54,18 @@ const uiState: UiState = {
quickPane: settings,
};
describe("QuickPane model catalog errors", () => {
function snapshot(answer: string, isStreaming: boolean) {
return {
runtime: {},
state: { data: { isStreaming } },
transcript: {
data: { messages: [{ role: "assistant", content: answer }] },
},
extensions: [],
};
}
describe("QuickPane", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.focusHandler = undefined;
@@ -115,4 +133,175 @@ describe("QuickPane model catalog errors", () => {
);
expect(screen.queryByRole("button", { name: "Continue" })).toBeNull();
});
it("allows only one runtime creation while a submission is starting", async () => {
let resolveCreate:
| ((value: { runtime: { agentId: string; runtimeId: string } }) => void)
| undefined;
const createRuntime = new Promise<{
runtime: { agentId: string; runtimeId: string };
}>((resolve) => {
resolveCreate = resolve;
});
mocks.invoke.mockImplementation((command: string) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
return createRuntime;
case "get_session_runtime_snapshot":
return Promise.resolve({
runtime: {},
state: { data: { isStreaming: true } },
extensions: [],
});
default:
return Promise.resolve(undefined);
}
});
render(<QuickPane />);
const input = await screen.findByPlaceholderText(/Ask a quick question/);
fireEvent.change(input, { target: { value: "Inspect once" } });
const ask = screen.getByRole("button", { name: "Ask" });
fireEvent.click(ask);
fireEvent.click(ask);
expect(
mocks.invoke.mock.calls.filter(
([command]) => command === "create_quick_runtime",
),
).toHaveLength(1);
expect(screen.getByRole("button", { name: "Starting…" })).toBeDisabled();
await act(async () => {
resolveCreate?.({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
await createRuntime;
});
});
it("clears only the submitted request after a completed non-streaming answer", async () => {
const user = userEvent.setup();
let currentSnapshot = snapshot("Partial answer", true);
mocks.invoke.mockImplementation((command: string) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime":
return Promise.resolve({
runtime: { agentId: "agent-1", runtimeId: "runtime-1" },
});
case "get_session_runtime_snapshot":
return Promise.resolve(currentSnapshot);
default:
return Promise.resolve(undefined);
}
});
render(<QuickPane />);
const input = await screen.findByPlaceholderText(/Ask a quick question/);
await user.type(input, "Inspect this");
await user.click(screen.getByRole("button", { name: "Ask" }));
expect(await screen.findByText("Partial answer")).toBeInTheDocument();
expect(input).toHaveValue("Inspect this");
currentSnapshot = snapshot("Final answer", false);
expect(
await screen.findByText("Final answer", {}, { timeout: 2_000 }),
).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
await user.type(input, "Next draft");
await new Promise((resolve) => window.setTimeout(resolve, 900));
expect(input).toHaveValue("Next draft");
});
it("keeps five in-memory answers and history selection preserves current answer", async () => {
const user = userEvent.setup();
let runtimeCount = 0;
mocks.invoke.mockImplementation(
(command: string, payload?: { runtimeId?: string }) => {
switch (command) {
case "load_ui_state":
return Promise.resolve(uiState);
case "get_model_catalog":
return Promise.resolve({
models: [{ provider: "provider", id: "quick/model" }],
});
case "create_quick_runtime": {
runtimeCount += 1;
return Promise.resolve({
runtime: {
agentId: `agent-${runtimeCount}`,
runtimeId: `runtime-${runtimeCount}`,
},
});
}
case "get_session_runtime_snapshot": {
const segments = payload?.runtimeId?.split("-") ?? [];
const id = Number(segments[segments.length - 1]);
return Promise.resolve(snapshot(`Answer ${id}`, false));
}
default:
return Promise.resolve(undefined);
}
},
);
render(<QuickPane />);
const input = await screen.findByPlaceholderText(/Ask a quick question/);
for (let index = 1; index <= 6; index += 1) {
await user.type(input, `Request ${index}`);
await user.click(screen.getByRole("button", { name: "Ask" }));
expect(await screen.findByText(`Answer ${index}`)).toBeInTheDocument();
await waitFor(() => expect(input).toHaveValue(""));
}
const history = screen.getByRole("combobox", { name: "Answer history" });
expect(within(history).getAllByRole("option")).toHaveLength(6);
expect(
within(history).queryByRole("option", { name: "Request 1" }),
).toBeNull();
expect(
within(history).getByRole("option", { name: "Request 2" }),
).toBeInTheDocument();
await user.selectOptions(history, "runtime-2");
expect(screen.getByText("Answer 2")).toBeInTheDocument();
expect(screen.queryByText("Answer 6")).toBeNull();
await user.selectOptions(history, "");
expect(screen.getByText("Answer 6")).toBeInTheDocument();
expect(mocks.invoke).not.toHaveBeenCalledWith(
"save_ui_state",
expect.anything(),
);
});
it("leaves close interception native while preserving close and Escape hiding", async () => {
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "get_model_catalog")
return Promise.resolve({ models: [] });
return Promise.resolve(undefined);
});
render(<QuickPane />);
await screen.findByPlaceholderText(/Ask a quick question/);
expect(mocks.onCloseRequested).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole("button", { name: "Close" }));
fireEvent.keyDown(window, { key: "Escape" });
expect(mocks.hide).toHaveBeenCalledTimes(2);
});
});