mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(routing): escape hatch out of mix mode — clear-all overrides + pins warning (#663)
This commit is contained in:
@@ -597,18 +597,100 @@ describe("AIRoutingCard", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("saving the mix with no picks shows an error and never calls applyMode", async () => {
|
||||
it("saving the mix with no picks confirms, then clears every override via an empty per_agent map", async () => {
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
render(withQueryClient(<AIRoutingCard />));
|
||||
await screen.findByText("Per-agent override (mix mode)");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save mix" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
"Pick a model for at least one agent",
|
||||
),
|
||||
expect(applyMode).toHaveBeenCalledWith({ mode: "mix", per_agent: {} }),
|
||||
);
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("declining the empty-save confirm never calls applyMode", async () => {
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
|
||||
render(withQueryClient(<AIRoutingCard />));
|
||||
await screen.findByText("Per-agent override (mix mode)");
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save mix" }));
|
||||
|
||||
await waitFor(() => expect(confirmSpy).toHaveBeenCalled());
|
||||
expect(applyMode).not.toHaveBeenCalled();
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("Clear all overrides", () => {
|
||||
const PINNED_SNAPSHOT = {
|
||||
mode: "mix",
|
||||
assignments: [
|
||||
{
|
||||
scope: "agent_slug",
|
||||
scope_value: "be-dev-1",
|
||||
model_name: "glm-5.2:cloud",
|
||||
},
|
||||
{
|
||||
scope: "agent_slug",
|
||||
scope_value: "auditor",
|
||||
model_name: "glm-5.2:cloud",
|
||||
},
|
||||
],
|
||||
} as Awaited<ReturnType<typeof getMode>>;
|
||||
|
||||
it("is disabled when nothing is picked and nothing is pinned", async () => {
|
||||
render(withQueryClient(<AIRoutingCard />));
|
||||
await screen.findByText("Per-agent override (mix mode)");
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Clear all" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
|
||||
it("clears persisted pins via an empty per_agent map on confirm", async () => {
|
||||
getMode.mockResolvedValueOnce(PINNED_SNAPSHOT);
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
|
||||
render(withQueryClient(<AIRoutingCard />));
|
||||
await screen.findByText("Per-agent override (mix mode)");
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Clear all" }),
|
||||
).toBeEnabled(),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(applyMode).toHaveBeenCalledWith({ mode: "mix", per_agent: {} }),
|
||||
);
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("declining the confirm never calls applyMode", async () => {
|
||||
getMode.mockResolvedValueOnce(PINNED_SNAPSHOT);
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(false);
|
||||
render(withQueryClient(<AIRoutingCard />));
|
||||
await screen.findByText("Per-agent override (mix mode)");
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Clear all" }),
|
||||
).toBeEnabled(),
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Clear all" }));
|
||||
|
||||
await waitFor(() => expect(confirmSpy).toHaveBeenCalled());
|
||||
expect(applyMode).not.toHaveBeenCalled();
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("pinned overrides surface the outrank warning in the Routing-mode section", async () => {
|
||||
getMode.mockResolvedValueOnce(PINNED_SNAPSHOT);
|
||||
render(withQueryClient(<AIRoutingCard />));
|
||||
|
||||
await screen.findByText(/2 per-agent overrides outrank the global mode/);
|
||||
});
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@@ -275,6 +275,10 @@ export function AIRoutingCard() {
|
||||
setMixMap(initialMix);
|
||||
}, [initialMix]);
|
||||
|
||||
// Server-persisted pins (not local edits) — these outrank every mode
|
||||
// button, so the Routing-mode section warns when any exist.
|
||||
const pinnedCount = Object.keys(initialMix).length;
|
||||
|
||||
const catalogForMix = catalog;
|
||||
const catalogOllamaOnly = catalog.filter(
|
||||
(c: { provider_type: ModelProvider }) =>
|
||||
@@ -426,6 +430,26 @@ export function AIRoutingCard() {
|
||||
}
|
||||
};
|
||||
|
||||
const clearAllOverrides = async () => {
|
||||
if (
|
||||
!confirm(
|
||||
"Clear every per-agent override? All agents go back to " +
|
||||
"inherit-global (the active routing mode).",
|
||||
)
|
||||
)
|
||||
return;
|
||||
setMixMap({});
|
||||
if (pinnedCount === 0) return; // nothing persisted server-side
|
||||
try {
|
||||
await applyMode.mutateAsync({ mode: "mix", per_agent: {} });
|
||||
toast.success(
|
||||
"Per-agent overrides cleared — all agents follow the global mode",
|
||||
);
|
||||
} catch (e) {
|
||||
toast.error("Clear failed: " + errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
const saveMix = async () => {
|
||||
// Filter out empty picks (nothing selected = inherit global).
|
||||
const per_agent: Record<string, string> = {};
|
||||
@@ -433,7 +457,24 @@ export function AIRoutingCard() {
|
||||
if (model) per_agent[slug] = model;
|
||||
}
|
||||
if (Object.keys(per_agent).length === 0) {
|
||||
toast.error("Pick a model for at least one agent");
|
||||
// Empty save = the explicit clear-all. Without it a fully-pinned
|
||||
// fleet has no way back to the global mode (pins survive every mode
|
||||
// switch by design).
|
||||
if (
|
||||
!confirm(
|
||||
"No models are picked — saving clears every per-agent override " +
|
||||
"so all agents follow the global routing mode. Continue?",
|
||||
)
|
||||
)
|
||||
return;
|
||||
try {
|
||||
await applyMode.mutateAsync({ mode: "mix", per_agent: {} });
|
||||
toast.success(
|
||||
"Per-agent overrides cleared — all agents follow the global mode",
|
||||
);
|
||||
} catch (e) {
|
||||
toast.error("Clear failed: " + errMsg(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const needsGrok = Object.values(per_agent).some((m) =>
|
||||
@@ -962,6 +1003,15 @@ export function AIRoutingCard() {
|
||||
labelHint="Unlike every button to the left this never wipes existing routing — it's a one-time additive seed you can re-run anytime. Edit or remove individual rows in the Complexity overrides section below."
|
||||
/>
|
||||
</div>
|
||||
{pinnedCount > 0 ? (
|
||||
<p className="text-xs text-amber-600 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3 shrink-0" />
|
||||
{pinnedCount} per-agent override{pinnedCount === 1 ? "" : "s"}{" "}
|
||||
outrank the global mode — switching modes won't change those
|
||||
agents. Clear rows in the mix table below (an empty save clears
|
||||
them all).
|
||||
</p>
|
||||
) : null}
|
||||
{currentMode === "mix" && !hasOllamaKey ? (
|
||||
<p className="text-xs text-amber-600 flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
@@ -1133,13 +1183,27 @@ export function AIRoutingCard() {
|
||||
Per-agent override (mix mode)
|
||||
</Label>
|
||||
</HelpTip>
|
||||
<Button size="sm" onClick={saveMix} disabled={applyMode.isPending}>
|
||||
{applyMode.isPending ? "Saving…" : "Save mix"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={clearAllOverrides}
|
||||
disabled={
|
||||
applyMode.isPending ||
|
||||
(pinnedCount === 0 && Object.values(mixMap).every((m) => !m))
|
||||
}
|
||||
>
|
||||
Clear all
|
||||
</Button>
|
||||
<Button size="sm" onClick={saveMix} disabled={applyMode.isPending}>
|
||||
{applyMode.isPending ? "Saving…" : "Save mix"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Leave a row blank to inherit from the global mode. Saving overwrites
|
||||
all per-agent overrides with what's picked here.
|
||||
all per-agent overrides with what's picked here; Clear all
|
||||
resets every agent back to inherit-global in one click.
|
||||
</p>
|
||||
{agentsError ? (
|
||||
<p className="flex items-center gap-1 rounded-md border p-4 text-xs text-amber-600">
|
||||
|
||||
Reference in New Issue
Block a user