aboutsummaryrefslogtreecommitdiff
path: root/src/lib/server/services/routines.test.ts
blob: 04151c2a519c1bef23325c1738d6ac19c9d6f4a1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

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';

const DAY_MS = 24 * 60 * 60 * 1000;

describe('routines service', () => {
	let db: Awaited<ReturnType<typeof createTestDb>>['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, Date.now(), 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(new Date(archived[0].archivedAt).getTime()).toBeGreaterThanOrEqual(
			new Date(archived[1].archivedAt).getTime(),
		);
	});

	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 = new Date().toISOString();
		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 createdAt = Date.parse(r.createdAt);
		await complete(r.id, null, new Date(createdAt).toISOString(), db);

		// 15 days after completion with a 7-day routine => very late
		const now = createdAt + 15 * DAY_MS;
		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);
		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 = Date.now();
		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),
		);
	});
});