diff --git a/src/main/repository/NoteRepository.ts b/src/main/repository/NoteRepository.ts index 644d535..44dff17 100644 --- a/src/main/repository/NoteRepository.ts +++ b/src/main/repository/NoteRepository.ts @@ -345,7 +345,9 @@ export class NoteRepository { getPendingCount(): number { const row = this.db - .prepare(`SELECT COUNT(*) AS c FROM notes WHERE ai_status='pending'`) + .prepare( + `SELECT COUNT(*) AS c FROM notes WHERE ai_status='pending' AND deleted_at IS NULL` + ) .get() as { c: number }; return row.c; } diff --git a/src/main/services/ContinuityService.ts b/src/main/services/ContinuityService.ts index 6b2d739..54e4263 100644 --- a/src/main/services/ContinuityService.ts +++ b/src/main/services/ContinuityService.ts @@ -32,7 +32,9 @@ export class ContinuityService { get(): WeeklyContinuity { const rows = this.db - .prepare(`SELECT created_at FROM notes ORDER BY created_at ASC`) + .prepare( + `SELECT created_at FROM notes WHERE deleted_at IS NULL ORDER BY created_at ASC` + ) .all() as Array<{ created_at: string }>; const dates = rows.map((r) => new Date(r.created_at)); if (dates.length === 0) { diff --git a/src/main/services/MediaGc.ts b/src/main/services/MediaGc.ts index 7c8fcb5..52928a6 100644 --- a/src/main/services/MediaGc.ts +++ b/src/main/services/MediaGc.ts @@ -6,6 +6,9 @@ export class MediaGc { async run(): Promise<{ removed: number }> { const dirs = await this.store.listNoteDirs(); + // Intentionally does NOT filter `deleted_at IS NULL` — trashed notes still own + // their media until permanentDelete/emptyTrash. Removing dirs of soft-deleted + // notes here would defeat restore. const rows = this.db.prepare('SELECT id FROM notes').all() as Array<{ id: string }>; const known = new Set(rows.map((r) => r.id)); let removed = 0; diff --git a/tests/unit/ContinuityService.test.ts b/tests/unit/ContinuityService.test.ts index 4cee360..f98974e 100644 --- a/tests/unit/ContinuityService.test.ts +++ b/tests/unit/ContinuityService.test.ts @@ -88,4 +88,18 @@ describe('ContinuityService', () => { const svc = new ContinuityService(db, () => new Date('2026-04-25T12:00:00+09:00')); expect(svc.get().showRecoveryToast).toBe(false); }); + + it('excludes trashed notes from streak/recovery math (v0.2.3 #4)', () => { + const db = dbWithDates([ + '2026-04-22T10:00:00+09:00', + '2026-04-25T11:00:00+09:00' + ]); + // 22일 노트를 trash → 25일이 마지막. 22일 미만이라 weekCount 1 이지만 lastNoteAt + // 은 25일 (마지막 active) 이어야 함. trashed 노트가 무시되어야 함. + db.prepare(`UPDATE notes SET deleted_at='2026-04-26T00:00:00Z' WHERE id='n0'`).run(); + const svc = new ContinuityService(db, () => new Date('2026-04-25T12:00:00+09:00')); + const r = svc.get(); + expect(r.weekCount).toBe(1); + expect(r.lastNoteAt).toBe('2026-04-25T02:00:00.000Z'); // KST 11:00 = UTC 02:00 + }); }); diff --git a/tests/unit/NoteRepository.test.ts b/tests/unit/NoteRepository.test.ts index 578b318..622b049 100644 --- a/tests/unit/NoteRepository.test.ts +++ b/tests/unit/NoteRepository.test.ts @@ -403,4 +403,14 @@ describe('Active queries exclude deleted notes', () => { expect(note).not.toBeNull(); expect(note!.deletedAt).toBe('2026-05-01T00:00:00.000Z'); }); + + it('getPendingCount() excludes trashed pending notes (drift guard)', () => { + const a = repo.create({ rawText: 'a' }).id; // ai_status=pending + repo.create({ rawText: 'b' }); // ai_status=pending + expect(repo.getPendingCount()).toBe(2); + // trash() 가 pending_jobs row 는 정리하지만 notes.ai_status 는 'pending' 그대로. + // getPendingCount 가 deleted_at IS NOT NULL 노트 포함하면 영구 over-count. + repo.trash(a, '2026-05-01T00:00:00.000Z'); + expect(repo.getPendingCount()).toBe(1); + }); }); diff --git a/tests/unit/migrations.due_date.test.ts b/tests/unit/migrations.due_date.test.ts index 431ec74..5ceb60a 100644 --- a/tests/unit/migrations.due_date.test.ts +++ b/tests/unit/migrations.due_date.test.ts @@ -1,19 +1,11 @@ import { describe, it, expect } from 'vitest'; import Database from 'better-sqlite3'; -import { runMigrations, latestVersion } from '@main/db/migrations/index.js'; +import { runMigrations } from '@main/db/migrations/index.js'; describe('migrations m002 due_date', () => { - it('latestVersion returns 2', () => { - expect(latestVersion()).toBe(2); - }); - - it('runMigrations on fresh DB advances user_version to 2', () => { - const db = new Database(':memory:'); - runMigrations(db); - const row = db.pragma('user_version', { simple: true }); - expect(row).toBe(2); - }); - + // v3 (m003 soft_delete) lands in v0.2.3 #4 — latest version + user_version + // assertions migrate to migrations.test.ts. Here we keep only the m002-specific + // assertion (due_date column existence) which is version-stable. it('due_date column exists with NULL default', () => { const db = new Database(':memory:'); runMigrations(db);