feat(intent): IntentService for set/dismiss with validation
Task 20 of the slice plan. Thin wrapper over the repo's intent methods that adds two preconditions: setIntent rejects empty trimmed text, both methods throw "note not found" when the note id doesn't exist. Repo-level COALESCE on intent_prompted_at preserves the first-prompt invariant (spec §3.3); IntentService's job is just input guarding so the IPC handler stays a one-liner. Verification: `npx vitest run tests/unit/IntentService.test.ts` 4 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
16
src/main/services/IntentService.ts
Normal file
16
src/main/services/IntentService.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { NoteRepository } from '../repository/NoteRepository.js';
|
||||
|
||||
export class IntentService {
|
||||
constructor(private repo: NoteRepository) {}
|
||||
|
||||
setIntent(noteId: string, text: string): void {
|
||||
if (text.trim().length === 0) throw new Error('empty intent text');
|
||||
if (!this.repo.findById(noteId)) throw new Error('note not found');
|
||||
this.repo.setIntent(noteId, text);
|
||||
}
|
||||
|
||||
dismissIntent(noteId: string): void {
|
||||
if (!this.repo.findById(noteId)) throw new Error('note not found');
|
||||
this.repo.dismissIntent(noteId);
|
||||
}
|
||||
}
|
||||
42
tests/unit/IntentService.test.ts
Normal file
42
tests/unit/IntentService.test.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
import { runMigrations } from '@main/db/migrations/index.js';
|
||||
import { NoteRepository } from '@main/repository/NoteRepository.js';
|
||||
import { IntentService } from '@main/services/IntentService.js';
|
||||
|
||||
describe('IntentService', () => {
|
||||
let db: Database.Database;
|
||||
let repo: NoteRepository;
|
||||
let svc: IntentService;
|
||||
|
||||
beforeEach(() => {
|
||||
db = new Database(':memory:');
|
||||
runMigrations(db);
|
||||
repo = new NoteRepository(db);
|
||||
svc = new IntentService(repo);
|
||||
});
|
||||
|
||||
it('setIntent stores text on note', () => {
|
||||
const { id } = repo.create({ rawText: 'x' });
|
||||
svc.setIntent(id, '내일의 나에게');
|
||||
expect(repo.findById(id)?.userIntent).toBe('내일의 나에게');
|
||||
});
|
||||
|
||||
it('dismissIntent stamps prompted_at without setting intent', () => {
|
||||
const { id } = repo.create({ rawText: 'x' });
|
||||
svc.dismissIntent(id);
|
||||
const note = repo.findById(id)!;
|
||||
expect(note.userIntent).toBeNull();
|
||||
expect(note.intentPromptedAt).not.toBeNull();
|
||||
});
|
||||
|
||||
it('rejects empty intent text', () => {
|
||||
const { id } = repo.create({ rawText: 'x' });
|
||||
expect(() => svc.setIntent(id, ' ')).toThrow(/empty/i);
|
||||
});
|
||||
|
||||
it('throws if note does not exist', () => {
|
||||
expect(() => svc.setIntent('nonexistent', 'x')).toThrow(/not found/i);
|
||||
expect(() => svc.dismissIntent('nonexistent')).toThrow(/not found/i);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user