import { describe, it, expect, beforeEach } from 'vitest'; import { createTestDb } from '$lib/server/db/test-helper'; import { add as addProject } from './projects'; import { add, edit, archive, complete, loadAllVersions, loadArchived, loadProjectDashboard, loadOverview, } from './routines'; describe('routines service', () => { let db: Awaited>['db']; let projectId: number; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; const project = await addProject('Household', db); projectId = project.id; }); it('creates a routine with version 1', async () => { const r = await add(projectId, 'clean kitchen', 7, db); expect(r.id).toBe(1); expect(r.version).toBe(1); expect(r.frequencyDays).toBe(7); }); it('edit inserts a new snapshot version and keeps history', async () => { const r = await add(projectId, 'clean kitchen', 7, db); const updated = await edit( r.id, { name: 'clean kitchen deeply', frequencyDays: 3 }, db, ); expect(updated.version).toBe(2); expect(updated.frequencyDays).toBe(3); const versions = await loadAllVersions(r.id, db); expect(versions).toHaveLength(2); expect(versions[0].frequencyDays).toBe(7); expect(versions[1].frequencyDays).toBe(3); }); it('new routines start with an empty description', async () => { const r = await add(projectId, 'clean kitchen', 7, db); expect(r.description).toBe(''); }); it('edit is partial: name/frequency edits keep the description and vice-versa', async () => { const r = await add(projectId, 'clean kitchen', 7, db); const described = await edit(r.id, { description: 'wax on, wax off' }, db); expect(described.name).toBe('clean kitchen'); expect(described.frequencyDays).toBe(7); expect(described.description).toBe('wax on, wax off'); const renamed = await edit(r.id, { name: 'clean the whole kitchen' }, db); expect(renamed.version).toBe(3); expect(renamed.description).toBe('wax on, wax off'); expect(renamed.frequencyDays).toBe(7); const refreq = await edit(r.id, { frequencyDays: 14 }, db); expect(refreq.version).toBe(4); expect(refreq.name).toBe('clean the whole kitchen'); expect(refreq.description).toBe('wax on, wax off'); const cleared = await edit(r.id, { description: '' }, db); expect(cleared.version).toBe(5); expect(cleared.description).toBe(''); expect(cleared.name).toBe('clean the whole kitchen'); }); it('archive inserts an archived snapshot without removing history', async () => { const r = await add(projectId, 'clean kitchen', 7, db); await archive(r.id, db); const versions = await loadAllVersions(r.id, db); expect(versions).toHaveLength(2); expect(versions[0].archived).toBe(false); expect(versions[1].archived).toBe(true); const dashboard = await loadProjectDashboard( projectId, Temporal.Now.instant(), db, ); expect(dashboard.routines).toHaveLength(0); expect(dashboard.healthScore).toBe(100); }); it('loadArchived lists archived routines; edits keep the archive date', async () => { const a = await add(projectId, 'clean kitchen', 7, db); await add(projectId, 'water plants', 3, db); await archive(a.id, db); const stamped = (await loadAllVersions(a.id, db))[1].archivedAt; expect(stamped).toBeTruthy(); const edited = await edit(a.id, { name: 'clean kitchen deeply' }, db); expect(edited.archived).toBe(true); expect(edited.archivedAt).toBe(stamped); const archived = await loadArchived(projectId, db); expect(archived.map((r) => r.id)).toEqual([a.id]); expect(archived[0].name).toBe('clean kitchen deeply'); expect(archived[0].frequencyDays).toBe(7); expect(archived[0].archivedAt).toBe(stamped); }); it('loadArchived sorts most recently archived first', async () => { const a = await add(projectId, 'first', 1, db); const b = await add(projectId, 'second', 1, db); await archive(a.id, db); await archive(b.id, db); const archived = await loadArchived(projectId, db); expect(archived).toHaveLength(2); // Descending archive date; equal timestamps keep insertion order. expect( Temporal.Instant.from(archived[0].archivedAt).epochMilliseconds, ).toBeGreaterThanOrEqual( Temporal.Instant.from(archived[1].archivedAt).epochMilliseconds, ); }); it('complete records the snapshot version it was made against', async () => { const r = await add(projectId, 'clean kitchen', 7, db); await edit(r.id, { frequencyDays: 5 }, db); const doneAt = Temporal.Now.instant().toString({ smallestUnit: 'millisecond', }); const completion = await complete(r.id, 'with vinegar', doneAt, db); expect(completion.routineVersion).toBe(2); expect(completion.note).toBe('with vinegar'); }); it('dashboard computes urgency and health from completions', async () => { const r = await add(projectId, 'clean kitchen', 7, db); const created = Temporal.Instant.from(r.createdAt); await complete( r.id, null, created.toString({ smallestUnit: 'millisecond' }), db, ); // 22 days after completion with a 7-day routine => two periods overdue const now = created.toZonedDateTimeISO('UTC').add({ days: 22 }).toInstant(); const dashboard = await loadProjectDashboard(projectId, now, db); expect(dashboard.routines).toHaveLength(1); expect(dashboard.routines[0].status).toBe('veryLate'); expect(dashboard.healthScore).toBe(0); const last = dashboard.healthSeries[dashboard.healthSeries.length - 1]; expect(last.at).toBe(now.epochMilliseconds); expect(last.health).toBe(dashboard.healthScore); }); it('overview aggregates per-project health and a global score', async () => { await add(projectId, 'clean kitchen', 7, db); const growth = await addProject('Personal growth', db); await add(growth.id, 'read 30 minutes', 1, db); const now = Temporal.Now.instant(); const overview = await loadOverview(now, db); expect(overview.projects).toHaveLength(2); expect(overview.globalScore).toBe( Math.round(overview.projects.reduce((s, p) => s + p.healthScore, 0) / 2), ); }); });