fix(quick-pane): refresh models and window lifecycle

This commit is contained in:
2026-08-26 15:58:01 +02:00
parent d34099771c
commit 4ec951bc05
7 changed files with 869 additions and 130 deletions
+164 -5
View File
@@ -1,4 +1,4 @@
import { act, render, screen, waitFor } from "@testing-library/react";
import { act, 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 { RuntimeSummary, UiState, Workspace } from "./workspace/types";
@@ -149,10 +149,10 @@ describe("App command progress and errors", () => {
return Promise.resolve(undefined);
});
render(<App />);
await user.click(screen.getByRole("button", { name: "Controls" }));
await waitFor(() =>
expect(mocks.invoke).toHaveBeenCalledWith("get_model_catalog"),
);
await user.click(screen.getByRole("button", { name: "Controls" }));
const quickModel = screen.getByRole("combobox", { name: /Quick model/ });
expect(quickModel).toBeInTheDocument();
expect(
@@ -185,13 +185,172 @@ describe("App command progress and errors", () => {
);
});
it("refreshes available models whenever Settings opens", async () => {
const user = userEvent.setup();
let catalogCalls = 0;
const loaded: UiState = {
...uiState,
quickPane: {
workspace: "/repo",
provider: "openai",
model: "old-model",
followUpProvider: "openai",
followUpModel: "old-model",
instructions: "Stay concise.",
maximumSeconds: 60,
maximumToolCalls: 6,
},
};
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(loaded);
if (command === "get_model_catalog") {
catalogCalls += 1;
return Promise.resolve({
models: [
{
provider: "openai",
id: catalogCalls === 1 ? "old-model" : "new-model",
},
],
});
}
return Promise.resolve(undefined);
});
render(<App />);
await user.click(screen.getByRole("button", { name: "Controls" }));
const quickModel = await screen.findByRole("combobox", {
name: /Quick model/,
});
await waitFor(() =>
expect(
within(quickModel).getByRole("option", { name: "openai/old-model" }),
).toBeInTheDocument(),
);
await user.click(screen.getByRole("button", { name: "Back" }));
await user.click(screen.getByRole("button", { name: "Controls" }));
await waitFor(() => expect(catalogCalls).toBe(2));
const refreshedQuickModel = screen.getByRole("combobox", {
name: /Quick model/,
});
await waitFor(() =>
expect(
within(refreshedQuickModel).getByRole("option", {
name: "openai/new-model",
}),
).toBeInTheDocument(),
);
expect(
within(refreshedQuickModel).queryByRole("option", {
name: "openai/old-model",
}),
).toBeNull();
expect(refreshedQuickModel).toHaveValue(
modelIdentityValue({ provider: "openai", modelId: "new-model" }),
);
});
it("ignores an older catalog response that resolves after a newer refresh", async () => {
const user = userEvent.setup();
const firstCatalog = deferred<{ models: { provider: string; id: string }[] }>();
const secondCatalog = deferred<{ models: { provider: string; id: string }[] }>();
let catalogCalls = 0;
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "get_model_catalog") {
catalogCalls += 1;
return catalogCalls === 1 ? firstCatalog.promise : secondCatalog.promise;
}
return Promise.resolve(undefined);
});
render(<App />);
await user.click(screen.getByRole("button", { name: "Controls" }));
await waitFor(() => expect(catalogCalls).toBe(1));
await user.click(screen.getByRole("button", { name: "Back" }));
await user.click(screen.getByRole("button", { name: "Controls" }));
await waitFor(() => expect(catalogCalls).toBe(2));
await act(async () =>
secondCatalog.resolve({
models: [{ provider: "openai", id: "new-model" }],
}),
);
const quickModel = screen.getByRole("combobox", { name: /Quick model/ });
await waitFor(() =>
expect(
within(quickModel).getByRole("option", { name: "openai/new-model" }),
).toBeInTheDocument(),
);
await act(async () =>
firstCatalog.resolve({
models: [{ provider: "openai", id: "stale-model" }],
}),
);
expect(
within(quickModel).getByRole("option", { name: "openai/new-model" }),
).toBeInTheDocument();
expect(
within(quickModel).queryByRole("option", { name: "openai/stale-model" }),
).toBeNull();
});
it("clears catalog refresh error after a successful retry", async () => {
const user = userEvent.setup();
let catalogCalls = 0;
mocks.invoke.mockImplementation((command: string) => {
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "get_model_catalog") {
catalogCalls += 1;
if (catalogCalls === 1)
return Promise.reject(new Error("catalog offline"));
return Promise.resolve({
models: [{ provider: "openai", id: "recovered-model" }],
});
}
return Promise.resolve(undefined);
});
render(<App />);
await user.click(screen.getByRole("button", { name: "Controls" }));
expect(
await screen.findByText(
"Could not load model catalog: Error: catalog offline",
),
).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Back" }));
await user.click(screen.getByRole("button", { name: "Controls" }));
await waitFor(() => expect(catalogCalls).toBe(2));
await waitFor(() =>
expect(
screen.queryByText(
"Could not load model catalog: Error: catalog offline",
),
).toBeNull(),
);
expect(
within(screen.getByRole("combobox", { name: /Quick model/ })).getByRole(
"option",
{ name: "openai/recovered-model" },
),
).toBeInTheDocument();
});
it("shows restart progress until the replacement snapshot is restored", async () => {
const user = userEvent.setup();
const restart = deferred<unknown>();
const snapshot = deferred<boolean>();
mocks.invoke.mockImplementation((command: string) =>
command === "restart" ? restart.promise : Promise.resolve(undefined),
);
mocks.invoke.mockImplementation((command: string) => {
if (command === "restart") return restart.promise;
if (command === "load_ui_state") return Promise.resolve(uiState);
if (command === "get_model_catalog")
return Promise.resolve({ models: [] });
return Promise.resolve(undefined);
});
mocks.loadSnapshot.mockReturnValue(snapshot.promise);
render(<App />);
await user.click(screen.getByRole("button", { name: "Controls" }));