mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 14:03:49 +00:00
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
import { join } from "path";
|
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
import { SettingsManager } from "../src/core/settings-manager.js";
|
|
|
|
/**
|
|
* Tests for the fix to a bug where external file changes to arrays were overwritten.
|
|
*
|
|
* The bug scenario was:
|
|
* 1. Pi starts with settings.json containing packages: ["npm:some-pkg"]
|
|
* 2. User externally edits file to packages: []
|
|
* 3. User changes an unrelated setting (e.g., theme) via UI
|
|
* 4. save() would overwrite packages back to ["npm:some-pkg"] from stale in-memory state
|
|
*
|
|
* The fix tracks which fields were explicitly modified during the session, and only
|
|
* those fields override file values during save().
|
|
*/
|
|
describe("SettingsManager - External Edit Preservation", () => {
|
|
const testDir = join(process.cwd(), "test-settings-bug-tmp");
|
|
const agentDir = join(testDir, "agent");
|
|
const projectDir = join(testDir, "project");
|
|
|
|
beforeEach(() => {
|
|
if (existsSync(testDir)) {
|
|
rmSync(testDir, { recursive: true });
|
|
}
|
|
mkdirSync(agentDir, { recursive: true });
|
|
mkdirSync(join(projectDir, ".pi"), { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (existsSync(testDir)) {
|
|
rmSync(testDir, { recursive: true });
|
|
}
|
|
});
|
|
|
|
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
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
theme: "dark",
|
|
packages: ["npm:pi-mcp-adapter"],
|
|
}),
|
|
);
|
|
|
|
// Pi starts up, loads settings into memory
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// At this point, globalSettings.packages = ["npm:pi-mcp-adapter"]
|
|
expect(manager.getPackages()).toEqual(["npm:pi-mcp-adapter"]);
|
|
|
|
// User externally edits settings.json to remove the package
|
|
const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
currentSettings.packages = []; // User wants to remove this!
|
|
writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2));
|
|
|
|
// Verify file was changed
|
|
expect(JSON.parse(readFileSync(settingsPath, "utf-8")).packages).toEqual([]);
|
|
|
|
// 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"));
|
|
|
|
expect(savedSettings.packages).toEqual([]);
|
|
expect(savedSettings.theme).toBe("light");
|
|
});
|
|
|
|
it("should preserve file changes to extensions array when changing unrelated setting", async () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
theme: "dark",
|
|
extensions: ["/old/extension.ts"],
|
|
}),
|
|
);
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// User externally updates extensions
|
|
const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
currentSettings.extensions = ["/new/extension.ts"];
|
|
writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2));
|
|
|
|
// 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"]);
|
|
});
|
|
});
|