fix: validate workspace drafts and reconcile models
This commit is contained in:
@@ -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 = () =>
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user