mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 08:03:39 +00:00
291 lines
9.6 KiB
TypeScript
291 lines
9.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";
|
|
|
|
describe("SettingsManager", () => {
|
|
const testDir = join(process.cwd(), "test-settings-tmp");
|
|
const agentDir = join(testDir, "agent");
|
|
const projectDir = join(testDir, "project");
|
|
|
|
beforeEach(() => {
|
|
// Clean up and create fresh directories
|
|
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 });
|
|
}
|
|
});
|
|
|
|
describe("preserves externally added settings", () => {
|
|
it("should preserve enabledModels when changing thinking level", async () => {
|
|
// Create initial settings file
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
theme: "dark",
|
|
defaultModel: "claude-sonnet",
|
|
}),
|
|
);
|
|
|
|
// Create SettingsManager (simulates pi starting up)
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// Simulate user editing settings.json externally to add enabledModels
|
|
const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
currentSettings.enabledModels = ["claude-opus-4-5", "gpt-5.2-codex"];
|
|
writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2));
|
|
|
|
// 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"));
|
|
expect(savedSettings.enabledModels).toEqual(["claude-opus-4-5", "gpt-5.2-codex"]);
|
|
expect(savedSettings.defaultThinkingLevel).toBe("high");
|
|
expect(savedSettings.theme).toBe("dark");
|
|
expect(savedSettings.defaultModel).toBe("claude-sonnet");
|
|
});
|
|
|
|
it("should preserve custom settings when changing theme", async () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
defaultModel: "claude-sonnet",
|
|
}),
|
|
);
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// User adds custom settings externally
|
|
const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
currentSettings.shellPath = "/bin/zsh";
|
|
currentSettings.extensions = ["/path/to/extension.ts"];
|
|
writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2));
|
|
|
|
// User changes theme
|
|
manager.setTheme("light");
|
|
await manager.flush();
|
|
|
|
// Verify all settings preserved
|
|
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
expect(savedSettings.shellPath).toBe("/bin/zsh");
|
|
expect(savedSettings.extensions).toEqual(["/path/to/extension.ts"]);
|
|
expect(savedSettings.theme).toBe("light");
|
|
});
|
|
|
|
it("should let in-memory changes override file changes for same key", async () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
theme: "dark",
|
|
}),
|
|
);
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// User externally sets thinking level to "low"
|
|
const currentSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
currentSettings.defaultThinkingLevel = "low";
|
|
writeFileSync(settingsPath, JSON.stringify(currentSettings, null, 2));
|
|
|
|
// 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"));
|
|
expect(savedSettings.defaultThinkingLevel).toBe("high");
|
|
});
|
|
});
|
|
|
|
describe("packages migration", () => {
|
|
it("should keep local-only extensions in extensions array", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
extensions: ["/local/ext.ts", "./relative/ext.ts"],
|
|
}),
|
|
);
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
expect(manager.getPackages()).toEqual([]);
|
|
expect(manager.getExtensionPaths()).toEqual(["/local/ext.ts", "./relative/ext.ts"]);
|
|
});
|
|
|
|
it("should handle packages with filtering objects", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
packages: [
|
|
"npm:simple-pkg",
|
|
{
|
|
source: "npm:shitty-extensions",
|
|
extensions: ["extensions/oracle.ts"],
|
|
skills: [],
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
const packages = manager.getPackages();
|
|
expect(packages).toHaveLength(2);
|
|
expect(packages[0]).toBe("npm:simple-pkg");
|
|
expect(packages[1]).toEqual({
|
|
source: "npm:shitty-extensions",
|
|
extensions: ["extensions/oracle.ts"],
|
|
skills: [],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("reload", () => {
|
|
it("should reload global settings from disk", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
theme: "dark",
|
|
extensions: ["/before.ts"],
|
|
}),
|
|
);
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify({
|
|
theme: "light",
|
|
extensions: ["/after.ts"],
|
|
defaultModel: "claude-sonnet",
|
|
}),
|
|
);
|
|
|
|
manager.reload();
|
|
|
|
expect(manager.getTheme()).toBe("light");
|
|
expect(manager.getExtensionPaths()).toEqual(["/after.ts"]);
|
|
expect(manager.getDefaultModel()).toBe("claude-sonnet");
|
|
});
|
|
|
|
it("should keep previous settings when file is invalid", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ theme: "dark" }));
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
writeFileSync(settingsPath, "{ invalid json");
|
|
manager.reload();
|
|
|
|
expect(manager.getTheme()).toBe("dark");
|
|
});
|
|
});
|
|
|
|
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("project settings directory creation", () => {
|
|
it("should not create .pi folder when only reading project settings", () => {
|
|
// Create agent dir with global settings, but NO .pi folder in project
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ theme: "dark" }));
|
|
|
|
// Delete the .pi folder that beforeEach created
|
|
rmSync(join(projectDir, ".pi"), { recursive: true });
|
|
|
|
// Create SettingsManager (reads both global and project settings)
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// .pi folder should NOT have been created just from reading
|
|
expect(existsSync(join(projectDir, ".pi"))).toBe(false);
|
|
|
|
// Settings should still be loaded from global
|
|
expect(manager.getTheme()).toBe("dark");
|
|
});
|
|
|
|
it("should create .pi folder when writing project settings", async () => {
|
|
// Create agent dir with global settings, but NO .pi folder in project
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ theme: "dark" }));
|
|
|
|
// Delete the .pi folder that beforeEach created
|
|
rmSync(join(projectDir, ".pi"), { recursive: true });
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
// .pi folder should NOT exist yet
|
|
expect(existsSync(join(projectDir, ".pi"))).toBe(false);
|
|
|
|
// Write a project-specific setting
|
|
manager.setProjectPackages([{ source: "npm:test-pkg" }]);
|
|
await manager.flush();
|
|
|
|
// Now .pi folder should exist
|
|
expect(existsSync(join(projectDir, ".pi"))).toBe(true);
|
|
|
|
// And settings file should be created
|
|
expect(existsSync(join(projectDir, ".pi", "settings.json"))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("shellCommandPrefix", () => {
|
|
it("should load shellCommandPrefix from settings", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ shellCommandPrefix: "shopt -s expand_aliases" }));
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
expect(manager.getShellCommandPrefix()).toBe("shopt -s expand_aliases");
|
|
});
|
|
|
|
it("should return undefined when shellCommandPrefix is not set", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ theme: "dark" }));
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
|
|
expect(manager.getShellCommandPrefix()).toBeUndefined();
|
|
});
|
|
|
|
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");
|
|
expect(savedSettings.theme).toBe("light");
|
|
});
|
|
});
|
|
});
|