From f71d2c9b668cd04102590bb39c77484478a61880 Mon Sep 17 00:00:00 2001 From: Shubham Naik Date: Thu, 30 Oct 2025 13:48:07 -0700 Subject: [PATCH] Revert "Refactor settings manager" (#40) --- src/agent/client.ts | 6 +- src/agent/create.ts | 6 +- src/cli/App.tsx | 4 +- src/headless.ts | 10 +- src/index.ts | 43 +-- src/settings-manager.ts | 377 --------------------- src/tests/settings-manager.test.ts | 512 ----------------------------- 7 files changed, 29 insertions(+), 929 deletions(-) delete mode 100644 src/settings-manager.ts delete mode 100644 src/tests/settings-manager.test.ts diff --git a/src/agent/client.ts b/src/agent/client.ts index c0cbcda..c1094bb 100644 --- a/src/agent/client.ts +++ b/src/agent/client.ts @@ -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 }); diff --git a/src/agent/create.ts b/src/agent/create.ts index 4d03249..d6dad9b 100644 --- a/src/agent/create.ts +++ b/src/agent/create.ts @@ -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, diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 5fa561c..f09c2bf 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -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, { diff --git a/src/headless.ts b/src/headless.ts index 4fa7884..f16cec3 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -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 = diff --git a/src/index.ts b/src/index.ts index cbe9027..d03c72f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -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; @@ -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); diff --git a/src/settings-manager.ts b/src/settings-manager.ts deleted file mode 100644 index ca9cfd4..0000000 --- a/src/settings-manager.ts +++ /dev/null @@ -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; - permissions?: PermissionRules; - env?: Record; -} - -export interface ProjectSettings { - localSharedBlockIds: Record; -} - -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 = new Map(); - private localProjectSettings: Map = new Map(); - private initialized = false; - - /** - * Initialize the settings manager (loads from disk) - * Should be called once at app startup - */ - async initialize(): Promise { - 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(key: K): Settings[K] { - return this.getSettings()[key]; - } - - /** - * Update settings (synchronous in-memory, async persist) - */ - updateSettings(updates: Partial): 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 { - // 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; - - const projectSettings: ProjectSettings = { - localSharedBlockIds: - (rawSettings.localSharedBlockIds as Record) ?? {}, - }; - - 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, - 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 { - 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 { - 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 = {}; - if (exists(settingsPath)) { - const content = await readFile(settingsPath); - existingSettings = JSON.parse(content) as Record; - } - - // 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 { - // 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, - 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 { - 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(); diff --git a/src/tests/settings-manager.test.ts b/src/tests/settings-manager.test.ts deleted file mode 100644 index 8c7ee22..0000000 --- a/src/tests/settings-manager.test.ts +++ /dev/null @@ -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 - }); -});