Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/cli/commands/profile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Command } from "commander";
import {
baseConfig,
extractProfileSnapshot,
getProfileSnapshot,
listProfiles,
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -243,7 +242,7 @@ export function registerProfileCommand(program: Command): void {

const named: ReadonlyArray<{ action: Exclude<ProfileAction, "list">; 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" },
];
Expand Down
34 changes: 32 additions & 2 deletions tests/profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" } },
Expand All @@ -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");
});

Expand All @@ -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");
Expand Down
Loading