feat(ai): AI-primary due_date flow — rule as prompt candidates only
Flow 반전 (F7-D 채택): - 기존: rule.iso ?? ai.dueDate (rule 우선) - 신규: ai.dueDate ?? null (AI 우선) 규칙은 parseAllCandidates 로 모든 매치를 추출 → prompt 에 후보 힌트로 주입. AI 가 종합 판단. AI 실패 시 due_date null (별 fallback 없음). 해결되는 케이스: '내일 모레' → AI 가 ambiguous 인지 → null. PROMPT_VERSION → 3. GenerateInput.dueDateCandidates 신규. buildPrompt(rawText, todayKst, candidates) — 빈 배열일 때 hint 섹션 생략. Tests: - AiWorker.test.ts — 'rule priority' 테스트 → 'AI dueDate wins' flip - AiWorker.test.ts — passes todayKst 테스트 확장 (dueDateCandidates 도 검증) - AiWorker.test.ts — 신규 'passes parseAllCandidates result as dueDateCandidates' - LocalOllamaProvider.test.ts / ollama-golden.test.ts — generate 호출에 dueDateCandidates: [] 추가 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ describe.skipIf(skip)('LocalOllamaProvider integration', () => {
|
||||
];
|
||||
|
||||
it.each(cases)('Korean title + 3 lines for: %s', async (input) => {
|
||||
const r = await provider.generate({ text: input, todayKst: '2026-04-26' });
|
||||
const r = await provider.generate({ text: input, todayKst: '2026-04-26', dueDateCandidates: [] });
|
||||
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]+)*$/);
|
||||
|
||||
@@ -82,14 +82,14 @@ describe('AiWorker', () => {
|
||||
expect(max).toBe(1);
|
||||
});
|
||||
|
||||
it('rule parser match takes priority over AI dueDate', async () => {
|
||||
it('AI dueDate wins over rule candidates (flow reversed from F1)', async () => {
|
||||
const provider = {
|
||||
name: 'mock',
|
||||
generate: async (_input: any) => ({
|
||||
generate: async () => ({
|
||||
title: '내일',
|
||||
summary: 'a\nb\nc',
|
||||
tags: [],
|
||||
dueDate: '2026-12-31' // AI returns far-future
|
||||
dueDate: '2026-12-31' // AI says far future, rule has '내일'
|
||||
}),
|
||||
healthCheck: async () => ({ ok: true })
|
||||
} as any;
|
||||
@@ -101,7 +101,7 @@ describe('AiWorker', () => {
|
||||
await w.enqueue(id);
|
||||
await w.drain();
|
||||
const note = repo.findById(id)!;
|
||||
expect(note.dueDate).toBe('2026-04-27'); // 내일 from rule, not AI's 12-31
|
||||
expect(note.dueDate).toBe('2026-12-31'); // AI value, not rule
|
||||
});
|
||||
|
||||
it('rule null + AI value → AI used', async () => {
|
||||
@@ -154,6 +154,7 @@ describe('AiWorker', () => {
|
||||
name: 'mock',
|
||||
generate: async (input: any) => {
|
||||
seen.todayKst = input.todayKst;
|
||||
seen.dueDateCandidates = input.dueDateCandidates;
|
||||
return { title: '메모', summary: 'a\nb\nc', tags: [], dueDate: null };
|
||||
},
|
||||
healthCheck: async () => ({ ok: true })
|
||||
@@ -166,5 +167,29 @@ describe('AiWorker', () => {
|
||||
await w.enqueue(id);
|
||||
await w.drain();
|
||||
expect(seen.todayKst).toBe('2026-04-27');
|
||||
expect(Array.isArray(seen.dueDateCandidates)).toBe(true);
|
||||
expect(seen.dueDateCandidates.length).toBe(0);
|
||||
});
|
||||
|
||||
it('passes parseAllCandidates result to provider.generate as dueDateCandidates', async () => {
|
||||
let captured: any = null;
|
||||
const provider = {
|
||||
name: 'mock',
|
||||
generate: async (input: any) => {
|
||||
captured = input;
|
||||
return { title: '내일', summary: 'a\nb\nc', tags: [], dueDate: null };
|
||||
},
|
||||
healthCheck: async () => ({ ok: true })
|
||||
} as any;
|
||||
const w = new AiWorker(repo, provider, {
|
||||
backoffsMs: [0],
|
||||
now: () => new Date('2026-04-26T00:00:00.000Z')
|
||||
});
|
||||
const { id } = repo.create({ rawText: '내일 모레 회의' });
|
||||
await w.enqueue(id);
|
||||
await w.drain();
|
||||
expect(captured.dueDateCandidates).toBeDefined();
|
||||
expect(Array.isArray(captured.dueDateCandidates)).toBe(true);
|
||||
expect(captured.dueDateCandidates.length).toBe(2); // 내일 + 모레
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('LocalOllamaProvider', () => {
|
||||
mock.get('http://localhost:11434').intercept({ path: '/api/generate', method: 'POST' }).reply(200, {
|
||||
response: JSON.stringify({ title: '회의', summary: '첫\n둘\n셋', tags: ['api'] })
|
||||
});
|
||||
const r = await new LocalOllamaProvider().generate({ text: 'x', todayKst: '2026-04-26' });
|
||||
const r = await new LocalOllamaProvider().generate({ text: 'x', todayKst: '2026-04-26', dueDateCandidates: [] });
|
||||
expect(r.title).toBe('회의');
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('LocalOllamaProvider', () => {
|
||||
response: 'not json'
|
||||
});
|
||||
await expect(
|
||||
new LocalOllamaProvider().generate({ text: 'x', todayKst: '2026-04-26' })
|
||||
new LocalOllamaProvider().generate({ text: 'x', todayKst: '2026-04-26', dueDateCandidates: [] })
|
||||
).rejects.toThrow(/json/i);
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('LocalOllamaProvider', () => {
|
||||
return { statusCode: 200, data: '{}' };
|
||||
}) as never);
|
||||
await expect(
|
||||
new LocalOllamaProvider({ timeoutMs: 50 }).generate({ text: 'x', todayKst: '2026-04-26' })
|
||||
new LocalOllamaProvider({ timeoutMs: 50 }).generate({ text: 'x', todayKst: '2026-04-26', dueDateCandidates: [] })
|
||||
).rejects.toThrow();
|
||||
}, 2000);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user