mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
235 lines
8.0 KiB
TypeScript
235 lines
8.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import {
|
|
DroidRawSettingsConflictError,
|
|
DroidRawSettingsValidationError,
|
|
getDroidDashboardDiagnostics,
|
|
getDroidRawSettings,
|
|
maskApiKeyPreview,
|
|
resolveDroidConfigPaths,
|
|
saveDroidRawSettings,
|
|
summarizeDroidCustomModels,
|
|
} from '../../../src/web-server/services/droid-dashboard-service';
|
|
|
|
const testRoot = path.join(os.tmpdir(), `ccs-droid-dashboard-test-${Date.now()}`);
|
|
|
|
beforeEach(() => {
|
|
fs.mkdirSync(testRoot, { recursive: true });
|
|
process.env.CCS_HOME = testRoot;
|
|
});
|
|
|
|
afterEach(() => {
|
|
delete process.env.CCS_HOME;
|
|
if (fs.existsSync(testRoot)) {
|
|
fs.rmSync(testRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
describe('droid-dashboard-service', () => {
|
|
it('resolves droid config paths on unix-like platforms', () => {
|
|
const resolved = resolveDroidConfigPaths({
|
|
platform: 'darwin',
|
|
env: {
|
|
CCS_HOME: '/tmp/ccs-home',
|
|
} as NodeJS.ProcessEnv,
|
|
homeDir: '/Users/tester',
|
|
});
|
|
|
|
expect(resolved.settingsPath).toBe('/tmp/ccs-home/.factory/settings.json');
|
|
expect(resolved.legacyConfigPath).toBe('/tmp/ccs-home/.factory/config.json');
|
|
expect(resolved.settingsDisplayPath).toBe('~/.factory/settings.json');
|
|
expect(resolved.legacyConfigDisplayPath).toBe('~/.factory/config.json');
|
|
});
|
|
|
|
it('resolves droid config paths on windows platforms', () => {
|
|
const resolved = resolveDroidConfigPaths({
|
|
platform: 'win32',
|
|
env: {} as NodeJS.ProcessEnv,
|
|
homeDir: 'C:/Users/test',
|
|
});
|
|
|
|
expect(resolved.settingsPath).toBe(path.join('C:/Users/test', '.factory', 'settings.json'));
|
|
expect(resolved.legacyConfigPath).toBe(path.join('C:/Users/test', '.factory', 'config.json'));
|
|
expect(resolved.legacyConfigDisplayPath).toBe('~/.factory/config.json');
|
|
});
|
|
|
|
it('masks api key preview with only suffix', () => {
|
|
expect(maskApiKeyPreview('sk-abcdefghijklmnop')).toBe('***mnop');
|
|
});
|
|
|
|
it('summarizes custom model entries with provider breakdown and ownership', () => {
|
|
const summary = summarizeDroidCustomModels([
|
|
{
|
|
displayName: 'CCS codex',
|
|
model: 'gpt-5-codex',
|
|
baseUrl: 'http://127.0.0.1:8317/v1',
|
|
apiKey: 'secret-token-1234',
|
|
provider: 'openai',
|
|
},
|
|
{
|
|
displayName: 'Factory team profile',
|
|
model: 'claude-sonnet-4-5',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
apiKey: 'another-token-9999',
|
|
provider: 'anthropic',
|
|
},
|
|
{
|
|
displayName: 'bad entry',
|
|
},
|
|
]);
|
|
|
|
expect(summary.customModelCount).toBe(2);
|
|
expect(summary.ccsManagedCount).toBe(1);
|
|
expect(summary.userManagedCount).toBe(1);
|
|
expect(summary.invalidModelEntryCount).toBe(1);
|
|
expect(summary.providerBreakdown.openai).toBe(1);
|
|
expect(summary.providerBreakdown.anthropic).toBe(1);
|
|
expect(summary.customModels[0].apiKeyPreview).toBe('***1234');
|
|
});
|
|
|
|
it('supports legacy snake_case model fields in summaries', () => {
|
|
const summary = summarizeDroidCustomModels([
|
|
{
|
|
model_display_name: 'Kimi K2 Thinking Nvidia',
|
|
model: 'moonshotai/kimi-k2-thinking',
|
|
base_url: 'https://integrate.api.nvidia.com/v1',
|
|
api_key: 'legacy-token-1234',
|
|
provider: 'generic-chat-completion-api',
|
|
max_tokens: 220000,
|
|
},
|
|
]);
|
|
|
|
expect(summary.customModelCount).toBe(1);
|
|
expect(summary.invalidModelEntryCount).toBe(0);
|
|
expect(summary.providerBreakdown['generic-chat-completion-api']).toBe(1);
|
|
expect(summary.customModels[0].displayName).toBe('Kimi K2 Thinking Nvidia');
|
|
expect(summary.customModels[0].maxOutputTokens).toBe(220000);
|
|
expect(summary.customModels[0].apiKeyPreview).toBe('***1234');
|
|
});
|
|
|
|
it('returns raw settings payload for missing settings file', async () => {
|
|
const raw = await getDroidRawSettings();
|
|
|
|
expect(raw.exists).toBe(false);
|
|
expect(raw.path).toBe('~/.factory/settings.json');
|
|
expect(raw.rawText).toBe('{}');
|
|
expect(raw.settings).toBeNull();
|
|
});
|
|
|
|
it('returns parseError when settings.json is invalid JSON', async () => {
|
|
const settingsDir = path.join(testRoot, '.factory');
|
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
fs.writeFileSync(path.join(settingsDir, 'settings.json'), '{ invalid-json');
|
|
|
|
const raw = await getDroidRawSettings();
|
|
|
|
expect(raw.exists).toBe(true);
|
|
expect(raw.parseError).toBeString();
|
|
expect(raw.settings).toBeNull();
|
|
expect(raw.rawText).toContain('invalid-json');
|
|
});
|
|
|
|
it('includes structured docs links for fact-checking providers', async () => {
|
|
const diagnostics = await getDroidDashboardDiagnostics();
|
|
|
|
expect(diagnostics.docsReference.links.length).toBeGreaterThan(0);
|
|
expect(diagnostics.docsReference.providerDocs.length).toBeGreaterThan(0);
|
|
expect(diagnostics.docsReference.links.every((link) => link.url.startsWith('https://'))).toBe(
|
|
true
|
|
);
|
|
expect(
|
|
diagnostics.docsReference.providerDocs.some((doc) => doc.provider === 'anthropic')
|
|
).toBe(true);
|
|
});
|
|
|
|
it('falls back to legacy config custom_models when settings customModels is absent', async () => {
|
|
const settingsDir = path.join(testRoot, '.factory');
|
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({ model: 'custom:legacy' }));
|
|
fs.writeFileSync(
|
|
path.join(settingsDir, 'config.json'),
|
|
JSON.stringify({
|
|
custom_models: [
|
|
{
|
|
model_display_name: 'Legacy OpenAI',
|
|
model: 'gpt-5.2',
|
|
base_url: 'https://api.openai.com/v1',
|
|
api_key: 'legacy-openai-1234',
|
|
provider: 'openai',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const diagnostics = await getDroidDashboardDiagnostics();
|
|
|
|
expect(diagnostics.byok.customModelCount).toBe(1);
|
|
expect(diagnostics.byok.customModels[0].displayName).toBe('Legacy OpenAI');
|
|
expect(diagnostics.byok.customModels[0].provider).toBe('openai');
|
|
});
|
|
|
|
it('warns when settings.json uses legacy custom_models key', async () => {
|
|
const settingsDir = path.join(testRoot, '.factory');
|
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(settingsDir, 'settings.json'),
|
|
JSON.stringify({
|
|
custom_models: [
|
|
{
|
|
model_display_name: 'Legacy Generic',
|
|
model: 'glm-4.7',
|
|
base_url: 'https://api.z.ai/api/coding/paas/v4',
|
|
api_key: 'legacy-zai-1234',
|
|
provider: 'generic-chat-completion-api',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const diagnostics = await getDroidDashboardDiagnostics();
|
|
|
|
expect(
|
|
diagnostics.warnings.some((warning) => warning.includes('legacy "custom_models" key'))
|
|
).toBe(true);
|
|
expect(diagnostics.byok.customModelCount).toBe(1);
|
|
});
|
|
|
|
it('saves valid raw settings content', async () => {
|
|
const result = await saveDroidRawSettings({
|
|
rawText: JSON.stringify({
|
|
model: 'custom:test-model',
|
|
customModels: [],
|
|
}),
|
|
});
|
|
|
|
const settingsPath = path.join(testRoot, '.factory', 'settings.json');
|
|
const written = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(result.mtime).toBeGreaterThan(0);
|
|
expect(written.model).toBe('custom:test-model');
|
|
});
|
|
|
|
it('rejects invalid JSON while saving raw settings', async () => {
|
|
await expect(saveDroidRawSettings({ rawText: '{ invalid-json' })).rejects.toThrow(
|
|
DroidRawSettingsValidationError
|
|
);
|
|
});
|
|
|
|
it('rejects stale writes with conflict error', async () => {
|
|
const settingsDir = path.join(testRoot, '.factory');
|
|
fs.mkdirSync(settingsDir, { recursive: true });
|
|
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
fs.writeFileSync(settingsPath, JSON.stringify({ customModels: [] }));
|
|
|
|
await expect(
|
|
saveDroidRawSettings({
|
|
rawText: JSON.stringify({ model: 'custom:next', customModels: [] }),
|
|
expectedMtime: 1,
|
|
})
|
|
).rejects.toThrow(DroidRawSettingsConflictError);
|
|
});
|
|
});
|