Files
minabot/src/habits/performance.test.ts

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();}
});