feat(recall): NoteRepository — findRecallCandidate + markRecallOpened + dismissRecall (#6 v0.2.3)
- findRecallCandidate(): 7일+ 안 본 + 30일+ dismiss 만료 + ai='done' + 마감 안 임박 + LIMIT 1
- markRecallOpened(id, now): last_recalled_at 갱신
- dismissRecall(id, now): recall_dismissed_at 갱신
- KST 보정 SQL date('now','+9 hours')
- 단위 +5 cases (empty/recent/old/dismiss expiry/exclude variants)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -214,6 +214,59 @@ describe('NoteRepository', () => {
|
||||
expect(typeof n).toBe('number');
|
||||
expect(n).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
it('findRecallCandidate returns null for empty db', () => {
|
||||
expect(repo.findRecallCandidate()).toBeNull();
|
||||
});
|
||||
|
||||
it('findRecallCandidate excludes notes recalled within 7 days', () => {
|
||||
const id = repo.create({ rawText: 'x' }).id;
|
||||
repo.updateAiResult(id, { title: 't', summary: 'a\nb\nc', tags: ['x'], provider: 'p' });
|
||||
// 5일 전 본 노트 → 제외
|
||||
const fiveDaysAgo = new Date(Date.now() - 5 * 86_400_000).toISOString();
|
||||
repo.markRecallOpened(id, fiveDaysAgo);
|
||||
expect(repo.findRecallCandidate()).toBeNull();
|
||||
});
|
||||
|
||||
it('findRecallCandidate includes notes recalled 8+ days ago', () => {
|
||||
const id = repo.create({ rawText: 'x' }).id;
|
||||
repo.updateAiResult(id, { title: 't', summary: 'a\nb\nc', tags: ['x'], provider: 'p' });
|
||||
const eightDaysAgo = new Date(Date.now() - 8 * 86_400_000).toISOString();
|
||||
repo.markRecallOpened(id, eightDaysAgo);
|
||||
expect(repo.findRecallCandidate()?.id).toBe(id);
|
||||
});
|
||||
|
||||
it('findRecallCandidate respects dismiss expiry (25일 제외, 35일 후보)', () => {
|
||||
const a = repo.create({ rawText: 'a' }).id;
|
||||
const b = repo.create({ rawText: 'b' }).id;
|
||||
repo.updateAiResult(a, { title: 't', summary: 'a\nb\nc', tags: ['x'], provider: 'p' });
|
||||
repo.updateAiResult(b, { title: 't', summary: 'a\nb\nc', tags: ['x'], provider: 'p' });
|
||||
const twentyFiveDaysAgo = new Date(Date.now() - 25 * 86_400_000).toISOString();
|
||||
const thirtyFiveDaysAgo = new Date(Date.now() - 35 * 86_400_000).toISOString();
|
||||
repo.dismissRecall(a, twentyFiveDaysAgo); // 25일 — 아직 dismiss 만료 안 됨
|
||||
repo.dismissRecall(b, thirtyFiveDaysAgo); // 35일 — dismiss 만료, 재추천 가능
|
||||
const candidate = repo.findRecallCandidate();
|
||||
expect(candidate?.id).toBe(b);
|
||||
});
|
||||
|
||||
it('findRecallCandidate excludes deleted/pending/imminent due', () => {
|
||||
const todayKst = new Date(Date.now() + 9 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
||||
const yesterdayKst = new Date(Date.now() + 9 * 60 * 60 * 1000 - 86_400_000).toISOString().slice(0, 10);
|
||||
// (a) deleted
|
||||
const a = repo.create({ rawText: 'a' }).id;
|
||||
repo.updateAiResult(a, { title: 't', summary: 'a\nb\nc', tags: ['x'], provider: 'p' });
|
||||
repo.trash(a, new Date().toISOString());
|
||||
// (b) pending (no AI)
|
||||
repo.create({ rawText: 'b' });
|
||||
// (c) due_date 어제
|
||||
const c = repo.create({ rawText: 'c' }).id;
|
||||
repo.updateAiResult(c, { title: 't', summary: 'a\nb\nc', tags: ['x'], dueDate: yesterdayKst, provider: 'p' });
|
||||
expect(repo.findRecallCandidate()).toBeNull();
|
||||
// (d) due_date today 는 OK (>=today 통과)
|
||||
const d = repo.create({ rawText: 'd' }).id;
|
||||
repo.updateAiResult(d, { title: 't', summary: 'a\nb\nc', tags: ['x'], dueDate: todayKst, provider: 'p' });
|
||||
expect(repo.findRecallCandidate()?.id).toBe(d);
|
||||
});
|
||||
});
|
||||
|
||||
describe('NoteRepository.trash', () => {
|
||||
|
||||
Reference in New Issue
Block a user