diff --git a/apps/api/tests/agent-versioned-config-plan.test.ts b/apps/api/tests/agent-versioned-config-plan.test.ts index 826658d5..54595fc9 100644 --- a/apps/api/tests/agent-versioned-config-plan.test.ts +++ b/apps/api/tests/agent-versioned-config-plan.test.ts @@ -134,6 +134,48 @@ describe("agent versioned config plan", () => { ); }); + test("keeps MCP binding order changes visible", () => { + const plan = planVersionedAgentConfigChange({ + agentStatus: "published", + current: createAgentConfigChangeSnapshot({ + agent, + environment, + mcpServerIds: ["mcp_linear", "mcp_github"], + skillIds: [], + }), + next: createAgentConfigChangeSnapshot({ + agent, + environment, + mcpServerIds: ["mcp_github", "mcp_linear"], + skillIds: [], + }), + }); + + expect(plan.action).toBe("patch-and-restart"); + expect(plan.fieldLabels).toEqual(["MCP Servers"]); + }); + + test("keeps skill order changes visible", () => { + const plan = planVersionedAgentConfigChange({ + agentStatus: "published", + current: createAgentConfigChangeSnapshot({ + agent, + environment, + mcpServerIds: [], + skillIds: ["skill_browser", "skill_shell"], + }), + next: createAgentConfigChangeSnapshot({ + agent, + environment, + mcpServerIds: [], + skillIds: ["skill_shell", "skill_browser"], + }), + }); + + expect(plan.action).toBe("patch-and-restart"); + expect(plan.fieldLabels).toEqual(["Skills"]); + }); + test("classifies advanced provider option edits as patch-and-restart", () => { const plan = planVersionedAgentConfigChange({ agentStatus: "published", diff --git a/pkgs/contracts/src/agent/agent-config-change-plan.contract.ts b/pkgs/contracts/src/agent/agent-config-change-plan.contract.ts index d8f1139a..77aa0960 100644 --- a/pkgs/contracts/src/agent/agent-config-change-plan.contract.ts +++ b/pkgs/contracts/src/agent/agent-config-change-plan.contract.ts @@ -73,6 +73,27 @@ function changed(left: unknown, right: unknown): boolean { return stableStringify(left) !== stableStringify(right); } +function orderedStringArraysChanged(left: readonly string[], right: readonly string[]): boolean { + if (left.length !== right.length) { + return true; + } + + return left.some((value, index) => value !== right[index]); +} + +function skillsChanged( + left: readonly AgentConfigChangeSkill[], + right: readonly AgentConfigChangeSkill[], +): boolean { + if (left.length !== right.length) { + return true; + } + + return left.some( + (skill, index) => skill.id !== right[index]?.id || skill.state !== right[index]?.state, + ); +} + function pushIfChanged(plans: FieldPlan[], input: FieldPlan & { changed: boolean }): void { if (!input.changed) { return; @@ -126,7 +147,7 @@ export function classifyAgentConfigChanges(input: { }); pushIfChanged(fieldPlans, { action: "patch-and-restart", - changed: changed(input.current.mcpServerIds, input.saved.mcpServerIds), + changed: orderedStringArraysChanged(input.current.mcpServerIds, input.saved.mcpServerIds), label: "MCP Servers", rank: 2, }); @@ -156,7 +177,7 @@ export function classifyAgentConfigChanges(input: { }); pushIfChanged(fieldPlans, { action: "patch-and-restart", - changed: changed(input.current.skills, input.saved.skills), + changed: skillsChanged(input.current.skills, input.saved.skills), label: "Skills", rank: 2, });