mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 07:03:18 +00:00
Draft setups and form preferences validated option values as booleans, strings or null, but the options they now receive come from agents and profiles and can be objects, arrays or numbers. One such value failed validation and wiped the whole saved layout or the saved preferences. Both schemas now accept any JSON value. The legacy-daemon translation skipped the snapshots carried by agent_created, agent_resumed and timeline responses, so a created agent or a timeline refresh left the SDK handle without options and controls. The profile journey asserted the create request's options field; the wire field is providerOptions.
303 lines
9.6 KiB
TypeScript
303 lines
9.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { CreateAgentPreferencesService } from "./service";
|
|
import {
|
|
applyAgentProfilePreferences,
|
|
mergeCreateAgentSelectionPreferences,
|
|
mergeProviderPreferences,
|
|
parseFormPreferences,
|
|
} from "./preferences";
|
|
import { FakeCreateAgentPreferenceStorage } from "./test-utils/fake-preference-storage";
|
|
|
|
describe("create agent preferences", () => {
|
|
it("keeps the selected mode after saving model and thinking", async () => {
|
|
const storage = new FakeCreateAgentPreferenceStorage();
|
|
const preferences = new CreateAgentPreferencesService(storage);
|
|
|
|
const modelWrite = preferences.update((current) =>
|
|
mergeProviderPreferences({
|
|
preferences: current,
|
|
provider: "codex",
|
|
updates: { model: "gpt-5.5", thinkingByModel: { "gpt-5.5": "high" } },
|
|
}),
|
|
);
|
|
await storage.nextWrite();
|
|
|
|
const modeWrite = preferences.update((current) =>
|
|
mergeProviderPreferences({
|
|
preferences: current,
|
|
provider: "codex",
|
|
updates: { mode: "full-access" },
|
|
}),
|
|
);
|
|
|
|
expect(storage.pendingWriteCount()).toBe(1);
|
|
storage.finishOldestWrite();
|
|
await modelWrite;
|
|
|
|
await storage.nextWrite();
|
|
storage.finishOldestWrite();
|
|
await modeWrite;
|
|
|
|
expect(storage.savedPreferences()).toEqual({
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.5",
|
|
thinkingByModel: { "gpt-5.5": "high" },
|
|
mode: "full-access",
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not commit a failed write or leak it into the next update", async () => {
|
|
const storage = new FakeCreateAgentPreferenceStorage();
|
|
const preferences = new CreateAgentPreferencesService(storage);
|
|
|
|
const failedWrite = preferences.update({ provider: "claude" });
|
|
await storage.nextWrite();
|
|
storage.failOldestWrite(new Error("disk full"));
|
|
await expect(failedWrite).rejects.toThrow("disk full");
|
|
|
|
expect(await preferences.load()).toEqual({});
|
|
|
|
const successfulWrite = preferences.update({ isolation: "worktree" });
|
|
await storage.nextWrite();
|
|
storage.finishOldestWrite();
|
|
await successfulWrite;
|
|
|
|
expect(storage.savedPreferences()).toEqual({ isolation: "worktree" });
|
|
});
|
|
|
|
it("flushes the full create-agent selection into provider preferences", async () => {
|
|
const storage = new FakeCreateAgentPreferenceStorage();
|
|
const preferences = new CreateAgentPreferencesService(storage);
|
|
|
|
const saveSelection = preferences.update((current) =>
|
|
mergeCreateAgentSelectionPreferences({
|
|
preferences: current,
|
|
provider: "codex",
|
|
modelId: "gpt-5.5",
|
|
modeId: "full-access",
|
|
thinkingOptionId: "high",
|
|
options: { fast_mode: true },
|
|
}),
|
|
);
|
|
|
|
await storage.nextWrite();
|
|
storage.finishOldestWrite();
|
|
await saveSelection;
|
|
|
|
expect(storage.savedPreferences()).toEqual({
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.5",
|
|
mode: "full-access",
|
|
thinkingByModel: { "gpt-5.5": "high" },
|
|
options: { fast_mode: true },
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("does not erase a saved mode when a later partial update has no mode", () => {
|
|
expect(
|
|
mergeProviderPreferences({
|
|
preferences: {
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.5",
|
|
mode: "full-access",
|
|
thinkingByModel: { "gpt-5.5": "high" },
|
|
},
|
|
},
|
|
},
|
|
provider: "codex",
|
|
updates: {
|
|
model: "gpt-5.6",
|
|
mode: undefined,
|
|
thinkingByModel: undefined,
|
|
options: undefined,
|
|
},
|
|
}),
|
|
).toEqual({
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.6",
|
|
mode: "full-access",
|
|
thinkingByModel: { "gpt-5.5": "high" },
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("erases a saved mode when a complete selection explicitly has no mode", () => {
|
|
expect(
|
|
mergeCreateAgentSelectionPreferences({
|
|
preferences: {
|
|
provider: "pi",
|
|
providerPreferences: { pi: { model: "anthropic/sonnet", mode: "full-access" } },
|
|
},
|
|
provider: "pi",
|
|
modelId: "anthropic/sonnet",
|
|
modeId: null,
|
|
}),
|
|
).toEqual({
|
|
provider: "pi",
|
|
providerPreferences: { pi: { model: "anthropic/sonnet" } },
|
|
});
|
|
});
|
|
|
|
it("repairs the previous provider while applying a profile", () => {
|
|
expect(
|
|
applyAgentProfilePreferences({
|
|
preferences: {
|
|
provider: "pi",
|
|
providerPreferences: {
|
|
pi: { model: "anthropic/sonnet", mode: "full-access" },
|
|
mock: { model: "ten-second-stream", mode: "load-test" },
|
|
},
|
|
},
|
|
previousProvider: "pi",
|
|
previousProviderModeIds: [],
|
|
provider: "mock",
|
|
modelId: "one-minute-stream",
|
|
modeId: "approval-test",
|
|
thinkingOptionId: "",
|
|
options: {},
|
|
}),
|
|
).toEqual({
|
|
provider: "mock",
|
|
providerPreferences: {
|
|
pi: { model: "anthropic/sonnet" },
|
|
mock: { model: "one-minute-stream", mode: "approval-test", options: {} },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("loads invalid stored preferences as empty preferences", () => {
|
|
expect(parseFormPreferences({ providerPreferences: { codex: { mode: 42 } } })).toEqual({});
|
|
});
|
|
|
|
it("strips the explicitly supported legacy location fields", () => {
|
|
expect(
|
|
parseFormPreferences({
|
|
workingDir: "/old/workspace",
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.4-mini",
|
|
mode: "full-access",
|
|
thinkingOptionId: "high",
|
|
},
|
|
},
|
|
serverId: "old-host",
|
|
}),
|
|
).toEqual({
|
|
provider: "codex",
|
|
providerPreferences: {
|
|
codex: {
|
|
model: "gpt-5.4-mini",
|
|
mode: "full-access",
|
|
thinkingByModel: { "gpt-5.4-mini": "high" },
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("reads preferences saved as featureValues as options", () => {
|
|
expect(
|
|
parseFormPreferences({
|
|
provider: "codex",
|
|
providerPreferences: { codex: { featureValues: { service_tier: "priority" } } },
|
|
}),
|
|
).toEqual({
|
|
provider: "codex",
|
|
providerPreferences: { codex: { options: { service_tier: "priority" } } },
|
|
});
|
|
});
|
|
|
|
it("keeps options that are not booleans or strings, such as a profile's sandbox settings", () => {
|
|
const options = {
|
|
sandbox_workspace_write: { network_access: true, writable_roots: ["/tmp"] },
|
|
max_turns: 3,
|
|
};
|
|
|
|
expect(
|
|
parseFormPreferences({ provider: "codex", providerPreferences: { codex: { options } } }),
|
|
).toEqual({ provider: "codex", providerPreferences: { codex: { options } } });
|
|
});
|
|
|
|
it("rejects unknown persisted fields outside the explicit legacy shape", () => {
|
|
expect(parseFormPreferences({ provider: "codex", surprise: true })).toEqual({});
|
|
});
|
|
|
|
it("persists and reloads the workspace isolation choice", async () => {
|
|
const storage = new FakeCreateAgentPreferenceStorage();
|
|
const preferences = new CreateAgentPreferencesService(storage);
|
|
|
|
const save = preferences.update({ isolation: "worktree" });
|
|
await storage.nextWrite();
|
|
storage.finishOldestWrite();
|
|
await save;
|
|
|
|
expect(storage.savedPreferences()).toEqual({ isolation: "worktree" });
|
|
expect(await new CreateAgentPreferencesService(storage).load()).toEqual({
|
|
isolation: "worktree",
|
|
});
|
|
});
|
|
|
|
it("preserves legacy favourites across preference writes until host migration", async () => {
|
|
const favoriteModels = [{ provider: "claude", modelId: "opus" }];
|
|
const storage = new FakeCreateAgentPreferenceStorage({ stored: { favoriteModels } });
|
|
const preferences = new CreateAgentPreferencesService(storage);
|
|
|
|
const save = preferences.update({ isolation: "worktree" });
|
|
await storage.nextWrite();
|
|
storage.finishOldestWrite();
|
|
await save;
|
|
|
|
expect(storage.savedPreferences()).toEqual({ favoriteModels, isolation: "worktree" });
|
|
});
|
|
|
|
it("treats stored preferences without an isolation choice as undefined", () => {
|
|
expect(parseFormPreferences({ provider: "codex" }).isolation).toBeUndefined();
|
|
});
|
|
|
|
it("rejects an unknown isolation value as invalid stored preferences", () => {
|
|
expect(parseFormPreferences({ provider: "codex", isolation: "sandbox" })).toEqual({});
|
|
});
|
|
|
|
it("persists and reloads a terminal launch target", async () => {
|
|
const storage = new FakeCreateAgentPreferenceStorage();
|
|
const preferences = new CreateAgentPreferencesService(storage);
|
|
|
|
const save = preferences.update({ launchTarget: { kind: "terminal", profileId: "claude" } });
|
|
await storage.nextWrite();
|
|
storage.finishOldestWrite();
|
|
await save;
|
|
|
|
expect(storage.savedPreferences()).toEqual({
|
|
launchTarget: { kind: "terminal", profileId: "claude" },
|
|
});
|
|
expect(await new CreateAgentPreferencesService(storage).load()).toEqual({
|
|
launchTarget: { kind: "terminal", profileId: "claude" },
|
|
});
|
|
});
|
|
|
|
it("treats stored preferences without a launch target as undefined, defaulting to chat", () => {
|
|
expect(parseFormPreferences({ provider: "codex" }).launchTarget).toBeUndefined();
|
|
});
|
|
|
|
it("rejects a terminal launch target missing a profileId as invalid stored preferences", () => {
|
|
expect(parseFormPreferences({ launchTarget: { kind: "terminal" } })).toEqual({});
|
|
});
|
|
|
|
it("rejects an unknown launch target kind as invalid stored preferences", () => {
|
|
expect(parseFormPreferences({ launchTarget: { kind: "shell" } })).toEqual({});
|
|
});
|
|
});
|