feat(trash): #4 휴지통 + migration v3 (v0.2.3 2/7) #14

Merged
altair823 merged 26 commits from feat/v023-trash into main 2026-05-01 14:06:24 +00:00
2 changed files with 27 additions and 1 deletions
Showing only changes of commit 78c10e8817 - Show all commits

View File

@@ -121,7 +121,7 @@ export class AiWorker {
const startMs = this.now().getTime();
try {
const note = this.repo.findById(job.noteId);
if (!note || note.aiStatus !== 'pending') return;
if (!note || note.deletedAt !== null || note.aiStatus !== 'pending') return;

Race guard 가 provider.generate() 호출 직전 단일 시점만 검사. 네트워크 latency (1~30s) 동안 사용자가 trash 하면 updateAiResult 가 trashed 노트에 ai_status='done' 을 그대로 써서 deleted_at IS NOT NULL + ai_status='done' 모순 상태 발생 (DB 자체는 일관, telemetry ai_succeeded 만 '낭비'). updateAiResult 직전에 findById 재확인 + deletedAt !== null 시 early return 추가 권장. 빈도 낮지만 invariant 명시 차원.

Race guard 가 `provider.generate()` 호출 직전 단일 시점만 검사. 네트워크 latency (1~30s) 동안 사용자가 trash 하면 `updateAiResult` 가 trashed 노트에 ai_status='done' 을 그대로 써서 deleted_at IS NOT NULL + ai_status='done' 모순 상태 발생 (DB 자체는 일관, telemetry ai_succeeded 만 '낭비'). updateAiResult 직전에 `findById` 재확인 + `deletedAt !== null` 시 early return 추가 권장. 빈도 낮지만 invariant 명시 차원.
const nowDate = this.now();
const todayDate = todayKstAsDate(nowDate);
const todayIso = todayKstAsIso(nowDate);

View File

@@ -278,3 +278,29 @@ describe('AiWorker telemetry emit', () => {
expect(failed!.payload.reason).toBe('other');
});
});
describe('AiWorker — deletedAt guard (v0.2.3 #4)', () => {
let db: Database.Database;
let repo: NoteRepository;
beforeEach(() => {
db = new Database(':memory:');
runMigrations(db);
repo = new NoteRepository(db);
});
it('skips notes with deleted_at IS NOT NULL — provider.generate not called', async () => {
const { id } = repo.create({ rawText: 'x' });
// 먼저 trash — pending_jobs cleanup 됨
repo.trash(id, '2026-05-01T12:00:00.000Z');
// 강제로 pending_jobs row 다시 삽입 (race 시뮬레이션 — AiWorker 가 이미 dequeue 한 상태 흉내)
db.prepare(`INSERT INTO pending_jobs (note_id, attempts, next_run_at) VALUES (?, 0, ?)`).run(id, '2026-05-01T12:00:00.000Z');
const generate = vi.fn();
const provider = makeProvider({ generate: generate as any });
const w = new AiWorker(repo, provider, { backoffsMs: [0, 0, 0] });
await w.loadFromDb();
await w.drain();
expect(generate).not.toHaveBeenCalled();
expect(repo.findById(id)!.aiStatus).toBe('pending');
});
});