test(frontend): cover NewSessionDialog model degradation + invalidate sessions on resume

- Add NewSessionDialog.test.tsx covering §9 graceful degradation:
  empty models -> free-text input; non-empty models -> dropdown; plus a
  createSession body-keys assertion
- Normalize model entries to {value,label} so object-shaped models
  ({provider,id}) render correctly in the dropdown (was assuming strings)
- NavSessions: invalidate the ["sessions"] query after a successful
  resumeSession so the list/status refreshes immediately

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-28 13:11:40 +02:00
co-authored by Claude Opus 4.8
parent 544882f95b
commit 3dec4c3af0
3 changed files with 99 additions and 5 deletions
@@ -0,0 +1,82 @@
// frontend/src/shell/NewSessionDialog.test.tsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { server } from "../test/msw";
import { NewSessionDialog } from "./NewSessionDialog";
function renderDialog() {
const onCreated = vi.fn();
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(
<QueryClientProvider client={client}>
<NewSessionDialog onCreated={onCreated} />
</QueryClientProvider>,
);
return { onCreated };
}
beforeEach(() => {
server.use(
http.get("http://localhost:8787/workspaces", () =>
HttpResponse.json([{ name: "default", file: "default.db" }]),
),
http.post("http://localhost:8787/sessions", () =>
HttpResponse.json({ id: "s1" }),
),
);
});
test("§9 degradation: empty models -> model field is a FREE-TEXT input, not a select", async () => {
server.use(
http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [] })),
);
renderDialog();
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
// Free-text model input present; no model dropdown.
const freetext = await screen.findByTestId("model-freetext");
expect(freetext.tagName).toBe("INPUT");
// The only <select> in the dialog is the workspace one (id nsd-workspace).
expect(screen.queryByRole("combobox", { name: /modello/i })).not.toBeInTheDocument();
});
test("§9: non-empty models -> model field is a dropdown (select), not free text", async () => {
server.use(
http.get("http://localhost:8787/models", () =>
HttpResponse.json({ models: [{ provider: "zai", id: "glm-5.2" }] }),
),
);
renderDialog();
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
// Model dropdown present; no free-text input.
const modelSelect = await screen.findByRole("combobox", { name: /modello/i });
expect(modelSelect.tagName).toBe("SELECT");
expect(screen.queryByTestId("model-freetext")).not.toBeInTheDocument();
// Option label combines provider/id.
expect(screen.getByRole("option", { name: "zai/glm-5.2" })).toBeInTheDocument();
});
test("submitting calls createSession with the expected body keys", async () => {
let body: unknown = null;
server.use(
http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [] })),
http.post("http://localhost:8787/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
const { onCreated } = renderDialog();
await userEvent.click(screen.getByRole("button", { name: /nuova/i }));
const textarea = await screen.findByLabelText(/domanda/i);
await userEvent.type(textarea, "Quante vendite nel 2025?");
await userEvent.click(screen.getByRole("button", { name: /^crea$/i }));
await waitFor(() => {
expect(body).toEqual({ workspace: "default", question: "Quante vendite nel 2025?" });
});
await waitFor(() => expect(onCreated).toHaveBeenCalledWith("s1"));
});