fix(ui): thread session_affinity and classifier context fields through preset selection

handlePresetChange builds a fresh ComplexityRouterConfigValue from the preset's config on every selection; four fields added to that payload since the object literal was written (session_affinity, classifier_context_window_size, classifier_context_per_turn_chars, classifier_context_include_assistant_turns) were never added to it. Since setComplexityRouterConfig replaces the whole value rather than merging, applying a preset silently dropped all four to undefined regardless of what the preset specified or what the caller had set manually; for session_affinity specifically that resolved to false at submit time via the existing destructuring default, so both bundled presets (already false) masked it. Verified directly: toggling session affinity on and then applying a preset reverted the switch to off and submitted false, not the caller's prior choice.

Thread all four fields through from the preset's config, matching how every other optional field here is already carried over. Extended the existing falsy-preset test to assert session_affinity survives when a preset sets it true, and added a dedicated regression for the reported flow: toggle on, then apply a preset, and confirm the preset's own value wins over the stale manual edit rather than being silently discarded.
This commit is contained in:
Tin Chi Lo 2026-08-03 18:08:25 -07:00
parent b140c02a91
commit af5af4d0d7
2 changed files with 38 additions and 0 deletions

View file

@ -79,6 +79,7 @@ const { FALSY_PRESET } = vi.hoisted(() => ({
match_threshold: 0,
keyword_tier_rules: [{ keywords: ["foo"], tier: "SIMPLE" as const }],
escalation_keywords: [],
session_affinity: true,
},
},
}));
@ -380,6 +381,7 @@ describe("AddAutoRouterTab", () => {
const payload = mockHandleAddAutoRouterSubmit.mock.calls.at(-1)?.[0];
expect(payload.complexity_router_config.match_threshold).toBe(0);
expect(payload.complexity_router_config.escalation_keywords).toEqual([]);
expect(payload.complexity_router_config.session_affinity).toBe(true);
});
it("offers no team selector to a proxy admin, who may create an unscoped router", () => {
@ -474,4 +476,36 @@ describe("AddAutoRouterTab", () => {
session_affinity: true,
});
});
// handlePresetChange fully replaces complexityRouterConfig, so a field missing from the object
// it builds isn't "carried forward" from a prior manual edit, it's silently dropped to whatever
// the destructuring default resolves to downstream. Turn affinity on via Custom, then apply a
// preset whose own value is off, and confirm the preset's off wins in both the UI and the
// payload, the same authoritative-replace behavior every other preset field already has.
it("applies the preset's own session affinity instead of a previously toggled-on value", async () => {
const user = userEvent.setup();
mockFetchAvailableModels.mockResolvedValue(ALL_FAMILY_MODELS);
renderWithProviders(<Harness />);
openTemplateDropdown();
fireEvent.click(optionByLabel("Custom Configuration")!);
await user.click(screen.getByText("Advanced: Session Affinity"));
await user.click(await screen.findByRole("switch", { name: "Pin a session to its first model" }));
expect(screen.getByRole("switch", { name: "Pin a session to its first model" })).toBeChecked();
openTemplateDropdown();
await waitFor(() => expect(isOptionDisabled(optionByLabel("Anthropic Family")!)).toBe(false));
fireEvent.click(optionByLabel("Anthropic Family")!);
expect(screen.getByRole("switch", { name: "Pin a session to its first model" })).not.toBeChecked();
await user.type(screen.getByPlaceholderText(/smart_router/i), "affinity-then-preset-router");
await user.click(screen.getByRole("button", { name: /add auto router/i }));
await waitFor(() => expect(mockHandleAddAutoRouterSubmit).toHaveBeenCalled());
expect(mockHandleAddAutoRouterSubmit.mock.calls.at(-1)?.[0].complexity_router_config).toMatchObject({
session_affinity: false,
});
});
});

View file

@ -171,6 +171,10 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
tiers: config.tiers,
classifier_type: config.classifier_type,
classifier_llm_config: config.classifier_llm_config,
classifier_context_window_size: config.classifier_context_window_size,
classifier_context_per_turn_chars: config.classifier_context_per_turn_chars,
classifier_context_include_assistant_turns: config.classifier_context_include_assistant_turns,
session_affinity: config.session_affinity ?? DEFAULT_SESSION_AFFINITY,
adaptive: config.adaptive,
adaptive_weights: config.adaptive_weights,
tier_distance_penalty: config.tier_distance_penalty,