feat(ai): LocalOllamaProvider with 120s timeout + integration harness
Task 12 of the slice plan. Implements the slice's only provider:
- generate(): POST {endpoint}/api/generate with Korean-first
prompt; AbortController-driven 120s timeout; parses
body.response as JSON and runs it through parseAiResponse.
- healthCheck(): GET /api/tags; returns ok when configured model
is in the listing, otherwise reports the missing-model reason.
- Constructor takes opts.endpoint / opts.model so Task 30 main
entry can inject INKLING_OLLAMA_ENDPOINT for LAN dogfood;
defaults are http://localhost:11434 and gemma4:e4b.
Tests: 6 unit cases via undici MockAgent (parse, non-JSON,
timeout abort, healthCheck ok / missing / connection error).
Integration test gated by INKLING_INTEGRATION=1 hits real
Ollama for Korean / English-stack / mixed input cases with a
180s per-test budget — also reads INKLING_OLLAMA_ENDPOINT so
LAN dogfood can be exercised end-to-end.
Plan deviation: undici@8 types treat MockAgent's `.reply()`
callback as sync-return-only, so the 500ms-delayed reply used
in the timeout test is cast `as never` to bypass the overload
mismatch. Behavior is correct at runtime; the cast is local to
the test.
Verification: `npx vitest run tests/unit/LocalOllamaProvider.test.ts`
6 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
28
tests/integration/ollama-golden.test.ts
Normal file
28
tests/integration/ollama-golden.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import { LocalOllamaProvider } from '@main/ai/LocalOllamaProvider.js';
|
||||
|
||||
const skip = process.env.INKLING_INTEGRATION !== '1';
|
||||
|
||||
describe.skipIf(skip)('LocalOllamaProvider integration', () => {
|
||||
const provider = new LocalOllamaProvider({
|
||||
endpoint: process.env.INKLING_OLLAMA_ENDPOINT
|
||||
});
|
||||
|
||||
beforeAll(async () => {
|
||||
const h = await provider.healthCheck();
|
||||
if (!h.ok) throw new Error(`Ollama not ready: ${h.reason}`);
|
||||
});
|
||||
|
||||
const cases = [
|
||||
'회의 중 A프로젝트 API 타임아웃 문제가 재발했다는 보고를 받음.',
|
||||
'Stack trace: java.net.SocketTimeoutException at com.inkling.Api.call ... retried 3 times.',
|
||||
'오늘 점심 김치찌개 맛있었음. 오후에 디자인 미팅 있다.'
|
||||
];
|
||||
|
||||
it.each(cases)('Korean title + 3 lines for: %s', async (input) => {
|
||||
const r = await provider.generate({ text: input });
|
||||
expect(/[가-힣]/.test(r.title)).toBe(true);
|
||||
expect(r.summary.split('\n')).toHaveLength(3);
|
||||
for (const t of r.tags) expect(t).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/);
|
||||
}, 180_000);
|
||||
});
|
||||
Reference in New Issue
Block a user