open-design/apps/web/tests/state/config.test.ts
marco 5dd70b5016
Some checks failed
ci / Validate workspace (push) Successful in 12m32s
landing-page-ci / Validate landing page (push) Successful in 9m41s
landing-page-deploy / Deploy landing page (push) Failing after 5m23s
github-metrics / Generate repository metrics SVG (push) Failing after 2m3s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 11s
Initial import: open-design source for helix-mind.ai distribution
This repository contains the open-design daemon CLI source code, built
and packaged at https://helix-mind.ai/cli/open-design/latest.tgz for use
by the HelixMind /design slash command.

Licenses: Apache-2.0 (root) + MIT (skills/*)
2026-05-06 20:50:24 +02:00

273 lines
8.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
DEFAULT_CONFIG,
loadConfig,
mergeDaemonConfig,
syncComposioConfigToDaemon,
syncConfigToDaemon,
} from '../../src/state/config';
import type { AppConfig } from '../../src/types';
const store = new Map<string, string>();
const originalFetch = globalThis.fetch;
vi.stubGlobal('localStorage', {
getItem: vi.fn((key: string) => store.get(key) ?? null),
setItem: vi.fn((key: string, value: string) => {
store.set(key, value);
}),
removeItem: vi.fn((key: string) => {
store.delete(key);
}),
clear: vi.fn(() => {
store.clear();
}),
});
describe('syncComposioConfigToDaemon', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.stubGlobal('fetch', originalFetch);
});
it('sends a pending Composio API key to the daemon', async () => {
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
await syncComposioConfigToDaemon({ apiKey: 'cmp_secret', apiKeyConfigured: false });
expect(fetchMock).toHaveBeenCalledWith('/api/connectors/composio/config', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ apiKey: 'cmp_secret' }),
});
});
it('does not clear a daemon-saved key when local state only has the saved marker', async () => {
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
await syncComposioConfigToDaemon({ apiKey: '', apiKeyConfigured: true, apiKeyTail: 'test' });
expect(fetchMock).toHaveBeenCalledWith('/api/connectors/composio/config', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({}),
});
});
});
describe('syncConfigToDaemon', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.stubGlobal('fetch', originalFetch);
});
it('syncs per-agent CLI env prefs to the daemon app config', async () => {
const fetchMock = vi.fn(async () => new Response('{}', { status: 200 }));
vi.stubGlobal('fetch', fetchMock);
await syncConfigToDaemon({
...DEFAULT_CONFIG,
agentCliEnv: {
claude: { CLAUDE_CONFIG_DIR: '~/.claude-2' },
codex: { CODEX_HOME: '~/.codex-alt' },
},
});
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = fetchMock.mock.calls[0] as unknown as [
string,
RequestInit,
];
expect(url).toBe('/api/app-config');
expect(init.method).toBe('PUT');
expect(init.headers).toEqual({ 'content-type': 'application/json' });
expect(JSON.parse(String(init.body))).toMatchObject({
onboardingCompleted: DEFAULT_CONFIG.onboardingCompleted,
agentId: DEFAULT_CONFIG.agentId,
agentModels: DEFAULT_CONFIG.agentModels,
skillId: DEFAULT_CONFIG.skillId,
designSystemId: DEFAULT_CONFIG.designSystemId,
agentCliEnv: {
claude: { CLAUDE_CONFIG_DIR: '~/.claude-2' },
codex: { CODEX_HOME: '~/.codex-alt' },
},
});
});
});
describe('mergeDaemonConfig', () => {
it('clears stale local CLI env prefs when the daemon has none', () => {
const merged = mergeDaemonConfig(
{
...DEFAULT_CONFIG,
agentCliEnv: {
claude: { CLAUDE_CONFIG_DIR: '~/.claude-old' },
},
},
{
agentId: 'codex',
},
);
expect(merged.agentId).toBe('codex');
expect(merged.agentCliEnv).toEqual({});
});
it('uses daemon CLI env prefs instead of merging with stale local entries', () => {
const merged = mergeDaemonConfig(
{
...DEFAULT_CONFIG,
agentCliEnv: {
claude: { CLAUDE_CONFIG_DIR: '~/.claude-old' },
},
},
{
agentCliEnv: {
codex: { CODEX_HOME: '~/.codex-new' },
},
},
);
expect(merged.agentCliEnv).toEqual({
codex: { CODEX_HOME: '~/.codex-new' },
});
});
});
afterEach(() => {
store.clear();
});
describe('loadConfig', () => {
it('migrates legacy OpenAI-compatible API configs to an explicit apiProtocol', () => {
const legacyConfig: Partial<AppConfig> = {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
agentId: null,
skillId: null,
designSystemId: null,
};
store.set('open-design:config', JSON.stringify(legacyConfig));
const config = loadConfig();
expect(config.mode).toBe('api');
expect(config.baseUrl).toBe('https://api.deepseek.com');
expect(config.model).toBe('deepseek-chat');
expect(config.apiProtocol).toBe('openai');
expect(config.configMigrationVersion).toBe(1);
});
it('migrates legacy Anthropic API configs to an explicit apiProtocol', () => {
const legacyConfig: Partial<AppConfig> = {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://api.anthropic.com',
model: 'claude-sonnet-4-5',
agentId: null,
skillId: null,
designSystemId: null,
};
store.set('open-design:config', JSON.stringify(legacyConfig));
const config = loadConfig();
expect(config.apiProtocol).toBe('anthropic');
});
it('infers protocol for legacy daemon-mode API fields without changing mode', () => {
const daemonConfig: Partial<AppConfig> = {
mode: 'daemon',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
agentId: 'codex',
skillId: null,
designSystemId: null,
};
store.set('open-design:config', JSON.stringify(daemonConfig));
const config = loadConfig();
expect(config.mode).toBe('daemon');
expect(config.apiProtocol).toBe('openai');
expect(config.configMigrationVersion).toBe(1);
});
it('does not overwrite an already explicit apiProtocol', () => {
const explicitConfig: Partial<AppConfig> = {
mode: 'api',
apiProtocol: 'anthropic',
apiKey: 'sk-test',
baseUrl: 'https://api.deepseek.com',
model: 'deepseek-chat',
agentId: null,
skillId: null,
designSystemId: null,
};
store.set('open-design:config', JSON.stringify(explicitConfig));
const config = loadConfig();
expect(config.apiProtocol).toBe('anthropic');
});
it('preserves saved settings when migration sees a malformed base URL', () => {
const legacyConfig: Partial<AppConfig> = {
mode: 'api',
apiKey: 'sk-test',
baseUrl: 'https://[broken-ipv6',
model: 'custom-model',
agentId: null,
skillId: null,
designSystemId: null,
};
store.set('open-design:config', JSON.stringify(legacyConfig));
const config = loadConfig();
expect(config.mode).toBe('api');
expect(config.apiKey).toBe('sk-test');
expect(config.baseUrl).toBe('https://[broken-ipv6');
expect(config.model).toBe('custom-model');
expect(config.apiProtocol).toBe('anthropic');
});
it('preserves a valid saved accent color', () => {
const savedConfig: Partial<AppConfig> = {
theme: 'dark',
accentColor: '#4F46E5',
};
store.set('open-design:config', JSON.stringify(savedConfig));
const config = loadConfig();
expect(config.theme).toBe('dark');
expect(config.accentColor).toBe('#4f46e5');
});
it('falls back to the default accent color for malformed saved colors', () => {
const savedConfig: Partial<AppConfig> = {
accentColor: 'blue',
};
store.set('open-design:config', JSON.stringify(savedConfig));
expect(loadConfig().accentColor).toBe(DEFAULT_CONFIG.accentColor);
});
it('returns defaults for malformed localStorage JSON', () => {
store.set('open-design:config', '{broken-json');
expect(loadConfig()).toEqual(DEFAULT_CONFIG);
});
it('sets an explicit apiProtocol for new default configs', () => {
expect(DEFAULT_CONFIG.apiProtocol).toBe('anthropic');
expect(DEFAULT_CONFIG.configMigrationVersion).toBe(1);
});
});