clanker-agent/packages/coding-agent/test/settings-manager-bug.test.ts
Harivansh Rathi 536241053c refactor: finish companion rename migration
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures.

Verification:
- semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references
- npm run check in apps/companion-os (fails in this worktree: biome not found)

Co-authored-by: Codex <noreply@openai.com>
2026-03-10 07:39:32 -05:00

165 lines
5.6 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. Companion 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, ".companion"), { 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:companion-mcp-adapter"],
}),
);
// Companion starts up and loads settings into memory
const manager = SettingsManager.create(projectDir, agentDir);
// At this point, globalSettings.packages = ["npm:companion-mcp-adapter"]
expect(manager.getPackages()).toEqual(["npm:companion-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, ".companion", "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, ".companion", "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",
]);
});
});