diff --git a/packages/aiCore/src/core/providers/__tests__/types.test.ts b/packages/aiCore/src/core/providers/__tests__/types.test.ts new file mode 100644 index 0000000000..389d83ff90 --- /dev/null +++ b/packages/aiCore/src/core/providers/__tests__/types.test.ts @@ -0,0 +1,275 @@ +/** + * Provider Types - Type-level Tests + * Tests type utilities and type inference for provider extensions + */ + +import type { ProviderV3 } from '@ai-sdk/provider' +import { describe, expectTypeOf, it } from 'vitest' + +import type { ProviderExtensionConfig } from '../core/ProviderExtension' +import type { + CoreProviderSettingsMap, + ExtensionConfigToIdResolutionMap, + ExtractExtensionIds, + ExtractProviderIds, + StringKeys, + UnionToIntersection +} from '../types' + +describe('Type Utilities', () => { + describe('StringKeys', () => { + it('should extract only string keys from object type', () => { + type TestObj = { foo: 1; bar: 2; 0: 3; 1: 4 } + type Result = StringKeys + + expectTypeOf().toEqualTypeOf<'foo' | 'bar'>() + }) + + it('should return never for object with no string keys', () => { + type TestObj = { 0: 'a'; 1: 'b' } + type Result = StringKeys + + expectTypeOf().toEqualTypeOf() + }) + + it('should handle empty object', () => { + type Result = StringKeys<{}> + + expectTypeOf().toEqualTypeOf() + }) + + it('should preserve literal string keys', () => { + type TestObj = { openai: 1; anthropic: 2; google: 3 } + type Result = StringKeys + + expectTypeOf().toEqualTypeOf<'openai' | 'anthropic' | 'google'>() + }) + }) + + describe('UnionToIntersection', () => { + it('should convert union to intersection', () => { + type Union = { a: 1 } | { b: 2 } + type Result = UnionToIntersection + + expectTypeOf().toEqualTypeOf<{ a: 1 } & { b: 2 }>() + }) + + it('should handle single type', () => { + type Single = { a: 1 } + type Result = UnionToIntersection + + expectTypeOf().toEqualTypeOf<{ a: 1 }>() + }) + }) + + describe('ExtractProviderIds', () => { + it('should extract base name', () => { + type Config = { name: 'openai' } + type Result = ExtractProviderIds + + expectTypeOf().toEqualTypeOf<'openai'>() + }) + + it('should extract name and aliases', () => { + type Config = { name: 'anthropic'; aliases: readonly ['claude'] } + type Result = ExtractProviderIds + + expectTypeOf().toEqualTypeOf<'anthropic' | 'claude'>() + }) + + it('should extract name and variants', () => { + type Config = { name: 'openai'; variants: readonly [{ suffix: 'chat' }] } + type Result = ExtractProviderIds + + expectTypeOf().toEqualTypeOf<'openai' | 'openai-chat'>() + }) + + it('should extract name, aliases, and variants', () => { + type Config = { + name: 'azure' + aliases: readonly ['azure-openai'] + variants: readonly [{ suffix: 'responses' }] + } + type Result = ExtractProviderIds + + expectTypeOf().toEqualTypeOf<'azure' | 'azure-openai' | 'azure-responses'>() + }) + + it('should handle multiple variants', () => { + type Config = { + name: 'openai' + variants: readonly [{ suffix: 'chat' }, { suffix: 'responses' }] + } + type Result = ExtractProviderIds + + expectTypeOf().toEqualTypeOf<'openai' | 'openai-chat' | 'openai-responses'>() + }) + }) + + describe('ExtensionConfigToIdResolutionMap', () => { + it('should map base name to itself', () => { + type Config = { name: 'openai' } + type Result = ExtensionConfigToIdResolutionMap + + expectTypeOf().toEqualTypeOf<{ readonly openai: 'openai' }>() + }) + + it('should map aliases to base name', () => { + type Config = { name: 'anthropic'; aliases: readonly ['claude'] } + type Result = ExtensionConfigToIdResolutionMap + + expectTypeOf().toEqualTypeOf<{ + readonly anthropic: 'anthropic' + readonly claude: 'anthropic' + }>() + }) + + it('should map variants to themselves (self-referential)', () => { + type Config = { name: 'azure'; variants: readonly [{ suffix: 'responses' }] } + type Result = ExtensionConfigToIdResolutionMap + + expectTypeOf().toEqualTypeOf<{ + readonly azure: 'azure' + readonly 'azure-responses': 'azure-responses' + }>() + }) + + it('should handle combined aliases and variants correctly', () => { + type Config = { + name: 'azure' + aliases: readonly ['azure-openai'] + variants: readonly [{ suffix: 'responses' }] + } + type Result = ExtensionConfigToIdResolutionMap + + expectTypeOf().toEqualTypeOf<{ + readonly azure: 'azure' + readonly 'azure-openai': 'azure' + readonly 'azure-responses': 'azure-responses' + }>() + }) + }) + + describe('ExtractExtensionIds', () => { + it('should extract IDs from extension with config property', () => { + type MockExtension = { + config: { name: 'test'; aliases: readonly ['test-alias'] } + } + type Result = ExtractExtensionIds + + expectTypeOf().toEqualTypeOf<'test' | 'test-alias'>() + }) + }) + + describe('ExtensionToSettingsMap', () => { + it('should map provider IDs to settings type', () => { + type MockSettings = { apiKey: string } + type MockConfig = { name: 'mock' } + + // Simulate ProviderExtension structure + type MockExtension = { + config: MockConfig + // ProviderExtension + } & { __settings: MockSettings } + + // This tests the concept - actual implementation depends on ProviderExtension structure + type Result = { [K in ExtractProviderIds]: MockSettings } + + expectTypeOf().toEqualTypeOf<{ mock: MockSettings }>() + }) + }) + + describe('CoreProviderSettingsMap', () => { + it('should include openai provider', () => { + expectTypeOf().toHaveProperty('openai') + }) + + it('should include anthropic provider', () => { + expectTypeOf().toHaveProperty('anthropic') + }) + + it('should include google provider', () => { + expectTypeOf().toHaveProperty('google') + }) + + it('should include azure provider', () => { + expectTypeOf().toHaveProperty('azure') + }) + + it('should include xai provider', () => { + expectTypeOf().toHaveProperty('xai') + }) + + it('should include deepseek provider', () => { + expectTypeOf().toHaveProperty('deepseek') + }) + + it('should include openrouter provider', () => { + expectTypeOf().toHaveProperty('openrouter') + }) + + it('should include aliases like claude', () => { + expectTypeOf().toHaveProperty('claude') + }) + + it('should include variants like openai-chat', () => { + expectTypeOf().toHaveProperty('openai-chat') + }) + + it('should include variants like azure-responses', () => { + expectTypeOf().toHaveProperty('azure-responses') + }) + }) +}) + +describe('ProviderExtensionConfig Type Constraints', () => { + it('should accept valid minimal config', () => { + type ValidConfig = ProviderExtensionConfig<{ apiKey: string }, {}, ProviderV3, 'test'> + + // Should compile without errors + const config: ValidConfig = { + name: 'test', + create: () => ({}) as ProviderV3 + } + + expectTypeOf(config.name).toEqualTypeOf<'test'>() + }) + + it('should accept config with aliases', () => { + type ConfigWithAliases = { + name: 'anthropic' + aliases: readonly ['claude'] + create: () => ProviderV3 + } + + const config: ConfigWithAliases = { + name: 'anthropic', + aliases: ['claude'] as const, + create: () => ({}) as ProviderV3 + } + + expectTypeOf(config.aliases).toEqualTypeOf() + }) + + it('should accept config with variants', () => { + type ConfigWithVariants = { + name: 'openai' + variants: readonly [{ suffix: 'chat'; name: string; transform: (p: ProviderV3) => ProviderV3 }] + create: () => ProviderV3 + } + + const config: ConfigWithVariants = { + name: 'openai', + variants: [ + { + suffix: 'chat', + name: 'OpenAI Chat', + transform: (p) => p + } + ] as const, + create: () => ({}) as ProviderV3 + } + + expectTypeOf(config.variants[0].suffix).toEqualTypeOf<'chat'>() + }) +})