diff --git a/src/cli/commands/profile.ts b/src/cli/commands/profile.ts index d1fc276..0f4089a 100644 --- a/src/cli/commands/profile.ts +++ b/src/cli/commands/profile.ts @@ -1,6 +1,5 @@ import type { Command } from "commander"; import { - baseConfig, extractProfileSnapshot, getProfileSnapshot, listProfiles, @@ -120,11 +119,11 @@ export function applyProfileAction( } if (action === "save") { - // Snapshot the explicit target, never the active profile. Resolving - // without a name would fall back to activeProfile and stamp the other - // profile's agents into this one. A new name starts from the base setup. + // Re-save an existing target explicitly. A new name clones the effective + // setup currently in use, so saving a profile works as a "save as" action + // without changing which profile remains active. const existing = getProfileSnapshot(config, name); - const source = existing === undefined ? baseConfig(config) : resolveEffectiveConfig(config, name); + const source = existing === undefined ? resolveEffectiveConfig(config) : resolveEffectiveConfig(config, name); const snapshot = extractProfileSnapshot(source); const profiles = { ...(config.profiles ?? {}), [name]: snapshot }; const next: RunAgentConfig = { ...config, profiles }; @@ -243,7 +242,7 @@ export function registerProfileCommand(program: Command): void { const named: ReadonlyArray<{ action: Exclude; description: string }> = [ { action: "show", description: "print a saved profile" }, - { action: "save", description: "snapshot the base setup as a profile (the active profile itself re-saves its own setup)" }, + { action: "save", description: "snapshot the current setup as a profile (the active profile itself re-saves its own setup)" }, { action: "use", description: "make a profile the active setup" }, { action: "delete", description: "delete a saved profile" }, ]; diff --git a/tests/profiles.test.ts b/tests/profiles.test.ts index 66bbc5e..2c48663 100644 --- a/tests/profiles.test.ts +++ b/tests/profiles.test.ts @@ -140,7 +140,7 @@ describe("profile actions", () => { expect(result.config).not.toHaveProperty("activeProfile"); }); - it("saves a new profile from the base without active-profile contamination", () => { + it("saves a new profile from the active effective setup", () => { const config: RunAgentConfig = { defaultAgent: "claude", agents: { general: { harness: "claude", model: "base-model" } }, @@ -149,7 +149,7 @@ describe("profile actions", () => { }; const result = applyProfileAction(config, "save", "b"); expect(result.save).toBe(true); - expect(result.config.profiles?.b?.agents?.general?.model).toBe("base-model"); + expect(result.config.profiles?.b?.agents?.general?.model).toBe("from-a"); expect(result.config.profiles?.a?.agents?.general?.model).toBe("from-a"); }); @@ -164,6 +164,36 @@ describe("profile actions", () => { expect(result.config.profiles?.a?.agents?.general?.model).toBe("from-a"); }); + it("re-saving an existing profile snapshots the target, not the active one", () => { + const config: RunAgentConfig = { + defaultAgent: "claude", + agents: { general: { harness: "claude", model: "base-model" } }, + activeProfile: "a", + profiles: { + a: { agents: { general: { harness: "codex", model: "from-a" } } }, + b: { agents: { general: { harness: "codex", model: "from-b" } } }, + }, + }; + const result = applyProfileAction(config, "save", "b"); + expect(result.save).toBe(true); + // The named target owns the snapshot: b must keep b's setup even while + // a is active, otherwise "profile save b" silently copies a into b. + expect(result.config.profiles?.b?.agents?.general?.model).toBe("from-b"); + expect(result.config.profiles?.a?.agents?.general?.model).toBe("from-a"); + expect(result.config.activeProfile).toBe("a"); + }); + + it("saving a new profile with no active profile falls back to the base", () => { + const config: RunAgentConfig = { + defaultAgent: "claude", + agents: { general: { harness: "claude", model: "base-model" } }, + profiles: { a: { agents: { general: { harness: "codex", model: "from-a" } } } }, + }; + const result = applyProfileAction(config, "save", "b"); + expect(result.config.profiles?.b?.agents?.general?.model).toBe("base-model"); + expect(result.config.profiles?.b?.agents?.general?.harness).toBe("claude"); + }); + it("uses, lists and shows profiles", () => { const withProfile = applyProfileAction(saved, "save", "max").config; const used = applyProfileAction(withProfile, "use", "max");