diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts index d7617c5884..d9757d5b11 100644 --- a/src/server/management/agent-settings-routes.ts +++ b/src/server/management/agent-settings-routes.ts @@ -948,10 +948,16 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise 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..60bc012fa2 100644 --- a/tests/claude-integration/claude-management-api.test.ts +++ b/tests/claude-integration/claude-management-api.test.ts @@ -895,3 +895,64 @@ 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 modifying or 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); + + // 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: 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); + expect((await putAdd.json() as { error: string }).error).toContain("현재 사용할 수 없는 모델은 추가할 수 없습니다: missing/new-model"); + } finally { + await server.stop(true); + } +});