86 lines
4.6 KiB
TypeScript
86 lines
4.6 KiB
TypeScript
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();}
|
|
});
|