mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 23:01:30 +00:00
When 429/500 errors occur during tool execution, empty assistant messages with stopReason='error' get persisted. These break the tool_use -> tool_result chain for Claude/Gemini APIs. Added centralized filtering in transformMessages to skip assistant messages with empty content and no tool calls. Provider-level filters remain for defense-in-depth.
140 lines
4.9 KiB
TypeScript
140 lines
4.9 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", () => {
|
|
// 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");
|
|
|
|
// 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", () => {
|
|
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");
|
|
|
|
// 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", () => {
|
|
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");
|
|
|
|
// In-memory change should win
|
|
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
expect(savedSettings.defaultThinkingLevel).toBe("high");
|
|
});
|
|
});
|
|
|
|
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", () => {
|
|
const settingsPath = join(agentDir, "settings.json");
|
|
writeFileSync(settingsPath, JSON.stringify({ shellCommandPrefix: "shopt -s expand_aliases" }));
|
|
|
|
const manager = SettingsManager.create(projectDir, agentDir);
|
|
manager.setTheme("light");
|
|
|
|
const savedSettings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
expect(savedSettings.shellCommandPrefix).toBe("shopt -s expand_aliases");
|
|
expect(savedSettings.theme).toBe("light");
|
|
});
|
|
});
|
|
});
|