Task 10 of the slice plan. parseAiResponse runs the model JSON through a zod RawResponseSchema, then enforces slice rules: - title MUST contain Korean characters; throw otherwise. - summary normalized to exactly 3 lines (pad with empty when too few; collapse trailing lines into line 3 when too many). - tags filtered to /^[a-z0-9]+(-[a-z0-9]+)*$/ kebab-case and capped at 3. buildPrompt assembles the user-facing prompt with explicit Korean-output / kebab-tag / no-fence rules (PROMPT_VERSION=1). Verification: `npx vitest run tests/unit/ai-schema.test.ts` 7 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
56 lines
1.6 KiB
TypeScript
56 lines
1.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { parseAiResponse } from '@main/ai/schema.js';
|
|
|
|
describe('parseAiResponse', () => {
|
|
it('accepts valid Korean title, 3-line summary, kebab tags', () => {
|
|
const r = parseAiResponse({
|
|
title: '회의 요약',
|
|
summary: '첫 줄\n둘째 줄\n셋째 줄',
|
|
tags: ['api-timeout', 'meeting']
|
|
});
|
|
expect(r.title).toBe('회의 요약');
|
|
expect(r.summary.split('\n')).toHaveLength(3);
|
|
expect(r.tags).toEqual(['api-timeout', 'meeting']);
|
|
});
|
|
|
|
it('rejects title without Korean', () => {
|
|
expect(() =>
|
|
parseAiResponse({ title: 'English only', summary: 'a\nb\nc', tags: [] })
|
|
).toThrow(/korean/i);
|
|
});
|
|
|
|
it('pads short summary to 3 lines', () => {
|
|
const r = parseAiResponse({ title: '제목', summary: '한 줄', tags: [] });
|
|
expect(r.summary.split('\n')).toHaveLength(3);
|
|
});
|
|
|
|
it('compresses long summary to 3 lines', () => {
|
|
const r = parseAiResponse({
|
|
title: '제목', summary: 'a\nb\nc\nd\ne', tags: []
|
|
});
|
|
const lines = r.summary.split('\n');
|
|
expect(lines).toHaveLength(3);
|
|
expect(lines[2]).toBe('c d e');
|
|
});
|
|
|
|
it('filters invalid tags', () => {
|
|
const r = parseAiResponse({
|
|
title: '제목', summary: 'a\nb\nc',
|
|
tags: ['good-tag', 'BadCase', 'has space', 'ok2', '']
|
|
});
|
|
expect(r.tags).toEqual(['good-tag', 'ok2']);
|
|
});
|
|
|
|
it('caps tags to 3', () => {
|
|
const r = parseAiResponse({
|
|
title: '제목', summary: 'a\nb\nc',
|
|
tags: ['a', 'b', 'c', 'd', 'e']
|
|
});
|
|
expect(r.tags).toHaveLength(3);
|
|
});
|
|
|
|
it('rejects non-object input', () => {
|
|
expect(() => parseAiResponse('nope')).toThrow();
|
|
});
|
|
});
|