fix(quick-pane): refresh models and window lifecycle
This commit is contained in:
+164
-5
@@ -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" }));
|
||||
|
||||
Reference in New Issue
Block a user