Files
connectai/tests/meetRegistration.test.ts
T
koriweb 70ea421827 feat(meet): 확신 게이트 등록 + /meet confirm + 데일리 브리핑 (v2.2.216)
캘린더 등록 정책을 "확신 없으면 등록 대신 질문"으로 전환:
- 액션 표에 상태 컬럼(확정/진행미정/기한미정/조건부:선행/반복:주기) — LLM 분류.
- 확정+기한만 자동 등록. 진행미정·기한미정·조건부는 보류 목록으로 질문,
  `/meet confirm 1=6/20 2=ok 3=skip` 답변으로 등록 완결 (/meet pending 재확인).
- 조건부 규칙: ok=날짜 없는 Tasks 로 [조건부] 등록(선행조건 노트 명시),
  날짜=그날을 '조건 확인일'로 등록 — 의존 대상이 제목/노트에서 즉시 인지됨.
- 반복 업무: 반복 등록 없이 첫 1회만(다음 해당 요일) — 까먹음 방지.
- 기한 해석 불가 확정건: 구버전의 +5일 추측 등록 제거 → 보류 질문.
- 과거 날짜(옛 녹취): 과거 날짜 그대로 등록 + "과거자료·완료확인 필요" 표기.
- 중복 방지: 녹취 sha256 해시 레지스트리(.astra/meet_registered.json)로
  같은 녹취 재실행 시 이중 등록 차단.
- tasksApi: due 옵션화(날짜 없는 task 지원).

데일리 브리핑 (신규):
- 평일 KST 09:30(설정 가능) 오늘의 캘린더 일정 + Tasks(오늘 마감/기한 경과/
  조건부 대기)를 텔레그램 발송. 텔레그램·캘린더 미연결 시 조용히 skip.
- g1nation.dailyBriefing.enabled(기본 true) / .time("09:30").

테스트: meetRegistration 15건 (분류 게이트·confirm 파싱·날짜 정규화·중복 키).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:22:19 +09:00

111 lines
5.0 KiB
TypeScript

/**
* /meet 확신 게이트 — 분류·confirm 파싱·날짜 정규화 테스트.
* 정책: 확정+기한만 자동, 진행미정/기한미정/조건부는 보류, 반복은 첫 1회,
* 과거 날짜는 등록하되 완료확인 표기, 기한 해석 불가 확정건은 보류(추측 등록 금지).
*/
import { classifyAction, parseConfirmArgs, normalizeDate, nextWeekday, taskKey } from '../src/features/datacollect/scheduling/meetRegistration';
const MEET = new Date('2026-06-10');
const TODAY = new Date('2026-06-11');
const row = (due: string, status: string) => ({ owner: '나', work: '테스트 작업', detail: '', due, status });
describe('classifyAction — 등록 게이트 분기', () => {
test('확정 + 명시 기한 → auto', () => {
const c = classifyAction(row('2026-06-20', '확정'), MEET, TODAY);
expect(c).toMatchObject({ route: 'auto', date: '2026-06-20', pastNote: false });
});
test('진행미정 → hold(undecided)', () => {
const c = classifyAction(row('2026-06-20', '진행미정'), MEET, TODAY);
expect(c).toMatchObject({ route: 'hold', kind: 'undecided' });
});
test('기한미정 → hold(nodate) + 제안 날짜 제공', () => {
const c = classifyAction(row('', '기한미정'), MEET, TODAY);
expect(c.route).toBe('hold');
if (c.route === 'hold') {
expect(c.kind).toBe('nodate');
expect(c.suggestedDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
}
});
test('조건부 → hold(conditional) + 선행작업 보존', () => {
const c = classifyAction(row('', '조건부: 계약 체결 후'), MEET, TODAY);
expect(c).toMatchObject({ route: 'hold', kind: 'conditional', condition: '계약 체결 후' });
});
test('반복(매주 목요일) → 첫 1회만 auto, 다음 목요일', () => {
const c = classifyAction(row('', '반복: 매주 목요일'), MEET, TODAY);
// 2026-06-11(목) 기준 다음 목요일 = 06-18 (오늘이 그 요일이면 다음 주)
expect(c).toMatchObject({ route: 'auto', date: '2026-06-18', recurNote: '매주 목요일' });
});
test('반복인데 요일 해석 불가 → 보류(추측 등록 금지)', () => {
const c = classifyAction(row('', '반복: 격주'), MEET, TODAY);
expect(c.route).toBe('hold');
});
test('과거 날짜(옛 녹취) → auto + pastNote (완료확인 표기 대상)', () => {
const c = classifyAction(row('2026-05-01', '확정'), MEET, TODAY);
expect(c).toMatchObject({ route: 'auto', date: '2026-05-01', pastNote: true });
});
test('확정이지만 기한 해석 불가 → 보류 (구버전의 +5일 추측 등록 제거)', () => {
const c = classifyAction(row('추후 논의', '확정'), MEET, TODAY);
expect(c.route).toBe('hold');
if (c.route === 'hold') expect(c.kind).toBe('nodate');
});
test('상태 누락(구표 호환) — 기한 있으면 auto', () => {
const c = classifyAction(row('2026-07-01', ''), MEET, TODAY);
expect(c).toMatchObject({ route: 'auto', date: '2026-07-01' });
});
});
describe('parseConfirmArgs / normalizeDate', () => {
test('혼합 답변 파싱', () => {
const { decisions, errors } = parseConfirmArgs('1=6/20 2=ok 3=skip 4=2026-07-01', 2026);
expect(errors).toEqual([]);
expect(decisions).toEqual([
{ idx: 1, action: 'date', date: '2026-06-20' },
{ idx: 2, action: 'ok' },
{ idx: 3, action: 'skip' },
{ idx: 4, action: 'date', date: '2026-07-01' },
]);
});
test('한글 별칭(등록/취소) + M월D일', () => {
const { decisions } = parseConfirmArgs('1=등록 2=취소 3=7월3일', 2026);
expect(decisions).toEqual([
{ idx: 1, action: 'ok' },
{ idx: 2, action: 'skip' },
{ idx: 3, action: 'date', date: '2026-07-03' },
]);
});
test('형식 오류는 errors 로 분리', () => {
const { decisions, errors } = parseConfirmArgs('1=언젠가 foo', 2026);
expect(decisions).toEqual([]);
expect(errors.length).toBe(2);
});
test('normalizeDate 변형들', () => {
expect(normalizeDate('2026-6-5', 2026)).toBe('2026-06-05');
expect(normalizeDate('6/20', 2026)).toBe('2026-06-20');
expect(normalizeDate('12월3일', 2026)).toBe('2026-12-03');
expect(normalizeDate('내일', 2026)).toBeNull();
});
});
describe('보조 유틸', () => {
test('nextWeekday — 오늘이 해당 요일이면 다음 주', () => {
expect(nextWeekday(new Date('2026-06-11'), '목')!.toISOString().slice(0, 10)).toBe('2026-06-18');
expect(nextWeekday(new Date('2026-06-11'), '금')!.toISOString().slice(0, 10)).toBe('2026-06-12');
expect(nextWeekday(new Date('2026-06-11'), 'X')).toBeNull();
});
test('taskKey — 표기 변형에도 같은 키 (중복 방지)', () => {
expect(taskKey('DRM 라이선스 검토')).toBe(taskKey('drm 라이선스 검토'));
});
});