import { mkdir, readFile, rm, writeFile } from 'fs/promises'; import { join } from 'path'; import { beforeEach, describe, expect, it, vi } from 'vitest'; const { testHome, testUserData } = vi.hoisted(() => { const suffix = Math.random().toString(36).slice(2); return { testHome: `/tmp/clawx-openclaw-auth-${suffix}`, testUserData: `/tmp/clawx-openclaw-auth-user-data-${suffix}`, }; }); vi.mock('os', async () => { const actual = await vi.importActual('os'); const mocked = { ...actual, homedir: () => testHome, }; return { ...mocked, default: mocked, }; }); vi.mock('electron', () => ({ app: { isPackaged: false, getPath: () => testUserData, getVersion: () => '0.0.0-test', }, })); async function writeOpenClawJson(config: unknown): Promise { const openclawDir = join(testHome, '.openclaw'); await mkdir(openclawDir, { recursive: true }); await writeFile(join(openclawDir, 'openclaw.json'), JSON.stringify(config, null, 2), 'utf8'); } async function readAuthProfiles(agentId: string): Promise> { const content = await readFile(join(testHome, '.openclaw', 'agents', agentId, 'agent', 'auth-profiles.json'), 'utf8'); return JSON.parse(content) as Record; } describe('saveProviderKeyToOpenClaw', () => { beforeEach(async () => { vi.resetModules(); vi.restoreAllMocks(); await rm(testHome, { recursive: true, force: true }); await rm(testUserData, { recursive: true, force: true }); }); it('only syncs auth profiles for configured agents', async () => { await writeOpenClawJson({ agents: { list: [ { id: 'main', name: 'Main', default: true, workspace: '~/.openclaw/workspace', agentDir: '~/.openclaw/agents/main/agent', }, { id: 'test3', name: 'test3', workspace: '~/.openclaw/workspace-test3', agentDir: '~/.openclaw/agents/test3/agent', }, ], }, }); await mkdir(join(testHome, '.openclaw', 'agents', 'test2', 'agent'), { recursive: true }); await writeFile( join(testHome, '.openclaw', 'agents', 'test2', 'agent', 'auth-profiles.json'), JSON.stringify({ version: 1, profiles: { 'legacy:default': { type: 'api_key', provider: 'legacy', key: 'legacy-key', }, }, }, null, 2), 'utf8', ); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); const { saveProviderKeyToOpenClaw } = await import('@electron/utils/openclaw-auth'); await saveProviderKeyToOpenClaw('openrouter', 'sk-test'); const mainProfiles = await readAuthProfiles('main'); const test3Profiles = await readAuthProfiles('test3'); const staleProfiles = await readAuthProfiles('test2'); expect((mainProfiles.profiles as Record)['openrouter:default'].key).toBe('sk-test'); expect((test3Profiles.profiles as Record)['openrouter:default'].key).toBe('sk-test'); expect(staleProfiles.profiles).toEqual({ 'legacy:default': { type: 'api_key', provider: 'legacy', key: 'legacy-key', }, }); expect(logSpy).toHaveBeenCalledWith( 'Saved API key for provider "openrouter" to OpenClaw auth-profiles (agents: main, test3)', ); logSpy.mockRestore(); }); }); describe('sanitizeOpenClawConfig', () => { beforeEach(async () => { vi.resetModules(); vi.restoreAllMocks(); await rm(testHome, { recursive: true, force: true }); await rm(testUserData, { recursive: true, force: true }); }); it('skips sanitization when openclaw.json does not exist', async () => { // Ensure the .openclaw dir doesn't exist at all const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); // Should not throw and should not create the file await expect(sanitizeOpenClawConfig()).resolves.toBeUndefined(); const configPath = join(testHome, '.openclaw', 'openclaw.json'); await expect(readFile(configPath, 'utf8')).rejects.toThrow(); logSpy.mockRestore(); }); it('skips sanitization when openclaw.json contains invalid JSON', async () => { // Simulate a corrupted file: readJsonFile returns null, sanitize must bail out const openclawDir = join(testHome, '.openclaw'); await mkdir(openclawDir, { recursive: true }); const configPath = join(openclawDir, 'openclaw.json'); await writeFile(configPath, 'NOT VALID JSON {{{', 'utf8'); const before = await readFile(configPath, 'utf8'); const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); await sanitizeOpenClawConfig(); const after = await readFile(configPath, 'utf8'); // Corrupt file must not be overwritten expect(after).toBe(before); logSpy.mockRestore(); }); it('properly sanitizes a genuinely empty {} config (fresh install)', async () => { // A fresh install with {} is a valid config — sanitize should proceed // and enforce tools.profile, commands.restart, etc. await writeOpenClawJson({}); const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); await sanitizeOpenClawConfig(); const configPath = join(testHome, '.openclaw', 'openclaw.json'); const result = JSON.parse(await readFile(configPath, 'utf8')) as Record; // Fresh install should get tools settings enforced const tools = result.tools as Record; expect(tools.profile).toBe('full'); logSpy.mockRestore(); }); it('preserves user config (memory, agents, channels) when enforcing tools settings', async () => { await writeOpenClawJson({ agents: { defaults: { model: { primary: 'openai/gpt-4' } } }, channels: { discord: { token: 'tok', enabled: true } }, memory: { enabled: true, limit: 100 }, }); const { sanitizeOpenClawConfig } = await import('@electron/utils/openclaw-auth'); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); await sanitizeOpenClawConfig(); const configPath = join(testHome, '.openclaw', 'openclaw.json'); const result = JSON.parse(await readFile(configPath, 'utf8')) as Record; // User-owned sections must survive the sanitize pass expect(result.memory).toEqual({ enabled: true, limit: 100 }); expect(result.channels).toEqual({ discord: { token: 'tok', enabled: true } }); expect((result.agents as Record).defaults).toEqual({ model: { primary: 'openai/gpt-4' }, }); // tools settings should now be enforced const tools = result.tools as Record; expect(tools.profile).toBe('full'); logSpy.mockRestore(); }); });