Revert "Refactor settings manager" (#40)

This commit is contained in:
Shubham Naik
2025-10-30 13:48:07 -07:00
committed by GitHub
parent d144dd50d9
commit f71d2c9b66
7 changed files with 29 additions and 929 deletions

View File

@@ -1,8 +1,8 @@
import Letta from "@letta-ai/letta-client";
import { settingsManager } from "../settings-manager";
import { loadSettings, updateSettings } from "../settings";
export async function getClient() {
const settings = settingsManager.getSettings();
const settings = await loadSettings();
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
if (!apiKey) {
@@ -34,7 +34,7 @@ export async function getClient() {
}
if (needsUpdate) {
settingsManager.updateSettings({ env: updatedEnv });
await updateSettings({ env: updatedEnv });
}
return new Letta({ apiKey, baseURL });

View File

@@ -11,7 +11,7 @@ import {
loadProjectSettings,
updateProjectSettings,
} from "../project-settings";
import { settingsManager } from "../settings-manager";
import { loadSettings, updateSettings } from "../settings";
import { getToolNames } from "../tools/manager";
import { getClient } from "./client";
import { getDefaultMemoryBlocks } from "./memory";
@@ -37,7 +37,7 @@ export async function createAgent(
const defaultMemoryBlocks = await getDefaultMemoryBlocks();
// Load global shared memory blocks from user settings
const settings = settingsManager.getSettings();
const settings = await loadSettings();
const globalSharedBlockIds = settings.globalSharedBlockIds;
// Load project-local shared blocks from project settings
@@ -117,7 +117,7 @@ export async function createAgent(
// Save newly created global block IDs to user settings
if (Object.keys(newGlobalBlockIds).length > 0) {
settingsManager.updateSettings({
await updateSettings({
globalSharedBlockIds: {
...globalSharedBlockIds,
...newGlobalBlockIds,

View File

@@ -638,8 +638,8 @@ export default function App({
setTokenStreamingEnabled(newValue);
// Save to settings
const { settingsManager } = await import("../settings-manager");
settingsManager.updateSettings({ tokenStreaming: newValue });
const { updateSettings } = await import("../settings");
await updateSettings({ tokenStreaming: newValue });
// Update the same command with final result
buffersRef.current.byId.set(cmdId, {

View File

@@ -12,11 +12,11 @@ import { SessionStats } from "./agent/stats";
import { createBuffers, toLines } from "./cli/helpers/accumulator";
import { safeJsonParseOr } from "./cli/helpers/safeJsonParse";
import { drainStream } from "./cli/helpers/stream";
import { settingsManager } from "./settings-manager";
import { loadSettings, updateSettings } from "./settings";
import { checkToolPermission, executeTool } from "./tools/manager";
export async function handleHeadlessCommand(argv: string[]) {
const settings = settingsManager.getSettings();
const settings = await loadSettings();
// Parse CLI args
const { values, positionals } = parseArgs({
@@ -105,9 +105,9 @@ export async function handleHeadlessCommand(argv: string[]) {
}
// Save agent ID to both project and global settings
await settingsManager.loadLocalProjectSettings();
settingsManager.updateLocalProjectSettings({ lastAgent: agent.id });
settingsManager.updateSettings({ lastAgent: agent.id });
const { updateProjectSettings } = await import("./settings");
await updateProjectSettings({ lastAgent: agent.id });
await updateSettings({ lastAgent: agent.id });
// Validate output format
const outputFormat =

View File

@@ -4,7 +4,7 @@ import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents"
import { getResumeData, type ResumeData } from "./agent/check-approval";
import { getClient } from "./agent/client";
import { permissionMode } from "./permissions/mode";
import { settingsManager } from "./settings-manager";
import { loadSettings } from "./settings";
import { loadTools, upsertToolsToServer } from "./tools/manager";
function printHelp() {
@@ -51,17 +51,8 @@ EXAMPLES
}
async function main() {
// Initialize settings manager (loads settings once into memory)
await settingsManager.initialize();
const settings = settingsManager.getSettings();
// set LETTA_API_KEY from environment if available
if (process.env.LETTA_API_KEY && !settings.env?.LETTA_API_KEY) {
settings.env = settings.env || {};
settings.env.LETTA_API_KEY = process.env.LETTA_API_KEY;
settingsManager.updateSettings({ env: settings.env });
}
// Load settings first (creates default settings file if it doesn't exist)
const settings = await loadSettings();
// Parse command-line arguments (Bun-idiomatic approach using parseArgs)
let values: Record<string, unknown>;
@@ -121,7 +112,7 @@ async function main() {
const isHeadless = values.prompt || values.run || !process.stdin.isTTY;
// Validate API key early before any UI rendering
const apiKey = settings.env?.LETTA_API_KEY;
const apiKey = process.env.LETTA_API_KEY || settings.env?.LETTA_API_KEY;
if (!apiKey) {
console.error("Missing LETTA_API_KEY");
console.error(
@@ -222,6 +213,8 @@ async function main() {
setLoadingState("initializing");
const { createAgent } = await import("./agent/create");
const { updateSettings, loadProjectSettings, updateProjectSettings } =
await import("./settings");
let agent: AgentState | null = null;
@@ -245,18 +238,14 @@ async function main() {
// Priority 3: Try to resume from project settings (.letta/settings.local.json)
if (!agent) {
await settingsManager.loadLocalProjectSettings();
const localProjectSettings =
settingsManager.getLocalProjectSettings();
if (localProjectSettings?.lastAgent) {
const projectSettings = await loadProjectSettings();
if (projectSettings?.lastAgent) {
try {
agent = await client.agents.retrieve(
localProjectSettings.lastAgent,
);
// console.log(`Resuming project agent ${localProjectSettings.lastAgent}...`);
agent = await client.agents.retrieve(projectSettings.lastAgent);
// console.log(`Resuming project agent ${projectSettings.lastAgent}...`);
} catch (error) {
console.error(
`Project agent ${localProjectSettings.lastAgent} not found (error: ${JSON.stringify(error)}), creating new one...`,
`Project agent ${projectSettings.lastAgent} not found (error: ${JSON.stringify(error)}), creating new one...`,
);
}
}
@@ -280,15 +269,15 @@ async function main() {
}
// Save agent ID to both project and global settings
settingsManager.updateLocalProjectSettings({ lastAgent: agent.id });
settingsManager.updateSettings({ lastAgent: agent.id });
await updateProjectSettings({ lastAgent: agent.id });
await updateSettings({ lastAgent: agent.id });
// Check if we're resuming an existing agent
const localProjectSettings = settingsManager.getLocalProjectSettings();
const projectSettings = await loadProjectSettings();
const isResumingProject =
!forceNew &&
localProjectSettings?.lastAgent &&
agent.id === localProjectSettings.lastAgent;
projectSettings?.lastAgent &&
agent.id === projectSettings.lastAgent;
const resuming = continueSession || !!agentIdArg || isResumingProject;
setIsResumingSession(resuming);

View File

@@ -1,377 +0,0 @@
// src/settings-manager.ts
// In-memory settings manager that loads once and provides sync access
import { homedir } from "node:os";
import { join } from "node:path";
import type { PermissionRules } from "./permissions/types";
import { exists, mkdir, readFile, writeFile } from "./utils/fs.js";
export type UIMode = "simple" | "rich";
export interface Settings {
uiMode: UIMode;
lastAgent: string | null;
tokenStreaming: boolean;
globalSharedBlockIds: Record<string, string>;
permissions?: PermissionRules;
env?: Record<string, string>;
}
export interface ProjectSettings {
localSharedBlockIds: Record<string, string>;
}
export interface LocalProjectSettings {
lastAgent: string | null;
permissions?: PermissionRules;
}
const DEFAULT_SETTINGS: Settings = {
uiMode: "simple",
lastAgent: null,
tokenStreaming: false,
globalSharedBlockIds: {},
};
const DEFAULT_PROJECT_SETTINGS: ProjectSettings = {
localSharedBlockIds: {},
};
const DEFAULT_LOCAL_PROJECT_SETTINGS: LocalProjectSettings = {
lastAgent: null,
};
class SettingsManager {
private settings: Settings | null = null;
private projectSettings: Map<string, ProjectSettings> = new Map();
private localProjectSettings: Map<string, LocalProjectSettings> = new Map();
private initialized = false;
/**
* Initialize the settings manager (loads from disk)
* Should be called once at app startup
*/
async initialize(): Promise<void> {
if (this.initialized) return;
const settingsPath = this.getSettingsPath();
try {
// Check if settings file exists
if (!exists(settingsPath)) {
// Create default settings file
this.settings = { ...DEFAULT_SETTINGS };
await this.persistSettings();
} else {
// Read and parse settings
const content = await readFile(settingsPath);
const loadedSettings = JSON.parse(content) as Settings;
// Merge with defaults in case new fields were added
this.settings = { ...DEFAULT_SETTINGS, ...loadedSettings };
}
this.initialized = true;
} catch (error) {
console.error("Error loading settings, using defaults:", error);
this.settings = { ...DEFAULT_SETTINGS };
this.initialized = true;
}
}
/**
* Get all settings (synchronous, from memory)
*/
getSettings(): Settings {
if (!this.initialized || !this.settings) {
throw new Error(
"Settings not initialized. Call settingsManager.initialize() first.",
);
}
return { ...this.settings };
}
/**
* Get a specific setting value (synchronous)
*/
getSetting<K extends keyof Settings>(key: K): Settings[K] {
return this.getSettings()[key];
}
/**
* Update settings (synchronous in-memory, async persist)
*/
updateSettings(updates: Partial<Settings>): void {
if (!this.initialized || !this.settings) {
throw new Error(
"Settings not initialized. Call settingsManager.initialize() first.",
);
}
this.settings = { ...this.settings, ...updates };
// Persist asynchronously (fire and forget, errors logged)
this.persistSettings().catch((error) => {
console.error("Failed to persist settings:", error);
});
}
/**
* Load project settings for a specific directory
*/
async loadProjectSettings(
workingDirectory: string = process.cwd(),
): Promise<ProjectSettings> {
// Check cache first
const cached = this.projectSettings.get(workingDirectory);
if (cached) {
return { ...cached };
}
const settingsPath = this.getProjectSettingsPath(workingDirectory);
try {
if (!exists(settingsPath)) {
const defaults = { ...DEFAULT_PROJECT_SETTINGS };
this.projectSettings.set(workingDirectory, defaults);
return defaults;
}
const content = await readFile(settingsPath);
const rawSettings = JSON.parse(content) as Record<string, unknown>;
const projectSettings: ProjectSettings = {
localSharedBlockIds:
(rawSettings.localSharedBlockIds as Record<string, string>) ?? {},
};
this.projectSettings.set(workingDirectory, projectSettings);
return { ...projectSettings };
} catch (error) {
console.error("Error loading project settings, using defaults:", error);
const defaults = { ...DEFAULT_PROJECT_SETTINGS };
this.projectSettings.set(workingDirectory, defaults);
return defaults;
}
}
/**
* Get project settings (synchronous, from memory)
*/
getProjectSettings(
workingDirectory: string = process.cwd(),
): ProjectSettings {
const cached = this.projectSettings.get(workingDirectory);
if (!cached) {
throw new Error(
`Project settings for ${workingDirectory} not loaded. Call loadProjectSettings() first.`,
);
}
return { ...cached };
}
/**
* Update project settings (synchronous in-memory, async persist)
*/
updateProjectSettings(
updates: Partial<ProjectSettings>,
workingDirectory: string = process.cwd(),
): void {
const current = this.projectSettings.get(workingDirectory);
if (!current) {
throw new Error(
`Project settings for ${workingDirectory} not loaded. Call loadProjectSettings() first.`,
);
}
const updated = { ...current, ...updates };
this.projectSettings.set(workingDirectory, updated);
// Persist asynchronously
this.persistProjectSettings(workingDirectory).catch((error) => {
console.error("Failed to persist project settings:", error);
});
}
/**
* Persist settings to disk (private helper)
*/
private async persistSettings(): Promise<void> {
if (!this.settings) return;
const settingsPath = this.getSettingsPath();
const dirPath = join(homedir(), ".letta");
try {
if (!exists(dirPath)) {
await mkdir(dirPath, { recursive: true });
}
await writeFile(settingsPath, JSON.stringify(this.settings, null, 2));
} catch (error) {
console.error("Error saving settings:", error);
throw error;
}
}
/**
* Persist project settings to disk (private helper)
*/
private async persistProjectSettings(
workingDirectory: string,
): Promise<void> {
const settings = this.projectSettings.get(workingDirectory);
if (!settings) return;
const settingsPath = this.getProjectSettingsPath(workingDirectory);
const dirPath = join(workingDirectory, ".letta");
try {
// Read existing settings (might have permissions, etc.)
let existingSettings: Record<string, unknown> = {};
if (exists(settingsPath)) {
const content = await readFile(settingsPath);
existingSettings = JSON.parse(content) as Record<string, unknown>;
}
// Create directory if needed
if (!exists(dirPath)) {
await mkdir(dirPath, { recursive: true });
}
// Merge updates with existing settings
const newSettings = {
...existingSettings,
...settings,
};
await writeFile(settingsPath, JSON.stringify(newSettings, null, 2));
} catch (error) {
console.error("Error saving project settings:", error);
throw error;
}
}
private getSettingsPath(): string {
return join(homedir(), ".letta", "settings.json");
}
private getProjectSettingsPath(workingDirectory: string): string {
return join(workingDirectory, ".letta", "settings.json");
}
private getLocalProjectSettingsPath(workingDirectory: string): string {
return join(workingDirectory, ".letta", "settings.local.json");
}
/**
* Load local project settings (.letta/settings.local.json)
*/
async loadLocalProjectSettings(
workingDirectory: string = process.cwd(),
): Promise<LocalProjectSettings> {
// Check cache first
const cached = this.localProjectSettings.get(workingDirectory);
if (cached) {
return { ...cached };
}
const settingsPath = this.getLocalProjectSettingsPath(workingDirectory);
try {
if (!exists(settingsPath)) {
const defaults = { ...DEFAULT_LOCAL_PROJECT_SETTINGS };
this.localProjectSettings.set(workingDirectory, defaults);
return defaults;
}
const content = await readFile(settingsPath);
const localSettings = JSON.parse(content) as LocalProjectSettings;
this.localProjectSettings.set(workingDirectory, localSettings);
return { ...localSettings };
} catch (error) {
console.error(
"Error loading local project settings, using defaults:",
error,
);
const defaults = { ...DEFAULT_LOCAL_PROJECT_SETTINGS };
this.localProjectSettings.set(workingDirectory, defaults);
return defaults;
}
}
/**
* Get local project settings (synchronous, from memory)
*/
getLocalProjectSettings(
workingDirectory: string = process.cwd(),
): LocalProjectSettings {
const cached = this.localProjectSettings.get(workingDirectory);
if (!cached) {
throw new Error(
`Local project settings for ${workingDirectory} not loaded. Call loadLocalProjectSettings() first.`,
);
}
return { ...cached };
}
/**
* Update local project settings (synchronous in-memory, async persist)
*/
updateLocalProjectSettings(
updates: Partial<LocalProjectSettings>,
workingDirectory: string = process.cwd(),
): void {
const current = this.localProjectSettings.get(workingDirectory);
if (!current) {
throw new Error(
`Local project settings for ${workingDirectory} not loaded. Call loadLocalProjectSettings() first.`,
);
}
const updated = { ...current, ...updates };
this.localProjectSettings.set(workingDirectory, updated);
// Persist asynchronously
this.persistLocalProjectSettings(workingDirectory).catch((error) => {
console.error("Failed to persist local project settings:", error);
});
}
/**
* Persist local project settings to disk (private helper)
*/
private async persistLocalProjectSettings(
workingDirectory: string,
): Promise<void> {
const settings = this.localProjectSettings.get(workingDirectory);
if (!settings) return;
const settingsPath = this.getLocalProjectSettingsPath(workingDirectory);
const dirPath = join(workingDirectory, ".letta");
try {
// Create directory if needed
if (!exists(dirPath)) {
await mkdir(dirPath, { recursive: true });
}
await writeFile(settingsPath, JSON.stringify(settings, null, 2));
} catch (error) {
console.error("Error saving local project settings:", error);
throw error;
}
}
/**
* Reset the manager (mainly for testing)
*/
reset(): void {
this.settings = null;
this.projectSettings.clear();
this.localProjectSettings.clear();
this.initialized = false;
}
}
// Singleton instance
export const settingsManager = new SettingsManager();

View File

@@ -1,512 +0,0 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { settingsManager } from "../settings-manager";
// Store original HOME to restore after tests
const originalHome = process.env.HOME;
let testHomeDir: string;
let testProjectDir: string;
beforeEach(async () => {
// Reset settings manager FIRST before changing HOME
settingsManager.reset();
// Create temporary directories for testing
testHomeDir = await mkdtemp(join(tmpdir(), "letta-test-home-"));
testProjectDir = await mkdtemp(join(tmpdir(), "letta-test-project-"));
// Override HOME for tests (must be done BEFORE initialize is called)
process.env.HOME = testHomeDir;
});
afterEach(async () => {
// Clean up test directories
await rm(testHomeDir, { recursive: true, force: true });
await rm(testProjectDir, { recursive: true, force: true });
// Restore original HOME
process.env.HOME = originalHome;
// Reset settings manager after each test
settingsManager.reset();
});
// ============================================================================
// Initialization Tests
// ============================================================================
describe("Settings Manager - Initialization", () => {
test("Initialize makes settings accessible", async () => {
await settingsManager.initialize();
// Settings should be accessible immediately after initialization
const settings = settingsManager.getSettings();
expect(settings).toBeDefined();
expect(settings.uiMode).toBeDefined();
expect(typeof settings.tokenStreaming).toBe("boolean");
expect(settings.globalSharedBlockIds).toBeDefined();
expect(typeof settings.globalSharedBlockIds).toBe("object");
});
test("Initialize loads existing settings from disk", async () => {
// First initialize and set some settings
await settingsManager.initialize();
settingsManager.updateSettings({
uiMode: "rich",
tokenStreaming: true,
lastAgent: "agent-123",
});
// Wait for persist to complete
await new Promise((resolve) => setTimeout(resolve, 100));
// Reset and re-initialize
settingsManager.reset();
await settingsManager.initialize();
const settings = settingsManager.getSettings();
expect(settings.uiMode).toBe("rich");
expect(settings.tokenStreaming).toBe(true);
expect(settings.lastAgent).toBe("agent-123");
});
test("Initialize only runs once", async () => {
await settingsManager.initialize();
const settings1 = settingsManager.getSettings();
// Call initialize again
await settingsManager.initialize();
const settings2 = settingsManager.getSettings();
// Should be same instance
expect(settings1).toEqual(settings2);
});
test("Throws error if accessing settings before initialization", () => {
expect(() => settingsManager.getSettings()).toThrow(
"Settings not initialized",
);
});
});
// ============================================================================
// Global Settings Tests
// ============================================================================
describe("Settings Manager - Global Settings", () => {
beforeEach(async () => {
await settingsManager.initialize();
});
test("Get settings returns a copy", () => {
const settings1 = settingsManager.getSettings();
const settings2 = settingsManager.getSettings();
expect(settings1).toEqual(settings2);
expect(settings1).not.toBe(settings2); // Different object instances
});
test("Get specific setting", () => {
settingsManager.updateSettings({ uiMode: "rich" });
const uiMode = settingsManager.getSetting("uiMode");
expect(uiMode).toBe("rich");
});
test("Update single setting", () => {
// Verify initial state first
const initialSettings = settingsManager.getSettings();
const initialUiMode = initialSettings.uiMode;
settingsManager.updateSettings({ tokenStreaming: true });
const settings = settingsManager.getSettings();
expect(settings.tokenStreaming).toBe(true);
expect(settings.uiMode).toBe(initialUiMode); // Other settings unchanged
});
test("Update multiple settings", () => {
settingsManager.updateSettings({
uiMode: "rich",
tokenStreaming: true,
lastAgent: "agent-456",
});
const settings = settingsManager.getSettings();
expect(settings.uiMode).toBe("rich");
expect(settings.tokenStreaming).toBe(true);
expect(settings.lastAgent).toBe("agent-456");
});
test("Update global shared block IDs", () => {
settingsManager.updateSettings({
globalSharedBlockIds: {
persona: "block-1",
human: "block-2",
},
});
const settings = settingsManager.getSettings();
expect(settings.globalSharedBlockIds).toEqual({
persona: "block-1",
human: "block-2",
});
});
test("Update env variables", () => {
settingsManager.updateSettings({
env: {
LETTA_API_KEY: "sk-test-123",
CUSTOM_VAR: "value",
},
});
const settings = settingsManager.getSettings();
expect(settings.env).toEqual({
LETTA_API_KEY: "sk-test-123",
CUSTOM_VAR: "value",
});
});
test("Settings persist to disk", async () => {
settingsManager.updateSettings({
uiMode: "rich",
lastAgent: "agent-789",
});
// Wait for async persist
await new Promise((resolve) => setTimeout(resolve, 100));
// Reset and reload
settingsManager.reset();
await settingsManager.initialize();
const settings = settingsManager.getSettings();
expect(settings.uiMode).toBe("rich");
expect(settings.lastAgent).toBe("agent-789");
});
});
// ============================================================================
// Project Settings Tests (.letta/settings.json)
// ============================================================================
describe("Settings Manager - Project Settings", () => {
beforeEach(async () => {
await settingsManager.initialize();
});
test("Load project settings creates defaults if none exist", async () => {
const projectSettings =
await settingsManager.loadProjectSettings(testProjectDir);
expect(projectSettings.localSharedBlockIds).toEqual({});
});
test("Get project settings returns cached value", async () => {
await settingsManager.loadProjectSettings(testProjectDir);
const settings1 = settingsManager.getProjectSettings(testProjectDir);
const settings2 = settingsManager.getProjectSettings(testProjectDir);
expect(settings1).toEqual(settings2);
expect(settings1).not.toBe(settings2); // Different instances
});
test("Update project settings", async () => {
await settingsManager.loadProjectSettings(testProjectDir);
settingsManager.updateProjectSettings(
{
localSharedBlockIds: {
style: "block-style-1",
project: "block-project-1",
},
},
testProjectDir,
);
const settings = settingsManager.getProjectSettings(testProjectDir);
expect(settings.localSharedBlockIds).toEqual({
style: "block-style-1",
project: "block-project-1",
});
});
test("Project settings persist to disk", async () => {
await settingsManager.loadProjectSettings(testProjectDir);
settingsManager.updateProjectSettings(
{
localSharedBlockIds: {
test: "block-test-1",
},
},
testProjectDir,
);
// Wait for persist
await new Promise((resolve) => setTimeout(resolve, 100));
// Clear cache and reload
settingsManager.reset();
await settingsManager.initialize();
const reloaded = await settingsManager.loadProjectSettings(testProjectDir);
expect(reloaded.localSharedBlockIds).toEqual({
test: "block-test-1",
});
});
test("Throw error if accessing project settings before loading", async () => {
expect(() => settingsManager.getProjectSettings(testProjectDir)).toThrow(
"Project settings for",
);
});
});
// ============================================================================
// Local Project Settings Tests (.letta/settings.local.json)
// ============================================================================
describe("Settings Manager - Local Project Settings", () => {
beforeEach(async () => {
await settingsManager.initialize();
});
test("Load local project settings creates defaults if none exist", async () => {
const localSettings =
await settingsManager.loadLocalProjectSettings(testProjectDir);
expect(localSettings.lastAgent).toBe(null);
});
test("Get local project settings returns cached value", async () => {
await settingsManager.loadLocalProjectSettings(testProjectDir);
const settings1 = settingsManager.getLocalProjectSettings(testProjectDir);
const settings2 = settingsManager.getLocalProjectSettings(testProjectDir);
expect(settings1).toEqual(settings2);
expect(settings1).not.toBe(settings2);
});
test("Update local project settings - last agent", async () => {
await settingsManager.loadLocalProjectSettings(testProjectDir);
settingsManager.updateLocalProjectSettings(
{ lastAgent: "agent-local-1" },
testProjectDir,
);
const settings = settingsManager.getLocalProjectSettings(testProjectDir);
expect(settings.lastAgent).toBe("agent-local-1");
});
test("Update local project settings - permissions", async () => {
await settingsManager.loadLocalProjectSettings(testProjectDir);
settingsManager.updateLocalProjectSettings(
{
permissions: {
allow: ["Bash(ls:*)"],
deny: ["Read(.env)"],
},
},
testProjectDir,
);
const settings = settingsManager.getLocalProjectSettings(testProjectDir);
expect(settings.permissions).toEqual({
allow: ["Bash(ls:*)"],
deny: ["Read(.env)"],
});
});
test("Local project settings persist to disk", async () => {
await settingsManager.loadLocalProjectSettings(testProjectDir);
settingsManager.updateLocalProjectSettings(
{
lastAgent: "agent-persist-1",
permissions: {
allow: ["Bash(*)"],
},
},
testProjectDir,
);
// Wait for persist
await new Promise((resolve) => setTimeout(resolve, 100));
// Clear cache and reload
settingsManager.reset();
await settingsManager.initialize();
const reloaded =
await settingsManager.loadLocalProjectSettings(testProjectDir);
expect(reloaded.lastAgent).toBe("agent-persist-1");
expect(reloaded.permissions).toEqual({
allow: ["Bash(*)"],
});
});
test("Throw error if accessing local project settings before loading", async () => {
expect(() =>
settingsManager.getLocalProjectSettings(testProjectDir),
).toThrow("Local project settings for");
});
});
// ============================================================================
// Multiple Projects Tests
// ============================================================================
describe("Settings Manager - Multiple Projects", () => {
let testProjectDir2: string;
beforeEach(async () => {
await settingsManager.initialize();
testProjectDir2 = await mkdtemp(join(tmpdir(), "letta-test-project2-"));
});
afterEach(async () => {
await rm(testProjectDir2, { recursive: true, force: true });
});
test("Can manage settings for multiple projects independently", async () => {
// Load settings for both projects
await settingsManager.loadLocalProjectSettings(testProjectDir);
await settingsManager.loadLocalProjectSettings(testProjectDir2);
// Update different values
settingsManager.updateLocalProjectSettings(
{ lastAgent: "agent-project-1" },
testProjectDir,
);
settingsManager.updateLocalProjectSettings(
{ lastAgent: "agent-project-2" },
testProjectDir2,
);
// Verify independence
const settings1 = settingsManager.getLocalProjectSettings(testProjectDir);
const settings2 = settingsManager.getLocalProjectSettings(testProjectDir2);
expect(settings1.lastAgent).toBe("agent-project-1");
expect(settings2.lastAgent).toBe("agent-project-2");
});
test("Project settings are cached separately", async () => {
await settingsManager.loadProjectSettings(testProjectDir);
await settingsManager.loadProjectSettings(testProjectDir2);
settingsManager.updateProjectSettings(
{ localSharedBlockIds: { test: "block-1" } },
testProjectDir,
);
settingsManager.updateProjectSettings(
{ localSharedBlockIds: { test: "block-2" } },
testProjectDir2,
);
const settings1 = settingsManager.getProjectSettings(testProjectDir);
const settings2 = settingsManager.getProjectSettings(testProjectDir2);
expect(settings1.localSharedBlockIds.test).toBe("block-1");
expect(settings2.localSharedBlockIds.test).toBe("block-2");
});
});
// ============================================================================
// Reset Tests
// ============================================================================
describe("Settings Manager - Reset", () => {
test("Reset clears all cached data", async () => {
await settingsManager.initialize();
settingsManager.updateSettings({ lastAgent: "agent-reset-test" });
settingsManager.reset();
// Should throw error after reset
expect(() => settingsManager.getSettings()).toThrow();
});
test("Can reinitialize after reset", async () => {
await settingsManager.initialize();
settingsManager.updateSettings({ uiMode: "rich" });
// Wait for persist
await new Promise((resolve) => setTimeout(resolve, 100));
settingsManager.reset();
await settingsManager.initialize();
const settings = settingsManager.getSettings();
expect(settings.uiMode).toBe("rich");
});
});
// ============================================================================
// Edge Cases and Error Handling
// ============================================================================
describe("Settings Manager - Edge Cases", () => {
test("Handles corrupted settings file gracefully", async () => {
// Create corrupted settings file
const { writeFile, mkdir } = await import("../utils/fs.js");
const settingsDir = join(testHomeDir, ".letta");
await mkdir(settingsDir, { recursive: true });
await writeFile(join(settingsDir, "settings.json"), "{ invalid json");
// Should fall back to defaults
await settingsManager.initialize();
const settings = settingsManager.getSettings();
// Should have default values (not corrupt)
expect(settings).toBeDefined();
expect(settings.uiMode).toBeDefined();
expect(settings.tokenStreaming).toBeDefined();
expect(typeof settings.tokenStreaming).toBe("boolean");
});
test("Modifying returned settings doesn't affect internal state", async () => {
await settingsManager.initialize();
settingsManager.updateSettings({
lastAgent: "agent-123",
globalSharedBlockIds: {},
});
const settings = settingsManager.getSettings();
settings.lastAgent = "modified-agent";
settings.globalSharedBlockIds = { modified: "block" };
// Internal state should be unchanged
const actualSettings = settingsManager.getSettings();
expect(actualSettings.lastAgent).toBe("agent-123");
expect(actualSettings.globalSharedBlockIds).toEqual({});
});
test("Partial updates preserve existing values", async () => {
await settingsManager.initialize();
settingsManager.updateSettings({
uiMode: "rich",
tokenStreaming: true,
lastAgent: "agent-1",
});
// Partial update
settingsManager.updateSettings({
lastAgent: "agent-2",
});
const settings = settingsManager.getSettings();
expect(settings.uiMode).toBe("rich"); // Preserved
expect(settings.tokenStreaming).toBe(true); // Preserved
expect(settings.lastAgent).toBe("agent-2"); // Updated
});
});