apple-notes-mcp 1.1.0

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,684 @@
1
+ /**
2
+ * Unit Tests for Apple Notes Manager
3
+ *
4
+ * These tests verify the AppleNotesManager class and its helper functions.
5
+ * The AppleScript execution is mocked to allow testing without macOS.
6
+ *
7
+ * Test Strategy:
8
+ * - Helper functions (escapeForAppleScript, parseAppleScriptDate) are tested
9
+ * with various inputs to ensure correct escaping and parsing
10
+ * - Manager methods are tested for success/failure paths
11
+ * - Script generation is verified by checking for expected AppleScript patterns
12
+ */
13
+ import { describe, it, expect, vi, beforeEach } from "vitest";
14
+ import { AppleNotesManager, escapeForAppleScript, parseAppleScriptDate, } from "./appleNotesManager.js";
15
+ // Mock the AppleScript execution module
16
+ // This prevents actual osascript calls during testing
17
+ vi.mock("@/utils/applescript.js", () => ({
18
+ executeAppleScript: vi.fn(),
19
+ }));
20
+ import { executeAppleScript } from "../utils/applescript.js";
21
+ const mockExecuteAppleScript = vi.mocked(executeAppleScript);
22
+ // =============================================================================
23
+ // Text Escaping Tests
24
+ // =============================================================================
25
+ describe("escapeForAppleScript", () => {
26
+ describe("empty and null handling", () => {
27
+ it("returns empty string for empty input", () => {
28
+ expect(escapeForAppleScript("")).toBe("");
29
+ });
30
+ it("returns empty string for null-like input", () => {
31
+ // TypeScript prevents actual null, but runtime might have undefined
32
+ expect(escapeForAppleScript(undefined)).toBe("");
33
+ });
34
+ });
35
+ describe("single quote escaping (shell safety)", () => {
36
+ it("escapes single quotes for shell embedding", () => {
37
+ // Single quotes in: osascript -e 'tell app...'
38
+ // Need to become: '\'' (end quote, escaped quote, start quote)
39
+ const result = escapeForAppleScript("it's working");
40
+ expect(result).toBe("it'\\''s working");
41
+ });
42
+ it("handles multiple single quotes", () => {
43
+ const result = escapeForAppleScript("Rob's mom's note");
44
+ expect(result).toBe("Rob'\\''s mom'\\''s note");
45
+ });
46
+ });
47
+ describe("double quote escaping (AppleScript strings)", () => {
48
+ it("escapes double quotes for AppleScript", () => {
49
+ // AppleScript strings: "hello \"quoted\" world"
50
+ const result = escapeForAppleScript('say "hello"');
51
+ expect(result).toBe('say \\"hello\\"');
52
+ });
53
+ it("handles mixed quotes", () => {
54
+ const result = escapeForAppleScript('He said "it\'s fine"');
55
+ expect(result).toBe("He said \\\"it'\\''s fine\\\"");
56
+ });
57
+ });
58
+ describe("control character conversion (HTML for Notes.app)", () => {
59
+ it("converts newlines to <br> tags", () => {
60
+ const result = escapeForAppleScript("line 1\nline 2\nline 3");
61
+ expect(result).toBe("line 1<br>line 2<br>line 3");
62
+ });
63
+ it("converts tabs to <br> tags", () => {
64
+ const result = escapeForAppleScript("col1\tcol2\tcol3");
65
+ expect(result).toBe("col1<br>col2<br>col3");
66
+ });
67
+ it("handles mixed control characters", () => {
68
+ const result = escapeForAppleScript("row1\tcol2\nrow2\tcol2");
69
+ expect(result).toBe("row1<br>col2<br>row2<br>col2");
70
+ });
71
+ });
72
+ describe("complex content", () => {
73
+ it("handles real-world note content", () => {
74
+ const content = 'John\'s "Meeting Notes"\n- Item 1\n- Item 2';
75
+ const result = escapeForAppleScript(content);
76
+ expect(result).toBe("John'\\''s \\\"Meeting Notes\\\"<br>- Item 1<br>- Item 2");
77
+ });
78
+ });
79
+ describe("unicode and special characters", () => {
80
+ it("preserves unicode characters", () => {
81
+ const result = escapeForAppleScript("日本語テスト 🎉");
82
+ expect(result).toBe("日本語テスト 🎉");
83
+ });
84
+ it("preserves emoji in content", () => {
85
+ const result = escapeForAppleScript("Shopping 🛒\n- Eggs 🥚\n- Milk 🥛");
86
+ expect(result).toBe("Shopping 🛒<br>- Eggs 🥚<br>- Milk 🥛");
87
+ });
88
+ it("handles accented characters", () => {
89
+ const result = escapeForAppleScript("Café résumé naïve");
90
+ expect(result).toBe("Café résumé naïve");
91
+ });
92
+ it("handles backslashes", () => {
93
+ const result = escapeForAppleScript("path\\to\\file");
94
+ expect(result).toBe("path\\to\\file");
95
+ });
96
+ it("handles angle brackets (HTML-like content)", () => {
97
+ // Single quotes become '\'' (shell escape pattern)
98
+ const result = escapeForAppleScript("<script>alert('xss')</script>");
99
+ expect(result).toBe("<script>alert('\\''xss'\\'')</script>");
100
+ });
101
+ });
102
+ describe("boundary conditions", () => {
103
+ it("handles very short strings", () => {
104
+ expect(escapeForAppleScript("a")).toBe("a");
105
+ expect(escapeForAppleScript("'")).toBe("'\\''");
106
+ expect(escapeForAppleScript('"')).toBe('\\"');
107
+ });
108
+ it("handles string with only whitespace", () => {
109
+ expect(escapeForAppleScript(" ")).toBe(" ");
110
+ });
111
+ it("handles multiple consecutive special characters", () => {
112
+ // Three single quotes become three '\'' sequences
113
+ // Three double quotes become three \" sequences
114
+ const result = escapeForAppleScript("'''\"\"\"");
115
+ expect(result).toBe("'\\'''\\'''\\''\\\"\\\"\\\"");
116
+ });
117
+ });
118
+ });
119
+ // =============================================================================
120
+ // Date Parsing Tests
121
+ // =============================================================================
122
+ describe("parseAppleScriptDate", () => {
123
+ describe("standard format parsing", () => {
124
+ it("parses AppleScript date with 'date' prefix", () => {
125
+ const dateStr = "date Saturday, December 27, 2025 at 3:44:02 PM";
126
+ const result = parseAppleScriptDate(dateStr);
127
+ expect(result.getFullYear()).toBe(2025);
128
+ expect(result.getMonth()).toBe(11); // December is month 11 (0-indexed)
129
+ expect(result.getDate()).toBe(27);
130
+ });
131
+ it("parses date without 'date' prefix", () => {
132
+ const dateStr = "Saturday, December 27, 2025 at 3:44:02 PM";
133
+ const result = parseAppleScriptDate(dateStr);
134
+ expect(result.getFullYear()).toBe(2025);
135
+ expect(result.getMonth()).toBe(11);
136
+ });
137
+ it("correctly handles AM/PM times", () => {
138
+ const morningDate = "date Monday, January 1, 2025 at 9:30:00 AM";
139
+ const eveningDate = "date Monday, January 1, 2025 at 9:30:00 PM";
140
+ const morning = parseAppleScriptDate(morningDate);
141
+ const evening = parseAppleScriptDate(eveningDate);
142
+ expect(morning.getHours()).toBe(9);
143
+ expect(evening.getHours()).toBe(21);
144
+ });
145
+ });
146
+ describe("fallback behavior", () => {
147
+ it("returns current date for invalid input", () => {
148
+ const before = new Date();
149
+ const result = parseAppleScriptDate("not a valid date");
150
+ const after = new Date();
151
+ // Result should be between before and after (i.e., "now")
152
+ expect(result.getTime()).toBeGreaterThanOrEqual(before.getTime());
153
+ expect(result.getTime()).toBeLessThanOrEqual(after.getTime());
154
+ });
155
+ it("returns current date for empty string", () => {
156
+ const before = new Date();
157
+ const result = parseAppleScriptDate("");
158
+ const after = new Date();
159
+ expect(result.getTime()).toBeGreaterThanOrEqual(before.getTime());
160
+ expect(result.getTime()).toBeLessThanOrEqual(after.getTime());
161
+ });
162
+ });
163
+ });
164
+ // =============================================================================
165
+ // AppleNotesManager Tests
166
+ // =============================================================================
167
+ describe("AppleNotesManager", () => {
168
+ let manager;
169
+ beforeEach(() => {
170
+ manager = new AppleNotesManager();
171
+ vi.clearAllMocks();
172
+ });
173
+ // ---------------------------------------------------------------------------
174
+ // Note Creation
175
+ // ---------------------------------------------------------------------------
176
+ describe("createNote", () => {
177
+ it("returns Note object on successful creation", () => {
178
+ mockExecuteAppleScript.mockReturnValue({
179
+ success: true,
180
+ output: "note id x-coredata://12345/ICNote/p100",
181
+ });
182
+ const result = manager.createNote("Shopping List", "Eggs, Milk, Bread");
183
+ expect(result).not.toBeNull();
184
+ expect(result?.title).toBe("Shopping List");
185
+ expect(result?.content).toBe("Eggs, Milk, Bread");
186
+ expect(result?.account).toBe("iCloud"); // Default account
187
+ });
188
+ it("returns null when AppleScript fails", () => {
189
+ mockExecuteAppleScript.mockReturnValue({
190
+ success: false,
191
+ output: "",
192
+ error: "Notes.app not responding",
193
+ });
194
+ const result = manager.createNote("Test", "Content");
195
+ expect(result).toBeNull();
196
+ });
197
+ it("uses specified account instead of default", () => {
198
+ mockExecuteAppleScript.mockReturnValue({
199
+ success: true,
200
+ output: "note id x-coredata://...",
201
+ });
202
+ const result = manager.createNote("Draft", "Email content", [], undefined, "Gmail");
203
+ expect(result?.account).toBe("Gmail");
204
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Gmail"'));
205
+ });
206
+ it("creates note in specified folder", () => {
207
+ mockExecuteAppleScript.mockReturnValue({
208
+ success: true,
209
+ output: "note id x-coredata://...",
210
+ });
211
+ manager.createNote("Work Note", "Content", [], "Work Projects");
212
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('at folder "Work Projects"'));
213
+ });
214
+ it("stores tags in returned Note object", () => {
215
+ mockExecuteAppleScript.mockReturnValue({
216
+ success: true,
217
+ output: "note id x-coredata://...",
218
+ });
219
+ const result = manager.createNote("Tagged Note", "Content", ["work", "urgent"]);
220
+ expect(result?.tags).toEqual(["work", "urgent"]);
221
+ });
222
+ });
223
+ // ---------------------------------------------------------------------------
224
+ // Note Search
225
+ // ---------------------------------------------------------------------------
226
+ describe("searchNotes", () => {
227
+ it("returns array of matching notes with folder info", () => {
228
+ mockExecuteAppleScript.mockReturnValue({
229
+ success: true,
230
+ output: "Meeting Notes|||Work|||ITEM|||Project Plan|||Notes|||ITEM|||Weekly Review|||Archive",
231
+ });
232
+ const results = manager.searchNotes("notes");
233
+ expect(results).toHaveLength(3);
234
+ expect(results[0].title).toBe("Meeting Notes");
235
+ expect(results[0].folder).toBe("Work");
236
+ expect(results[1].title).toBe("Project Plan");
237
+ expect(results[1].folder).toBe("Notes");
238
+ expect(results[2].title).toBe("Weekly Review");
239
+ expect(results[2].folder).toBe("Archive");
240
+ });
241
+ it("returns empty array when no matches found", () => {
242
+ mockExecuteAppleScript.mockReturnValue({
243
+ success: true,
244
+ output: "",
245
+ });
246
+ const results = manager.searchNotes("nonexistent");
247
+ expect(results).toHaveLength(0);
248
+ });
249
+ it("returns empty array on AppleScript error", () => {
250
+ mockExecuteAppleScript.mockReturnValue({
251
+ success: false,
252
+ output: "",
253
+ error: "Search failed",
254
+ });
255
+ const results = manager.searchNotes("test");
256
+ expect(results).toHaveLength(0);
257
+ });
258
+ it("searches content when searchContent is true", () => {
259
+ mockExecuteAppleScript.mockReturnValue({
260
+ success: true,
261
+ output: "Note with keyword|||Notes",
262
+ });
263
+ manager.searchNotes("project alpha", true);
264
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('body contains "project alpha"'));
265
+ });
266
+ it("searches titles when searchContent is false", () => {
267
+ mockExecuteAppleScript.mockReturnValue({
268
+ success: true,
269
+ output: "Project Alpha Notes|||Notes",
270
+ });
271
+ manager.searchNotes("Project Alpha", false);
272
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('name contains "Project Alpha"'));
273
+ });
274
+ it("identifies notes in Recently Deleted folder", () => {
275
+ mockExecuteAppleScript.mockReturnValue({
276
+ success: true,
277
+ output: "Old Note|||Recently Deleted|||ITEM|||Active Note|||Notes",
278
+ });
279
+ const results = manager.searchNotes("note");
280
+ expect(results).toHaveLength(2);
281
+ expect(results[0].title).toBe("Old Note");
282
+ expect(results[0].folder).toBe("Recently Deleted");
283
+ expect(results[1].title).toBe("Active Note");
284
+ expect(results[1].folder).toBe("Notes");
285
+ });
286
+ it("scopes search to specified account", () => {
287
+ mockExecuteAppleScript.mockReturnValue({
288
+ success: true,
289
+ output: "",
290
+ });
291
+ manager.searchNotes("work", false, "Exchange");
292
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Exchange"'));
293
+ });
294
+ });
295
+ // ---------------------------------------------------------------------------
296
+ // Note Content Retrieval
297
+ // ---------------------------------------------------------------------------
298
+ describe("getNoteContent", () => {
299
+ it("returns HTML content of note", () => {
300
+ mockExecuteAppleScript.mockReturnValue({
301
+ success: true,
302
+ output: "<div>Shopping List</div><div>- Eggs<br>- Milk</div>",
303
+ });
304
+ const content = manager.getNoteContent("Shopping List");
305
+ expect(content).toBe("<div>Shopping List</div><div>- Eggs<br>- Milk</div>");
306
+ });
307
+ it("returns empty string when note not found", () => {
308
+ mockExecuteAppleScript.mockReturnValue({
309
+ success: false,
310
+ output: "",
311
+ error: 'Can\'t get note "Missing"',
312
+ });
313
+ const content = manager.getNoteContent("Missing Note");
314
+ expect(content).toBe("");
315
+ });
316
+ it("uses specified account", () => {
317
+ mockExecuteAppleScript.mockReturnValue({
318
+ success: true,
319
+ output: "<div>Content</div>",
320
+ });
321
+ manager.getNoteContent("My Note", "Gmail");
322
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Gmail"'));
323
+ });
324
+ });
325
+ // ---------------------------------------------------------------------------
326
+ // Get Note By ID
327
+ // ---------------------------------------------------------------------------
328
+ describe("getNoteById", () => {
329
+ it("returns Note object with metadata for valid ID", () => {
330
+ mockExecuteAppleScript.mockReturnValue({
331
+ success: true,
332
+ output: "My Note, x-coredata://ABC123/ICNote/p100, date Saturday, December 27, 2025 at 3:00:00 PM, date Saturday, December 27, 2025 at 4:00:00 PM, false, false",
333
+ });
334
+ const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
335
+ expect(result).not.toBeNull();
336
+ expect(result?.title).toBe("My Note");
337
+ expect(result?.id).toBe("x-coredata://ABC123/ICNote/p100");
338
+ expect(result?.shared).toBe(false);
339
+ expect(result?.passwordProtected).toBe(false);
340
+ });
341
+ it("returns null when note ID not found", () => {
342
+ mockExecuteAppleScript.mockReturnValue({
343
+ success: false,
344
+ output: "",
345
+ error: "Can't get note id",
346
+ });
347
+ const result = manager.getNoteById("x-coredata://invalid");
348
+ expect(result).toBeNull();
349
+ });
350
+ it("returns null when response format is unexpected (no commas)", () => {
351
+ mockExecuteAppleScript.mockReturnValue({
352
+ success: true,
353
+ output: "incomplete data with no commas",
354
+ });
355
+ const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
356
+ expect(result).toBeNull();
357
+ });
358
+ it("returns null when response format is missing second comma", () => {
359
+ mockExecuteAppleScript.mockReturnValue({
360
+ success: true,
361
+ output: "title only, no more data",
362
+ });
363
+ const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
364
+ // The new parsing requires at least title and ID separated by commas
365
+ expect(result).toBeNull();
366
+ });
367
+ it("correctly parses shared and passwordProtected as true", () => {
368
+ mockExecuteAppleScript.mockReturnValue({
369
+ success: true,
370
+ output: "Shared Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, true, true",
371
+ });
372
+ const result = manager.getNoteById("x-coredata://ABC/ICNote/p1");
373
+ expect(result?.shared).toBe(true);
374
+ expect(result?.passwordProtected).toBe(true);
375
+ });
376
+ });
377
+ // ---------------------------------------------------------------------------
378
+ // Get Note Details
379
+ // ---------------------------------------------------------------------------
380
+ describe("getNoteDetails", () => {
381
+ it("returns Note object with full metadata", () => {
382
+ mockExecuteAppleScript.mockReturnValue({
383
+ success: true,
384
+ output: "Project Notes, x-coredata://ABC123/ICNote/p200, date Friday, December 20, 2025 at 10:00:00 AM, date Saturday, December 27, 2025 at 2:30:00 PM, false, false",
385
+ });
386
+ const result = manager.getNoteDetails("Project Notes");
387
+ expect(result).not.toBeNull();
388
+ expect(result?.title).toBe("Project Notes");
389
+ expect(result?.id).toBe("x-coredata://ABC123/ICNote/p200");
390
+ expect(result?.account).toBe("iCloud");
391
+ });
392
+ it("returns null when note not found", () => {
393
+ mockExecuteAppleScript.mockReturnValue({
394
+ success: false,
395
+ output: "",
396
+ error: "Can't get note",
397
+ });
398
+ const result = manager.getNoteDetails("Nonexistent");
399
+ expect(result).toBeNull();
400
+ });
401
+ it("uses specified account", () => {
402
+ mockExecuteAppleScript.mockReturnValue({
403
+ success: true,
404
+ output: "Note, id123, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, false, false",
405
+ });
406
+ const result = manager.getNoteDetails("My Note", "Exchange");
407
+ expect(result?.account).toBe("Exchange");
408
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Exchange"'));
409
+ });
410
+ it("handles shared notes correctly", () => {
411
+ mockExecuteAppleScript.mockReturnValue({
412
+ success: true,
413
+ output: "Shared Doc, id456, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, true, false",
414
+ });
415
+ const result = manager.getNoteDetails("Shared Doc");
416
+ expect(result?.shared).toBe(true);
417
+ });
418
+ });
419
+ // ---------------------------------------------------------------------------
420
+ // Note Deletion
421
+ // ---------------------------------------------------------------------------
422
+ describe("deleteNote", () => {
423
+ it("returns true on successful deletion", () => {
424
+ mockExecuteAppleScript.mockReturnValue({
425
+ success: true,
426
+ output: "",
427
+ });
428
+ const result = manager.deleteNote("Old Note");
429
+ expect(result).toBe(true);
430
+ });
431
+ it("returns false when deletion fails", () => {
432
+ mockExecuteAppleScript.mockReturnValue({
433
+ success: false,
434
+ output: "",
435
+ error: "Cannot delete protected note",
436
+ });
437
+ const result = manager.deleteNote("Protected Note");
438
+ expect(result).toBe(false);
439
+ });
440
+ it("uses specified account", () => {
441
+ mockExecuteAppleScript.mockReturnValue({
442
+ success: true,
443
+ output: "",
444
+ });
445
+ manager.deleteNote("Draft", "Gmail");
446
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('tell account "Gmail"'));
447
+ });
448
+ });
449
+ // ---------------------------------------------------------------------------
450
+ // Note Updates
451
+ // ---------------------------------------------------------------------------
452
+ describe("updateNote", () => {
453
+ it("returns true on successful update", () => {
454
+ mockExecuteAppleScript.mockReturnValue({
455
+ success: true,
456
+ output: "",
457
+ });
458
+ const result = manager.updateNote("Old Title", "New Title", "Updated content");
459
+ expect(result).toBe(true);
460
+ });
461
+ it("returns false when update fails", () => {
462
+ mockExecuteAppleScript.mockReturnValue({
463
+ success: false,
464
+ output: "",
465
+ error: "Note not found",
466
+ });
467
+ const result = manager.updateNote("Missing", "New Title", "Content");
468
+ expect(result).toBe(false);
469
+ });
470
+ it("preserves original title when newTitle is undefined", () => {
471
+ mockExecuteAppleScript.mockReturnValue({
472
+ success: true,
473
+ output: "",
474
+ });
475
+ manager.updateNote("Keep This Title", undefined, "New content only");
476
+ // The generated body should use the original title
477
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining("<div>Keep This Title</div>"));
478
+ });
479
+ it("uses new title when provided", () => {
480
+ mockExecuteAppleScript.mockReturnValue({
481
+ success: true,
482
+ output: "",
483
+ });
484
+ manager.updateNote("Old Title", "Brand New Title", "Content");
485
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining("<div>Brand New Title</div>"));
486
+ });
487
+ });
488
+ // ---------------------------------------------------------------------------
489
+ // Note Listing
490
+ // ---------------------------------------------------------------------------
491
+ describe("listNotes", () => {
492
+ it("returns array of note titles", () => {
493
+ mockExecuteAppleScript.mockReturnValue({
494
+ success: true,
495
+ output: "Note A, Note B, Note C",
496
+ });
497
+ const titles = manager.listNotes();
498
+ expect(titles).toEqual(["Note A", "Note B", "Note C"]);
499
+ });
500
+ it("filters out empty entries", () => {
501
+ mockExecuteAppleScript.mockReturnValue({
502
+ success: true,
503
+ output: "Note A, , Note B, , ",
504
+ });
505
+ const titles = manager.listNotes();
506
+ expect(titles).toEqual(["Note A", "Note B"]);
507
+ });
508
+ it("returns empty array on failure", () => {
509
+ mockExecuteAppleScript.mockReturnValue({
510
+ success: false,
511
+ output: "",
512
+ error: "Account not found",
513
+ });
514
+ const titles = manager.listNotes();
515
+ expect(titles).toEqual([]);
516
+ });
517
+ it("filters by folder when specified", () => {
518
+ mockExecuteAppleScript.mockReturnValue({
519
+ success: true,
520
+ output: "Work Note 1, Work Note 2",
521
+ });
522
+ manager.listNotes("iCloud", "Work");
523
+ expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
524
+ });
525
+ });
526
+ // ---------------------------------------------------------------------------
527
+ // Folder Operations
528
+ // ---------------------------------------------------------------------------
529
+ describe("listFolders", () => {
530
+ it("returns array of Folder objects", () => {
531
+ mockExecuteAppleScript.mockReturnValue({
532
+ success: true,
533
+ output: "Notes, Archive, Work",
534
+ });
535
+ const folders = manager.listFolders();
536
+ expect(folders).toHaveLength(3);
537
+ expect(folders[0].name).toBe("Notes");
538
+ expect(folders[1].name).toBe("Archive");
539
+ expect(folders[2].name).toBe("Work");
540
+ });
541
+ it("includes account in Folder objects", () => {
542
+ mockExecuteAppleScript.mockReturnValue({
543
+ success: true,
544
+ output: "Notes",
545
+ });
546
+ const folders = manager.listFolders("Gmail");
547
+ expect(folders[0].account).toBe("Gmail");
548
+ });
549
+ });
550
+ describe("createFolder", () => {
551
+ it("returns Folder object on success", () => {
552
+ mockExecuteAppleScript.mockReturnValue({
553
+ success: true,
554
+ output: "folder id x-coredata://ABC123/ICFolder/p456",
555
+ });
556
+ const result = manager.createFolder("New Project");
557
+ expect(result).not.toBeNull();
558
+ expect(result?.name).toBe("New Project");
559
+ expect(result?.id).toBe("x-coredata://ABC123/ICFolder/p456");
560
+ });
561
+ it("returns null on failure", () => {
562
+ mockExecuteAppleScript.mockReturnValue({
563
+ success: false,
564
+ output: "",
565
+ error: "Folder already exists",
566
+ });
567
+ const result = manager.createFolder("Existing Folder");
568
+ expect(result).toBeNull();
569
+ });
570
+ });
571
+ describe("deleteFolder", () => {
572
+ it("returns true on successful deletion", () => {
573
+ mockExecuteAppleScript.mockReturnValue({
574
+ success: true,
575
+ output: "",
576
+ });
577
+ const result = manager.deleteFolder("Empty Folder");
578
+ expect(result).toBe(true);
579
+ });
580
+ it("returns false when deletion fails", () => {
581
+ mockExecuteAppleScript.mockReturnValue({
582
+ success: false,
583
+ output: "",
584
+ error: "Folder contains notes",
585
+ });
586
+ const result = manager.deleteFolder("Non-Empty Folder");
587
+ expect(result).toBe(false);
588
+ });
589
+ });
590
+ // ---------------------------------------------------------------------------
591
+ // Note Moving
592
+ // ---------------------------------------------------------------------------
593
+ describe("moveNote", () => {
594
+ it("returns true when move completes successfully", () => {
595
+ // Mock sequence: getNoteContent -> createNote -> deleteNote
596
+ mockExecuteAppleScript
597
+ .mockReturnValueOnce({
598
+ success: true,
599
+ output: "<div>Note Title</div><div>Content</div>",
600
+ })
601
+ .mockReturnValueOnce({
602
+ success: true,
603
+ output: "note id x-coredata://...",
604
+ })
605
+ .mockReturnValueOnce({
606
+ success: true,
607
+ output: "",
608
+ });
609
+ const result = manager.moveNote("My Note", "Archive");
610
+ expect(result).toBe(true);
611
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(3);
612
+ });
613
+ it("returns false when source note cannot be read", () => {
614
+ mockExecuteAppleScript.mockReturnValueOnce({
615
+ success: false,
616
+ output: "",
617
+ error: "Note not found",
618
+ });
619
+ const result = manager.moveNote("Missing Note", "Archive");
620
+ expect(result).toBe(false);
621
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(1); // Only tried to read
622
+ });
623
+ it("returns false when copy to destination fails", () => {
624
+ mockExecuteAppleScript
625
+ .mockReturnValueOnce({
626
+ success: true,
627
+ output: "<div>Content</div>",
628
+ })
629
+ .mockReturnValueOnce({
630
+ success: false,
631
+ output: "",
632
+ error: "Folder not found",
633
+ });
634
+ const result = manager.moveNote("My Note", "Nonexistent Folder");
635
+ expect(result).toBe(false);
636
+ expect(mockExecuteAppleScript).toHaveBeenCalledTimes(2); // Read + failed create
637
+ });
638
+ it("returns true even if delete fails (note exists in new location)", () => {
639
+ // This is partial success - note was copied but original couldn't be deleted
640
+ mockExecuteAppleScript
641
+ .mockReturnValueOnce({
642
+ success: true,
643
+ output: "<div>Content</div>",
644
+ })
645
+ .mockReturnValueOnce({
646
+ success: true,
647
+ output: "note id x-coredata://...",
648
+ })
649
+ .mockReturnValueOnce({
650
+ success: false,
651
+ output: "",
652
+ error: "Cannot delete original",
653
+ });
654
+ const result = manager.moveNote("My Note", "Archive");
655
+ // Should still return true because the note exists in the destination
656
+ expect(result).toBe(true);
657
+ });
658
+ });
659
+ // ---------------------------------------------------------------------------
660
+ // Account Operations
661
+ // ---------------------------------------------------------------------------
662
+ describe("listAccounts", () => {
663
+ it("returns array of Account objects", () => {
664
+ mockExecuteAppleScript.mockReturnValue({
665
+ success: true,
666
+ output: "iCloud, Gmail, Exchange",
667
+ });
668
+ const accounts = manager.listAccounts();
669
+ expect(accounts).toHaveLength(3);
670
+ expect(accounts[0].name).toBe("iCloud");
671
+ expect(accounts[1].name).toBe("Gmail");
672
+ expect(accounts[2].name).toBe("Exchange");
673
+ });
674
+ it("returns empty array on failure", () => {
675
+ mockExecuteAppleScript.mockReturnValue({
676
+ success: false,
677
+ output: "",
678
+ error: "Notes.app not available",
679
+ });
680
+ const accounts = manager.listAccounts();
681
+ expect(accounts).toEqual([]);
682
+ });
683
+ });
684
+ });