From 4fa0f82b2f5e6af99315f5222ae9d0f8a2dd2b83 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 01:36:36 -0700 Subject: [PATCH 1/2] Add tests for tenant default leaving agent models unchanged (CL-6782) Changing Settings default must not PATCH agent capabilities, and the roster Model column must keep stored values (unset stays Default). --- apps/web/test/agents-page.test.tsx | 37 ++++++++++ .../connections-default-model-pick.test.tsx | 74 +++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/apps/web/test/agents-page.test.tsx b/apps/web/test/agents-page.test.tsx index a26156df5..f313024f0 100644 --- a/apps/web/test/agents-page.test.tsx +++ b/apps/web/test/agents-page.test.tsx @@ -144,6 +144,43 @@ describe("agentModelSettledContent (CL-6848)", () => { ).toEqual({ kind: "model", label: "Default" }); }); + test("an unset model stays Default when the catalog winner changes (CL-6782)", () => { + const unset = { + status: "ready" as const, + data: { name: "triage-bot" }, + }; + expect( + agentModelSettledContent(unset, [ + { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" }, + ]), + ).toEqual({ kind: "model", label: "Default" }); + expect( + agentModelSettledContent(unset, [ + { canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" }, + { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" }, + ]), + ).toEqual({ kind: "model", label: "Default" }); + }); + + test("a stored model is unchanged when the catalog default becomes a different winner (CL-6782)", () => { + const stored = { + status: "ready" as const, + data: { name: "triage-bot", model: "claude-sonnet-4" }, + }; + expect( + agentModelSettledContent(stored, [ + { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" }, + { canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" }, + ]), + ).toEqual({ kind: "model", label: "Claude Sonnet 4" }); + expect( + agentModelSettledContent(stored, [ + { canonicalName: "claude-haiku-5", displayName: "Claude Haiku 5" }, + { canonicalName: "claude-sonnet-4", displayName: "Claude Sonnet 4" }, + ]), + ).toEqual({ kind: "model", label: "Claude Sonnet 4" }); + }); + test("a ready model maps to the catalog displayName, not the raw id", () => { expect( agentModelSettledContent( diff --git a/packages/settings-ui/test/connections-default-model-pick.test.tsx b/packages/settings-ui/test/connections-default-model-pick.test.tsx index 7a0cc3cc2..8424e232f 100644 --- a/packages/settings-ui/test/connections-default-model-pick.test.tsx +++ b/packages/settings-ui/test/connections-default-model-pick.test.tsx @@ -202,4 +202,78 @@ describe("Global model route", () => { container.remove(); } }); + + test("choosing a default model PATCHes offerings only, never agent capabilities (CL-6782)", async () => { + const calls: { url: string; method: string }[] = []; + let patched = false; + + globalThis.fetch = ((url: string, init?: RequestInit) => { + calls.push({ + url, + method: init?.method ?? "GET", + }); + if (url === "/api/tenants/ten_1/credentials") { + return Promise.resolve( + json({ data: [ANTHROPIC_CREDENTIAL], nextCursor: null }), + ); + } + if (url === "/api/tenants/ten_1/providers") { + return Promise.resolve( + json({ data: [ANTHROPIC_PROVIDER], nextCursor: null }), + ); + } + if (url === "/api/tenants/ten_1/connections/oauth-configured") { + return Promise.resolve(json({})); + } + if (url === "/api/tenants/ten_1/models") { + return Promise.resolve( + json(resolvedModels(patched ? 0 : 0, patched ? -1 : 1)), + ); + } + if (url === "/api/tenants/ten_1/catalog/offerings") { + return Promise.resolve(json({ data: OWN_OFFERINGS, nextCursor: null })); + } + if (url === "/api/tenants/ten_1/catalog/offerings/offering_haiku") { + patched = true; + return Promise.resolve(json({ ...OWN_OFFERINGS[1], priority: -1 })); + } + throw new Error(`unexpected fetch: ${url}`); + }) as unknown as typeof fetch; + + const { container, root } = renderSection(); + try { + await settle(); + + expect(container.textContent).toContain( + "does not rewrite models already stored on existing agents", + ); + + const defaultModel = container.querySelector( + 'select[aria-label="Default model"]', + ) as HTMLSelectElement | null; + act(() => { + if (defaultModel !== null) { + defaultModel.value = "claude-haiku-5"; + defaultModel.dispatchEvent(new Event("change", { bubbles: true })); + } + }); + await settle(); + + const writes = calls.filter( + (call) => call.method !== "GET" && call.method !== "HEAD", + ); + expect(writes.length).toBeGreaterThan(0); + expect( + writes.every((call) => + call.url.startsWith("/api/tenants/ten_1/catalog/offerings/"), + ), + ).toBe(true); + expect( + calls.some((call) => call.url.includes("/agent-definitions")), + ).toBe(false); + } finally { + act(() => root.unmount()); + container.remove(); + } + }); }); From 4e730ccc2639e4cdde689324b080988a5f5b7468 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 01:36:42 -0700 Subject: [PATCH 2/2] Keep tenant default from rewriting existing agent models (CL-6782) Settings default-model pick stays an offering-priority write. Existing agents keep stored capabilities.model; new agents may still default at create. Unset roster cells stay Default, never the live catalog winner. --- packages/settings-ui/src/connections-section.tsx | 8 ++++++-- packages/settings-ui/src/strings.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/settings-ui/src/connections-section.tsx b/packages/settings-ui/src/connections-section.tsx index efe799633..9dd06bc0f 100644 --- a/packages/settings-ui/src/connections-section.tsx +++ b/packages/settings-ui/src/connections-section.tsx @@ -465,6 +465,10 @@ function ModelRoutePanel({ } function chooseModel(canonicalName: string) { + // Tenant default is an offering-priority write only (CL-6782). Existing + // agents keep whatever `capabilities.model` they already store; new + // agents may still pick up the default at create. Never PATCH + // `/agent-definitions`. const target = ordered.find((row) => row.canonicalName === canonicalName); applyPatches( target === undefined @@ -485,8 +489,8 @@ function ModelRoutePanel({

Default model & fallbacks

- Used by Myra and new workbenches unless an agent chooses its own - model. + Used by Myra and new agents. Changing the default does not rewrite + models already stored on existing agents.

diff --git a/packages/settings-ui/src/strings.ts b/packages/settings-ui/src/strings.ts index a1ecf43a0..2929c8e80 100644 --- a/packages/settings-ui/src/strings.ts +++ b/packages/settings-ui/src/strings.ts @@ -193,7 +193,7 @@ export const SETTINGS_STRINGS = { connectionsSectionTitle: "AI providers", connectionsSectionDescription: - "Choose the model route Myra and new workbenches use, then connect the providers behind it.", + "Choose the model route Myra and new agents use, then connect the providers behind it. Changing the default does not rewrite existing agents.", connectionsLoadError: "connections", connectionsStatusNotConnected: "Not connected", connectionsStatusConnected: "Connected",