From d612717bff7f492001dcecde397e3992ffe6f791 Mon Sep 17 00:00:00 2001 From: chilung Date: Thu, 10 Sep 2026 07:44:11 +0000 Subject: [PATCH 1/2] fix(claude): allow deleting unavailable routes in desktop profile --- .../management/agent-settings-routes.ts | 16 +++++-- .../claude-management-api.test.ts | 47 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index d7617c5884..278baf42aa 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -945,13 +945,19 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise let body: { profile?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } try { - const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile"); - const parsed = parseDesktopProfile(body.profile); - const current = await buildClaudeDesktopState(config); - for (const model of current.models.filter(item => !item.available)) { + const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile"); + const parsed = parseDesktopProfile(body.profile); + const current = await buildClaudeDesktopState(config); + const availableRoutes = new Set(current.models.filter(item => item.available).map(item => item.route)); + for (const route of Object.keys(parsed.assignments)) { + if (!current.profile.assignments[route] && !availableRoutes.has(route)) { + throw new Error(`현재 사용할 수 없는 모델은 추가할 수 없습니다: ${route}`); + } + } + for (const model of current.models.filter(item => !item.available)) { const before = current.profile.assignments[model.route]; const after = parsed.assignments[model.route]; - if (JSON.stringify(before) !== JSON.stringify(after)) { + if (after !== undefined && JSON.stringify(before) !== JSON.stringify(after)) { throw new Error(`현재 사용할 수 없는 모델은 옮길 수 없습니다: ${model.route}`); } } diff --git a/tests/claude-integration/claude-management-api.test.ts b/tests/claude-integration/claude-management-api.test.ts index fa99f3c14f..dc0371a470 100644 --- a/tests/claude-integration/claude-management-api.test.ts +++ b/tests/claude-integration/claude-management-api.test.ts @@ -895,3 +895,50 @@ test("Claude Desktop PUT retains but cannot move an unavailable route", async () await server.stop(true); } }); + +test("Claude Desktop PUT allows deleting an unavailable route, but rejects adding one", async () => { + const seeded = loadConfig(); + seeded.claudeCode = { + desktopProfile: { + version: 1, + assignments: { + "missing/old-model": { family: "opus", alias: "claude-opus-4-8-20260101" }, + }, + defaults: { opus: "missing/old-model", fable: null, sonnet: null, haiku: null }, + }, + }; + saveConfig(seeded); + const server = startServer(0); + try { + const state = await fetch(new URL("/api/claude-desktop", server.url)).then(r => r.json()) as Record; + expect(state.models.find((model: { route: string }) => model.route === "missing/old-model")?.available).toBe(false); + + const deleteEdit = structuredClone(state.profile); + delete deleteEdit.assignments["missing/old-model"]; + deleteEdit.defaults.opus = Object.keys(deleteEdit.assignments).filter(route => deleteEdit.assignments[route].family === "opus").sort()[0] ?? null; + + const putDelete = await fetch(new URL("/api/claude-desktop", server.url), { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile: deleteEdit }), + }); + expect(putDelete.status).toBe(200); + const deleteResult = await putDelete.json() as Record; + expect(deleteResult.models.some((model: { route: string }) => model.route === "missing/old-model")).toBe(false); + expect(deleteResult.profile.assignments["missing/old-model"]).toBeUndefined(); + expect(loadConfig().claudeCode?.desktopProfile?.assignments["missing/old-model"]).toBeUndefined(); + + const addEdit = structuredClone(deleteResult.profile); + addEdit.assignments["missing/new-model"] = { family: "fable", alias: "claude-opus-4-8-20260102" }; + addEdit.defaults.fable = "missing/new-model"; + const putAdd = await fetch(new URL("/api/claude-desktop", server.url), { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile: addEdit }), + }); + expect(putAdd.status).toBe(400); + expect((await putAdd.json() as { error: string }).error).toContain("현재 사용할 수 없는 모델은 추가할 수 없습니다: missing/new-model"); +} finally { + await server.stop(true); + } +}); From 74e4024db22a98acfd9adbd37923dc4e2a063b76 Mon Sep 17 00:00:00 2001 From: chilung Date: Fri, 11 Sep 2026 02:12:30 +0000 Subject: [PATCH 2/2] test(claude): cover rejecting modifications to unavailable routes --- .../management/agent-settings-routes.ts | 8 +-- .../claude-management-api.test.ts | 58 ++++++++++++------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index 278baf42aa..d9757d5b11 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -945,16 +945,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise let body: { profile?: unknown }; try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); } try { - const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile"); - const parsed = parseDesktopProfile(body.profile); - const current = await buildClaudeDesktopState(config); + const { parseDesktopProfile, reconcileDesktopProfile } = await import("../../claude/desktop-profile"); + const parsed = parseDesktopProfile(body.profile); + const current = await buildClaudeDesktopState(config); const availableRoutes = new Set(current.models.filter(item => item.available).map(item => item.route)); for (const route of Object.keys(parsed.assignments)) { if (!current.profile.assignments[route] && !availableRoutes.has(route)) { throw new Error(`현재 사용할 수 없는 모델은 추가할 수 없습니다: ${route}`); } } - for (const model of current.models.filter(item => !item.available)) { + for (const model of current.models.filter(item => !item.available)) { const before = current.profile.assignments[model.route]; const after = parsed.assignments[model.route]; if (after !== undefined && JSON.stringify(before) !== JSON.stringify(after)) { diff --git a/tests/claude-integration/claude-management-api.test.ts b/tests/claude-integration/claude-management-api.test.ts index dc0371a470..60bc012fa2 100644 --- a/tests/claude-integration/claude-management-api.test.ts +++ b/tests/claude-integration/claude-management-api.test.ts @@ -896,7 +896,7 @@ test("Claude Desktop PUT retains but cannot move an unavailable route", async () } }); -test("Claude Desktop PUT allows deleting an unavailable route, but rejects adding one", async () => { +test("Claude Desktop PUT allows deleting an unavailable route, but rejects modifying or adding one", async () => { const seeded = loadConfig(); seeded.claudeCode = { desktopProfile: { @@ -908,37 +908,51 @@ test("Claude Desktop PUT allows deleting an unavailable route, but rejects addin }, }; saveConfig(seeded); - const server = startServer(0); - try { - const state = await fetch(new URL("/api/claude-desktop", server.url)).then(r => r.json()) as Record; + const server = startServer(0); + try { + const state = await fetch(new URL("/api/claude-desktop", server.url)).then(r => r.json()) as Record; expect(state.models.find((model: { route: string }) => model.route === "missing/old-model")?.available).toBe(false); - const deleteEdit = structuredClone(state.profile); - delete deleteEdit.assignments["missing/old-model"]; - deleteEdit.defaults.opus = Object.keys(deleteEdit.assignments).filter(route => deleteEdit.assignments[route].family === "opus").sort()[0] ?? null; - - const putDelete = await fetch(new URL("/api/claude-desktop", server.url), { + // Modifying an existing unavailable assignment (e.g. changing alias) is rejected with 400. + const modifyEdit = structuredClone(state.profile); + modifyEdit.assignments["missing/old-model"].alias = "claude-opus-4-8-20260202"; + const putModify = await fetch(new URL("/api/claude-desktop", server.url), { method: "PUT", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ profile: deleteEdit }), - }); - expect(putDelete.status).toBe(200); + body: JSON.stringify({ profile: modifyEdit }), + }); + expect(putModify.status).toBe(400); + expect((await putModify.json() as { error: string }).error).toContain("현재 사용할 수 없는 모델은 옮길 수 없습니다: missing/old-model"); + expect(loadConfig().claudeCode?.desktopProfile?.assignments["missing/old-model"]?.alias).toBe("claude-opus-4-8-20260101"); + + // Deleting an existing unavailable assignment succeeds with 200. + const deleteEdit = structuredClone(state.profile); + delete deleteEdit.assignments["missing/old-model"]; + deleteEdit.defaults.opus = Object.keys(deleteEdit.assignments).filter(route => deleteEdit.assignments[route].family === "opus").sort()[0] ?? null; + + const putDelete = await fetch(new URL("/api/claude-desktop", server.url), { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile: deleteEdit }), + }); + expect(putDelete.status).toBe(200); const deleteResult = await putDelete.json() as Record; expect(deleteResult.models.some((model: { route: string }) => model.route === "missing/old-model")).toBe(false); expect(deleteResult.profile.assignments["missing/old-model"]).toBeUndefined(); expect(loadConfig().claudeCode?.desktopProfile?.assignments["missing/old-model"]).toBeUndefined(); - + + // Adding a newly unavailable assignment is rejected with 400. const addEdit = structuredClone(deleteResult.profile); addEdit.assignments["missing/new-model"] = { family: "fable", alias: "claude-opus-4-8-20260102" }; addEdit.defaults.fable = "missing/new-model"; - const putAdd = await fetch(new URL("/api/claude-desktop", server.url), { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ profile: addEdit }), - }); - expect(putAdd.status).toBe(400); + const putAdd = await fetch(new URL("/api/claude-desktop", server.url), { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ profile: addEdit }), + }); + expect(putAdd.status).toBe(400); expect((await putAdd.json() as { error: string }).error).toContain("현재 사용할 수 없는 모델은 추가할 수 없습니다: missing/new-model"); -} finally { - await server.stop(true); - } + } finally { + await server.stop(true); + } });