fix(quick-pane): preserve lifecycle and answer history
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user