Files
inkling/tests/unit/dueDateParser.test.ts
altair823 1c72b64c2f feat(due-date): parseAllCandidates — extract all matches (text order)
기존 parseDueDate (first-match-wins) 는 backward compat 로 보존.
parseAllCandidates 가 모든 high/medium 매치를 text 순서로 반환 — F7
AI-primary flow 의 prompt 후보 주입 입력으로 사용.

Overlapping-span suppression: 다음 주 월요일 같은 케이스에서 rule 10
(전체) 이 rule 13 (다음 주 alone) 을 포함하면 후자 매치 제거.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 13:04:17 +09:00

176 lines
6.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { parseDueDate, parseAllCandidates } from '@main/services/dueDateParser.js';
// 2026-04-26 is a Sunday (KST). Use that as "today" for fixtures.
const TODAY = new Date('2026-04-26T00:00:00.000Z'); // KST midnight = UTC 15:00 prior day, but for parser logic we treat input Date as KST-aligned. The parser treats the passed Date as the "today reference" without further timezone math.
describe('parseDueDate (Korean rule parser)', () => {
it('returns null for empty / no-token text', () => {
expect(parseDueDate('아무 일정도 없는 메모', TODAY).iso).toBeNull();
});
it('parses YYYY-MM-DD literal', () => {
expect(parseDueDate('2026-05-15 마감', TODAY).iso).toBe('2026-05-15');
});
it('rejects invalid YYYY-MM-DD (silent coercion)', () => {
// 2026-02-30 is invalid; parser should NOT match it as the literal rule
const r = parseDueDate('2026-02-30 마감', TODAY);
// Falls through to other rules — but no other token matches "2026-02-30 마감"
expect(r.iso).toBeNull();
});
it('parses N월 N일 in current year if future', () => {
expect(parseDueDate('5월 1일 휴가 신청', TODAY).iso).toBe('2026-05-01');
});
it('parses N월 N일 in next year if past', () => {
expect(parseDueDate('3월 1일 발표', TODAY).iso).toBe('2027-03-01');
});
it('parses MM/DD', () => {
expect(parseDueDate('5/3 데모', TODAY).iso).toBe('2026-05-03');
});
it('parses N일 뒤', () => {
expect(parseDueDate('3일 뒤 데모', TODAY).iso).toBe('2026-04-29');
});
it('parses N일 후', () => {
expect(parseDueDate('5일 후 미팅', TODAY).iso).toBe('2026-05-01');
});
it('parses N주 뒤', () => {
expect(parseDueDate('2주 뒤 회의', TODAY).iso).toBe('2026-05-10');
});
it('parses N개월 뒤', () => {
expect(parseDueDate('1개월 뒤 점검', TODAY).iso).toBe('2026-05-26');
});
it('parses 모레', () => {
expect(parseDueDate('모레 발표', TODAY).iso).toBe('2026-04-28');
});
it('parses 내일', () => {
expect(parseDueDate('내일 회의 준비', TODAY).iso).toBe('2026-04-27');
});
it('parses 글피', () => {
expect(parseDueDate('글피까지 마무리', TODAY).iso).toBe('2026-04-29');
});
it('parses 오늘', () => {
expect(parseDueDate('오늘 PR 리뷰', TODAY).iso).toBe('2026-04-26');
});
it('parses 다음 주 월요일', () => {
expect(parseDueDate('다음 주 월요일까지 슬라이드', TODAY).iso).toBe('2026-05-04');
});
it('parses 다음 주 금요일', () => {
expect(parseDueDate('다음 주 금요일에 점검', TODAY).iso).toBe('2026-05-08');
});
it('parses 이번 주 일요일 = today (Sunday)', () => {
expect(parseDueDate('이번 주 일요일 약속', TODAY).iso).toBe('2026-04-26');
});
it('parses 이번 주 수요일', () => {
// This week's Mon = 04-20, so this week's Wed = 04-22. But that's past — still resolve to 04-22 (parser does not skip past dates within current week — caller can decide to ignore).
expect(parseDueDate('이번 주 수요일 회의', TODAY).iso).toBe('2026-04-22');
});
it('parses 다음 달 1일', () => {
expect(parseDueDate('다음 달 1일', TODAY).iso).toBe('2026-05-01');
});
it('parses 다음 달 (alone) → first of next month', () => {
expect(parseDueDate('다음 달 마감', TODAY).iso).toBe('2026-05-01');
});
it('returns medium confidence with iso null for 월말', () => {
const r = parseDueDate('월말 마감', TODAY);
expect(r.iso).toBeNull();
expect(r.confidence).toBe('medium');
expect(r.matchedToken).toBe('월말');
});
it('returns medium confidence with iso null for 주말', () => {
const r = parseDueDate('주말까지 정리', TODAY);
expect(r.iso).toBeNull();
expect(r.confidence).toBe('medium');
});
it('returns medium confidence for 퇴근 전', () => {
const r = parseDueDate('퇴근 전 답장', TODAY);
expect(r.iso).toBeNull();
expect(r.confidence).toBe('medium');
});
it('returns null for time-only (오후 3시)', () => {
// Time tokens get medium confidence (defer to AI) since they imply same-day but not a date directly.
const r = parseDueDate('오후 3시 미팅', TODAY);
expect(r.iso).toBeNull();
expect(r.confidence).toBe('medium');
});
it('first match wins when multiple tokens present', () => {
expect(parseDueDate('내일도 모레도 바쁨', TODAY).iso).toBe('2026-04-27');
});
it('matches 다음 주 alone → next Monday', () => {
// If not followed by 요일, "다음 주" alone defaults to next Monday
expect(parseDueDate('다음 주 발표', TODAY).iso).toBe('2026-05-04');
});
});
describe('parseAllCandidates', () => {
it('returns empty array when no token', () => {
expect(parseAllCandidates('아무 일정 없음', TODAY)).toEqual([]);
});
it('returns 2 high-confidence candidates for "내일 모레"', () => {
const r = parseAllCandidates('내일 모레', TODAY);
expect(r.length).toBe(2);
const isos = r.map((c) => c.iso);
expect(isos).toContain('2026-04-27');
expect(isos).toContain('2026-04-28');
expect(r.every((c) => c.confidence === 'high')).toBe(true);
});
it('returns candidates in text-position order', () => {
const r = parseAllCandidates('내일 모레', TODAY);
expect(r[0]!.matchedToken).toBe('내일');
expect(r[1]!.matchedToken).toBe('모레');
});
it('returns 1 candidate for single token "내일"', () => {
const r = parseAllCandidates('내일 회의', TODAY);
expect(r.length).toBe(1);
expect(r[0]!.iso).toBe('2026-04-27');
});
it('returns 1 medium-confidence candidate for "월말 마감"', () => {
const r = parseAllCandidates('월말 마감', TODAY);
expect(r.length).toBe(1);
expect(r[0]!.iso).toBeNull();
expect(r[0]!.confidence).toBe('medium');
expect(r[0]!.matchedToken).toBe('월말');
});
it('returns mix of high + medium candidates', () => {
const r = parseAllCandidates('내일 월말 회의', TODAY);
expect(r.length).toBe(2);
expect(r[0]!.confidence).toBe('high');
expect(r[1]!.confidence).toBe('medium');
});
it('returns 2 candidates for "5월 1일 이후 다음 주 월요일까지"', () => {
const r = parseAllCandidates('5월 1일 이후 다음 주 월요일까지', TODAY);
expect(r.length).toBe(2);
expect(r[0]!.iso).toBe('2026-05-01');
expect(r[1]!.iso).toBe('2026-05-04');
});
});