Files
inkling/tests/unit/classifyStatus.test.ts
altair823 d3150976d4 feat(v029): classifyStatus AI prompt + ai:classify-status 정식 구현 (Task 8 stub 대체)
- src/main/ai/classifyStatus.ts: prompt + JSON parse + 안전 fallback (archived).
- InferenceProvider.generateRaw 추가 (optional) + LocalOllamaProvider 구현
  (Ollama /api/generate format:'json' 으로 raw JSON 응답 반환).
- inboxApi 의 ai:classify-status 핸들러를 stub 에서 정식 호출로 교체
  (deps.repo.findById + deps.providerHolder.get + classifyStatus()).
- 신규 테스트 7건 (classifyStatus 단위) + IPC 3건 (note 없음 / AI throw / 정상).
- 회귀: 513 → 522 통과.
2026-05-09 16:09:33 +09:00

110 lines
3.3 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { classifyStatus } from '../../src/main/ai/classifyStatus';
import type { InferenceProvider } from '../../src/main/ai/InferenceProvider';
function makeProvider(generateRaw?: (p: string) => Promise<string>): InferenceProvider {
return {
name: 'mock',
generate: vi.fn(async () => {
throw new Error('not used');
}),
healthCheck: vi.fn(async () => ({ ok: true })),
...(generateRaw !== undefined ? { generateRaw } : {})
} as InferenceProvider;
}
describe('classifyStatus', () => {
it('parses recommended status and rationale from valid AI response', async () => {
const provider = makeProvider(
vi.fn(async () => '{"recommended":"completed","rationale":"처리됨"}')
);
const r = await classifyStatus({
provider,
rawText: 't',
summary: '',
reason: '결재 끝'
});
expect(r.recommended).toBe('completed');
expect(r.rationale).toBe('처리됨');
});
it('falls back to archived on parse failure (invalid JSON)', async () => {
const provider = makeProvider(vi.fn(async () => 'not json'));
const r = await classifyStatus({
provider,
rawText: 't',
summary: '',
reason: 'r'
});
expect(r.recommended).toBe('archived');
expect(r.rationale).toMatch(/판단 실패|보관/);
});
it('falls back to archived on invalid status value', async () => {
const provider = makeProvider(
vi.fn(async () => '{"recommended":"unknown","rationale":"x"}')
);
const r = await classifyStatus({
provider,
rawText: 't',
summary: '',
reason: 'r'
});
expect(r.recommended).toBe('archived');
});
it('handles provider throw', async () => {
const provider = makeProvider(
vi.fn(async () => {
throw new Error('network');
})
);
const r = await classifyStatus({
provider,
rawText: 't',
summary: '',
reason: 'r'
});
expect(r.recommended).toBe('archived');
expect(r.rationale).toMatch(/판단 실패|보관/);
});
it('falls back when provider lacks generateRaw method', async () => {
const provider = makeProvider();
const r = await classifyStatus({
provider,
rawText: 't',
summary: '',
reason: 'r'
});
expect(r.recommended).toBe('archived');
expect(r.rationale).toMatch(/판단 실패|보관/);
});
it('substitutes empty inputs with placeholder text in prompt', async () => {
const generateRaw = vi.fn(
async (_p: string) => '{"recommended":"archived","rationale":"ok"}'
);
const provider = makeProvider(generateRaw);
await classifyStatus({ provider, rawText: '', summary: '', reason: '' });
const prompt = generateRaw.mock.calls[0]?.[0] ?? '';
expect(prompt).toContain('(빈 메모)');
expect(prompt).toContain('(요약 없음)');
expect(prompt).toContain('(사유 없음)');
});
it('rationale defaults to empty string when missing/non-string', async () => {
const provider = makeProvider(
vi.fn(async () => '{"recommended":"completed"}')
);
const r = await classifyStatus({
provider,
rawText: 't',
summary: '',
reason: 'r'
});
expect(r.recommended).toBe('completed');
expect(r.rationale).toBe('');
});
});