feat(db): add versioned habits and calendar domain contracts

This commit is contained in:
syntaxbullet
2026-09-04 09:00:21 +02:00
parent d3cbf2f7dc
commit 7fefe0c7d8
10 changed files with 1170 additions and 1 deletions

View File

@@ -0,0 +1,38 @@
import { expect, test } from 'bun:test';
import { addDays, endOfDay, localDate, scheduled, shade } from './calendar';
import { calendarSettingsSchema, dateSchema, habitInput, scheduleSchema } from './contracts';
test('fixed calendar recurrence uses anchors and Monday weeks independently of completions', () => {
expect(scheduled({ type: 'daily' }, '2026-09-04')).toBe(true);
expect(scheduled({ type: 'interval', every: 3, anchor: '2026-09-04' }, '2026-09-07')).toBe(true);
expect(scheduled({ type: 'interval', every: 3, anchor: '2026-09-04' }, '2026-09-01')).toBe(false);
expect(scheduled({ type: 'interval', every: 3, anchor: '2026-09-04' }, '2026-09-06')).toBe(false);
expect(scheduled({ type: 'weekdays', days: [1] }, '2026-09-07')).toBe(true);
expect(scheduled({ type: 'weekdays', days: [1] }, '2026-09-08')).toBe(false);
const rule = { type: 'weekly' as const, every: 2, weekday: 3, anchor: '2026-08-31' };
expect(scheduled(rule, '2026-09-02')).toBe(true);
expect(scheduled(rule, '2026-09-09')).toBe(false);
expect(scheduled(rule, '2026-09-16')).toBe(true);
});
test('real dates, strict settings, and exclusive methods are validated', () => {
for (const d of ['2026-02-29', '2026-13-01', '2026-9-1', 'garbage']) expect(dateSchema.safeParse(d).success).toBe(false);
expect(addDays('2024-02-28', 1)).toBe('2024-02-29');
expect(habitInput.safeParse({ name: 'mixed', method: 'manual', target: 8 }).success).toBe(false);
expect(scheduleSchema.safeParse({ type: 'weekdays', days: [1, 1] }).success).toBe(false);
expect(calendarSettingsSchema.safeParse({ daySpacing: 4 }).success).toBe(false);
});
test('local midnight handles spring, autumn, and fractional timezone offsets', () => {
expect(endOfDay('2026-03-29', 'Europe/Belgrade') - endOfDay('2026-03-28', 'Europe/Belgrade')).toBe(23 * 3600000);
expect(endOfDay('2026-10-25', 'Europe/Belgrade') - endOfDay('2026-10-24', 'Europe/Belgrade')).toBe(25 * 3600000);
expect(new Date(endOfDay('2026-09-04', 'Asia/Kathmandu')).toISOString()).toBe('2026-09-04T18:15:00.000Z');
expect(localDate(Date.parse('2026-09-03T22:00Z'), 'Europe/Belgrade')).toBe('2026-09-04');
});
test('eight positive count shades, full completion, neutral and empty stay distinct', () => {
const settings = calendarSettingsSchema.parse({});
const values = Array.from({ length: 8 }, (_, i) => shade((i + 1) / 8, 8, settings, true));
expect(new Set(values.map(v => v.color)).size).toBe(8);
expect(values.map(v => v.level)).toEqual([1,2,3,4,5,6,7,8]);
expect(shade(1, 1, settings, true).color).toBe(settings.mainColor);
expect(shade(0, 8, settings, true).color).toBe(settings.emptyColor);
expect(shade(null, 8, settings, true).color).toBe(settings.notDueColor);
expect(shade(0.75, 4, settings, false).level).toBe(3);
});