fix: validate workspace drafts and reconcile models

This commit is contained in:
2026-08-04 05:38:43 +02:00
parent cee4cfa63d
commit 959c6871ee
4 changed files with 279 additions and 80 deletions
+50
View File
@@ -181,6 +181,56 @@ test("footer limits model choices to the selected workspace policy", async () =>
await waitFor(() => expect(selector).not.toHaveTextContent("DeepSeek V4 Pro"));
});
test("switching workspaces replaces an out-of-policy model before session creation", async () => {
let body: unknown;
localStorage.setItem("thothii.workspace-registry.v1.preferences", JSON.stringify({
workspaceId: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
}));
const revision = (id: string) => ({
id, commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/snapshot", state: "operational" as const,
});
server.use(
http.get("http://localhost:8787/settings", () => HttpResponse.json({
workspace: "research", provider: "deepseek", model: "deepseek-v4-pro", thinking: "medium",
})),
http.get("http://localhost:8787/workspaces", () => HttpResponse.json([
{ id: "research", name: "research", file: "research.yaml", displayName: "Research", revision: revision("research") },
{ id: "psd-clinical", name: "psd-clinical", file: "psd-clinical.yaml", displayName: "PSD Clinical", revision: revision("psd-clinical") },
])),
http.get("http://localhost:8787/workspaces/research", () => HttpResponse.json({
workspace: { workspace: { id: "research" }, llm_policy: { allowed: ["deepseek/deepseek-v4-pro"] } },
revision: revision("research"),
})),
http.get("http://localhost:8787/workspaces/psd-clinical", () => HttpResponse.json({
workspace: { workspace: { id: "psd-clinical" }, llm_policy: { default: "zai/glm-5.2", allowed: ["zai/glm-5.2"] } },
revision: revision("psd-clinical"),
})),
http.get("http://localhost:8787/models", () => HttpResponse.json({ models: [
{ provider: "zai", id: "glm-5.2", name: "GLM-5.2", reasoning: true },
{ provider: "deepseek", id: "deepseek-v4-pro", name: "DeepSeek V4 Pro", reasoning: true },
] })),
http.post("http://localhost:8787/sessions", async ({ request }) => {
body = await request.json();
return HttpResponse.json({ id: "s1" });
}),
);
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
render(<QueryClientProvider client={client}><ComposerFooter /><SteerInput sessionId={null} /></QueryClientProvider>);
const workspaceSelector = await screen.findByRole("combobox", { name: "Workspace" });
await waitFor(() => expect(workspaceSelector).toHaveTextContent("psd-clinical"));
await userEvent.selectOptions(workspaceSelector, "psd-clinical");
await waitFor(() => expect(screen.getByRole("combobox", { name: "Model" })).toHaveValue("glm-5.2"));
expect(screen.getByRole("combobox", { name: "Model" })).not.toHaveTextContent("DeepSeek V4 Pro");
await userEvent.type(screen.getByRole("textbox", { name: /new question/i }), "q");
await userEvent.click(screen.getByRole("button", { name: /send/i }));
await waitFor(() => expect(body).toEqual({
question: "q", workspaceId: "psd-clinical", provider: "zai", model: "glm-5.2", thinking: "medium",
}));
});
test("pulses the stop dot only while the harness is working", () => {
const { rerender } = render(<SteerInput sessionId="s1" working />);
const dot = () =>
+23 -1
View File
@@ -189,6 +189,27 @@ export function ComposerFooter() {
? models.filter((candidate) => allowedModels.includes(`${candidate.provider}/${candidate.id}`))
: models;
useEffect(() => {
if (!allowedModels?.length) return;
const selected = preferences.provider && preferences.model
? `${preferences.provider}/${preferences.model}`
: undefined;
if (selected && allowedModels.some((allowed) => allowed === selected)) return;
const replacement = workspaceRecord?.workspace.llm_policy.default
&& allowedModels.includes(workspaceRecord.workspace.llm_policy.default)
? workspaceRecord.workspace.llm_policy.default
: allowedModels[0];
const separator = replacement.indexOf("/");
if (separator <= 0 || separator === replacement.length - 1) return;
const next = {
...preferences,
provider: replacement.slice(0, separator),
model: replacement.slice(separator + 1),
};
workspacePreferences.save(next);
setPreferences(next);
}, [allowedModels, preferences, workspaceRecord]);
function update(patch: WorkspacePreference) {
const next = workspacePreferences.save({ ...preferences, ...patch });
setPreferences(next);
@@ -200,6 +221,7 @@ export function ComposerFooter() {
}
const knownModel = policyModels.some((m) => m.id === model);
const showModelFallback = !allowedModels && !knownModel;
const contextPct = tokenUsage && tokenUsage.contextWindow > 0
? tokenUsage.totalTokens / tokenUsage.contextWindow
: 0;
@@ -232,7 +254,7 @@ export function ComposerFooter() {
<option value={model}>{model || "— model —"}</option>
) : (
<>
{!knownModel && <option value={model}>{model || "— model —"}</option>}
{showModelFallback && <option value={model}>{model || "— model —"}</option>}
{policyModels.map((m) => (
<option key={`${m.provider}/${m.id}`} value={m.id}>
{m.name}