refactor(coding-agent): improve settings storage semantics and error handling

This commit is contained in:
Mario Zechner 2026-02-17 00:08:32 +01:00
parent 5133697bc4
commit de2736bad0
7 changed files with 386 additions and 152 deletions

View file

@ -34,7 +34,7 @@ describe("SettingsManager - External Edit Preservation", () => {
}
});
it("should preserve file changes to packages array when changing unrelated setting", () => {
it("should preserve file changes to packages array when changing unrelated setting", async () => {
const settingsPath = join(agentDir, "settings.json");
// Initial state: packages has one item
@ -62,6 +62,7 @@ describe("SettingsManager - External Edit Preservation", () => {
// User changes an UNRELATED setting via UI (this triggers save)
manager.setTheme("light");
await manager.flush();
// With the fix, packages should be preserved as [] (not reverted to startup value)
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
@ -70,7 +71,7 @@ describe("SettingsManager - External Edit Preservation", () => {
expect(savedSettings.theme).toBe("light");
});
it("should preserve file changes to extensions array when changing unrelated setting", () => {
it("should preserve file changes to extensions array when changing unrelated setting", async () => {
const settingsPath = join(agentDir, "settings.json");
writeFileSync(
@ -90,10 +91,57 @@ describe("SettingsManager - External Edit Preservation", () => {
// Change unrelated setting
manager.setDefaultThinkingLevel("high");
await manager.flush();
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
// With the fix, extensions should be preserved (not reverted to startup value)
expect(savedSettings.extensions).toEqual(["/new/extension.ts"]);
});
it("should preserve external project settings changes when updating unrelated project field", async () => {
const projectSettingsPath = join(projectDir, ".pi", "settings.json");
writeFileSync(
projectSettingsPath,
JSON.stringify({
extensions: ["./old-extension.ts"],
prompts: ["./old-prompt.md"],
}),
);
const manager = SettingsManager.create(projectDir, agentDir);
const currentProjectSettings = JSON.parse(readFileSync(projectSettingsPath, "utf-8"));
currentProjectSettings.prompts = ["./new-prompt.md"];
writeFileSync(projectSettingsPath, JSON.stringify(currentProjectSettings, null, 2));
manager.setProjectExtensionPaths(["./updated-extension.ts"]);
await manager.flush();
const savedProjectSettings = JSON.parse(readFileSync(projectSettingsPath, "utf-8"));
expect(savedProjectSettings.prompts).toEqual(["./new-prompt.md"]);
expect(savedProjectSettings.extensions).toEqual(["./updated-extension.ts"]);
});
it("should let in-memory project changes override external changes for the same project field", async () => {
const projectSettingsPath = join(projectDir, ".pi", "settings.json");
writeFileSync(
projectSettingsPath,
JSON.stringify({
extensions: ["./initial-extension.ts"],
}),
);
const manager = SettingsManager.create(projectDir, agentDir);
const currentProjectSettings = JSON.parse(readFileSync(projectSettingsPath, "utf-8"));
currentProjectSettings.extensions = ["./external-extension.ts"];
writeFileSync(projectSettingsPath, JSON.stringify(currentProjectSettings, null, 2));
manager.setProjectExtensionPaths(["./in-memory-extension.ts"]);
await manager.flush();
const savedProjectSettings = JSON.parse(readFileSync(projectSettingsPath, "utf-8"));
expect(savedProjectSettings.extensions).toEqual(["./in-memory-extension.ts"]);
});
});

View file

@ -24,7 +24,7 @@ describe("SettingsManager", () => {
});
describe("preserves externally added settings", () => {
it("should preserve enabledModels when changing thinking level", () => {
it("should preserve enabledModels when changing thinking level", async () => {
// Create initial settings file
const settingsPath = join(agentDir, "settings.json");
writeFileSync(
@ -45,6 +45,7 @@ describe("SettingsManager", () => {
// User changes thinking level via Shift+Tab
manager.setDefaultThinkingLevel("high");
await manager.flush();
// Verify enabledModels is preserved
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
@ -54,7 +55,7 @@ describe("SettingsManager", () => {
expect(savedSettings.defaultModel).toBe("claude-sonnet");
});
it("should preserve custom settings when changing theme", () => {
it("should preserve custom settings when changing theme", async () => {
const settingsPath = join(agentDir, "settings.json");
writeFileSync(
settingsPath,
@ -73,6 +74,7 @@ describe("SettingsManager", () => {
// User changes theme
manager.setTheme("light");
await manager.flush();
// Verify all settings preserved
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
@ -81,7 +83,7 @@ describe("SettingsManager", () => {
expect(savedSettings.theme).toBe("light");
});
it("should let in-memory changes override file changes for same key", () => {
it("should let in-memory changes override file changes for same key", async () => {
const settingsPath = join(agentDir, "settings.json");
writeFileSync(
settingsPath,
@ -99,6 +101,7 @@ describe("SettingsManager", () => {
// But then changes it via UI to "high"
manager.setDefaultThinkingLevel("high");
await manager.flush();
// In-memory change should win
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
@ -193,6 +196,22 @@ describe("SettingsManager", () => {
});
});
describe("error tracking", () => {
it("should collect and clear load errors via drainErrors", () => {
const globalSettingsPath = join(agentDir, "settings.json");
const projectSettingsPath = join(projectDir, ".pi", "settings.json");
writeFileSync(globalSettingsPath, "{ invalid global json");
writeFileSync(projectSettingsPath, "{ invalid project json");
const manager = SettingsManager.create(projectDir, agentDir);
const errors = manager.drainErrors();
expect(errors).toHaveLength(2);
expect(errors.map((e) => e.scope).sort()).toEqual(["global", "project"]);
expect(manager.drainErrors()).toEqual([]);
});
});
describe("shellCommandPrefix", () => {
it("should load shellCommandPrefix from settings", () => {
const settingsPath = join(agentDir, "settings.json");
@ -212,12 +231,13 @@ describe("SettingsManager", () => {
expect(manager.getShellCommandPrefix()).toBeUndefined();
});
it("should preserve shellCommandPrefix when saving unrelated settings", () => {
it("should preserve shellCommandPrefix when saving unrelated settings", async () => {
const settingsPath = join(agentDir, "settings.json");
writeFileSync(settingsPath, JSON.stringify({ shellCommandPrefix: "shopt -s expand_aliases" }));
const manager = SettingsManager.create(projectDir, agentDir);
manager.setTheme("light");
await manager.flush();
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
expect(savedSettings.shellCommandPrefix).toBe("shopt -s expand_aliases");