@hasna/recordings 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,574 @@
1
+ import { describe, test, expect, beforeEach, afterEach, mock } from "bun:test";
2
+ import {
3
+ needsEnhancement,
4
+ enhanceText,
5
+ processText,
6
+ resetEnhancementClient,
7
+ } from "../lib/enhancer.js";
8
+ import { DEFAULT_CONFIG } from "../lib/config.js";
9
+ import type { RecordingsConfig } from "../types/index.js";
10
+ import { EnhancementError } from "../types/index.js";
11
+
12
+ const config: RecordingsConfig = {
13
+ ...DEFAULT_CONFIG,
14
+ openai_api_key: "sk-test-key",
15
+ enhancement_api_key: "sk-test-key",
16
+ };
17
+
18
+ beforeEach(() => {
19
+ resetEnhancementClient();
20
+ });
21
+
22
+ afterEach(() => {
23
+ resetEnhancementClient();
24
+ });
25
+
26
+ // ── needsEnhancement: Explicit Triggers ─────────────────────────────────────
27
+
28
+ describe("needsEnhancement - explicit triggers", () => {
29
+ test("detects 'say it better' trigger", () => {
30
+ const result = needsEnhancement("This is my draft say it better", config);
31
+ expect(result.needs).toBe(true);
32
+ expect(result.reason).toContain("say it better");
33
+ });
34
+
35
+ test("detects 'rewrite this' trigger", () => {
36
+ const result = needsEnhancement("rewrite this paragraph for me", config);
37
+ expect(result.needs).toBe(true);
38
+ expect(result.reason).toContain("rewrite this");
39
+ });
40
+
41
+ test("detects 'make it sound' trigger", () => {
42
+ const result = needsEnhancement("make it sound more professional please", config);
43
+ expect(result.needs).toBe(true);
44
+ expect(result.reason).toContain("make it sound");
45
+ });
46
+
47
+ test("detects 'clean this up' trigger", () => {
48
+ const result = needsEnhancement("clean this up for the meeting", config);
49
+ expect(result.needs).toBe(true);
50
+ expect(result.reason).toContain("clean this up");
51
+ });
52
+
53
+ test("detects 'fix this' trigger", () => {
54
+ const result = needsEnhancement("fix this email draft", config);
55
+ expect(result.needs).toBe(true);
56
+ expect(result.reason).toContain("fix this");
57
+ });
58
+
59
+ test("detects 'rephrase' trigger", () => {
60
+ const result = needsEnhancement("can you rephrase it", config);
61
+ expect(result.needs).toBe(true);
62
+ expect(result.reason).toContain("rephrase");
63
+ });
64
+
65
+ test("detects 'write it properly' trigger", () => {
66
+ const result = needsEnhancement("write it properly for the boss", config);
67
+ expect(result.needs).toBe(true);
68
+ expect(result.reason).toContain("write it properly");
69
+ });
70
+
71
+ test("detects 'make it professional' trigger", () => {
72
+ const result = needsEnhancement("make it professional for the client", config);
73
+ expect(result.needs).toBe(true);
74
+ expect(result.reason).toContain("make it professional");
75
+ });
76
+
77
+ test("detects 'improve this' trigger", () => {
78
+ const result = needsEnhancement("improve this paragraph", config);
79
+ expect(result.needs).toBe(true);
80
+ expect(result.reason).toContain("improve this");
81
+ });
82
+
83
+ test("detects 'polish this' trigger", () => {
84
+ const result = needsEnhancement("polish this draft before sending", config);
85
+ expect(result.needs).toBe(true);
86
+ expect(result.reason).toContain("polish this");
87
+ });
88
+
89
+ test("is case-insensitive for triggers", () => {
90
+ const result = needsEnhancement("SAY IT BETTER please", config);
91
+ expect(result.needs).toBe(true);
92
+ });
93
+
94
+ test("extracts instruction with content before trigger", () => {
95
+ const longBefore = "This is a really long draft that I worked on for a while and it needs to be improved significantly. say it better";
96
+ const result = needsEnhancement(longBefore, config);
97
+ expect(result.needs).toBe(true);
98
+ // The instruction should be the content before the trigger (since before > after and before > 10)
99
+ expect(result.instruction).not.toBe(longBefore);
100
+ });
101
+
102
+ test("uses full text as instruction when trigger is at start", () => {
103
+ const text = "say it better";
104
+ const result = needsEnhancement(text, config);
105
+ expect(result.needs).toBe(true);
106
+ expect(result.instruction).toBe(text);
107
+ });
108
+ });
109
+
110
+ // ── needsEnhancement: Instruction Patterns ──────────────────────────────────
111
+
112
+ describe("needsEnhancement - instruction patterns", () => {
113
+ test("detects 'write an email' pattern", () => {
114
+ const result = needsEnhancement("write an email saying we need to reschedule", config);
115
+ expect(result.needs).toBe(true);
116
+ expect(result.reason).toBe("Instruction pattern detected");
117
+ });
118
+
119
+ test("detects 'draft a message' pattern", () => {
120
+ const result = needsEnhancement("draft a message to the team about the deadline", config);
121
+ expect(result.needs).toBe(true);
122
+ expect(result.reason).toBe("Instruction pattern detected");
123
+ });
124
+
125
+ test("detects 'compose a response' pattern", () => {
126
+ const result = needsEnhancement("compose a response to the client complaint", config);
127
+ expect(result.needs).toBe(true);
128
+ expect(result.reason).toBe("Instruction pattern detected");
129
+ });
130
+
131
+ test("detects 'create a reply' pattern", () => {
132
+ const result = needsEnhancement("create a reply to his email", config);
133
+ expect(result.needs).toBe(true);
134
+ expect(result.reason).toBe("Instruction pattern detected");
135
+ });
136
+
137
+ test("detects 'give them instructions' pattern", () => {
138
+ const result = needsEnhancement("give them instructions on how to deploy", config);
139
+ expect(result.needs).toBe(true);
140
+ expect(result.reason).toBe("Instruction pattern detected");
141
+ });
142
+
143
+ test("detects 'give the agent full instructions' pattern", () => {
144
+ const result = needsEnhancement("give the agent full instructions to build the API", config);
145
+ expect(result.needs).toBe(true);
146
+ expect(result.reason).toBe("Instruction pattern detected");
147
+ });
148
+
149
+ test("detects 'provide him instructions' pattern", () => {
150
+ const result = needsEnhancement("provide him instructions on the setup", config);
151
+ expect(result.needs).toBe(true);
152
+ expect(result.reason).toBe("Instruction pattern detected");
153
+ });
154
+
155
+ test("detects 'tell them to' pattern", () => {
156
+ const result = needsEnhancement("tell them to fix the bug by Friday", config);
157
+ expect(result.needs).toBe(true);
158
+ expect(result.reason).toBe("Instruction pattern detected");
159
+ });
160
+
161
+ test("detects 'ask him that' pattern", () => {
162
+ const result = needsEnhancement("ask him that we need the report", config);
163
+ expect(result.needs).toBe(true);
164
+ expect(result.reason).toBe("Instruction pattern detected");
165
+ });
166
+
167
+ test("detects 'tell the agent to' pattern", () => {
168
+ const result = needsEnhancement("tell the agent to refactor the module", config);
169
+ expect(result.needs).toBe(true);
170
+ expect(result.reason).toBe("Instruction pattern detected");
171
+ });
172
+
173
+ test("detects 'make it sound more professional' pattern", () => {
174
+ const result = needsEnhancement("make it sound more professional", config);
175
+ expect(result.needs).toBe(true);
176
+ });
177
+
178
+ test("detects 'make this look formal' pattern", () => {
179
+ const result = needsEnhancement("make this look formal", config);
180
+ expect(result.needs).toBe(true);
181
+ });
182
+
183
+ test("detects 'make it read better' pattern", () => {
184
+ const result = needsEnhancement("make it read better", config);
185
+ expect(result.needs).toBe(true);
186
+ });
187
+
188
+ test("detects 'ok so say something' pattern", () => {
189
+ const result = needsEnhancement("ok so say something nice about the product", config);
190
+ expect(result.needs).toBe(true);
191
+ expect(result.reason).toBe("Instruction pattern detected");
192
+ });
193
+
194
+ test("detects 'okay so write' pattern", () => {
195
+ const result = needsEnhancement("okay so write a thank you note", config);
196
+ expect(result.needs).toBe(true);
197
+ expect(result.reason).toBe("Instruction pattern detected");
198
+ });
199
+
200
+ test("detects 'alright so tell' pattern", () => {
201
+ const result = needsEnhancement("alright so tell them we agree", config);
202
+ expect(result.needs).toBe(true);
203
+ expect(result.reason).toBe("Instruction pattern detected");
204
+ });
205
+
206
+ test("detects 'i need the agent to build' pattern", () => {
207
+ const result = needsEnhancement("i need the agent to build a dashboard", config);
208
+ expect(result.needs).toBe(true);
209
+ expect(result.reason).toBe("Instruction pattern detected");
210
+ });
211
+
212
+ test("detects 'i want it to create' pattern", () => {
213
+ const result = needsEnhancement("i want it to create a login page", config);
214
+ expect(result.needs).toBe(true);
215
+ expect(result.reason).toBe("Instruction pattern detected");
216
+ });
217
+
218
+ test("detects 'i need them to implement' pattern", () => {
219
+ const result = needsEnhancement("i need them to implement the search feature", config);
220
+ expect(result.needs).toBe(true);
221
+ expect(result.reason).toBe("Instruction pattern detected");
222
+ });
223
+
224
+ test("detects 'write a slack message' pattern", () => {
225
+ const result = needsEnhancement("write a slack message to the channel about downtime", config);
226
+ expect(result.needs).toBe(true);
227
+ });
228
+
229
+ test("detects 'send her instructions' pattern", () => {
230
+ const result = needsEnhancement("send her instructions about the onboarding", config);
231
+ expect(result.needs).toBe(true);
232
+ });
233
+
234
+ test("detects 'write a note' pattern", () => {
235
+ const result = needsEnhancement("write a note about the meeting outcomes", config);
236
+ expect(result.needs).toBe(true);
237
+ });
238
+
239
+ test("detects 'draft a letter' pattern", () => {
240
+ const result = needsEnhancement("draft a letter to the landlord about the lease", config);
241
+ expect(result.needs).toBe(true);
242
+ });
243
+
244
+ test("detects 'compose a text' pattern", () => {
245
+ const result = needsEnhancement("compose a text to my mom about dinner", config);
246
+ expect(result.needs).toBe(true);
247
+ });
248
+
249
+ test("detects 'create a dm' pattern", () => {
250
+ const result = needsEnhancement("create a dm for the manager about PTO", config);
251
+ expect(result.needs).toBe(true);
252
+ });
253
+
254
+ test("uses full text as instruction for pattern matches", () => {
255
+ const text = "write an email saying we need more time";
256
+ const result = needsEnhancement(text, config);
257
+ expect(result.instruction).toBe(text);
258
+ });
259
+ });
260
+
261
+ // ── needsEnhancement: Negative Cases (Raw Dictation) ────────────────────────
262
+
263
+ describe("needsEnhancement - raw dictation (no enhancement)", () => {
264
+ test("plain statement does not trigger enhancement", () => {
265
+ const result = needsEnhancement("I went to the store today", config);
266
+ expect(result.needs).toBe(false);
267
+ expect(result.reason).toBe("Direct dictation");
268
+ });
269
+
270
+ test("question does not trigger enhancement", () => {
271
+ const result = needsEnhancement("What time is the meeting tomorrow", config);
272
+ expect(result.needs).toBe(false);
273
+ });
274
+
275
+ test("technical note does not trigger enhancement", () => {
276
+ const result = needsEnhancement("The API returns a 404 when the ID is missing", config);
277
+ expect(result.needs).toBe(false);
278
+ });
279
+
280
+ test("grocery list does not trigger enhancement", () => {
281
+ const result = needsEnhancement("Milk eggs bread butter cheese", config);
282
+ expect(result.needs).toBe(false);
283
+ });
284
+
285
+ test("short note does not trigger enhancement", () => {
286
+ const result = needsEnhancement("Remember to call dentist", config);
287
+ expect(result.needs).toBe(false);
288
+ });
289
+
290
+ test("empty string does not trigger enhancement", () => {
291
+ const result = needsEnhancement("", config);
292
+ expect(result.needs).toBe(false);
293
+ });
294
+
295
+ test("returns full text as instruction when no enhancement needed", () => {
296
+ const text = "Just a regular note";
297
+ const result = needsEnhancement(text, config);
298
+ expect(result.instruction).toBe(text);
299
+ });
300
+ });
301
+
302
+ // ── needsEnhancement: Custom Triggers ───────────────────────────────────────
303
+
304
+ describe("needsEnhancement - custom triggers", () => {
305
+ test("uses custom enhance_triggers from config", () => {
306
+ const customConfig = {
307
+ ...config,
308
+ enhance_triggers: ["make it fancy", "jazz it up"],
309
+ };
310
+
311
+ const result1 = needsEnhancement("make it fancy please", customConfig);
312
+ expect(result1.needs).toBe(true);
313
+
314
+ const result2 = needsEnhancement("jazz it up a bit", customConfig);
315
+ expect(result2.needs).toBe(true);
316
+
317
+ // Default triggers should NOT work with custom config
318
+ const result3 = needsEnhancement("say it better", customConfig);
319
+ expect(result3.needs).toBe(false);
320
+ });
321
+ });
322
+
323
+ // ── enhanceText ─────────────────────────────────────────────────────────────
324
+
325
+ describe("enhanceText", () => {
326
+ test("throws EnhancementError when no API key", async () => {
327
+ const noKeyConfig = { ...config, openai_api_key: "", enhancement_api_key: "" };
328
+ try {
329
+ await enhanceText("test", "test", noKeyConfig);
330
+ expect(true).toBe(false); // Should not reach
331
+ } catch (err) {
332
+ expect(err).toBeInstanceOf(EnhancementError);
333
+ expect((err as Error).message).toContain("API key not configured");
334
+ }
335
+ });
336
+
337
+ test("calls OpenAI chat completion and returns enhanced text", async () => {
338
+ mock.module("openai", () => ({
339
+ default: class MockOpenAI {
340
+ chat = {
341
+ completions: {
342
+ create: mock(() =>
343
+ Promise.resolve({
344
+ choices: [{ message: { content: "Polished professional text" } }],
345
+ })
346
+ ),
347
+ },
348
+ };
349
+ },
350
+ }));
351
+
352
+ resetEnhancementClient();
353
+ const { enhanceText: enhance } = await import("../lib/enhancer.js");
354
+ resetEnhancementClient();
355
+
356
+ const result = await enhance("raw messy text", "raw messy text", config);
357
+ expect(result.original).toBe("raw messy text");
358
+ expect(result.enhanced).toBe("Polished professional text");
359
+ expect(result.model).toBe(config.enhancement_model);
360
+ expect(result.reasoning).toBeNull();
361
+
362
+ resetEnhancementClient();
363
+ });
364
+
365
+ test("falls back to rawText when API returns empty content", async () => {
366
+ mock.module("openai", () => ({
367
+ default: class MockOpenAI {
368
+ chat = {
369
+ completions: {
370
+ create: mock(() =>
371
+ Promise.resolve({
372
+ choices: [{ message: { content: null } }],
373
+ })
374
+ ),
375
+ },
376
+ };
377
+ },
378
+ }));
379
+
380
+ resetEnhancementClient();
381
+ const { enhanceText: enhance } = await import("../lib/enhancer.js");
382
+ resetEnhancementClient();
383
+
384
+ const result = await enhance("original text", "instruction", config);
385
+ expect(result.enhanced).toBe("original text"); // Falls back to rawText
386
+
387
+ resetEnhancementClient();
388
+ });
389
+
390
+ test("wraps API errors in EnhancementError", async () => {
391
+ mock.module("openai", () => ({
392
+ default: class MockOpenAI {
393
+ chat = {
394
+ completions: {
395
+ create: mock(() => Promise.reject(new Error("Rate limit exceeded"))),
396
+ },
397
+ };
398
+ },
399
+ }));
400
+
401
+ resetEnhancementClient();
402
+ const { enhanceText: enhance } = await import("../lib/enhancer.js");
403
+ resetEnhancementClient();
404
+
405
+ try {
406
+ await enhance("text", "instruction", config);
407
+ expect(true).toBe(false);
408
+ } catch (err) {
409
+ expect(err).toBeInstanceOf(EnhancementError);
410
+ expect((err as Error).message).toContain("Enhancement failed");
411
+ expect((err as Error).message).toContain("Rate limit exceeded");
412
+ }
413
+
414
+ resetEnhancementClient();
415
+ });
416
+
417
+ test("wraps non-Error exceptions in EnhancementError", async () => {
418
+ mock.module("openai", () => ({
419
+ default: class MockOpenAI {
420
+ chat = {
421
+ completions: {
422
+ create: mock(() => Promise.reject("string rejection")),
423
+ },
424
+ };
425
+ },
426
+ }));
427
+
428
+ resetEnhancementClient();
429
+ const { enhanceText: enhance } = await import("../lib/enhancer.js");
430
+ resetEnhancementClient();
431
+
432
+ try {
433
+ await enhance("text", "instruction", config);
434
+ expect(true).toBe(false);
435
+ } catch (err) {
436
+ expect(err).toBeInstanceOf(EnhancementError);
437
+ expect((err as Error).message).toContain("Enhancement failed");
438
+ }
439
+
440
+ resetEnhancementClient();
441
+ });
442
+
443
+ test("uses enhancement_api_key when available", async () => {
444
+ let capturedKey = "";
445
+ mock.module("openai", () => ({
446
+ default: class MockOpenAI {
447
+ constructor(opts: { apiKey: string }) {
448
+ capturedKey = opts.apiKey;
449
+ }
450
+ chat = {
451
+ completions: {
452
+ create: mock(() =>
453
+ Promise.resolve({
454
+ choices: [{ message: { content: "enhanced" } }],
455
+ })
456
+ ),
457
+ },
458
+ };
459
+ },
460
+ }));
461
+
462
+ resetEnhancementClient();
463
+ const { enhanceText: enhance } = await import("../lib/enhancer.js");
464
+ resetEnhancementClient();
465
+
466
+ const customConfig = { ...config, enhancement_api_key: "sk-enhance-key", openai_api_key: "sk-openai-key" };
467
+ await enhance("text", "instruction", customConfig);
468
+ expect(capturedKey).toBe("sk-enhance-key");
469
+
470
+ resetEnhancementClient();
471
+ });
472
+ });
473
+
474
+ // ── processText ─────────────────────────────────────────────────────────────
475
+
476
+ describe("processText", () => {
477
+ test("returns raw when auto_enhance is false", async () => {
478
+ const noAutoConfig = { ...config, auto_enhance: false };
479
+ const result = await processText("hello world", noAutoConfig);
480
+ expect(result.mode).toBe("raw");
481
+ expect(result.text).toBe("hello world");
482
+ expect(result.enhancement_model).toBeNull();
483
+ });
484
+
485
+ test("returns raw for plain dictation even with auto_enhance", async () => {
486
+ const result = await processText("I went to the store today", config);
487
+ expect(result.mode).toBe("raw");
488
+ expect(result.text).toBe("I went to the store today");
489
+ expect(result.enhancement_model).toBeNull();
490
+ });
491
+
492
+ test("enhances text when trigger is detected", async () => {
493
+ mock.module("openai", () => ({
494
+ default: class MockOpenAI {
495
+ chat = {
496
+ completions: {
497
+ create: mock(() =>
498
+ Promise.resolve({
499
+ choices: [{ message: { content: "Enhanced output here" } }],
500
+ })
501
+ ),
502
+ },
503
+ };
504
+ },
505
+ }));
506
+
507
+ resetEnhancementClient();
508
+ const { processText: process } = await import("../lib/enhancer.js");
509
+ resetEnhancementClient();
510
+
511
+ const result = await process("This needs work say it better", config);
512
+ expect(result.mode).toBe("enhanced");
513
+ expect(result.text).toBe("Enhanced output here");
514
+ expect(result.enhancement_model).toBe(config.enhancement_model);
515
+
516
+ resetEnhancementClient();
517
+ });
518
+
519
+ test("enhances text when instruction pattern is detected", async () => {
520
+ mock.module("openai", () => ({
521
+ default: class MockOpenAI {
522
+ chat = {
523
+ completions: {
524
+ create: mock(() =>
525
+ Promise.resolve({
526
+ choices: [{ message: { content: "Dear Team,\n\nI wanted to reach out..." } }],
527
+ })
528
+ ),
529
+ },
530
+ };
531
+ },
532
+ }));
533
+
534
+ resetEnhancementClient();
535
+ const { processText: process } = await import("../lib/enhancer.js");
536
+ resetEnhancementClient();
537
+
538
+ const result = await process("write an email saying we need to reschedule the meeting", config);
539
+ expect(result.mode).toBe("enhanced");
540
+ expect(result.text).toContain("Dear Team");
541
+ expect(result.enhancement_model).toBe(config.enhancement_model);
542
+
543
+ resetEnhancementClient();
544
+ });
545
+ });
546
+
547
+ // ── extractInstruction (tested via needsEnhancement) ────────────────────────
548
+
549
+ describe("extractInstruction behavior via needsEnhancement", () => {
550
+ test("extracts content before trigger when before is longer", () => {
551
+ // "before" = long content, "after" = short, before.length > 10
552
+ const text = "This is a really long email draft that I wrote about the quarterly results and projections. Say it better";
553
+ const result = needsEnhancement(text, config);
554
+ expect(result.needs).toBe(true);
555
+ // The instruction should be the content before the trigger
556
+ expect(result.instruction).not.toContain("Say it better");
557
+ });
558
+
559
+ test("uses full text when after is longer than before", () => {
560
+ const text = "Say it better and make sure to include all the important details about the project timeline and deliverables";
561
+ const result = needsEnhancement(text, config);
562
+ expect(result.needs).toBe(true);
563
+ // After is longer, so full text is used
564
+ expect(result.instruction).toBe(text);
565
+ });
566
+
567
+ test("uses full text when before is short (<=10 chars)", () => {
568
+ const text = "Draft. Say it better and elaborate";
569
+ const result = needsEnhancement(text, config);
570
+ expect(result.needs).toBe(true);
571
+ // Before is <= 10 chars, so full text is used
572
+ expect(result.instruction).toBe(text);
573
+ });
574
+ });
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Test preload — currently a no-op since we use bun:sqlite natively.
3
+ * Kept for future test setup needs.
4
+ */
@@ -0,0 +1,109 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { mkdirSync, rmSync, existsSync } from "fs";
5
+ import {
6
+ getDatabase,
7
+ closeDatabase,
8
+ resetDatabase,
9
+ } from "../db/database.js";
10
+ import { registerProject, getProject, listProjects } from "../db/projects.js";
11
+ import { type Database } from "bun:sqlite";
12
+
13
+ let tempDir: string;
14
+ let db: Database;
15
+
16
+ beforeEach(() => {
17
+ resetDatabase();
18
+ tempDir = join(tmpdir(), `open-recordings-test-proj-${Date.now()}-${Math.random().toString(36).slice(2)}`);
19
+ mkdirSync(tempDir, { recursive: true });
20
+ const dbPath = join(tempDir, "test.db");
21
+ db = getDatabase(dbPath);
22
+ });
23
+
24
+ afterEach(() => {
25
+ closeDatabase();
26
+ resetDatabase();
27
+ if (existsSync(tempDir)) {
28
+ rmSync(tempDir, { recursive: true, force: true });
29
+ }
30
+ });
31
+
32
+ describe("registerProject", () => {
33
+ test("creates a new project", () => {
34
+ const project = registerProject("my-app", "/home/user/my-app", undefined, db);
35
+ expect(project).toBeDefined();
36
+ expect(project.name).toBe("my-app");
37
+ expect(project.path).toBe("/home/user/my-app");
38
+ expect(project.description).toBeNull();
39
+ expect(project.id).toBeDefined();
40
+ expect(project.id).toHaveLength(36); // full UUID
41
+ expect(project.created_at).toBeDefined();
42
+ expect(project.updated_at).toBeDefined();
43
+ });
44
+
45
+ test("creates a project with description", () => {
46
+ const project = registerProject("my-app", "/home/user/my-app", "A cool project", db);
47
+ expect(project.description).toBe("A cool project");
48
+ });
49
+
50
+ test("returns existing project by path (idempotent)", () => {
51
+ const first = registerProject("my-app", "/home/user/my-app", undefined, db);
52
+ const second = registerProject("my-app", "/home/user/my-app", undefined, db);
53
+
54
+ expect(second.id).toBe(first.id);
55
+ expect(new Date(second.updated_at).getTime()).toBeGreaterThanOrEqual(
56
+ new Date(first.updated_at).getTime()
57
+ );
58
+ });
59
+
60
+ test("creates separate projects for different paths", () => {
61
+ const p1 = registerProject("app-a", "/path/a", undefined, db);
62
+ const p2 = registerProject("app-b", "/path/b", undefined, db);
63
+
64
+ expect(p1.id).not.toBe(p2.id);
65
+ expect(listProjects(db)).toHaveLength(2);
66
+ });
67
+ });
68
+
69
+ describe("getProject", () => {
70
+ test("finds project by ID", () => {
71
+ const created = registerProject("test", "/tmp/test", undefined, db);
72
+ const found = getProject(created.id, db);
73
+ expect(found).toBeDefined();
74
+ expect(found!.name).toBe("test");
75
+ });
76
+
77
+ test("finds project by path", () => {
78
+ registerProject("finder", "/unique/path/here", undefined, db);
79
+ const found = getProject("/unique/path/here", db);
80
+ expect(found).toBeDefined();
81
+ expect(found!.name).toBe("finder");
82
+ });
83
+
84
+ test("returns null for non-existent project", () => {
85
+ const found = getProject("nonexistent", db);
86
+ expect(found).toBeNull();
87
+ });
88
+ });
89
+
90
+ describe("listProjects", () => {
91
+ test("returns empty array when no projects", () => {
92
+ const projects = listProjects(db);
93
+ expect(projects).toEqual([]);
94
+ });
95
+
96
+ test("returns all projects ordered by updated_at DESC", () => {
97
+ registerProject("alpha", "/path/alpha", undefined, db);
98
+ registerProject("beta", "/path/beta", undefined, db);
99
+ registerProject("gamma", "/path/gamma", undefined, db);
100
+
101
+ const projects = listProjects(db);
102
+ expect(projects).toHaveLength(3);
103
+ // All three projects are present
104
+ const names = projects.map((p) => p.name);
105
+ expect(names).toContain("alpha");
106
+ expect(names).toContain("beta");
107
+ expect(names).toContain("gamma");
108
+ });
109
+ });