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", () => { 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"); // 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", () => { 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"); 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"]); }); });