Files
srbhr--resume-matcher/apps/frontend/tests/api-config.test.ts
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

143 lines
4.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
FeaturePromptsError,
PROVIDER_INFO,
fetchFeatureConfig,
updateFeatureConfig,
updateFeaturePrompts,
type LLMProvider,
} from '@/lib/api/config';
/**
* Config-layer contracts: the provider table must stay in sync with the
* LLMProvider union, and the feature-prompts 422 path must surface the
* backend's structured `missing_placeholders` error as a typed exception.
*/
const ALL_PROVIDERS: LLMProvider[] = [
'openai',
'openai_compatible',
'anthropic',
'openrouter',
'gemini',
'deepseek',
'groq',
'ollama',
];
describe('PROVIDER_INFO', () => {
it('has a complete entry for every supported provider', () => {
for (const provider of ALL_PROVIDERS) {
const info = PROVIDER_INFO[provider];
expect(info, `missing PROVIDER_INFO for ${provider}`).toBeDefined();
expect(info.name.length).toBeGreaterThan(0);
expect(info.defaultModel.length).toBeGreaterThan(0);
expect(typeof info.requiresKey).toBe('boolean');
}
});
it('marks only local providers as not requiring a key', () => {
expect(PROVIDER_INFO.ollama.requiresKey).toBe(false);
expect(PROVIDER_INFO.openai_compatible.requiresKey).toBe(false);
expect(PROVIDER_INFO.openai.requiresKey).toBe(true);
expect(PROVIDER_INFO.anthropic.requiresKey).toBe(true);
});
});
describe('FeaturePromptsError', () => {
it('is an Error carrying the structured validation detail', () => {
const detail = {
code: 'missing_placeholders' as const,
field: 'cover_letter_prompt' as const,
missing: ['{resume_data}', '{output_language}'],
};
const err = new FeaturePromptsError(detail);
expect(err).toBeInstanceOf(Error);
expect(err.name).toBe('FeaturePromptsError');
expect(err.detail).toEqual(detail);
expect(err.message).toContain('cover_letter_prompt');
expect(err.message).toContain('{resume_data}');
});
});
describe('updateFeaturePrompts error mapping', () => {
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('throws FeaturePromptsError on a 422 missing_placeholders response', async () => {
const detail = {
code: 'missing_placeholders',
field: 'cover_letter_prompt',
missing: ['{resume_data}'],
};
fetchMock.mockResolvedValue(new Response(JSON.stringify({ detail }), { status: 422 }));
await expect(updateFeaturePrompts({ cover_letter_prompt: 'bad' })).rejects.toBeInstanceOf(
FeaturePromptsError
);
});
it('throws a generic Error with the string detail on other failures', async () => {
fetchMock.mockResolvedValue(
new Response(JSON.stringify({ detail: 'server boom' }), { status: 500 })
);
await expect(updateFeaturePrompts({ cover_letter_prompt: 'x' })).rejects.toThrow('server boom');
});
it('returns the parsed prompts on success', async () => {
const prompts = {
cover_letter_prompt: 'a',
outreach_message_prompt: 'b',
cover_letter_default: '',
outreach_message_default: '',
};
fetchMock.mockResolvedValue(new Response(JSON.stringify(prompts), { status: 200 }));
await expect(updateFeaturePrompts({ cover_letter_prompt: 'a' })).resolves.toEqual(prompts);
});
});
describe('feature config API', () => {
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
fetchMock = vi.fn();
vi.stubGlobal('fetch', fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('parses enable_interview_prep from GET /config/features', async () => {
const config = {
enable_cover_letter: true,
enable_outreach_message: false,
enable_interview_prep: true,
};
fetchMock.mockResolvedValue(new Response(JSON.stringify(config), { status: 200 }));
await expect(fetchFeatureConfig()).resolves.toEqual(config);
});
it('sends enable_interview_prep in PUT /config/features', async () => {
const config = {
enable_cover_letter: false,
enable_outreach_message: false,
enable_interview_prep: true,
};
fetchMock.mockResolvedValue(new Response(JSON.stringify(config), { status: 200 }));
await expect(updateFeatureConfig({ enable_interview_prep: true })).resolves.toEqual(config);
const [, init] = fetchMock.mock.calls[0];
expect(JSON.parse(init.body as string)).toEqual({ enable_interview_prep: true });
});
});