Merge remote-tracking branch 'origin/main' into feature/v0.13.0rc-1768936017583-e6ni

# Conflicts:
#	apps/ui/src/components/views/board-view.tsx
This commit is contained in:
Shirone
2026-01-21 08:47:16 +01:00
68 changed files with 4703 additions and 1248 deletions

View File

@@ -41,13 +41,14 @@ describe('model-resolver.ts', () => {
);
});
it('should treat unknown models as falling back to default', () => {
// Note: Don't include valid Cursor model IDs here (e.g., 'gpt-5.2' is in CURSOR_MODEL_MAP)
const models = ['o1', 'o1-mini', 'o3', 'unknown-model', 'fake-model-123'];
it('should pass through unknown models unchanged (may be provider models)', () => {
// Unknown models now pass through unchanged to support ClaudeCompatibleProvider models
// like GLM-4.7, MiniMax-M2.1, o1, etc.
const models = ['o1', 'o1-mini', 'o3', 'unknown-model', 'fake-model-123', 'GLM-4.7'];
models.forEach((model) => {
const result = resolveModelString(model);
// Should fall back to default since these aren't supported
expect(result).toBe(DEFAULT_MODELS.claude);
// Should pass through unchanged (could be provider models)
expect(result).toBe(model);
});
});
@@ -73,12 +74,12 @@ describe('model-resolver.ts', () => {
expect(result).toBe(customDefault);
});
it('should return default for unknown model key', () => {
it('should pass through unknown model key unchanged (no warning)', () => {
const result = resolveModelString('unknown-model');
expect(result).toBe(DEFAULT_MODELS.claude);
expect(consoleSpy.warn).toHaveBeenCalledWith(
expect.stringContaining('Unknown model key "unknown-model"')
);
// Unknown models pass through unchanged (could be provider models)
expect(result).toBe('unknown-model');
// No warning - unknown models are valid for providers
expect(consoleSpy.warn).not.toHaveBeenCalled();
});
it('should handle empty string', () => {

View File

@@ -1311,4 +1311,317 @@ describe('opencode-provider.ts', () => {
expect(args[modelIndex + 1]).toBe('provider/model-v1.2.3-beta');
});
});
// ==========================================================================
// parseProvidersOutput Tests
// ==========================================================================
describe('parseProvidersOutput', () => {
// Helper function to access private method
function parseProviders(output: string) {
return (
provider as unknown as {
parseProvidersOutput: (output: string) => Array<{
id: string;
name: string;
authenticated: boolean;
authMethod?: 'oauth' | 'api_key';
}>;
}
).parseProvidersOutput(output);
}
// =======================================================================
// Critical Fix Validation
// =======================================================================
describe('Critical Fix Validation', () => {
it('should map "z.ai coding plan" to "zai-coding-plan" (NOT "z-ai")', () => {
const output = '● z.ai coding plan oauth';
const result = parseProviders(output);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('zai-coding-plan');
expect(result[0].name).toBe('z.ai coding plan');
expect(result[0].authMethod).toBe('oauth');
});
it('should map "z.ai" to "z-ai" (different from coding plan)', () => {
const output = '● z.ai api';
const result = parseProviders(output);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('z-ai');
expect(result[0].name).toBe('z.ai');
expect(result[0].authMethod).toBe('api_key');
});
it('should distinguish between "z.ai coding plan" and "z.ai"', () => {
const output = '● z.ai coding plan oauth\n● z.ai api';
const result = parseProviders(output);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('zai-coding-plan');
expect(result[0].name).toBe('z.ai coding plan');
expect(result[1].id).toBe('z-ai');
expect(result[1].name).toBe('z.ai');
});
});
// =======================================================================
// Provider Name Mapping
// =======================================================================
describe('Provider Name Mapping', () => {
it('should map all 12 providers correctly', () => {
const output = `● anthropic oauth
● github copilot oauth
● google api
● openai api
● openrouter api
● azure api
● amazon bedrock oauth
● ollama api
● lm studio api
● opencode oauth
● z.ai coding plan oauth
● z.ai api`;
const result = parseProviders(output);
expect(result).toHaveLength(12);
expect(result.map((p) => p.id)).toEqual([
'anthropic',
'github-copilot',
'google',
'openai',
'openrouter',
'azure',
'amazon-bedrock',
'ollama',
'lmstudio',
'opencode',
'zai-coding-plan',
'z-ai',
]);
});
it('should handle case-insensitive provider names and preserve original casing', () => {
const output = '● Anthropic api\n● OPENAI oauth\n● GitHub Copilot oauth';
const result = parseProviders(output);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('anthropic');
expect(result[0].name).toBe('Anthropic'); // Preserves casing
expect(result[1].id).toBe('openai');
expect(result[1].name).toBe('OPENAI'); // Preserves casing
expect(result[2].id).toBe('github-copilot');
expect(result[2].name).toBe('GitHub Copilot'); // Preserves casing
});
it('should handle multi-word provider names with spaces', () => {
const output = '● Amazon Bedrock oauth\n● LM Studio api\n● GitHub Copilot oauth';
const result = parseProviders(output);
expect(result[0].id).toBe('amazon-bedrock');
expect(result[0].name).toBe('Amazon Bedrock');
expect(result[1].id).toBe('lmstudio');
expect(result[1].name).toBe('LM Studio');
expect(result[2].id).toBe('github-copilot');
expect(result[2].name).toBe('GitHub Copilot');
});
});
// =======================================================================
// Duplicate Aliases
// =======================================================================
describe('Duplicate Aliases', () => {
it('should map provider aliases to the same ID', () => {
// Test copilot variants
const copilot1 = parseProviders('● copilot oauth');
const copilot2 = parseProviders('● github copilot oauth');
expect(copilot1[0].id).toBe('github-copilot');
expect(copilot2[0].id).toBe('github-copilot');
// Test bedrock variants
const bedrock1 = parseProviders('● bedrock oauth');
const bedrock2 = parseProviders('● amazon bedrock oauth');
expect(bedrock1[0].id).toBe('amazon-bedrock');
expect(bedrock2[0].id).toBe('amazon-bedrock');
// Test lmstudio variants
const lm1 = parseProviders('● lmstudio api');
const lm2 = parseProviders('● lm studio api');
expect(lm1[0].id).toBe('lmstudio');
expect(lm2[0].id).toBe('lmstudio');
});
});
// =======================================================================
// Authentication Methods
// =======================================================================
describe('Authentication Methods', () => {
it('should detect oauth and api_key auth methods', () => {
const output = '● anthropic oauth\n● openai api\n● google api_key';
const result = parseProviders(output);
expect(result[0].authMethod).toBe('oauth');
expect(result[1].authMethod).toBe('api_key');
expect(result[2].authMethod).toBe('api_key');
});
it('should set authenticated to true and handle case-insensitive auth methods', () => {
const output = '● anthropic OAuth\n● openai API';
const result = parseProviders(output);
expect(result[0].authenticated).toBe(true);
expect(result[0].authMethod).toBe('oauth');
expect(result[1].authenticated).toBe(true);
expect(result[1].authMethod).toBe('api_key');
});
it('should return undefined authMethod for unknown auth types', () => {
const output = '● anthropic unknown-auth';
const result = parseProviders(output);
expect(result[0].authenticated).toBe(true);
expect(result[0].authMethod).toBeUndefined();
});
});
// =======================================================================
// ANSI Escape Sequences
// =======================================================================
describe('ANSI Escape Sequences', () => {
it('should strip ANSI color codes from output', () => {
const output = '\x1b[32m● anthropic oauth\x1b[0m';
const result = parseProviders(output);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('anthropic');
expect(result[0].name).toBe('anthropic');
});
it('should handle complex ANSI sequences and codes in provider names', () => {
const output =
'\x1b[1;32m●\x1b[0m \x1b[33mgit\x1b[32mhub\x1b[0m copilot\x1b[0m \x1b[36moauth\x1b[0m';
const result = parseProviders(output);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('github-copilot');
});
});
// =======================================================================
// Edge Cases
// =======================================================================
describe('Edge Cases', () => {
it('should return empty array for empty output or no ● symbols', () => {
expect(parseProviders('')).toEqual([]);
expect(parseProviders('anthropic oauth\nopenai api')).toEqual([]);
expect(parseProviders('No authenticated providers')).toEqual([]);
});
it('should skip malformed lines with ● but insufficient content', () => {
const output = '●\n● \n● anthropic\n● openai api';
const result = parseProviders(output);
// Only the last line has both provider name and auth method
expect(result).toHaveLength(1);
expect(result[0].id).toBe('openai');
});
it('should use fallback for unknown providers (spaces to hyphens)', () => {
const output = '● unknown provider name oauth';
const result = parseProviders(output);
expect(result[0].id).toBe('unknown-provider-name');
expect(result[0].name).toBe('unknown provider name');
});
it('should handle extra whitespace and mixed case', () => {
const output = '● AnThRoPiC oauth';
const result = parseProviders(output);
expect(result[0].id).toBe('anthropic');
expect(result[0].name).toBe('AnThRoPiC');
});
it('should handle multiple ● symbols on same line', () => {
const output = '● ● anthropic oauth';
const result = parseProviders(output);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('anthropic');
});
it('should handle different newline formats and trailing newlines', () => {
const outputUnix = '● anthropic oauth\n● openai api';
const outputWindows = '● anthropic oauth\r\n● openai api\r\n\r\n';
const resultUnix = parseProviders(outputUnix);
const resultWindows = parseProviders(outputWindows);
expect(resultUnix).toHaveLength(2);
expect(resultWindows).toHaveLength(2);
});
it('should handle provider names with numbers and special characters', () => {
const output = '● gpt-4o api';
const result = parseProviders(output);
expect(result[0].id).toBe('gpt-4o');
expect(result[0].name).toBe('gpt-4o');
});
});
// =======================================================================
// Real-world CLI Output
// =======================================================================
describe('Real-world CLI Output', () => {
it('should parse CLI output with box drawing characters and decorations', () => {
const output = `┌─────────────────────────────────────────────────┐
│ Authenticated Providers │
├─────────────────────────────────────────────────┤
● anthropic oauth
● openai api
└─────────────────────────────────────────────────┘`;
const result = parseProviders(output);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('anthropic');
expect(result[1].id).toBe('openai');
});
it('should parse output with ANSI colors and box characters', () => {
const output = `\x1b[1m┌─────────────────────────────────────────────────┐\x1b[0m
\x1b[1m│ Authenticated Providers │\x1b[0m
\x1b[1m├─────────────────────────────────────────────────┤\x1b[0m
\x1b[32m●\x1b[0m \x1b[33manthropic\x1b[0m \x1b[36moauth\x1b[0m
\x1b[32m●\x1b[0m \x1b[33mgoogle\x1b[0m \x1b[36mapi\x1b[0m
\x1b[1m└─────────────────────────────────────────────────┘\x1b[0m`;
const result = parseProviders(output);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('anthropic');
expect(result[1].id).toBe('google');
});
it('should handle "no authenticated providers" message', () => {
const output = `┌─────────────────────────────────────────────────┐
│ No authenticated providers found │
└─────────────────────────────────────────────────┘`;
const result = parseProviders(output);
expect(result).toEqual([]);
});
});
});
});

View File

@@ -63,7 +63,10 @@ describe('IdeationService', () => {
} as unknown as EventEmitter;
// Create mock settings service
mockSettingsService = {} as SettingsService;
mockSettingsService = {
getCredentials: vi.fn().mockResolvedValue({}),
getGlobalSettings: vi.fn().mockResolvedValue({}),
} as unknown as SettingsService;
// Create mock feature loader
mockFeatureLoader = {