apple-notes-mcp 1.4.4 → 2.0.1

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.
@@ -25,6 +25,11 @@ import { executeAppleScript } from "../utils/applescript.js";
25
25
  const mockExecuteAppleScript = vi.mocked(executeAppleScript);
26
26
  import { getChecklistItems } from "../utils/checklistParser.js";
27
27
  const mockGetChecklistItems = vi.mocked(getChecklistItems);
28
+ // Result delimiters (#18) — must match appleNotesManager.ts.
29
+ // FIELD_SEP (US, \x1f) separates fields within a record;
30
+ // RECORD_SEP (RS, \x1e) separates records within a list.
31
+ const F = "\x1f";
32
+ const R = "\x1e";
28
33
  // =============================================================================
29
34
  // Text Escaping Tests
30
35
  // =============================================================================
@@ -191,6 +196,23 @@ describe("parseAppleScriptDate", () => {
191
196
  expect(evening.getHours()).toBe(21);
192
197
  });
193
198
  });
199
+ describe("locale-independent numeric format (#25)", () => {
200
+ it("parses the Y-M-D-H-m-s form emitted by our producers", () => {
201
+ const result = parseAppleScriptDate("2025-12-27-15-44-2");
202
+ expect(result.getFullYear()).toBe(2025);
203
+ expect(result.getMonth()).toBe(11);
204
+ expect(result.getDate()).toBe(27);
205
+ expect(result.getHours()).toBe(15);
206
+ expect(result.getMinutes()).toBe(44);
207
+ expect(result.getSeconds()).toBe(2);
208
+ });
209
+ it("handles single-digit components and midnight", () => {
210
+ const result = parseAppleScriptDate("2025-1-5-0-0-0");
211
+ expect(result.getMonth()).toBe(0);
212
+ expect(result.getDate()).toBe(5);
213
+ expect(result.getHours()).toBe(0);
214
+ });
215
+ });
194
216
  describe("fallback behavior", () => {
195
217
  it("returns current date for invalid input", () => {
196
218
  const before = new Date();
@@ -437,7 +459,11 @@ describe("AppleNotesManager", () => {
437
459
  it("returns array of matching notes with folder info", () => {
438
460
  mockExecuteAppleScript.mockReturnValue({
439
461
  success: true,
440
- output: "Meeting Notes|||x-coredata://ABC/ICNote/p1|||Work|||ITEM|||Project Plan|||x-coredata://ABC/ICNote/p2|||Notes|||ITEM|||Weekly Review|||x-coredata://ABC/ICNote/p3|||Archive",
462
+ output: [
463
+ ["Meeting Notes", "x-coredata://ABC/ICNote/p1", "Work"].join(F),
464
+ ["Project Plan", "x-coredata://ABC/ICNote/p2", "Notes"].join(F),
465
+ ["Weekly Review", "x-coredata://ABC/ICNote/p3", "Archive"].join(F),
466
+ ].join(R),
441
467
  });
442
468
  const results = manager.searchNotes("notes");
443
469
  expect(results).toHaveLength(3);
@@ -459,19 +485,18 @@ describe("AppleNotesManager", () => {
459
485
  const results = manager.searchNotes("nonexistent");
460
486
  expect(results).toHaveLength(0);
461
487
  });
462
- it("returns empty array on AppleScript error", () => {
488
+ it("throws on AppleScript error rather than returning empty (#19)", () => {
463
489
  mockExecuteAppleScript.mockReturnValue({
464
490
  success: false,
465
491
  output: "",
466
492
  error: "Search failed",
467
493
  });
468
- const results = manager.searchNotes("test");
469
- expect(results).toHaveLength(0);
494
+ expect(() => manager.searchNotes("test")).toThrow(/Search failed/);
470
495
  });
471
496
  it("searches content when searchContent is true", () => {
472
497
  mockExecuteAppleScript.mockReturnValue({
473
498
  success: true,
474
- output: "Note with keyword|||x-coredata://ABC/ICNote/p1|||Notes",
499
+ output: ["Note with keyword", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
475
500
  });
476
501
  manager.searchNotes("project alpha", true);
477
502
  expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('body contains "project alpha"'));
@@ -479,7 +504,7 @@ describe("AppleNotesManager", () => {
479
504
  it("searches titles when searchContent is false", () => {
480
505
  mockExecuteAppleScript.mockReturnValue({
481
506
  success: true,
482
- output: "Project Alpha Notes|||x-coredata://ABC/ICNote/p1|||Notes",
507
+ output: ["Project Alpha Notes", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
483
508
  });
484
509
  manager.searchNotes("Project Alpha", false);
485
510
  expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('name contains "Project Alpha"'));
@@ -487,7 +512,10 @@ describe("AppleNotesManager", () => {
487
512
  it("identifies notes in Recently Deleted folder", () => {
488
513
  mockExecuteAppleScript.mockReturnValue({
489
514
  success: true,
490
- output: "Old Note|||x-coredata://ABC/ICNote/p1|||Recently Deleted|||ITEM|||Active Note|||x-coredata://ABC/ICNote/p2|||Notes",
515
+ output: [
516
+ ["Old Note", "x-coredata://ABC/ICNote/p1", "Recently Deleted"].join(F),
517
+ ["Active Note", "x-coredata://ABC/ICNote/p2", "Notes"].join(F),
518
+ ].join(R),
491
519
  });
492
520
  const results = manager.searchNotes("note");
493
521
  expect(results).toHaveLength(2);
@@ -509,7 +537,7 @@ describe("AppleNotesManager", () => {
509
537
  it("limits search to specified folder", () => {
510
538
  mockExecuteAppleScript.mockReturnValue({
511
539
  success: true,
512
- output: "Work Note|||x-coredata://ABC/ICNote/p1|||Work",
540
+ output: ["Work Note", "x-coredata://ABC/ICNote/p1", "Work"].join(F),
513
541
  });
514
542
  manager.searchNotes("note", false, undefined, "Work");
515
543
  expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
@@ -527,7 +555,7 @@ describe("AppleNotesManager", () => {
527
555
  it("adds date filter when modifiedSince is provided", () => {
528
556
  mockExecuteAppleScript.mockReturnValue({
529
557
  success: true,
530
- output: "Recent Note|||x-coredata://ABC/ICNote/p1|||Notes",
558
+ output: ["Recent Note", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
531
559
  });
532
560
  manager.searchNotes("note", false, undefined, undefined, "2025-06-15T00:00:00");
533
561
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -542,7 +570,7 @@ describe("AppleNotesManager", () => {
542
570
  it("combines date filter with content search", () => {
543
571
  mockExecuteAppleScript.mockReturnValue({
544
572
  success: true,
545
- output: "Note|||x-coredata://ABC/ICNote/p1|||Notes",
573
+ output: ["Note", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
546
574
  });
547
575
  manager.searchNotes("keyword", true, undefined, undefined, "2025-01-01");
548
576
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -553,7 +581,7 @@ describe("AppleNotesManager", () => {
553
581
  it("ignores invalid modifiedSince date", () => {
554
582
  mockExecuteAppleScript.mockReturnValue({
555
583
  success: true,
556
- output: "Note|||x-coredata://ABC/ICNote/p1|||Notes",
584
+ output: ["Note", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
557
585
  });
558
586
  manager.searchNotes("note", false, undefined, undefined, "not-a-date");
559
587
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -562,7 +590,7 @@ describe("AppleNotesManager", () => {
562
590
  it("applies limit to search results", () => {
563
591
  mockExecuteAppleScript.mockReturnValue({
564
592
  success: true,
565
- output: "Note 1|||x-coredata://ABC/ICNote/p1|||Notes",
593
+ output: ["Note 1", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
566
594
  });
567
595
  manager.searchNotes("note", false, undefined, undefined, undefined, 5);
568
596
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -572,7 +600,7 @@ describe("AppleNotesManager", () => {
572
600
  it("combines modifiedSince, limit, folder, and content search", () => {
573
601
  mockExecuteAppleScript.mockReturnValue({
574
602
  success: true,
575
- output: "Note|||x-coredata://ABC/ICNote/p1|||Work",
603
+ output: ["Note", "x-coredata://ABC/ICNote/p1", "Work"].join(F),
576
604
  });
577
605
  manager.searchNotes("project", true, "iCloud", "Work", "2025-03-01", 10);
578
606
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -605,6 +633,15 @@ describe("AppleNotesManager", () => {
605
633
  const content = manager.getNoteContent("Missing Note");
606
634
  expect(content).toBe("");
607
635
  });
636
+ it("looks up titles containing & literally, not HTML-escaped (regression)", () => {
637
+ // Bug found in live testing: titles with "&" were HTML-escaped to "&"
638
+ // in the `note "..."` lookup, so the note could never be found.
639
+ mockExecuteAppleScript.mockReturnValue({ success: true, output: "<div>x</div>" });
640
+ manager.getNoteContent("Tom & Jerry", "iCloud");
641
+ const script = mockExecuteAppleScript.mock.calls[0][0];
642
+ expect(script).toContain("Tom & Jerry");
643
+ expect(script).not.toContain("Tom &amp; Jerry");
644
+ });
608
645
  it("uses specified account", () => {
609
646
  mockExecuteAppleScript.mockReturnValue({
610
647
  success: true,
@@ -621,7 +658,14 @@ describe("AppleNotesManager", () => {
621
658
  it("returns true when note is password-protected", () => {
622
659
  mockExecuteAppleScript.mockReturnValue({
623
660
  success: true,
624
- output: "Locked Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, true",
661
+ output: [
662
+ "Locked Note",
663
+ "x-coredata://ABC/ICNote/p1",
664
+ "Monday, January 1, 2024 at 12:00:00 PM",
665
+ "Monday, January 1, 2024 at 12:00:00 PM",
666
+ "false",
667
+ "true",
668
+ ].join(F),
625
669
  });
626
670
  const result = manager.isNotePasswordProtected("Locked Note");
627
671
  expect(result).toBe(true);
@@ -629,7 +673,14 @@ describe("AppleNotesManager", () => {
629
673
  it("returns false when note is not password-protected", () => {
630
674
  mockExecuteAppleScript.mockReturnValue({
631
675
  success: true,
632
- output: "Open Note, x-coredata://ABC/ICNote/p2, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, false",
676
+ output: [
677
+ "Open Note",
678
+ "x-coredata://ABC/ICNote/p2",
679
+ "Monday, January 1, 2024 at 12:00:00 PM",
680
+ "Monday, January 1, 2024 at 12:00:00 PM",
681
+ "false",
682
+ "false",
683
+ ].join(F),
633
684
  });
634
685
  const result = manager.isNotePasswordProtected("Open Note");
635
686
  expect(result).toBe(false);
@@ -648,7 +699,14 @@ describe("AppleNotesManager", () => {
648
699
  it("returns true when note is password-protected", () => {
649
700
  mockExecuteAppleScript.mockReturnValue({
650
701
  success: true,
651
- output: "Locked Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, true",
702
+ output: [
703
+ "Locked Note",
704
+ "x-coredata://ABC/ICNote/p1",
705
+ "Monday, January 1, 2024 at 12:00:00 PM",
706
+ "Monday, January 1, 2024 at 12:00:00 PM",
707
+ "false",
708
+ "true",
709
+ ].join(F),
652
710
  });
653
711
  const result = manager.isNotePasswordProtectedById("x-coredata://ABC/ICNote/p1");
654
712
  expect(result).toBe(true);
@@ -656,7 +714,14 @@ describe("AppleNotesManager", () => {
656
714
  it("returns false when note is not password-protected", () => {
657
715
  mockExecuteAppleScript.mockReturnValue({
658
716
  success: true,
659
- output: "Open Note, x-coredata://ABC/ICNote/p2, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, false",
717
+ output: [
718
+ "Open Note",
719
+ "x-coredata://ABC/ICNote/p2",
720
+ "Monday, January 1, 2024 at 12:00:00 PM",
721
+ "Monday, January 1, 2024 at 12:00:00 PM",
722
+ "false",
723
+ "false",
724
+ ].join(F),
660
725
  });
661
726
  const result = manager.isNotePasswordProtectedById("x-coredata://ABC/ICNote/p2");
662
727
  expect(result).toBe(false);
@@ -678,7 +743,14 @@ describe("AppleNotesManager", () => {
678
743
  it("returns Note object with metadata for valid ID", () => {
679
744
  mockExecuteAppleScript.mockReturnValue({
680
745
  success: true,
681
- 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",
746
+ output: [
747
+ "My Note",
748
+ "x-coredata://ABC123/ICNote/p100",
749
+ "Saturday, December 27, 2025 at 3:00:00 PM",
750
+ "Saturday, December 27, 2025 at 4:00:00 PM",
751
+ "false",
752
+ "false",
753
+ ].join(F),
682
754
  });
683
755
  const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
684
756
  expect(result).not.toBeNull();
@@ -716,7 +788,14 @@ describe("AppleNotesManager", () => {
716
788
  it("correctly parses shared and passwordProtected as true", () => {
717
789
  mockExecuteAppleScript.mockReturnValue({
718
790
  success: true,
719
- 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",
791
+ output: [
792
+ "Shared Note",
793
+ "x-coredata://ABC/ICNote/p1",
794
+ "Monday, January 1, 2025 at 12:00:00 PM",
795
+ "Monday, January 1, 2025 at 12:00:00 PM",
796
+ "true",
797
+ "true",
798
+ ].join(F),
720
799
  });
721
800
  const result = manager.getNoteById("x-coredata://ABC/ICNote/p1");
722
801
  expect(result?.shared).toBe(true);
@@ -730,7 +809,14 @@ describe("AppleNotesManager", () => {
730
809
  it("returns Note object with full metadata", () => {
731
810
  mockExecuteAppleScript.mockReturnValue({
732
811
  success: true,
733
- 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",
812
+ output: [
813
+ "Project Notes",
814
+ "x-coredata://ABC123/ICNote/p200",
815
+ "Friday, December 20, 2025 at 10:00:00 AM",
816
+ "Saturday, December 27, 2025 at 2:30:00 PM",
817
+ "false",
818
+ "false",
819
+ ].join(F),
734
820
  });
735
821
  const result = manager.getNoteDetails("Project Notes");
736
822
  expect(result).not.toBeNull();
@@ -750,7 +836,14 @@ describe("AppleNotesManager", () => {
750
836
  it("uses specified account", () => {
751
837
  mockExecuteAppleScript.mockReturnValue({
752
838
  success: true,
753
- 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",
839
+ output: [
840
+ "Note",
841
+ "id123",
842
+ "Monday, January 1, 2025 at 12:00:00 PM",
843
+ "Monday, January 1, 2025 at 12:00:00 PM",
844
+ "false",
845
+ "false",
846
+ ].join(F),
754
847
  });
755
848
  const result = manager.getNoteDetails("My Note", "Exchange");
756
849
  expect(result?.account).toBe("Exchange");
@@ -759,7 +852,14 @@ describe("AppleNotesManager", () => {
759
852
  it("handles shared notes correctly", () => {
760
853
  mockExecuteAppleScript.mockReturnValue({
761
854
  success: true,
762
- 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",
855
+ output: [
856
+ "Shared Doc",
857
+ "id456",
858
+ "Monday, January 1, 2025 at 12:00:00 PM",
859
+ "Monday, January 1, 2025 at 12:00:00 PM",
860
+ "true",
861
+ "false",
862
+ ].join(F),
763
863
  });
764
864
  const result = manager.getNoteDetails("Shared Doc");
765
865
  expect(result?.shared).toBe(true);
@@ -902,7 +1002,7 @@ describe("AppleNotesManager", () => {
902
1002
  it("returns array of note titles", () => {
903
1003
  mockExecuteAppleScript.mockReturnValue({
904
1004
  success: true,
905
- output: "Note A, Note B, Note C",
1005
+ output: ["Note A", "Note B", "Note C"].join(R),
906
1006
  });
907
1007
  const titles = manager.listNotes();
908
1008
  expect(titles).toEqual(["Note A", "Note B", "Note C"]);
@@ -910,24 +1010,23 @@ describe("AppleNotesManager", () => {
910
1010
  it("filters out empty entries", () => {
911
1011
  mockExecuteAppleScript.mockReturnValue({
912
1012
  success: true,
913
- output: "Note A, , Note B, , ",
1013
+ output: ["Note A", "", "Note B", "", ""].join(R),
914
1014
  });
915
1015
  const titles = manager.listNotes();
916
1016
  expect(titles).toEqual(["Note A", "Note B"]);
917
1017
  });
918
- it("returns empty array on failure", () => {
1018
+ it("throws on failure rather than returning empty (#19)", () => {
919
1019
  mockExecuteAppleScript.mockReturnValue({
920
1020
  success: false,
921
1021
  output: "",
922
1022
  error: "Account not found",
923
1023
  });
924
- const titles = manager.listNotes();
925
- expect(titles).toEqual([]);
1024
+ expect(() => manager.listNotes()).toThrow(/Account not found/);
926
1025
  });
927
1026
  it("filters by folder when specified", () => {
928
1027
  mockExecuteAppleScript.mockReturnValue({
929
1028
  success: true,
930
- output: "Work Note 1, Work Note 2",
1029
+ output: ["Work Note 1", "Work Note 2"].join(R),
931
1030
  });
932
1031
  manager.listNotes("iCloud", "Work");
933
1032
  expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
@@ -935,7 +1034,7 @@ describe("AppleNotesManager", () => {
935
1034
  it("uses whose clause when modifiedSince is provided", () => {
936
1035
  mockExecuteAppleScript.mockReturnValue({
937
1036
  success: true,
938
- output: "Recent Note 1|||Recent Note 2",
1037
+ output: ["Recent Note 1", "Recent Note 2"].join(R),
939
1038
  });
940
1039
  const results = manager.listNotes(undefined, undefined, "2025-06-15T00:00:00");
941
1040
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -950,7 +1049,7 @@ describe("AppleNotesManager", () => {
950
1049
  it("uses repeat loop when limit is provided", () => {
951
1050
  mockExecuteAppleScript.mockReturnValue({
952
1051
  success: true,
953
- output: "Note 1|||Note 2|||Note 3",
1052
+ output: ["Note 1", "Note 2", "Note 3"].join(R),
954
1053
  });
955
1054
  const results = manager.listNotes(undefined, undefined, undefined, 3);
956
1055
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -960,7 +1059,7 @@ describe("AppleNotesManager", () => {
960
1059
  it("combines folder, modifiedSince, and limit", () => {
961
1060
  mockExecuteAppleScript.mockReturnValue({
962
1061
  success: true,
963
- output: "Work Note|||Another Work Note",
1062
+ output: ["Work Note", "Another Work Note"].join(R),
964
1063
  });
965
1064
  manager.listNotes("iCloud", "Work", "2025-01-01", 10);
966
1065
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -979,7 +1078,7 @@ describe("AppleNotesManager", () => {
979
1078
  it("ignores invalid modifiedSince date and falls back to limit-only", () => {
980
1079
  mockExecuteAppleScript.mockReturnValue({
981
1080
  success: true,
982
- output: "Note 1|||Note 2",
1081
+ output: ["Note 1", "Note 2"].join(R),
983
1082
  });
984
1083
  const results = manager.listNotes(undefined, undefined, "not-a-date", 5);
985
1084
  const script = mockExecuteAppleScript.mock.calls[0][0];
@@ -1170,7 +1269,14 @@ describe("AppleNotesManager", () => {
1170
1269
  mockExecuteAppleScript
1171
1270
  .mockReturnValueOnce({
1172
1271
  success: true,
1173
- output: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
1272
+ output: [
1273
+ "My Note",
1274
+ "x-coredata://ABC/ICNote/p123",
1275
+ "Monday, January 1, 2024 at 12:00:00 PM",
1276
+ "Monday, January 1, 2024 at 12:00:00 PM",
1277
+ "false",
1278
+ "false",
1279
+ ].join(F),
1174
1280
  })
1175
1281
  .mockReturnValueOnce({
1176
1282
  success: true,
@@ -1202,7 +1308,14 @@ describe("AppleNotesManager", () => {
1202
1308
  mockExecuteAppleScript
1203
1309
  .mockReturnValueOnce({
1204
1310
  success: true,
1205
- output: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
1311
+ output: [
1312
+ "My Note",
1313
+ "x-coredata://ABC/ICNote/p123",
1314
+ "Monday, January 1, 2024 at 12:00:00 PM",
1315
+ "Monday, January 1, 2024 at 12:00:00 PM",
1316
+ "false",
1317
+ "false",
1318
+ ].join(F),
1206
1319
  })
1207
1320
  .mockReturnValueOnce({
1208
1321
  success: true,
@@ -1222,7 +1335,14 @@ describe("AppleNotesManager", () => {
1222
1335
  mockExecuteAppleScript
1223
1336
  .mockReturnValueOnce({
1224
1337
  success: true,
1225
- output: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
1338
+ output: [
1339
+ "My Note",
1340
+ "x-coredata://ABC/ICNote/p123",
1341
+ "Monday, January 1, 2024 at 12:00:00 PM",
1342
+ "Monday, January 1, 2024 at 12:00:00 PM",
1343
+ "false",
1344
+ "false",
1345
+ ].join(F),
1226
1346
  })
1227
1347
  .mockReturnValueOnce({
1228
1348
  success: true,
@@ -1249,7 +1369,7 @@ describe("AppleNotesManager", () => {
1249
1369
  it("returns array of Account objects", () => {
1250
1370
  mockExecuteAppleScript.mockReturnValue({
1251
1371
  success: true,
1252
- output: "iCloud, Gmail, Exchange",
1372
+ output: ["iCloud", "Gmail", "Exchange"].join(R),
1253
1373
  });
1254
1374
  const accounts = manager.listAccounts();
1255
1375
  expect(accounts).toHaveLength(3);
@@ -1257,14 +1377,13 @@ describe("AppleNotesManager", () => {
1257
1377
  expect(accounts[1].name).toBe("Gmail");
1258
1378
  expect(accounts[2].name).toBe("Exchange");
1259
1379
  });
1260
- it("returns empty array on failure", () => {
1380
+ it("throws on failure rather than returning empty (#19)", () => {
1261
1381
  mockExecuteAppleScript.mockReturnValue({
1262
1382
  success: false,
1263
1383
  output: "",
1264
1384
  error: "Notes.app not available",
1265
1385
  });
1266
- const accounts = manager.listAccounts();
1267
- expect(accounts).toEqual([]);
1386
+ expect(() => manager.listAccounts()).toThrow(/Notes.app not available/);
1268
1387
  });
1269
1388
  });
1270
1389
  // ---------------------------------------------------------------------------
@@ -1280,7 +1399,7 @@ describe("AppleNotesManager", () => {
1280
1399
  // Check 3: listAccounts
1281
1400
  .mockReturnValueOnce({ success: true, output: "iCloud" })
1282
1401
  // Check 4: listNotes
1283
- .mockReturnValueOnce({ success: true, output: "Note 1, Note 2" });
1402
+ .mockReturnValueOnce({ success: true, output: ["Note 1", "Note 2"].join(R) });
1284
1403
  const result = manager.healthCheck();
1285
1404
  expect(result.healthy).toBe(true);
1286
1405
  expect(result.checks).toHaveLength(4);
@@ -1321,7 +1440,7 @@ describe("AppleNotesManager", () => {
1321
1440
  mockExecuteAppleScript
1322
1441
  .mockReturnValueOnce({ success: true, output: "ok" })
1323
1442
  .mockReturnValueOnce({ success: true, output: "iCloud" })
1324
- .mockReturnValueOnce({ success: true, output: "iCloud, Gmail" })
1443
+ .mockReturnValueOnce({ success: true, output: ["iCloud", "Gmail"].join(R) })
1325
1444
  .mockReturnValueOnce({ success: true, output: "" });
1326
1445
  const result = manager.healthCheck();
1327
1446
  const accountCheck = result.checks.find((c) => c.name === "accounts");
@@ -1337,14 +1456,13 @@ describe("AppleNotesManager", () => {
1337
1456
  mockExecuteAppleScript
1338
1457
  // listAccounts
1339
1458
  .mockReturnValueOnce({ success: true, output: "iCloud" })
1340
- // listFolders for iCloud
1341
- .mockReturnValueOnce({ success: true, output: "id1\tNotes\nid2\tWork" })
1342
- // listNotes for Notes folder
1343
- .mockReturnValueOnce({ success: true, output: "Note 1, Note 2, Note 3" })
1344
- // listNotes for Work folder
1345
- .mockReturnValueOnce({ success: true, output: "Task 1, Task 2" })
1346
- // getRecentlyModifiedCounts
1347
- .mockReturnValueOnce({ success: true, output: "" });
1459
+ // per-account folder counts: name<F>count, records joined by R
1460
+ .mockReturnValueOnce({
1461
+ success: true,
1462
+ output: ["Notes", "3"].join(F) + R + ["Work", "2"].join(F) + R,
1463
+ })
1464
+ // getRecentlyModifiedCounts: c1<F>c7<F>c30
1465
+ .mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
1348
1466
  const stats = manager.getNotesStats();
1349
1467
  expect(stats.totalNotes).toBe(5);
1350
1468
  expect(stats.accounts).toHaveLength(1);
@@ -1356,9 +1474,8 @@ describe("AppleNotesManager", () => {
1356
1474
  it("returns zero counts when no notes exist", () => {
1357
1475
  mockExecuteAppleScript
1358
1476
  .mockReturnValueOnce({ success: true, output: "iCloud" })
1359
- .mockReturnValueOnce({ success: true, output: "id1\tNotes" })
1360
- .mockReturnValueOnce({ success: true, output: "" })
1361
- .mockReturnValueOnce({ success: true, output: "" });
1477
+ .mockReturnValueOnce({ success: true, output: ["Notes", "0"].join(F) + R })
1478
+ .mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
1362
1479
  const stats = manager.getNotesStats();
1363
1480
  expect(stats.totalNotes).toBe(0);
1364
1481
  expect(stats.recentlyModified.last24h).toBe(0);
@@ -1368,17 +1485,13 @@ describe("AppleNotesManager", () => {
1368
1485
  it("handles multiple accounts", () => {
1369
1486
  mockExecuteAppleScript
1370
1487
  // listAccounts
1371
- .mockReturnValueOnce({ success: true, output: "iCloud, Gmail" })
1372
- // listFolders for iCloud
1373
- .mockReturnValueOnce({ success: true, output: "id1\tNotes" })
1374
- // listNotes for iCloud/Notes
1375
- .mockReturnValueOnce({ success: true, output: "Note 1" })
1376
- // listFolders for Gmail
1377
- .mockReturnValueOnce({ success: true, output: "id2\tNotes" })
1378
- // listNotes for Gmail/Notes
1379
- .mockReturnValueOnce({ success: true, output: "Email Note" })
1488
+ .mockReturnValueOnce({ success: true, output: ["iCloud", "Gmail"].join(R) })
1489
+ // iCloud folder counts
1490
+ .mockReturnValueOnce({ success: true, output: ["Notes", "1"].join(F) + R })
1491
+ // Gmail folder counts
1492
+ .mockReturnValueOnce({ success: true, output: ["Notes", "1"].join(F) + R })
1380
1493
  // getRecentlyModifiedCounts
1381
- .mockReturnValueOnce({ success: true, output: "" });
1494
+ .mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
1382
1495
  const stats = manager.getNotesStats();
1383
1496
  expect(stats.totalNotes).toBe(2);
1384
1497
  expect(stats.accounts).toHaveLength(2);
@@ -1393,7 +1506,10 @@ describe("AppleNotesManager", () => {
1393
1506
  it("returns attachments for a note", () => {
1394
1507
  mockExecuteAppleScript.mockReturnValueOnce({
1395
1508
  success: true,
1396
- output: "x-coredata://ABC/ICAttachment/p1|||photo.jpg|||public.jpegITEMx-coredata://ABC/ICAttachment/p2|||document.pdf|||com.adobe.pdfITEM",
1509
+ output: [
1510
+ ["x-coredata://ABC/ICAttachment/p1", "photo.jpg", "public.jpeg"].join(F),
1511
+ ["x-coredata://ABC/ICAttachment/p2", "document.pdf", "com.adobe.pdf"].join(F),
1512
+ ].join(R),
1397
1513
  });
1398
1514
  const attachments = manager.listAttachmentsById("x-coredata://ABC/ICNote/p123");
1399
1515
  expect(attachments).toHaveLength(2);
@@ -1432,7 +1548,7 @@ describe("AppleNotesManager", () => {
1432
1548
  it("returns attachments for a note by title", () => {
1433
1549
  mockExecuteAppleScript.mockReturnValueOnce({
1434
1550
  success: true,
1435
- output: "attach-id|||image.png|||public.pngITEM",
1551
+ output: ["attach-id", "image.png", "public.png"].join(F),
1436
1552
  });
1437
1553
  const attachments = manager.listAttachments("My Note");
1438
1554
  expect(attachments).toHaveLength(1);
@@ -1463,7 +1579,14 @@ describe("AppleNotesManager", () => {
1463
1579
  // ---------------------------------------------------------------------------
1464
1580
  describe("batchDeleteNotes", () => {
1465
1581
  // Helper to create getNoteById mock output (matches AppleScript format)
1466
- const noteByIdOutput = (title, passwordProtected = false) => `${title}, x-coredata://ABC/ICNote/p1, date Sunday, January 1, 2025 at 1:00:00 PM, date Sunday, January 1, 2025 at 1:00:00 PM, false, ${passwordProtected}`;
1582
+ const noteByIdOutput = (title, passwordProtected = false) => [
1583
+ title,
1584
+ "x-coredata://ABC/ICNote/p1",
1585
+ "Sunday, January 1, 2025 at 1:00:00 PM",
1586
+ "Sunday, January 1, 2025 at 1:00:00 PM",
1587
+ "false",
1588
+ String(passwordProtected),
1589
+ ].join(F);
1467
1590
  it("deletes multiple notes successfully", () => {
1468
1591
  // For each note: getNoteById (which isNotePasswordProtectedById also calls), deleteNoteById
1469
1592
  mockExecuteAppleScript
@@ -1548,7 +1671,14 @@ describe("AppleNotesManager", () => {
1548
1671
  });
1549
1672
  describe("batchMoveNotes", () => {
1550
1673
  // Helper to create getNoteById mock output (matches AppleScript format)
1551
- const noteByIdOutput = (title, passwordProtected = false) => `${title}, x-coredata://ABC/ICNote/p1, date Sunday, January 1, 2025 at 1:00:00 PM, date Sunday, January 1, 2025 at 1:00:00 PM, false, ${passwordProtected}`;
1674
+ const noteByIdOutput = (title, passwordProtected = false) => [
1675
+ title,
1676
+ "x-coredata://ABC/ICNote/p1",
1677
+ "Sunday, January 1, 2025 at 1:00:00 PM",
1678
+ "Sunday, January 1, 2025 at 1:00:00 PM",
1679
+ "false",
1680
+ String(passwordProtected),
1681
+ ].join(F);
1552
1682
  it("moves multiple notes successfully", () => {
1553
1683
  // For each note: getNoteById, getNoteById (password check), getNoteContentById, create, delete
1554
1684
  mockExecuteAppleScript
@@ -1619,7 +1749,14 @@ describe("AppleNotesManager", () => {
1619
1749
  // ---------------------------------------------------------------------------
1620
1750
  describe("exportNotesAsJson", () => {
1621
1751
  // Note details output helper - format: title, id, date, date, shared, passwordProtected
1622
- const noteDetailsOutput = (title, passwordProtected = false) => `${title}, x-coredata://ABC/ICNote/p1, date Sunday, January 1, 2025 at 1:00:00 PM, date Sunday, January 1, 2025 at 1:00:00 PM, false, ${passwordProtected}`;
1752
+ const noteDetailsOutput = (title, passwordProtected = false) => [
1753
+ title,
1754
+ "x-coredata://ABC/ICNote/p1",
1755
+ "Sunday, January 1, 2025 at 1:00:00 PM",
1756
+ "Sunday, January 1, 2025 at 1:00:00 PM",
1757
+ "false",
1758
+ String(passwordProtected),
1759
+ ].join(F);
1623
1760
  it("exports notes with metadata and content", () => {
1624
1761
  mockExecuteAppleScript
1625
1762
  // listAccounts