@noteplanco/noteplan-mcp 1.1.23 → 1.1.25

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.
Files changed (165) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +6 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/noteplan/attachments-paths.d.ts +13 -0
  5. package/dist/noteplan/attachments-paths.d.ts.map +1 -0
  6. package/dist/noteplan/attachments-paths.js +27 -0
  7. package/dist/noteplan/attachments-paths.js.map +1 -0
  8. package/dist/noteplan/embeddings.js +1 -1
  9. package/dist/noteplan/embeddings.js.map +1 -1
  10. package/dist/noteplan/file-reader.d.ts +37 -46
  11. package/dist/noteplan/file-reader.d.ts.map +1 -1
  12. package/dist/noteplan/file-reader.js +200 -202
  13. package/dist/noteplan/file-reader.js.map +1 -1
  14. package/dist/noteplan/file-reader.test.d.ts +2 -0
  15. package/dist/noteplan/file-reader.test.d.ts.map +1 -0
  16. package/dist/noteplan/file-reader.test.js +67 -0
  17. package/dist/noteplan/file-reader.test.js.map +1 -0
  18. package/dist/noteplan/file-writer.d.ts +35 -31
  19. package/dist/noteplan/file-writer.d.ts.map +1 -1
  20. package/dist/noteplan/file-writer.js +280 -164
  21. package/dist/noteplan/file-writer.js.map +1 -1
  22. package/dist/noteplan/file-writer.test.js +704 -191
  23. package/dist/noteplan/file-writer.test.js.map +1 -1
  24. package/dist/noteplan/filter-store.d.ts +5 -5
  25. package/dist/noteplan/filter-store.d.ts.map +1 -1
  26. package/dist/noteplan/filter-store.js +94 -79
  27. package/dist/noteplan/filter-store.js.map +1 -1
  28. package/dist/noteplan/ripgrep-search.d.ts +25 -2
  29. package/dist/noteplan/ripgrep-search.d.ts.map +1 -1
  30. package/dist/noteplan/ripgrep-search.js +75 -2
  31. package/dist/noteplan/ripgrep-search.js.map +1 -1
  32. package/dist/noteplan/space-row-utils.d.ts +20 -0
  33. package/dist/noteplan/space-row-utils.d.ts.map +1 -0
  34. package/dist/noteplan/space-row-utils.js +78 -0
  35. package/dist/noteplan/space-row-utils.js.map +1 -0
  36. package/dist/noteplan/space-row-utils.test.d.ts +2 -0
  37. package/dist/noteplan/space-row-utils.test.d.ts.map +1 -0
  38. package/dist/noteplan/space-row-utils.test.js +123 -0
  39. package/dist/noteplan/space-row-utils.test.js.map +1 -0
  40. package/dist/noteplan/sqlite-reader.d.ts +12 -27
  41. package/dist/noteplan/sqlite-reader.d.ts.map +1 -1
  42. package/dist/noteplan/sqlite-reader.js +315 -221
  43. package/dist/noteplan/sqlite-reader.js.map +1 -1
  44. package/dist/noteplan/sqlite-writer.d.ts +1 -1
  45. package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
  46. package/dist/noteplan/sqlite-writer.js +2 -2
  47. package/dist/noteplan/sqlite-writer.js.map +1 -1
  48. package/dist/noteplan/unified-store.d.ts +41 -30
  49. package/dist/noteplan/unified-store.d.ts.map +1 -1
  50. package/dist/noteplan/unified-store.js +257 -159
  51. package/dist/noteplan/unified-store.js.map +1 -1
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +142 -61
  54. package/dist/server.js.map +1 -1
  55. package/dist/tools/attachments.d.ts +9 -9
  56. package/dist/tools/attachments.d.ts.map +1 -1
  57. package/dist/tools/attachments.js +74 -83
  58. package/dist/tools/attachments.js.map +1 -1
  59. package/dist/tools/attachments.test.js +170 -129
  60. package/dist/tools/attachments.test.js.map +1 -1
  61. package/dist/tools/calendar.d.ts +16 -13
  62. package/dist/tools/calendar.d.ts.map +1 -1
  63. package/dist/tools/calendar.js +17 -16
  64. package/dist/tools/calendar.js.map +1 -1
  65. package/dist/tools/embeddings.d.ts +6 -6
  66. package/dist/tools/embeddings.d.ts.map +1 -1
  67. package/dist/tools/embeddings.js +6 -6
  68. package/dist/tools/embeddings.js.map +1 -1
  69. package/dist/tools/events.d.ts +7 -3
  70. package/dist/tools/events.d.ts.map +1 -1
  71. package/dist/tools/events.js +51 -16
  72. package/dist/tools/events.js.map +1 -1
  73. package/dist/tools/filters.d.ts +28 -33
  74. package/dist/tools/filters.d.ts.map +1 -1
  75. package/dist/tools/filters.js +42 -105
  76. package/dist/tools/filters.js.map +1 -1
  77. package/dist/tools/notes.d.ts +80 -218
  78. package/dist/tools/notes.d.ts.map +1 -1
  79. package/dist/tools/notes.js +180 -177
  80. package/dist/tools/notes.js.map +1 -1
  81. package/dist/tools/notes.test.js +242 -21
  82. package/dist/tools/notes.test.js.map +1 -1
  83. package/dist/tools/search.d.ts +4 -3
  84. package/dist/tools/search.d.ts.map +1 -1
  85. package/dist/tools/search.js +9 -5
  86. package/dist/tools/search.js.map +1 -1
  87. package/dist/tools/search.test.d.ts +2 -0
  88. package/dist/tools/search.test.d.ts.map +1 -0
  89. package/dist/tools/search.test.js +37 -0
  90. package/dist/tools/search.test.js.map +1 -0
  91. package/dist/tools/spaces.d.ts +20 -20
  92. package/dist/tools/spaces.d.ts.map +1 -1
  93. package/dist/tools/spaces.js +28 -28
  94. package/dist/tools/spaces.js.map +1 -1
  95. package/dist/tools/tasks.d.ts +22 -22
  96. package/dist/tools/tasks.d.ts.map +1 -1
  97. package/dist/tools/tasks.js +22 -22
  98. package/dist/tools/tasks.js.map +1 -1
  99. package/dist/tools/templates.d.ts +7 -7
  100. package/dist/tools/templates.d.ts.map +1 -1
  101. package/dist/tools/templates.js +4 -4
  102. package/dist/tools/templates.js.map +1 -1
  103. package/dist/tools/themes.d.ts.map +1 -1
  104. package/dist/tools/themes.js +26 -35
  105. package/dist/tools/themes.js.map +1 -1
  106. package/dist/transport/bridge-availability.d.ts +5 -0
  107. package/dist/transport/bridge-availability.d.ts.map +1 -0
  108. package/dist/transport/bridge-availability.js +92 -0
  109. package/dist/transport/bridge-availability.js.map +1 -0
  110. package/dist/transport/bridge-cascade.d.ts +18 -0
  111. package/dist/transport/bridge-cascade.d.ts.map +1 -0
  112. package/dist/transport/bridge-cascade.js +78 -0
  113. package/dist/transport/bridge-cascade.js.map +1 -0
  114. package/dist/transport/bridge-cascade.test.d.ts +2 -0
  115. package/dist/transport/bridge-cascade.test.d.ts.map +1 -0
  116. package/dist/transport/bridge-cascade.test.js +160 -0
  117. package/dist/transport/bridge-cascade.test.js.map +1 -0
  118. package/dist/transport/bridge-client.d.ts +197 -0
  119. package/dist/transport/bridge-client.d.ts.map +1 -0
  120. package/dist/transport/bridge-client.js +288 -0
  121. package/dist/transport/bridge-client.js.map +1 -0
  122. package/dist/transport/bridge-client.test.d.ts +2 -0
  123. package/dist/transport/bridge-client.test.d.ts.map +1 -0
  124. package/dist/transport/bridge-client.test.js +384 -0
  125. package/dist/transport/bridge-client.test.js.map +1 -0
  126. package/dist/transport/bridge-context.d.ts +10 -0
  127. package/dist/transport/bridge-context.d.ts.map +1 -0
  128. package/dist/transport/bridge-context.js +18 -0
  129. package/dist/transport/bridge-context.js.map +1 -0
  130. package/dist/transport/bridge-fs.d.ts +25 -0
  131. package/dist/transport/bridge-fs.d.ts.map +1 -0
  132. package/dist/transport/bridge-fs.js +129 -0
  133. package/dist/transport/bridge-fs.js.map +1 -0
  134. package/dist/utils/date-utils.d.ts +24 -0
  135. package/dist/utils/date-utils.d.ts.map +1 -1
  136. package/dist/utils/date-utils.js +55 -0
  137. package/dist/utils/date-utils.js.map +1 -1
  138. package/dist/utils/date-utils.test.d.ts +2 -0
  139. package/dist/utils/date-utils.test.d.ts.map +1 -0
  140. package/dist/utils/date-utils.test.js +109 -0
  141. package/dist/utils/date-utils.test.js.map +1 -0
  142. package/dist/utils/folder-access.d.ts +23 -0
  143. package/dist/utils/folder-access.d.ts.map +1 -0
  144. package/dist/utils/folder-access.js +131 -0
  145. package/dist/utils/folder-access.js.map +1 -0
  146. package/dist/utils/folder-access.test.d.ts +2 -0
  147. package/dist/utils/folder-access.test.d.ts.map +1 -0
  148. package/dist/utils/folder-access.test.js +182 -0
  149. package/dist/utils/folder-access.test.js.map +1 -0
  150. package/dist/utils/folder-matcher.d.ts.map +1 -1
  151. package/dist/utils/folder-matcher.js +16 -0
  152. package/dist/utils/folder-matcher.js.map +1 -1
  153. package/dist/utils/folder-matcher.test.js +42 -0
  154. package/dist/utils/folder-matcher.test.js.map +1 -1
  155. package/dist/utils/server-config.d.ts +10 -2
  156. package/dist/utils/server-config.d.ts.map +1 -1
  157. package/dist/utils/server-config.js +16 -2
  158. package/dist/utils/server-config.js.map +1 -1
  159. package/dist/utils/version.d.ts +2 -0
  160. package/dist/utils/version.d.ts.map +1 -1
  161. package/dist/utils/version.js +5 -1
  162. package/dist/utils/version.js.map +1 -1
  163. package/package.json +4 -3
  164. package/scripts/calendar-helper +0 -0
  165. package/scripts/reminders-helper +0 -0
@@ -1,17 +1,62 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import * as path from 'path';
3
3
  // ── Mocks ──
4
- vi.mock('fs', () => ({
5
- existsSync: vi.fn(),
6
- mkdirSync: vi.fn(),
7
- writeFileSync: vi.fn(),
8
- readFileSync: vi.fn(),
9
- readdirSync: vi.fn(),
10
- statSync: vi.fn(),
11
- renameSync: vi.fn(),
12
- copyFileSync: vi.fn(),
13
- unlinkSync: vi.fn(),
14
- rmdirSync: vi.fn(),
4
+ vi.mock('fs', () => {
5
+ const sync = {
6
+ existsSync: vi.fn(),
7
+ mkdirSync: vi.fn(),
8
+ writeFileSync: vi.fn(),
9
+ readFileSync: vi.fn(),
10
+ readdirSync: vi.fn(),
11
+ statSync: vi.fn(),
12
+ renameSync: vi.fn(),
13
+ copyFileSync: vi.fn(),
14
+ unlinkSync: vi.fn(),
15
+ rmdirSync: vi.fn(),
16
+ rmSync: vi.fn(),
17
+ };
18
+ // bridge-fs / attachments use fs.promises after the async cascade. Each
19
+ // promise variant forwards to the sync mock so existing assertions on
20
+ // fs.*Sync still pass.
21
+ const promises = {
22
+ mkdir: vi.fn(async (p, opts) => sync.mkdirSync(p, opts)),
23
+ writeFile: vi.fn(async (p, content, opts) => sync.writeFileSync(p, content, opts)),
24
+ readFile: vi.fn(async (p, opts) => {
25
+ if (!sync.existsSync(p)) {
26
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
27
+ }
28
+ return sync.readFileSync(p, opts);
29
+ }),
30
+ readdir: vi.fn(async (p, opts) => sync.readdirSync(p, opts)),
31
+ stat: vi.fn(async (p) => {
32
+ if (!sync.existsSync(p))
33
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
34
+ const explicit = sync.statSync(p) ?? {};
35
+ // bridge-fs.statPath calls stats.isDirectory(); ensure it exists even
36
+ // when a test only mocked size/mtime.
37
+ return {
38
+ isDirectory: typeof explicit.isDirectory === 'function' ? explicit.isDirectory : () => false,
39
+ isFile: typeof explicit.isFile === 'function' ? explicit.isFile : () => true,
40
+ size: explicit.size ?? 0,
41
+ mtime: explicit.mtime ?? new Date(0),
42
+ birthtime: explicit.birthtime ?? new Date(0),
43
+ };
44
+ }),
45
+ rename: vi.fn(async (a, b) => sync.renameSync(a, b)),
46
+ copyFile: vi.fn(async (a, b) => sync.copyFileSync(a, b)),
47
+ unlink: vi.fn(async (p) => sync.unlinkSync(p)),
48
+ rm: vi.fn(async (p) => {
49
+ if (sync.rmSync)
50
+ sync.rmSync(p, { recursive: true });
51
+ else
52
+ sync.unlinkSync(p);
53
+ }),
54
+ };
55
+ return { ...sync, promises };
56
+ });
57
+ vi.mock('../transport/bridge-availability.js', () => ({
58
+ getBridgeClient: vi.fn(async () => null),
59
+ invalidateBridgeClient: vi.fn(),
15
60
  }));
16
61
  vi.mock('../noteplan/unified-store.js', () => ({
17
62
  getNote: vi.fn(),
@@ -44,7 +89,7 @@ beforeEach(() => {
44
89
  });
45
90
  // ── Schema ──
46
91
  describe('attachmentsSchema', () => {
47
- it('accepts valid input with all fields', () => {
92
+ it('accepts valid input with all fields', async () => {
48
93
  const result = attachmentsSchema.safeParse({
49
94
  action: 'add',
50
95
  id: '1',
@@ -53,7 +98,7 @@ describe('attachmentsSchema', () => {
53
98
  });
54
99
  expect(result.success).toBe(true);
55
100
  });
56
- it('rejects invalid action', () => {
101
+ it('rejects invalid action', async () => {
57
102
  const result = attachmentsSchema.safeParse({ action: 'delete' });
58
103
  expect(result.success).toBe(false);
59
104
  });
@@ -68,49 +113,49 @@ describe('addAttachment', () => {
68
113
  insertLink: false,
69
114
  includeData: false,
70
115
  };
71
- it('returns error when data is missing', () => {
72
- const result = addAttachment({ ...baseParams, data: undefined });
116
+ it('returns error when data is missing', async () => {
117
+ const result = await addAttachment({ ...baseParams, data: undefined });
73
118
  expect(result.success).toBe(false);
74
119
  expect(result.error).toMatch(/data.*required/i);
75
120
  });
76
- it('returns error when attachmentFilename is missing', () => {
77
- const result = addAttachment({ ...baseParams, attachmentFilename: undefined });
121
+ it('returns error when attachmentFilename is missing', async () => {
122
+ const result = await addAttachment({ ...baseParams, attachmentFilename: undefined });
78
123
  expect(result.success).toBe(false);
79
124
  expect(result.error).toMatch(/attachmentFilename.*required/i);
80
125
  });
81
- it('returns error when note not found', () => {
82
- vi.mocked(getNote).mockReturnValue(null);
83
- const result = addAttachment(baseParams);
126
+ it('returns error when note not found', async () => {
127
+ vi.mocked(getNote).mockResolvedValue(null);
128
+ const result = await addAttachment(baseParams);
84
129
  expect(result.success).toBe(false);
85
130
  expect(result.error).toMatch(/not found/i);
86
131
  });
87
- it('returns error for space notes', () => {
88
- vi.mocked(getNote).mockReturnValue(mockNote({ source: 'space' }));
89
- const result = addAttachment(baseParams);
132
+ it('returns error for space notes', async () => {
133
+ vi.mocked(getNote).mockResolvedValue(mockNote({ source: 'space' }));
134
+ const result = await addAttachment(baseParams);
90
135
  expect(result.success).toBe(false);
91
136
  expect(result.error).toMatch(/space/i);
92
137
  });
93
- it('returns error when filename sanitizes to empty', () => {
94
- vi.mocked(getNote).mockReturnValue(mockNote());
95
- const result = addAttachment({ ...baseParams, attachmentFilename: '()[]!' });
138
+ it('returns error when filename sanitizes to empty', async () => {
139
+ vi.mocked(getNote).mockResolvedValue(mockNote());
140
+ const result = await addAttachment({ ...baseParams, attachmentFilename: '()[]!' });
96
141
  expect(result.success).toBe(false);
97
142
  expect(result.error).toMatch(/invalid.*filename/i);
98
143
  });
99
- it('returns error for empty base64 (zero-length buffer)', () => {
100
- vi.mocked(getNote).mockReturnValue(mockNote());
144
+ it('returns error for empty base64 (zero-length buffer)', async () => {
145
+ vi.mocked(getNote).mockResolvedValue(mockNote());
101
146
  vi.mocked(fs.existsSync).mockReturnValue(true);
102
147
  // Use padding-only base64 that decodes to zero bytes
103
148
  // Note: An empty string '' is falsy and caught by the !data check first,
104
149
  // so we use a whitespace-only string that passes truthiness but decodes to empty
105
- const result = addAttachment({ ...baseParams, data: ' ' });
150
+ const result = await addAttachment({ ...baseParams, data: ' ' });
106
151
  expect(result.success).toBe(false);
107
152
  expect(result.error).toMatch(/empty/i);
108
153
  });
109
- it('successfully writes attachment and returns correct markdownLink', () => {
154
+ it('successfully writes attachment and returns correct markdownLink', async () => {
110
155
  const note = mockNote();
111
- vi.mocked(getNote).mockReturnValue(note);
156
+ vi.mocked(getNote).mockResolvedValue(note);
112
157
  vi.mocked(fs.existsSync).mockReturnValue(true);
113
- const result = addAttachment(baseParams);
158
+ const result = await addAttachment(baseParams);
114
159
  expect(result.success).toBe(true);
115
160
  expect(result).toHaveProperty('markdownLink');
116
161
  expect(result.markdownLink).toBe('![image](Test%20Note_attachments/photo.png)');
@@ -119,56 +164,52 @@ describe('addAttachment', () => {
119
164
  expect(result.noteFilename).toBe('Notes/Test Note.md');
120
165
  expect(fs.writeFileSync).toHaveBeenCalled();
121
166
  });
122
- it('creates the _attachments folder if it does not exist', () => {
167
+ it('ensures the _attachments parent folder exists when writing', async () => {
123
168
  const note = mockNote();
124
- vi.mocked(getNote).mockReturnValue(note);
169
+ vi.mocked(getNote).mockResolvedValue(note);
125
170
  vi.mocked(fs.existsSync).mockReturnValue(false);
126
- addAttachment(baseParams);
171
+ await addAttachment(baseParams);
172
+ // bridge-fs.writeFileBinary always mkdirs the parent (idempotent with
173
+ // `recursive: true`) before the write, so the explicit existsSync gate
174
+ // we used to have is no longer needed.
127
175
  expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(NP_PATH, 'Notes', 'Test Note_attachments'), { recursive: true });
128
176
  });
129
- it('does NOT create folder when it already exists', () => {
177
+ it('generates correct image markdown link for png/jpg', async () => {
130
178
  const note = mockNote();
131
- vi.mocked(getNote).mockReturnValue(note);
132
- vi.mocked(fs.existsSync).mockReturnValue(true);
133
- addAttachment(baseParams);
134
- expect(fs.mkdirSync).not.toHaveBeenCalled();
135
- });
136
- it('generates correct image markdown link for png/jpg', () => {
137
- const note = mockNote();
138
- vi.mocked(getNote).mockReturnValue(note);
179
+ vi.mocked(getNote).mockResolvedValue(note);
139
180
  vi.mocked(fs.existsSync).mockReturnValue(true);
140
181
  for (const ext of ['png', 'jpg', 'jpeg', 'gif', 'webp', 'heic']) {
141
- const result = addAttachment({ ...baseParams, attachmentFilename: `photo.${ext}` });
182
+ const result = await addAttachment({ ...baseParams, attachmentFilename: `photo.${ext}` });
142
183
  expect(result.markdownLink).toMatch(/^!\[image\]/);
143
184
  expect(result.isImage).toBe(true);
144
185
  }
145
186
  });
146
- it('generates correct file markdown link for pdf/txt', () => {
187
+ it('generates correct file markdown link for pdf/txt', async () => {
147
188
  const note = mockNote();
148
- vi.mocked(getNote).mockReturnValue(note);
189
+ vi.mocked(getNote).mockResolvedValue(note);
149
190
  vi.mocked(fs.existsSync).mockReturnValue(true);
150
191
  for (const ext of ['pdf', 'txt', 'csv', 'mp3']) {
151
- const result = addAttachment({ ...baseParams, attachmentFilename: `doc.${ext}` });
192
+ const result = await addAttachment({ ...baseParams, attachmentFilename: `doc.${ext}` });
152
193
  expect(result.markdownLink).toMatch(/^!\[file\]/);
153
194
  expect(result.isImage).toBe(false);
154
195
  }
155
196
  });
156
- it('insertLink=false (default) does NOT modify note content', () => {
197
+ it('insertLink=false (default) does NOT modify note content', async () => {
157
198
  const note = mockNote();
158
- vi.mocked(getNote).mockReturnValue(note);
199
+ vi.mocked(getNote).mockResolvedValue(note);
159
200
  vi.mocked(fs.existsSync).mockReturnValue(true);
160
- const result = addAttachment({ ...baseParams, insertLink: false });
201
+ const result = await addAttachment({ ...baseParams, insertLink: false });
161
202
  expect(result.success).toBe(true);
162
203
  expect(result.linkInserted).toBe(false);
163
204
  // writeFileSync should be called once (for the attachment), not for the note
164
205
  expect(vi.mocked(fs.writeFileSync).mock.calls.length).toBe(1);
165
206
  });
166
- it('insertLink=true appends link to note', () => {
207
+ it('insertLink=true appends link to note', async () => {
167
208
  const note = mockNote({ content: '# Test\n' });
168
- vi.mocked(getNote).mockReturnValue(note);
209
+ vi.mocked(getNote).mockResolvedValue(note);
169
210
  vi.mocked(fs.existsSync).mockReturnValue(true);
170
211
  vi.mocked(fs.readFileSync).mockReturnValue('# Test\n');
171
- const result = addAttachment({ ...baseParams, insertLink: true });
212
+ const result = await addAttachment({ ...baseParams, insertLink: true });
172
213
  expect(result.success).toBe(true);
173
214
  expect(result.linkInserted).toBe(true);
174
215
  // writeFileSync should be called twice: once for attachment, once for note
@@ -177,11 +218,11 @@ describe('addAttachment', () => {
177
218
  const writtenContent = noteWriteCall[1];
178
219
  expect(writtenContent).toContain('![image](Test%20Note_attachments/photo.png)');
179
220
  });
180
- it('cleans markdown-conflicting characters from filename', () => {
221
+ it('cleans markdown-conflicting characters from filename', async () => {
181
222
  const note = mockNote();
182
- vi.mocked(getNote).mockReturnValue(note);
223
+ vi.mocked(getNote).mockResolvedValue(note);
183
224
  vi.mocked(fs.existsSync).mockReturnValue(true);
184
- const result = addAttachment({
225
+ const result = await addAttachment({
185
226
  ...baseParams,
186
227
  attachmentFilename: 'photo (1) [copy]!.png',
187
228
  });
@@ -190,11 +231,11 @@ describe('addAttachment', () => {
190
231
  expect(result.markdownLink).toBe('![image](Test%20Note_attachments/photo%201%20copy.png)');
191
232
  expect(result.attachmentPath).toBe('Test Note_attachments/photo 1 copy.png');
192
233
  });
193
- it('percent-encodes special characters in the markdown link path', () => {
234
+ it('percent-encodes special characters in the markdown link path', async () => {
194
235
  const note = mockNote({ filename: 'Notes/My (Special) Note.md' });
195
- vi.mocked(getNote).mockReturnValue(note);
236
+ vi.mocked(getNote).mockResolvedValue(note);
196
237
  vi.mocked(fs.existsSync).mockReturnValue(true);
197
- const result = addAttachment({ ...baseParams, attachmentFilename: 'file name.png' });
238
+ const result = await addAttachment({ ...baseParams, attachmentFilename: 'file name.png' });
198
239
  expect(result.success).toBe(true);
199
240
  // Note name has special chars that get encoded in the path
200
241
  // "My (Special) Note_attachments/file name.png"
@@ -210,34 +251,34 @@ describe('listAttachments', () => {
210
251
  insertLink: false,
211
252
  includeData: false,
212
253
  };
213
- it('returns error when note not found', () => {
214
- vi.mocked(getNote).mockReturnValue(null);
215
- const result = listAttachments(baseParams);
254
+ it('returns error when note not found', async () => {
255
+ vi.mocked(getNote).mockResolvedValue(null);
256
+ const result = await listAttachments(baseParams);
216
257
  expect(result.success).toBe(false);
217
258
  expect(result.error).toMatch(/not found/i);
218
259
  });
219
- it('returns empty list when _attachments folder does not exist', () => {
220
- vi.mocked(getNote).mockReturnValue(mockNote());
260
+ it('returns empty list when _attachments folder does not exist', async () => {
261
+ vi.mocked(getNote).mockResolvedValue(mockNote());
221
262
  vi.mocked(fs.existsSync).mockReturnValue(false);
222
- const result = listAttachments(baseParams);
263
+ const result = await listAttachments(baseParams);
223
264
  expect(result.success).toBe(true);
224
265
  expect(result.count).toBe(0);
225
266
  expect(result.attachments).toEqual([]);
226
267
  });
227
- it('lists attachments with correct metadata', () => {
268
+ it('lists attachments with correct metadata', async () => {
228
269
  const note = mockNote();
229
- vi.mocked(getNote).mockReturnValue(note);
270
+ vi.mocked(getNote).mockResolvedValue(note);
230
271
  vi.mocked(fs.existsSync).mockReturnValue(true);
231
272
  const mockDate = new Date('2024-01-15T10:30:00Z');
232
273
  vi.mocked(fs.readdirSync).mockReturnValue([
233
- { name: 'photo.png', isFile: () => true },
234
- { name: 'doc.pdf', isFile: () => true },
274
+ { name: 'photo.png', isFile: () => true, isDirectory: () => false },
275
+ { name: 'doc.pdf', isFile: () => true, isDirectory: () => false },
235
276
  ]);
236
277
  vi.mocked(fs.statSync).mockReturnValue({
237
278
  size: 1024,
238
279
  mtime: mockDate,
239
280
  });
240
- const result = listAttachments(baseParams);
281
+ const result = await listAttachments(baseParams);
241
282
  expect(result.success).toBe(true);
242
283
  expect(result.count).toBe(2);
243
284
  expect(result.noteFilename).toBe('Notes/Test Note.md');
@@ -253,37 +294,37 @@ describe('listAttachments', () => {
253
294
  expect(attachments[1].isImage).toBe(true);
254
295
  expect(attachments[1].markdownLink).toBe('![image](Test%20Note_attachments/photo.png)');
255
296
  });
256
- it('skips hidden files (starting with .)', () => {
297
+ it('skips hidden files (starting with .)', async () => {
257
298
  const note = mockNote();
258
- vi.mocked(getNote).mockReturnValue(note);
299
+ vi.mocked(getNote).mockResolvedValue(note);
259
300
  vi.mocked(fs.existsSync).mockReturnValue(true);
260
301
  vi.mocked(fs.readdirSync).mockReturnValue([
261
- { name: '.DS_Store', isFile: () => true },
262
- { name: '.hidden', isFile: () => true },
263
- { name: 'visible.png', isFile: () => true },
302
+ { name: '.DS_Store', isFile: () => true, isDirectory: () => false },
303
+ { name: '.hidden', isFile: () => true, isDirectory: () => false },
304
+ { name: 'visible.png', isFile: () => true, isDirectory: () => false },
264
305
  ]);
265
306
  vi.mocked(fs.statSync).mockReturnValue({
266
307
  size: 100,
267
308
  mtime: new Date(),
268
309
  });
269
- const result = listAttachments(baseParams);
310
+ const result = await listAttachments(baseParams);
270
311
  expect(result.success).toBe(true);
271
312
  expect(result.count).toBe(1);
272
313
  expect(result.attachments[0].filename).toBe('visible.png');
273
314
  });
274
- it('skips directories', () => {
315
+ it('skips directories', async () => {
275
316
  const note = mockNote();
276
- vi.mocked(getNote).mockReturnValue(note);
317
+ vi.mocked(getNote).mockResolvedValue(note);
277
318
  vi.mocked(fs.existsSync).mockReturnValue(true);
278
319
  vi.mocked(fs.readdirSync).mockReturnValue([
279
- { name: 'subfolder', isFile: () => false },
280
- { name: 'photo.png', isFile: () => true },
320
+ { name: 'subfolder', isFile: () => false, isDirectory: () => true },
321
+ { name: 'photo.png', isFile: () => true, isDirectory: () => false },
281
322
  ]);
282
323
  vi.mocked(fs.statSync).mockReturnValue({
283
324
  size: 100,
284
325
  mtime: new Date(),
285
326
  });
286
- const result = listAttachments(baseParams);
327
+ const result = await listAttachments(baseParams);
287
328
  expect(result.success).toBe(true);
288
329
  expect(result.count).toBe(1);
289
330
  });
@@ -297,33 +338,33 @@ describe('getAttachment', () => {
297
338
  insertLink: false,
298
339
  includeData: false,
299
340
  };
300
- it('returns error when attachmentFilename is missing', () => {
301
- const result = getAttachment({ ...baseParams, attachmentFilename: undefined });
341
+ it('returns error when attachmentFilename is missing', async () => {
342
+ const result = await getAttachment({ ...baseParams, attachmentFilename: undefined });
302
343
  expect(result.success).toBe(false);
303
344
  expect(result.error).toMatch(/attachmentFilename.*required/i);
304
345
  });
305
- it('returns error when note not found', () => {
306
- vi.mocked(getNote).mockReturnValue(null);
307
- const result = getAttachment(baseParams);
346
+ it('returns error when note not found', async () => {
347
+ vi.mocked(getNote).mockResolvedValue(null);
348
+ const result = await getAttachment(baseParams);
308
349
  expect(result.success).toBe(false);
309
350
  expect(result.error).toMatch(/not found/i);
310
351
  });
311
- it('returns error when attachment file does not exist', () => {
312
- vi.mocked(getNote).mockReturnValue(mockNote());
352
+ it('returns error when attachment file does not exist', async () => {
353
+ vi.mocked(getNote).mockResolvedValue(mockNote());
313
354
  vi.mocked(fs.existsSync).mockReturnValue(false);
314
- const result = getAttachment(baseParams);
355
+ const result = await getAttachment(baseParams);
315
356
  expect(result.success).toBe(false);
316
357
  expect(result.error).toMatch(/not found/i);
317
358
  });
318
- it('returns metadata without data when includeData is false', () => {
359
+ it('returns metadata without data when includeData is false', async () => {
319
360
  const note = mockNote();
320
- vi.mocked(getNote).mockReturnValue(note);
361
+ vi.mocked(getNote).mockResolvedValue(note);
321
362
  vi.mocked(fs.existsSync).mockReturnValue(true);
322
363
  vi.mocked(fs.statSync).mockReturnValue({
323
364
  size: 2048,
324
365
  mtime: new Date('2024-06-01T12:00:00Z'),
325
366
  });
326
- const result = getAttachment({ ...baseParams, includeData: false });
367
+ const result = await getAttachment({ ...baseParams, includeData: false });
327
368
  expect(result.success).toBe(true);
328
369
  expect(result.filename).toBe('photo.png');
329
370
  expect(result.isImage).toBe(true);
@@ -332,9 +373,9 @@ describe('getAttachment', () => {
332
373
  expect(result.markdownLink).toBe('![image](Test%20Note_attachments/photo.png)');
333
374
  expect(result).not.toHaveProperty('data');
334
375
  });
335
- it('returns base64 data when includeData is true', () => {
376
+ it('returns base64 data when includeData is true', async () => {
336
377
  const note = mockNote();
337
- vi.mocked(getNote).mockReturnValue(note);
378
+ vi.mocked(getNote).mockResolvedValue(note);
338
379
  vi.mocked(fs.existsSync).mockReturnValue(true);
339
380
  vi.mocked(fs.statSync).mockReturnValue({
340
381
  size: 100,
@@ -342,13 +383,13 @@ describe('getAttachment', () => {
342
383
  });
343
384
  const fileBuffer = Buffer.from('file content');
344
385
  vi.mocked(fs.readFileSync).mockReturnValue(fileBuffer);
345
- const result = getAttachment({ ...baseParams, includeData: true });
386
+ const result = await getAttachment({ ...baseParams, includeData: true });
346
387
  expect(result.success).toBe(true);
347
388
  expect(result.data).toBe(fileBuffer.toString('base64'));
348
389
  });
349
- it('respects maxDataSize - returns dataTruncated=true for large images', () => {
390
+ it('respects maxDataSize - returns dataTruncated=true for large images', async () => {
350
391
  const note = mockNote();
351
- vi.mocked(getNote).mockReturnValue(note);
392
+ vi.mocked(getNote).mockResolvedValue(note);
352
393
  vi.mocked(fs.existsSync).mockReturnValue(true);
353
394
  vi.mocked(fs.statSync).mockReturnValue({
354
395
  size: 5000,
@@ -357,7 +398,7 @@ describe('getAttachment', () => {
357
398
  // Create a buffer larger than maxDataSize
358
399
  const largeBuffer = Buffer.alloc(5000, 'x');
359
400
  vi.mocked(fs.readFileSync).mockReturnValue(largeBuffer);
360
- const result = getAttachment({
401
+ const result = await getAttachment({
361
402
  ...baseParams,
362
403
  includeData: true,
363
404
  maxDataSize: 1000,
@@ -368,9 +409,9 @@ describe('getAttachment', () => {
368
409
  expect(result.originalSize).toBe(5000);
369
410
  expect(result.hint).toBeDefined();
370
411
  });
371
- it('does NOT truncate non-image files even if over maxDataSize', () => {
412
+ it('does NOT truncate non-image files even if over maxDataSize', async () => {
372
413
  const note = mockNote();
373
- vi.mocked(getNote).mockReturnValue(note);
414
+ vi.mocked(getNote).mockResolvedValue(note);
374
415
  vi.mocked(fs.existsSync).mockReturnValue(true);
375
416
  vi.mocked(fs.statSync).mockReturnValue({
376
417
  size: 5000,
@@ -378,7 +419,7 @@ describe('getAttachment', () => {
378
419
  });
379
420
  const largeBuffer = Buffer.alloc(5000, 'x');
380
421
  vi.mocked(fs.readFileSync).mockReturnValue(largeBuffer);
381
- const result = getAttachment({
422
+ const result = await getAttachment({
382
423
  ...baseParams,
383
424
  attachmentFilename: 'doc.pdf',
384
425
  includeData: true,
@@ -388,9 +429,9 @@ describe('getAttachment', () => {
388
429
  expect(result.data).toBe(largeBuffer.toString('base64'));
389
430
  expect(result.dataTruncated).toBeUndefined();
390
431
  });
391
- it('returns correct MIME type mapping', () => {
432
+ it('returns correct MIME type mapping', async () => {
392
433
  const note = mockNote();
393
- vi.mocked(getNote).mockReturnValue(note);
434
+ vi.mocked(getNote).mockResolvedValue(note);
394
435
  vi.mocked(fs.existsSync).mockReturnValue(true);
395
436
  vi.mocked(fs.statSync).mockReturnValue({
396
437
  size: 100,
@@ -412,7 +453,7 @@ describe('getAttachment', () => {
412
453
  ['unknown.xyz', 'application/octet-stream'],
413
454
  ];
414
455
  for (const [filename, expectedMime] of mimeTests) {
415
- const result = getAttachment({
456
+ const result = await getAttachment({
416
457
  ...baseParams,
417
458
  attachmentFilename: filename,
418
459
  includeData: false,
@@ -443,57 +484,57 @@ describe('moveAttachment', () => {
443
484
  insertLink: false,
444
485
  includeData: false,
445
486
  };
446
- it('returns error when attachmentFilename is missing', () => {
447
- const result = moveAttachment({ ...baseParams, attachmentFilename: undefined });
487
+ it('returns error when attachmentFilename is missing', async () => {
488
+ const result = await moveAttachment({ ...baseParams, attachmentFilename: undefined });
448
489
  expect(result.success).toBe(false);
449
490
  expect(result.error).toMatch(/attachmentFilename.*required/i);
450
491
  });
451
- it('returns error when source note not found', () => {
452
- vi.mocked(getNote).mockReturnValue(null);
453
- const result = moveAttachment(baseParams);
492
+ it('returns error when source note not found', async () => {
493
+ vi.mocked(getNote).mockResolvedValue(null);
494
+ const result = await moveAttachment(baseParams);
454
495
  expect(result.success).toBe(false);
455
496
  expect(result.error).toMatch(/not found/i);
456
497
  });
457
- it('returns error when destination note not found', () => {
498
+ it('returns error when destination note not found', async () => {
458
499
  // First call for source returns note, second call for destination returns null
459
500
  vi.mocked(getNote)
460
- .mockReturnValueOnce(sourceNote)
461
- .mockReturnValueOnce(null);
462
- const result = moveAttachment(baseParams);
501
+ .mockResolvedValueOnce(sourceNote)
502
+ .mockResolvedValueOnce(null);
503
+ const result = await moveAttachment(baseParams);
463
504
  expect(result.success).toBe(false);
464
505
  expect(result.error).toMatch(/not found/i);
465
506
  });
466
- it('returns error when attachment file does not exist at source', () => {
507
+ it('returns error when attachment file does not exist at source', async () => {
467
508
  vi.mocked(getNote)
468
509
  .mockReturnValueOnce(sourceNote)
469
510
  .mockReturnValueOnce(destNote);
470
511
  vi.mocked(fs.existsSync).mockReturnValue(false);
471
- const result = moveAttachment(baseParams);
512
+ const result = await moveAttachment(baseParams);
472
513
  expect(result.success).toBe(false);
473
514
  expect(result.error).toMatch(/not found.*source/i);
474
515
  });
475
- it('successfully moves file and returns new markdownLink', () => {
516
+ it('successfully moves file and returns new markdownLink', async () => {
476
517
  vi.mocked(getNote)
477
518
  .mockReturnValueOnce(sourceNote)
478
519
  .mockReturnValueOnce(destNote);
479
520
  vi.mocked(fs.existsSync).mockReturnValue(true);
480
521
  vi.mocked(fs.readFileSync).mockReturnValue('# Source\n![image](Source%20Note_attachments/photo.png)\n');
481
522
  vi.mocked(fs.readdirSync).mockReturnValue([]);
482
- const result = moveAttachment(baseParams);
523
+ const result = await moveAttachment(baseParams);
483
524
  expect(result.success).toBe(true);
484
525
  expect(result.markdownLink).toBe('![image](Dest%20Note_attachments/photo.png)');
485
526
  expect(result).toHaveProperty('movedFrom', 'Source Note_attachments/photo.png');
486
527
  expect(result).toHaveProperty('movedTo', 'Dest Note_attachments/photo.png');
487
528
  expect(fs.renameSync).toHaveBeenCalled();
488
529
  });
489
- it('removes old markdown link from source note content', () => {
530
+ it('removes old markdown link from source note content', async () => {
490
531
  vi.mocked(getNote)
491
532
  .mockReturnValueOnce(sourceNote)
492
533
  .mockReturnValueOnce(destNote);
493
534
  vi.mocked(fs.existsSync).mockReturnValue(true);
494
535
  vi.mocked(fs.readFileSync).mockReturnValue('# Source\n![image](Source%20Note_attachments/photo.png)\nMore text\n');
495
536
  vi.mocked(fs.readdirSync).mockReturnValue([]);
496
- const result = moveAttachment(baseParams);
537
+ const result = await moveAttachment(baseParams);
497
538
  expect(result.success).toBe(true);
498
539
  expect(result.oldLinkRemoved).toBe(true);
499
540
  // Check that writeFileSync was called for the source note with link removed
@@ -503,7 +544,7 @@ describe('moveAttachment', () => {
503
544
  expect(writtenContent).not.toContain('![image](Source%20Note_attachments/photo.png)');
504
545
  expect(writtenContent).toContain('More text');
505
546
  });
506
- it('creates destination folder if needed', () => {
547
+ it('creates destination folder if needed', async () => {
507
548
  vi.mocked(getNote)
508
549
  .mockReturnValueOnce(sourceNote)
509
550
  .mockReturnValueOnce(destNote);
@@ -516,10 +557,10 @@ describe('moveAttachment', () => {
516
557
  });
517
558
  vi.mocked(fs.readFileSync).mockReturnValue('# Source\n');
518
559
  vi.mocked(fs.readdirSync).mockReturnValue([]);
519
- moveAttachment(baseParams);
560
+ await moveAttachment(baseParams);
520
561
  expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(NP_PATH, 'Notes', 'Dest Note_attachments'), { recursive: true });
521
562
  });
522
- it('cleans up empty source folder after move', () => {
563
+ it('cleans up empty source folder after move', async () => {
523
564
  vi.mocked(getNote)
524
565
  .mockReturnValueOnce(sourceNote)
525
566
  .mockReturnValueOnce(destNote);
@@ -527,10 +568,10 @@ describe('moveAttachment', () => {
527
568
  vi.mocked(fs.readFileSync).mockReturnValue('# Source\n');
528
569
  // Empty remaining files after move
529
570
  vi.mocked(fs.readdirSync).mockReturnValue([]);
530
- moveAttachment(baseParams);
531
- expect(fs.rmdirSync).toHaveBeenCalledWith(path.join(NP_PATH, 'Notes', 'Source Note_attachments'));
571
+ await moveAttachment(baseParams);
572
+ expect(fs.rmSync).toHaveBeenCalledWith(path.join(NP_PATH, 'Notes', 'Source Note_attachments'), { recursive: true });
532
573
  });
533
- it('does NOT remove source folder when other files remain', () => {
574
+ it('does NOT remove source folder when other files remain', async () => {
534
575
  vi.mocked(getNote)
535
576
  .mockReturnValueOnce(sourceNote)
536
577
  .mockReturnValueOnce(destNote);
@@ -538,10 +579,10 @@ describe('moveAttachment', () => {
538
579
  vi.mocked(fs.readFileSync).mockReturnValue('# Source\n');
539
580
  // Other files remain
540
581
  vi.mocked(fs.readdirSync).mockReturnValue(['other.png']);
541
- moveAttachment(baseParams);
582
+ await moveAttachment(baseParams);
542
583
  expect(fs.rmdirSync).not.toHaveBeenCalled();
543
584
  });
544
- it('falls back to copy+unlink when renameSync throws EXDEV', () => {
585
+ it('falls back to copy+unlink when renameSync throws EXDEV', async () => {
545
586
  vi.mocked(getNote)
546
587
  .mockReturnValueOnce(sourceNote)
547
588
  .mockReturnValueOnce(destNote);
@@ -552,7 +593,7 @@ describe('moveAttachment', () => {
552
593
  vi.mocked(fs.renameSync).mockImplementation(() => {
553
594
  throw exdevError;
554
595
  });
555
- const result = moveAttachment(baseParams);
596
+ const result = await moveAttachment(baseParams);
556
597
  expect(result.success).toBe(true);
557
598
  expect(fs.copyFileSync).toHaveBeenCalled();
558
599
  expect(fs.unlinkSync).toHaveBeenCalled();