import { expect, test } from 'bun:test'; import { fixture } from './test-fixture'; import { HabitService } from './service'; import { publicUser } from '../auth'; import { users } from '../db/schema'; import { eq } from 'drizzle-orm'; test('single-day reads load only the selected latest snapshot and its tasks', async () => { const f = fixture(); try { const habit = await f.json('/habits', 'POST', { name: 'Tasks', method: 'tasks', tasks: [{name:'Read'}] }, 201); f.setTime('2027-09-04T12:00:00Z'); await f.json('/today'); const logs: string[] = []; (f.db as any).session.logger = { logQuery(query: string) { logs.push(query); } }; const service = new HabitService(f.db, publicUser(f.db.select().from(users).where(eq(users.id,'alice')).get()!), Date.parse('2027-09-04T12:00:00Z')); const day = service.day(habit.id, '2027-09-04'); expect(day.tasks).toHaveLength(1); expect((service as any).recordsCache.get(habit.id).size).toBe(1); expect((service as any).occurrencesCache.get(habit.id).size).toBe(1); expect(logs.find(query => query.includes('task_occurrences'))).toContain('"habit_days"."date" >='); } finally { f.close(); } }); test('calendar summaries omit editor payloads and mutation versions cover historical changes', async () => { const f = fixture(); try { const habit = await f.json('/habits', 'POST', { name: 'Tasks', method: 'tasks', tasks: [{name:'Read'}] }, 201); const first = (await f.json('/today')).habits[0]; expect((await f.json('/today')).habits[0].calendarRevision).toBe(first.calendarRevision); f.setTime('2026-09-05T12:00:00Z'); await f.json(`/habits/${habit.id}/days/2026-09-04/tasks/${first.tasks[0].taskId}`, 'PUT', {done:true}); const changed = (await f.json('/today')).habits[0].calendarRevision; expect(changed).toBeGreaterThan(first.calendarRevision); await f.json(`/habits/${habit.id}/calendar-settings`, 'PUT', {mainColor:'#123456'}); expect((await f.json('/today')).habits[0].calendarRevision).toBeGreaterThan(changed); const url = `/habits/${habit.id}/calendar?from=2026-09-04&to=2026-09-05`; const full = await f.json(url); const compact = await f.json(url+'&compact=true'); const detail = full.days[0].habits[0]; const {tasks, requirements, ...summary} = detail; expect(compact.days[0].habits[0]).toEqual(summary); expect(compact.data).toEqual(full.data); expect((await f.json(`/habits/${habit.id}/days/2026-09-04`)).tasks[0].done).toBe(true); } finally { f.close(); } }); test('unchanged dashboard and calendar reads write no rows after synchronization', async () => { const f = fixture(); try { const habit = await f.json('/habits', 'POST', {name:'Tasks', method:'tasks', tasks:[{name:'Read'}]}, 201); f.setTime('2027-09-04T12:00:00Z'); await f.json('/today'); const before = (f.sqlite.query('SELECT total_changes() AS n').get() as {n:number}).n; await f.json('/today'); await f.json(`/habits/${habit.id}/calendar?from=2027-09-01&to=2027-09-04&compact=true`); expect((f.sqlite.query('SELECT total_changes() AS n').get() as {n:number}).n).toBe(before); } finally {f.close();} }); test('catch-up yields to other users and serializes same-user edits until fully ready', async () => { const f = fixture(); try { const habit = await f.json('/habits', 'POST', {name:'Many tasks', method:'tasks', tasks:Array.from({length:50}, (_,i)=>({name:`Task ${i}`}))}, 201); f.setTime('2027-09-04T12:00:00Z'); let complete = false; const dashboard = f.json('/today').then(value => {complete=true; return value;}); await new Promise(resolve=>setTimeout(resolve,0)); expect(complete).toBe(false); // Another user's request can complete while Alice is still catching up. expect((await f.request('/today','GET',undefined,'b')).status).toBe(200); expect(complete).toBe(false); // The actual date can advance while catch-up yields; readiness revalidates it. f.setTime('2027-09-05T12:00:00Z'); const edit = f.json(`/habits/${habit.id}`, 'PATCH', {name:'Renamed'}); const [today, changed] = await Promise.all([dashboard, edit]); expect(today.date).toBe('2027-09-05'); expect(today.habits[0].tasks).toHaveLength(50); expect(changed.name).toBe('Renamed'); const through = f.sqlite.query('SELECT materialized_through AS date FROM habits WHERE id=?').get(habit.id) as {date:string}; expect(through.date).toBe('2027-09-05'); const day = await f.json(`/habits/${habit.id}/days/2026-09-05`); expect(day.tasks.every((task:any)=>task.expiredAt!==null && task.closedAt!==null)).toBe(true); } finally {f.close();} });