Initial import: open-design source for helix-mind.ai distribution
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 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
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 2m6s
refresh-contributors-wall / Refresh contributors wall cache bust (push) Failing after 12s
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/*)
This commit is contained in:
73
apps/web/tests/state/appearance.test.ts
Normal file
73
apps/web/tests/state/appearance.test.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
applyAppearanceToDocument,
|
||||
normalizeAccentColor,
|
||||
} from '../../src/state/appearance';
|
||||
|
||||
describe('normalizeAccentColor', () => {
|
||||
it('accepts six-digit hex colors and normalizes casing', () => {
|
||||
expect(normalizeAccentColor(' #4F46E5 ')).toBe('#4f46e5');
|
||||
});
|
||||
|
||||
it('rejects invalid accent colors', () => {
|
||||
expect(normalizeAccentColor('blue')).toBeNull();
|
||||
expect(normalizeAccentColor('#123')).toBeNull();
|
||||
expect(normalizeAccentColor('#12345g')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyAppearanceToDocument', () => {
|
||||
afterEach(() => {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
document.documentElement.style.removeProperty('--accent');
|
||||
document.documentElement.style.removeProperty('--accent-strong');
|
||||
document.documentElement.style.removeProperty('--accent-soft');
|
||||
document.documentElement.style.removeProperty('--accent-tint');
|
||||
document.documentElement.style.removeProperty('--accent-hover');
|
||||
});
|
||||
|
||||
it('applies the saved theme and accent variables to the root element', () => {
|
||||
applyAppearanceToDocument({ theme: 'dark', accentColor: '#4F46E5' });
|
||||
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#4f46e5');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-hover')).toContain('#4f46e5');
|
||||
});
|
||||
|
||||
it('applies accent variables while clearing an explicit theme for system mode', () => {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
|
||||
applyAppearanceToDocument({ theme: 'system', accentColor: '#10B981' });
|
||||
|
||||
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
|
||||
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#10b981');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-strong')).toContain('#10b981');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-soft')).toContain('#10b981');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-tint')).toContain('#10b981');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-hover')).toContain('#10b981');
|
||||
});
|
||||
|
||||
it('replaces existing accent variables when the saved color changes', () => {
|
||||
applyAppearanceToDocument({ theme: 'light', accentColor: '#4F46E5' });
|
||||
|
||||
applyAppearanceToDocument({ theme: 'light', accentColor: '#EF4444' });
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('#ef4444');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-strong')).toContain('#ef4444');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-strong')).not.toContain('#4f46e5');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-soft')).toContain('#ef4444');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-tint')).toContain('#ef4444');
|
||||
expect(document.documentElement.style.getPropertyValue('--accent-hover')).toContain('#ef4444');
|
||||
});
|
||||
|
||||
it('clears accent overrides when no valid accent is configured', () => {
|
||||
document.documentElement.style.setProperty('--accent', '#4f46e5');
|
||||
|
||||
applyAppearanceToDocument({ theme: 'system', accentColor: 'not-a-color' });
|
||||
|
||||
expect(document.documentElement.hasAttribute('data-theme')).toBe(false);
|
||||
expect(document.documentElement.style.getPropertyValue('--accent')).toBe('');
|
||||
});
|
||||
});
|
||||
272
apps/web/tests/state/config.test.ts
Normal file
272
apps/web/tests/state/config.test.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
82
apps/web/tests/state/maxTokens.test.ts
Normal file
82
apps/web/tests/state/maxTokens.test.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import litellmData from '../../src/state/litellm-models.json';
|
||||
import {
|
||||
effectiveMaxTokens,
|
||||
FALLBACK_MAX_TOKENS,
|
||||
MAX_MAX_TOKENS,
|
||||
MIN_MAX_TOKENS,
|
||||
modelMaxTokensDefault,
|
||||
} from '../../src/state/maxTokens';
|
||||
|
||||
describe('modelMaxTokensDefault', () => {
|
||||
it('falls through to LiteLLM data for canonical Anthropic ids', () => {
|
||||
// 64k for the 4.5 line is the upstream value; this guards against the
|
||||
// sync script silently dropping or rewriting these entries.
|
||||
expect(modelMaxTokensDefault('claude-sonnet-4-5')).toBe(64000);
|
||||
expect(modelMaxTokensDefault('claude-opus-4-5')).toBe(64000);
|
||||
expect(modelMaxTokensDefault('claude-haiku-4-5')).toBe(64000);
|
||||
});
|
||||
|
||||
it('lets OVERRIDES win over LiteLLM data', () => {
|
||||
// mimo-v2.5-pro is not in LiteLLM, so this asserts the OVERRIDES path
|
||||
// (not the LiteLLM path) supplied the answer.
|
||||
expect((litellmData.models as Record<string, number>)['mimo-v2.5-pro']).toBeUndefined();
|
||||
expect(modelMaxTokensDefault('mimo-v2.5-pro')).toBe(32768);
|
||||
});
|
||||
|
||||
it('returns FALLBACK_MAX_TOKENS for unknown ids', () => {
|
||||
expect(modelMaxTokensDefault('definitely-not-a-real-model-x9z')).toBe(FALLBACK_MAX_TOKENS);
|
||||
expect(FALLBACK_MAX_TOKENS).toBe(8192);
|
||||
});
|
||||
});
|
||||
|
||||
describe('effectiveMaxTokens', () => {
|
||||
it('honors an explicit user override over the model default', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: 12345, model: 'claude-sonnet-4-5' })).toBe(12345);
|
||||
});
|
||||
|
||||
it('uses the model default when no override is set', () => {
|
||||
expect(effectiveMaxTokens({ model: 'mimo-v2.5-pro' })).toBe(32768);
|
||||
expect(effectiveMaxTokens({ model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
});
|
||||
|
||||
it('falls back to FALLBACK_MAX_TOKENS for unknown models with no override', () => {
|
||||
expect(effectiveMaxTokens({ model: 'unknown-model' })).toBe(FALLBACK_MAX_TOKENS);
|
||||
});
|
||||
});
|
||||
|
||||
describe('effectiveMaxTokens override validation', () => {
|
||||
// Stale localStorage, hand-edited config, or future schema drift can put
|
||||
// anything in cfg.maxTokens. The Settings UI advertises a [1024, 200000]
|
||||
// integer-stepped range, and the daemon proxy already clamps `> 0`, so
|
||||
// we tighten this entry point to match the advertised contract.
|
||||
|
||||
it('rejects negative overrides and falls back to the model default', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: -5, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
});
|
||||
|
||||
it('rejects zero', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: 0, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
});
|
||||
|
||||
it('rejects overrides below MIN_MAX_TOKENS', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: MIN_MAX_TOKENS - 1, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
});
|
||||
|
||||
it('rejects overrides above MAX_MAX_TOKENS', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: MAX_MAX_TOKENS + 1, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
expect(effectiveMaxTokens({ maxTokens: 999_999_999, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
});
|
||||
|
||||
it('rejects non-integer overrides', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: 123.9, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
expect(effectiveMaxTokens({ maxTokens: Number.NaN, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
expect(effectiveMaxTokens({ maxTokens: Number.POSITIVE_INFINITY, model: 'claude-sonnet-4-5' })).toBe(64000);
|
||||
});
|
||||
|
||||
it('accepts the boundary values exactly', () => {
|
||||
expect(effectiveMaxTokens({ maxTokens: MIN_MAX_TOKENS, model: 'claude-sonnet-4-5' })).toBe(MIN_MAX_TOKENS);
|
||||
expect(effectiveMaxTokens({ maxTokens: MAX_MAX_TOKENS, model: 'claude-sonnet-4-5' })).toBe(MAX_MAX_TOKENS);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user