@bitbonsai/mcpvault 0.9.0 → 0.11.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.
- package/README.md +6 -6
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2 -641
- package/dist/src/createServer.d.ts +11 -0
- package/dist/src/createServer.d.ts.map +1 -0
- package/dist/src/createServer.js +400 -0
- package/dist/src/filesystem.d.ts +30 -0
- package/dist/src/filesystem.d.ts.map +1 -0
- package/dist/src/filesystem.js +118 -5
- package/dist/src/frontmatter.d.ts +16 -0
- package/dist/src/frontmatter.d.ts.map +1 -0
- package/dist/src/index.d.ts +8 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/pathfilter.d.ts +13 -0
- package/dist/src/pathfilter.d.ts.map +1 -0
- package/dist/src/search.d.ts +11 -0
- package/dist/src/search.d.ts.map +1 -0
- package/dist/src/types.d.ts +134 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/uri.d.ts +10 -0
- package/dist/src/uri.d.ts.map +1 -0
- package/package.json +10 -3
- package/dist/src/filesystem.test.js +0 -946
- package/dist/src/frontmatter.test.js +0 -118
- package/dist/src/integration.test.js +0 -168
- package/dist/src/pathfilter.test.js +0 -264
- package/dist/src/search.test.js +0 -212
- package/dist/src/uri.test.js +0 -46
|
@@ -1,946 +0,0 @@
|
|
|
1
|
-
import { test, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { FileSystemService } from "./filesystem.js";
|
|
3
|
-
import { PathFilter } from "./pathfilter.js";
|
|
4
|
-
import { writeFile, readFile, mkdir, mkdtemp, rm } from "fs/promises";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { tmpdir } from "os";
|
|
7
|
-
let testVaultPath;
|
|
8
|
-
let fileSystem;
|
|
9
|
-
beforeEach(async () => {
|
|
10
|
-
testVaultPath = await mkdtemp(join(tmpdir(), "mcpvault-test-"));
|
|
11
|
-
fileSystem = new FileSystemService(testVaultPath);
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
try {
|
|
15
|
-
await rm(testVaultPath, { recursive: true });
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// Ignore cleanup errors
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// PATCH TESTS
|
|
23
|
-
// ============================================================================
|
|
24
|
-
test("patch note with single occurrence", async () => {
|
|
25
|
-
const testPath = "test-note.md";
|
|
26
|
-
const content = "# Test Note\n\nThis is the old content.\n\nMore text here.";
|
|
27
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
28
|
-
const result = await fileSystem.patchNote({
|
|
29
|
-
path: testPath,
|
|
30
|
-
oldString: "old content",
|
|
31
|
-
newString: "new content",
|
|
32
|
-
replaceAll: false
|
|
33
|
-
});
|
|
34
|
-
expect(result.success).toBe(true);
|
|
35
|
-
expect(result.matchCount).toBe(1);
|
|
36
|
-
expect(result.message).toContain("Successfully replaced 1 occurrence");
|
|
37
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
38
|
-
expect(updatedNote.content).toContain("new content");
|
|
39
|
-
expect(updatedNote.content).not.toContain("old content");
|
|
40
|
-
});
|
|
41
|
-
test("patch note with multiple occurrences requires replaceAll", async () => {
|
|
42
|
-
const testPath = "test-note.md";
|
|
43
|
-
const content = "# Test\n\nrepeat word repeat word repeat";
|
|
44
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
45
|
-
const result = await fileSystem.patchNote({
|
|
46
|
-
path: testPath,
|
|
47
|
-
oldString: "repeat",
|
|
48
|
-
newString: "unique",
|
|
49
|
-
replaceAll: false
|
|
50
|
-
});
|
|
51
|
-
expect(result.success).toBe(false);
|
|
52
|
-
expect(result.matchCount).toBe(3);
|
|
53
|
-
expect(result.message).toContain("Found 3 occurrences");
|
|
54
|
-
expect(result.message).toContain("Use replaceAll=true");
|
|
55
|
-
});
|
|
56
|
-
test("patch note with replaceAll replaces all occurrences", async () => {
|
|
57
|
-
const testPath = "test-note.md";
|
|
58
|
-
const content = "# Test\n\nrepeat word repeat word repeat";
|
|
59
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
60
|
-
const result = await fileSystem.patchNote({
|
|
61
|
-
path: testPath,
|
|
62
|
-
oldString: "repeat",
|
|
63
|
-
newString: "unique",
|
|
64
|
-
replaceAll: true
|
|
65
|
-
});
|
|
66
|
-
expect(result.success).toBe(true);
|
|
67
|
-
expect(result.matchCount).toBe(3);
|
|
68
|
-
expect(result.message).toContain("Successfully replaced 3 occurrences");
|
|
69
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
70
|
-
expect(updatedNote.content).not.toContain("repeat");
|
|
71
|
-
expect(updatedNote.content.match(/unique/g)?.length).toBe(3);
|
|
72
|
-
});
|
|
73
|
-
test("patch note fails when string not found", async () => {
|
|
74
|
-
const testPath = "test-note.md";
|
|
75
|
-
const content = "# Test Note\n\nSome content here.";
|
|
76
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
77
|
-
const result = await fileSystem.patchNote({
|
|
78
|
-
path: testPath,
|
|
79
|
-
oldString: "non-existent string",
|
|
80
|
-
newString: "replacement",
|
|
81
|
-
replaceAll: false
|
|
82
|
-
});
|
|
83
|
-
expect(result.success).toBe(false);
|
|
84
|
-
expect(result.matchCount).toBe(0);
|
|
85
|
-
expect(result.message).toContain("String not found");
|
|
86
|
-
});
|
|
87
|
-
test("patch note with multiline replacement", async () => {
|
|
88
|
-
const testPath = "test-note.md";
|
|
89
|
-
const content = "# Test\n\n## Section A\nOld content\nOld lines\n\n## Section B\nOther content";
|
|
90
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
91
|
-
const result = await fileSystem.patchNote({
|
|
92
|
-
path: testPath,
|
|
93
|
-
oldString: "## Section A\nOld content\nOld lines",
|
|
94
|
-
newString: "## Section A\nNew content\nNew improved lines",
|
|
95
|
-
replaceAll: false
|
|
96
|
-
});
|
|
97
|
-
expect(result.success).toBe(true);
|
|
98
|
-
expect(result.matchCount).toBe(1);
|
|
99
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
100
|
-
expect(updatedNote.content).toContain("New content");
|
|
101
|
-
expect(updatedNote.content).toContain("New improved lines");
|
|
102
|
-
expect(updatedNote.content).not.toContain("Old content");
|
|
103
|
-
});
|
|
104
|
-
test("patch note with frontmatter preserved", async () => {
|
|
105
|
-
const testPath = "test-note.md";
|
|
106
|
-
const content = `---
|
|
107
|
-
title: My Note
|
|
108
|
-
tags: [test]
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
# Content
|
|
112
|
-
|
|
113
|
-
Old text here.`;
|
|
114
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
115
|
-
const result = await fileSystem.patchNote({
|
|
116
|
-
path: testPath,
|
|
117
|
-
oldString: "Old text here.",
|
|
118
|
-
newString: "New text here.",
|
|
119
|
-
replaceAll: false
|
|
120
|
-
});
|
|
121
|
-
expect(result.success).toBe(true);
|
|
122
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
123
|
-
expect(updatedNote.frontmatter.title).toBe("My Note");
|
|
124
|
-
expect(updatedNote.frontmatter.tags).toEqual(["test"]);
|
|
125
|
-
expect(updatedNote.content).toContain("New text here.");
|
|
126
|
-
});
|
|
127
|
-
test("patch note fails when oldString equals newString", async () => {
|
|
128
|
-
const testPath = "test-note.md";
|
|
129
|
-
const content = "# Test\n\nSome content";
|
|
130
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
131
|
-
const result = await fileSystem.patchNote({
|
|
132
|
-
path: testPath,
|
|
133
|
-
oldString: "same",
|
|
134
|
-
newString: "same",
|
|
135
|
-
replaceAll: false
|
|
136
|
-
});
|
|
137
|
-
expect(result.success).toBe(false);
|
|
138
|
-
expect(result.message).toContain("must be different");
|
|
139
|
-
});
|
|
140
|
-
test("patch note fails for filtered paths", async () => {
|
|
141
|
-
const testPath = ".obsidian/config.json";
|
|
142
|
-
const result = await fileSystem.patchNote({
|
|
143
|
-
path: testPath,
|
|
144
|
-
oldString: "old",
|
|
145
|
-
newString: "new",
|
|
146
|
-
replaceAll: false
|
|
147
|
-
});
|
|
148
|
-
expect(result.success).toBe(false);
|
|
149
|
-
expect(result.message).toContain("Access denied");
|
|
150
|
-
});
|
|
151
|
-
test("patch note fails when file doesn't exist", async () => {
|
|
152
|
-
const testPath = "non-existent-note.md";
|
|
153
|
-
const result = await fileSystem.patchNote({
|
|
154
|
-
path: testPath,
|
|
155
|
-
oldString: "old",
|
|
156
|
-
newString: "new",
|
|
157
|
-
replaceAll: false
|
|
158
|
-
});
|
|
159
|
-
expect(result.success).toBe(false);
|
|
160
|
-
expect(result.message).toContain("File not found");
|
|
161
|
-
});
|
|
162
|
-
test("patch note fails with empty oldString", async () => {
|
|
163
|
-
const testPath = "test-note.md";
|
|
164
|
-
const content = "# Test Note\n\nSome content.";
|
|
165
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
166
|
-
const result = await fileSystem.patchNote({
|
|
167
|
-
path: testPath,
|
|
168
|
-
oldString: "",
|
|
169
|
-
newString: "new",
|
|
170
|
-
replaceAll: false
|
|
171
|
-
});
|
|
172
|
-
expect(result.success).toBe(false);
|
|
173
|
-
expect(result.message).toMatch(/empty|filled|required/i);
|
|
174
|
-
});
|
|
175
|
-
test("patch note fails with empty newString", async () => {
|
|
176
|
-
const testPath = "test-note.md";
|
|
177
|
-
const content = "# Test Note\n\nSome content.";
|
|
178
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
179
|
-
const result = await fileSystem.patchNote({
|
|
180
|
-
path: testPath,
|
|
181
|
-
oldString: "content",
|
|
182
|
-
newString: "",
|
|
183
|
-
replaceAll: false
|
|
184
|
-
});
|
|
185
|
-
expect(result.success).toBe(false);
|
|
186
|
-
expect(result.message).toMatch(/empty|filled|required/i);
|
|
187
|
-
});
|
|
188
|
-
test("patch note fails with undefined newString", async () => {
|
|
189
|
-
const testPath = "test-note.md";
|
|
190
|
-
const content = "# Test Note\n\nSome content.";
|
|
191
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
192
|
-
const result = await fileSystem.patchNote({
|
|
193
|
-
path: testPath,
|
|
194
|
-
oldString: "content",
|
|
195
|
-
newString: undefined,
|
|
196
|
-
replaceAll: false
|
|
197
|
-
});
|
|
198
|
-
expect(result.success).toBe(false);
|
|
199
|
-
expect(result.message).toMatch(/empty|filled|required/i);
|
|
200
|
-
// Verify the note was NOT corrupted
|
|
201
|
-
const note = await fileSystem.readNote(testPath);
|
|
202
|
-
expect(note.content).not.toContain("undefined");
|
|
203
|
-
expect(note.content).toContain("Some content.");
|
|
204
|
-
});
|
|
205
|
-
test("patch note fails with null newString", async () => {
|
|
206
|
-
const testPath = "test-note.md";
|
|
207
|
-
const content = "# Test Note\n\nSome content.";
|
|
208
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
209
|
-
const result = await fileSystem.patchNote({
|
|
210
|
-
path: testPath,
|
|
211
|
-
oldString: "content",
|
|
212
|
-
newString: null,
|
|
213
|
-
replaceAll: false
|
|
214
|
-
});
|
|
215
|
-
expect(result.success).toBe(false);
|
|
216
|
-
expect(result.message).toMatch(/empty|filled|required/i);
|
|
217
|
-
// Verify the note was NOT corrupted
|
|
218
|
-
const note = await fileSystem.readNote(testPath);
|
|
219
|
-
expect(note.content).not.toContain("null");
|
|
220
|
-
expect(note.content).toContain("Some content.");
|
|
221
|
-
});
|
|
222
|
-
test("writeNote rejects undefined content", async () => {
|
|
223
|
-
const testPath = "test-note.md";
|
|
224
|
-
await expect(fileSystem.writeNote({
|
|
225
|
-
path: testPath,
|
|
226
|
-
content: undefined
|
|
227
|
-
})).rejects.toThrow(/Content is required/);
|
|
228
|
-
});
|
|
229
|
-
test("writeNote rejects null content", async () => {
|
|
230
|
-
const testPath = "test-note.md";
|
|
231
|
-
await expect(fileSystem.writeNote({
|
|
232
|
-
path: testPath,
|
|
233
|
-
content: null
|
|
234
|
-
})).rejects.toThrow(/Content is required/);
|
|
235
|
-
});
|
|
236
|
-
test("writeNote append with undefined content does not corrupt note", async () => {
|
|
237
|
-
const testPath = "test-note.md";
|
|
238
|
-
const content = "# Test Note\n\nOriginal content.";
|
|
239
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
240
|
-
await expect(fileSystem.writeNote({
|
|
241
|
-
path: testPath,
|
|
242
|
-
content: undefined,
|
|
243
|
-
mode: 'append'
|
|
244
|
-
})).rejects.toThrow(/Content is required/);
|
|
245
|
-
// Verify the note was NOT corrupted
|
|
246
|
-
const note = await fileSystem.readNote(testPath);
|
|
247
|
-
expect(note.content).not.toContain("undefined");
|
|
248
|
-
expect(note.content).toContain("Original content.");
|
|
249
|
-
});
|
|
250
|
-
test("patch note handles regex special characters literally", async () => {
|
|
251
|
-
const testPath = "test-note.md";
|
|
252
|
-
const content = "Price: $10.50 (special)";
|
|
253
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
254
|
-
const result = await fileSystem.patchNote({
|
|
255
|
-
path: testPath,
|
|
256
|
-
oldString: "$10.50",
|
|
257
|
-
newString: "$15.75",
|
|
258
|
-
replaceAll: false
|
|
259
|
-
});
|
|
260
|
-
expect(result.success).toBe(true);
|
|
261
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
262
|
-
expect(updatedNote.content).toContain("$15.75");
|
|
263
|
-
expect(updatedNote.content).not.toContain("$10.50");
|
|
264
|
-
});
|
|
265
|
-
test("patch note works with fenced code blocks", async () => {
|
|
266
|
-
const testPath = "code-fence-test.md";
|
|
267
|
-
const content = "# Example\n\n```rust\nfn main() {\n println!(\"hello\");\n}\n```\n";
|
|
268
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
269
|
-
const result = await fileSystem.patchNote({
|
|
270
|
-
path: testPath,
|
|
271
|
-
oldString: "println!(\"hello\");",
|
|
272
|
-
newString: "println!(\"hello world\");",
|
|
273
|
-
replaceAll: false
|
|
274
|
-
});
|
|
275
|
-
expect(result.success).toBe(true);
|
|
276
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
277
|
-
expect(updatedNote.originalContent).toContain("println!(\"hello world\");");
|
|
278
|
-
});
|
|
279
|
-
test("patch note works with markdown tables", async () => {
|
|
280
|
-
const testPath = "table-test.md";
|
|
281
|
-
const content = "| Tool | Status |\n|---|---|\n| patch_note | flaky |\n";
|
|
282
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
283
|
-
const result = await fileSystem.patchNote({
|
|
284
|
-
path: testPath,
|
|
285
|
-
oldString: "| patch_note | flaky |",
|
|
286
|
-
newString: "| patch_note | stable |",
|
|
287
|
-
replaceAll: false
|
|
288
|
-
});
|
|
289
|
-
expect(result.success).toBe(true);
|
|
290
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
291
|
-
expect(updatedNote.originalContent).toContain("| patch_note | stable |");
|
|
292
|
-
});
|
|
293
|
-
test("patch note preserves tabs and spaces", async () => {
|
|
294
|
-
const testPath = "test-note.md";
|
|
295
|
-
const content = "Line with\ttabs\n Line with spaces\n\tTabbed line";
|
|
296
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
297
|
-
const result = await fileSystem.patchNote({
|
|
298
|
-
path: testPath,
|
|
299
|
-
oldString: "tabs",
|
|
300
|
-
newString: "TABS",
|
|
301
|
-
replaceAll: false
|
|
302
|
-
});
|
|
303
|
-
expect(result.success).toBe(true);
|
|
304
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
305
|
-
expect(updatedNote.content).toContain("Line with\tTABS");
|
|
306
|
-
expect(updatedNote.content).toContain("\tTabbed line");
|
|
307
|
-
expect(updatedNote.content).toContain(" Line with spaces");
|
|
308
|
-
});
|
|
309
|
-
test("patch note is case sensitive", async () => {
|
|
310
|
-
const testPath = "test-note.md";
|
|
311
|
-
const content = "Hello world, hello again";
|
|
312
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
313
|
-
const result = await fileSystem.patchNote({
|
|
314
|
-
path: testPath,
|
|
315
|
-
oldString: "hello",
|
|
316
|
-
newString: "hi",
|
|
317
|
-
replaceAll: false
|
|
318
|
-
});
|
|
319
|
-
expect(result.success).toBe(true);
|
|
320
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
321
|
-
expect(updatedNote.content).toContain("Hello world");
|
|
322
|
-
expect(updatedNote.content).toContain("hi again");
|
|
323
|
-
});
|
|
324
|
-
test("patch note handles many replacements efficiently", async () => {
|
|
325
|
-
const testPath = "test-note.md";
|
|
326
|
-
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i}: replace_me`);
|
|
327
|
-
const content = lines.join("\n");
|
|
328
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
329
|
-
const startTime = Date.now();
|
|
330
|
-
const result = await fileSystem.patchNote({
|
|
331
|
-
path: testPath,
|
|
332
|
-
oldString: "replace_me",
|
|
333
|
-
newString: "replaced",
|
|
334
|
-
replaceAll: true
|
|
335
|
-
});
|
|
336
|
-
const duration = Date.now() - startTime;
|
|
337
|
-
expect(result.success).toBe(true);
|
|
338
|
-
expect(result.matchCount).toBe(100);
|
|
339
|
-
expect(duration).toBeLessThan(1000);
|
|
340
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
341
|
-
expect(updatedNote.content).not.toContain("replace_me");
|
|
342
|
-
expect(updatedNote.content.match(/replaced/g)?.length).toBe(100);
|
|
343
|
-
});
|
|
344
|
-
test("patch note works with path containing spaces", async () => {
|
|
345
|
-
const testPath = "folder name/note with spaces.md";
|
|
346
|
-
const content = "# Test Note\n\nOld content here.";
|
|
347
|
-
await mkdir(join(testVaultPath, "folder name"), { recursive: true });
|
|
348
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
349
|
-
const result = await fileSystem.patchNote({
|
|
350
|
-
path: testPath,
|
|
351
|
-
oldString: "Old content",
|
|
352
|
-
newString: "New content",
|
|
353
|
-
replaceAll: false
|
|
354
|
-
});
|
|
355
|
-
expect(result.success).toBe(true);
|
|
356
|
-
const updatedNote = await fileSystem.readNote(testPath);
|
|
357
|
-
expect(updatedNote.content).toContain("New content");
|
|
358
|
-
});
|
|
359
|
-
// ============================================================================
|
|
360
|
-
// DELETE TESTS
|
|
361
|
-
// ============================================================================
|
|
362
|
-
test("delete note with correct confirmation", async () => {
|
|
363
|
-
const testPath = "test-note.md";
|
|
364
|
-
const content = "# Test Note\n\nThis is a test note to be deleted.";
|
|
365
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
366
|
-
const result = await fileSystem.deleteNote({
|
|
367
|
-
path: testPath,
|
|
368
|
-
confirmPath: testPath
|
|
369
|
-
});
|
|
370
|
-
expect(result.success).toBe(true);
|
|
371
|
-
expect(result.path).toBe(testPath);
|
|
372
|
-
expect(result.message).toContain("Successfully deleted");
|
|
373
|
-
expect(result.message).toContain("cannot be undone");
|
|
374
|
-
});
|
|
375
|
-
test("reject deletion with incorrect confirmation", async () => {
|
|
376
|
-
const testPath = "test-note.md";
|
|
377
|
-
const content = "# Test Note\n\nThis note should not be deleted.";
|
|
378
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
379
|
-
const result = await fileSystem.deleteNote({
|
|
380
|
-
path: testPath,
|
|
381
|
-
confirmPath: "wrong-path.md"
|
|
382
|
-
});
|
|
383
|
-
expect(result.success).toBe(false);
|
|
384
|
-
expect(result.path).toBe(testPath);
|
|
385
|
-
expect(result.message).toContain("confirmation path does not match");
|
|
386
|
-
const fileStillExists = await fileSystem.exists(testPath);
|
|
387
|
-
expect(fileStillExists).toBe(true);
|
|
388
|
-
});
|
|
389
|
-
test("handle deletion of non-existent file", async () => {
|
|
390
|
-
const testPath = "non-existent.md";
|
|
391
|
-
const result = await fileSystem.deleteNote({
|
|
392
|
-
path: testPath,
|
|
393
|
-
confirmPath: testPath
|
|
394
|
-
});
|
|
395
|
-
expect(result.success).toBe(false);
|
|
396
|
-
expect(result.path).toBe(testPath);
|
|
397
|
-
expect(result.message).toContain("File not found");
|
|
398
|
-
});
|
|
399
|
-
test("reject deletion of filtered paths", async () => {
|
|
400
|
-
const testPath = ".obsidian/app.json";
|
|
401
|
-
const result = await fileSystem.deleteNote({
|
|
402
|
-
path: testPath,
|
|
403
|
-
confirmPath: testPath
|
|
404
|
-
});
|
|
405
|
-
expect(result.success).toBe(false);
|
|
406
|
-
expect(result.path).toBe(testPath);
|
|
407
|
-
expect(result.message).toContain("Access denied");
|
|
408
|
-
});
|
|
409
|
-
test("handle directory deletion attempt", async () => {
|
|
410
|
-
const testPath = "test-directory";
|
|
411
|
-
await mkdir(join(testVaultPath, testPath));
|
|
412
|
-
const result = await fileSystem.deleteNote({
|
|
413
|
-
path: testPath,
|
|
414
|
-
confirmPath: testPath
|
|
415
|
-
});
|
|
416
|
-
expect(result.success).toBe(false);
|
|
417
|
-
expect(result.path).toBe(testPath);
|
|
418
|
-
expect(result.message).toContain("is not a file");
|
|
419
|
-
});
|
|
420
|
-
test("delete note with frontmatter", async () => {
|
|
421
|
-
const testPath = "note-with-frontmatter.md";
|
|
422
|
-
const content = `---
|
|
423
|
-
title: Test Note
|
|
424
|
-
tags: [test, delete]
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
# Test Note
|
|
428
|
-
|
|
429
|
-
This note has frontmatter and should be deleted successfully.`;
|
|
430
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
431
|
-
const result = await fileSystem.deleteNote({
|
|
432
|
-
path: testPath,
|
|
433
|
-
confirmPath: testPath
|
|
434
|
-
});
|
|
435
|
-
expect(result.success).toBe(true);
|
|
436
|
-
expect(result.path).toBe(testPath);
|
|
437
|
-
expect(result.message).toContain("Successfully deleted");
|
|
438
|
-
});
|
|
439
|
-
// ============================================================================
|
|
440
|
-
// FRONTMATTER INTEGRATION TESTS
|
|
441
|
-
// ============================================================================
|
|
442
|
-
test("write_note with frontmatter", async () => {
|
|
443
|
-
await fileSystem.writeNote({
|
|
444
|
-
path: "test.md",
|
|
445
|
-
content: "This is test content.",
|
|
446
|
-
frontmatter: {
|
|
447
|
-
title: "Test Note",
|
|
448
|
-
tags: ["test", "example"],
|
|
449
|
-
created: "2023-01-01"
|
|
450
|
-
}
|
|
451
|
-
});
|
|
452
|
-
const note = await fileSystem.readNote("test.md");
|
|
453
|
-
expect(note.frontmatter.title).toBe("Test Note");
|
|
454
|
-
expect(note.frontmatter.tags).toEqual(["test", "example"]);
|
|
455
|
-
expect(note.frontmatter.created).toBe("2023-01-01");
|
|
456
|
-
expect(note.content.trim()).toBe("This is test content.");
|
|
457
|
-
});
|
|
458
|
-
test("write_note with append mode preserves frontmatter", async () => {
|
|
459
|
-
await fileSystem.writeNote({
|
|
460
|
-
path: "append-test.md",
|
|
461
|
-
content: "Original content.",
|
|
462
|
-
frontmatter: { title: "Original", status: "draft" }
|
|
463
|
-
});
|
|
464
|
-
await fileSystem.writeNote({
|
|
465
|
-
path: "append-test.md",
|
|
466
|
-
content: "\nAppended content.",
|
|
467
|
-
frontmatter: { updated: "2023-12-01" },
|
|
468
|
-
mode: "append"
|
|
469
|
-
});
|
|
470
|
-
const note = await fileSystem.readNote("append-test.md");
|
|
471
|
-
expect(note.frontmatter.title).toBe("Original");
|
|
472
|
-
expect(note.frontmatter.status).toBe("draft");
|
|
473
|
-
expect(note.frontmatter.updated).toBe("2023-12-01");
|
|
474
|
-
expect(note.content.trim()).toBe("Original content.\n\nAppended content.");
|
|
475
|
-
});
|
|
476
|
-
test("update_frontmatter merges with existing", async () => {
|
|
477
|
-
await fileSystem.writeNote({
|
|
478
|
-
path: "update-test.md",
|
|
479
|
-
content: "Test content.",
|
|
480
|
-
frontmatter: {
|
|
481
|
-
title: "Original Title",
|
|
482
|
-
tags: ["original"],
|
|
483
|
-
status: "draft"
|
|
484
|
-
}
|
|
485
|
-
});
|
|
486
|
-
await fileSystem.updateFrontmatter({
|
|
487
|
-
path: "update-test.md",
|
|
488
|
-
frontmatter: {
|
|
489
|
-
title: "Updated Title",
|
|
490
|
-
priority: "high"
|
|
491
|
-
},
|
|
492
|
-
merge: true
|
|
493
|
-
});
|
|
494
|
-
const note = await fileSystem.readNote("update-test.md");
|
|
495
|
-
expect(note.frontmatter.title).toBe("Updated Title");
|
|
496
|
-
expect(note.frontmatter.tags).toEqual(["original"]);
|
|
497
|
-
expect(note.frontmatter.status).toBe("draft");
|
|
498
|
-
expect(note.frontmatter.priority).toBe("high");
|
|
499
|
-
expect(note.content.trim()).toBe("Test content.");
|
|
500
|
-
});
|
|
501
|
-
test("update_frontmatter replaces when merge is false", async () => {
|
|
502
|
-
await fileSystem.writeNote({
|
|
503
|
-
path: "replace-test.md",
|
|
504
|
-
content: "Test content.",
|
|
505
|
-
frontmatter: {
|
|
506
|
-
title: "Original Title",
|
|
507
|
-
tags: ["original"],
|
|
508
|
-
status: "draft"
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
await fileSystem.updateFrontmatter({
|
|
512
|
-
path: "replace-test.md",
|
|
513
|
-
frontmatter: {
|
|
514
|
-
title: "New Title",
|
|
515
|
-
priority: "high"
|
|
516
|
-
},
|
|
517
|
-
merge: false
|
|
518
|
-
});
|
|
519
|
-
const note = await fileSystem.readNote("replace-test.md");
|
|
520
|
-
expect(note.frontmatter.title).toBe("New Title");
|
|
521
|
-
expect(note.frontmatter.priority).toBe("high");
|
|
522
|
-
expect(note.frontmatter.tags).toBeUndefined();
|
|
523
|
-
expect(note.frontmatter.status).toBeUndefined();
|
|
524
|
-
});
|
|
525
|
-
test("manage_tags add operation", async () => {
|
|
526
|
-
await fileSystem.writeNote({
|
|
527
|
-
path: "tags-add-test.md",
|
|
528
|
-
content: "Test content.",
|
|
529
|
-
frontmatter: {
|
|
530
|
-
title: "Test",
|
|
531
|
-
tags: ["existing"]
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
const result = await fileSystem.manageTags({
|
|
535
|
-
path: "tags-add-test.md",
|
|
536
|
-
operation: "add",
|
|
537
|
-
tags: ["new", "important"]
|
|
538
|
-
});
|
|
539
|
-
expect(result.success).toBe(true);
|
|
540
|
-
expect(result.tags).toEqual(["existing", "new", "important"]);
|
|
541
|
-
const note = await fileSystem.readNote("tags-add-test.md");
|
|
542
|
-
expect(note.frontmatter.tags).toEqual(["existing", "new", "important"]);
|
|
543
|
-
});
|
|
544
|
-
test("manage_tags remove operation", async () => {
|
|
545
|
-
await fileSystem.writeNote({
|
|
546
|
-
path: "tags-remove-test.md",
|
|
547
|
-
content: "Test content.",
|
|
548
|
-
frontmatter: {
|
|
549
|
-
title: "Test",
|
|
550
|
-
tags: ["keep", "remove1", "remove2"]
|
|
551
|
-
}
|
|
552
|
-
});
|
|
553
|
-
const result = await fileSystem.manageTags({
|
|
554
|
-
path: "tags-remove-test.md",
|
|
555
|
-
operation: "remove",
|
|
556
|
-
tags: ["remove1", "remove2"]
|
|
557
|
-
});
|
|
558
|
-
expect(result.success).toBe(true);
|
|
559
|
-
expect(result.tags).toEqual(["keep"]);
|
|
560
|
-
const note = await fileSystem.readNote("tags-remove-test.md");
|
|
561
|
-
expect(note.frontmatter.tags).toEqual(["keep"]);
|
|
562
|
-
});
|
|
563
|
-
test("manage_tags list operation", async () => {
|
|
564
|
-
await fileSystem.writeNote({
|
|
565
|
-
path: "tags-list-test.md",
|
|
566
|
-
content: "Test content with #inline-tag.",
|
|
567
|
-
frontmatter: {
|
|
568
|
-
title: "Test",
|
|
569
|
-
tags: ["frontmatter-tag"]
|
|
570
|
-
}
|
|
571
|
-
});
|
|
572
|
-
const result = await fileSystem.manageTags({
|
|
573
|
-
path: "tags-list-test.md",
|
|
574
|
-
operation: "list"
|
|
575
|
-
});
|
|
576
|
-
expect(result.success).toBe(true);
|
|
577
|
-
expect(result.tags).toContain("frontmatter-tag");
|
|
578
|
-
expect(result.tags).toContain("inline-tag");
|
|
579
|
-
});
|
|
580
|
-
test("manage_tags removes tags array when empty", async () => {
|
|
581
|
-
await fileSystem.writeNote({
|
|
582
|
-
path: "tags-empty-test.md",
|
|
583
|
-
content: "Test content.",
|
|
584
|
-
frontmatter: {
|
|
585
|
-
title: "Test",
|
|
586
|
-
tags: ["remove-me"]
|
|
587
|
-
}
|
|
588
|
-
});
|
|
589
|
-
await fileSystem.manageTags({
|
|
590
|
-
path: "tags-empty-test.md",
|
|
591
|
-
operation: "remove",
|
|
592
|
-
tags: ["remove-me"]
|
|
593
|
-
});
|
|
594
|
-
const note = await fileSystem.readNote("tags-empty-test.md");
|
|
595
|
-
expect(note.frontmatter.tags).toBeUndefined();
|
|
596
|
-
expect(note.frontmatter.title).toBe("Test");
|
|
597
|
-
});
|
|
598
|
-
test("frontmatter validation with invalid data", async () => {
|
|
599
|
-
await expect(fileSystem.writeNote({
|
|
600
|
-
path: "invalid-test.md",
|
|
601
|
-
content: "Test content.",
|
|
602
|
-
frontmatter: {
|
|
603
|
-
title: "Test",
|
|
604
|
-
invalidFunction: () => "not allowed"
|
|
605
|
-
}
|
|
606
|
-
})).rejects.toThrow(/Invalid frontmatter/);
|
|
607
|
-
});
|
|
608
|
-
test("listDirectory includes non-note files but readNote still blocks them", async () => {
|
|
609
|
-
const imagePath = "assets/diagram.png";
|
|
610
|
-
await mkdir(join(testVaultPath, "assets"), { recursive: true });
|
|
611
|
-
await writeFile(join(testVaultPath, imagePath), "fake-png-content");
|
|
612
|
-
const listing = await fileSystem.listDirectory("assets");
|
|
613
|
-
expect(listing.files).toContain("diagram.png");
|
|
614
|
-
await expect(fileSystem.readNote(imagePath)).rejects.toThrow(/Access denied/);
|
|
615
|
-
});
|
|
616
|
-
// ============================================================================
|
|
617
|
-
// NON-EXISTENT VAULT TESTS
|
|
618
|
-
// ============================================================================
|
|
619
|
-
test("read from non-existent vault throws error", async () => {
|
|
620
|
-
const nonExistentFs = new FileSystemService("/non/existent/vault/path");
|
|
621
|
-
await expect(nonExistentFs.readNote("test.md"))
|
|
622
|
-
.rejects.toThrow(/File not found|ENOENT/);
|
|
623
|
-
});
|
|
624
|
-
test("write to non-existent vault creates directories", async () => {
|
|
625
|
-
const tempVault = await mkdtemp(join(tmpdir(), "mcpvault-new-vault-"));
|
|
626
|
-
const newFs = new FileSystemService(tempVault);
|
|
627
|
-
try {
|
|
628
|
-
await newFs.writeNote({
|
|
629
|
-
path: "new-folder/nested/note.md",
|
|
630
|
-
content: "Test content"
|
|
631
|
-
});
|
|
632
|
-
const note = await newFs.readNote("new-folder/nested/note.md");
|
|
633
|
-
expect(note.content).toContain("Test content");
|
|
634
|
-
}
|
|
635
|
-
finally {
|
|
636
|
-
await rm(tempVault, { recursive: true });
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
test("list directory in non-existent vault", async () => {
|
|
640
|
-
const nonExistentFs = new FileSystemService("/non/existent/vault/path");
|
|
641
|
-
await expect(nonExistentFs.listDirectory("/"))
|
|
642
|
-
.rejects.toThrow();
|
|
643
|
-
});
|
|
644
|
-
// ============================================================================
|
|
645
|
-
// PATH TRAVERSAL WITH SPECIAL CHARACTERS
|
|
646
|
-
// ============================================================================
|
|
647
|
-
test("path traversal attempt with encoded dots blocked", async () => {
|
|
648
|
-
// Path traversal should be blocked even with URL encoding
|
|
649
|
-
await expect(fileSystem.readNote("..%2F..%2Fetc%2Fpasswd"))
|
|
650
|
-
.rejects.toThrow(/Path traversal not allowed/);
|
|
651
|
-
});
|
|
652
|
-
test("path traversal with .. is blocked", async () => {
|
|
653
|
-
await expect(fileSystem.readNote("../outside.md"))
|
|
654
|
-
.rejects.toThrow(/Path traversal not allowed/);
|
|
655
|
-
});
|
|
656
|
-
test("path traversal with nested .. is blocked", async () => {
|
|
657
|
-
await expect(fileSystem.readNote("folder/../../outside.md"))
|
|
658
|
-
.rejects.toThrow(/Path traversal not allowed/);
|
|
659
|
-
});
|
|
660
|
-
test("path with regex special chars is treated literally", async () => {
|
|
661
|
-
const testPath = "folder (copy)/note [1].md";
|
|
662
|
-
const content = "# Test with special chars";
|
|
663
|
-
await mkdir(join(testVaultPath, "folder (copy)"), { recursive: true });
|
|
664
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
665
|
-
const note = await fileSystem.readNote(testPath);
|
|
666
|
-
expect(note.content).toContain("Test with special chars");
|
|
667
|
-
});
|
|
668
|
-
test("path with dollar sign works", async () => {
|
|
669
|
-
const testPath = "$special/price$100.md";
|
|
670
|
-
const content = "# Price note";
|
|
671
|
-
await mkdir(join(testVaultPath, "$special"), { recursive: true });
|
|
672
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
673
|
-
const note = await fileSystem.readNote(testPath);
|
|
674
|
-
expect(note.content).toContain("Price note");
|
|
675
|
-
});
|
|
676
|
-
test("path with plus sign works", async () => {
|
|
677
|
-
const testPath = "C++/notes.md";
|
|
678
|
-
const content = "# C++ notes";
|
|
679
|
-
await mkdir(join(testVaultPath, "C++"), { recursive: true });
|
|
680
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
681
|
-
const note = await fileSystem.readNote(testPath);
|
|
682
|
-
expect(note.content).toContain("C++ notes");
|
|
683
|
-
});
|
|
684
|
-
test("path with pipe character works", async () => {
|
|
685
|
-
const testPath = "choice|option.md";
|
|
686
|
-
const content = "# Choice note";
|
|
687
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
688
|
-
const note = await fileSystem.readNote(testPath);
|
|
689
|
-
expect(note.content).toContain("Choice note");
|
|
690
|
-
});
|
|
691
|
-
test("delete note with special chars in path", async () => {
|
|
692
|
-
const testPath = "folder (archive)/note [old].md";
|
|
693
|
-
const content = "# Old note";
|
|
694
|
-
await mkdir(join(testVaultPath, "folder (archive)"), { recursive: true });
|
|
695
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
696
|
-
const result = await fileSystem.deleteNote({
|
|
697
|
-
path: testPath,
|
|
698
|
-
confirmPath: testPath
|
|
699
|
-
});
|
|
700
|
-
expect(result.success).toBe(true);
|
|
701
|
-
});
|
|
702
|
-
test("move note with special chars in both paths", async () => {
|
|
703
|
-
const oldPath = "source (1)/note [a].md";
|
|
704
|
-
const newPath = "dest (2)/note [b].md";
|
|
705
|
-
const content = "# Moving note";
|
|
706
|
-
await mkdir(join(testVaultPath, "source (1)"), { recursive: true });
|
|
707
|
-
await mkdir(join(testVaultPath, "dest (2)"), { recursive: true });
|
|
708
|
-
await writeFile(join(testVaultPath, oldPath), content);
|
|
709
|
-
const result = await fileSystem.moveNote({
|
|
710
|
-
oldPath,
|
|
711
|
-
newPath
|
|
712
|
-
});
|
|
713
|
-
expect(result.success).toBe(true);
|
|
714
|
-
const note = await fileSystem.readNote(newPath);
|
|
715
|
-
expect(note.content).toContain("Moving note");
|
|
716
|
-
});
|
|
717
|
-
test("move_file moves binary files without corruption", async () => {
|
|
718
|
-
const oldPath = "attachments/original image.png";
|
|
719
|
-
const newPath = "assets/original image.png";
|
|
720
|
-
const binaryContent = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x10, 0x42]);
|
|
721
|
-
await mkdir(join(testVaultPath, "attachments"), { recursive: true });
|
|
722
|
-
await writeFile(join(testVaultPath, oldPath), binaryContent);
|
|
723
|
-
const result = await fileSystem.moveFile({
|
|
724
|
-
oldPath,
|
|
725
|
-
newPath,
|
|
726
|
-
confirmOldPath: oldPath,
|
|
727
|
-
confirmNewPath: newPath
|
|
728
|
-
});
|
|
729
|
-
expect(result.success).toBe(true);
|
|
730
|
-
const moved = await readFile(join(testVaultPath, newPath));
|
|
731
|
-
expect(Buffer.compare(moved, binaryContent)).toBe(0);
|
|
732
|
-
await expect(readFile(join(testVaultPath, oldPath))).rejects.toMatchObject({ code: "ENOENT" });
|
|
733
|
-
});
|
|
734
|
-
test("move_file respects overwrite=false", async () => {
|
|
735
|
-
const oldPath = "attachments/image.png";
|
|
736
|
-
const newPath = "assets/image.png";
|
|
737
|
-
await mkdir(join(testVaultPath, "attachments"), { recursive: true });
|
|
738
|
-
await mkdir(join(testVaultPath, "assets"), { recursive: true });
|
|
739
|
-
await writeFile(join(testVaultPath, oldPath), Buffer.from([0x01, 0x02, 0x03]));
|
|
740
|
-
await writeFile(join(testVaultPath, newPath), Buffer.from([0xaa, 0xbb]));
|
|
741
|
-
const result = await fileSystem.moveFile({
|
|
742
|
-
oldPath,
|
|
743
|
-
newPath,
|
|
744
|
-
confirmOldPath: oldPath,
|
|
745
|
-
confirmNewPath: newPath,
|
|
746
|
-
overwrite: false
|
|
747
|
-
});
|
|
748
|
-
expect(result.success).toBe(false);
|
|
749
|
-
expect(result.message).toContain("Target file already exists");
|
|
750
|
-
});
|
|
751
|
-
test("move_file overwrites existing file when overwrite=true", async () => {
|
|
752
|
-
const oldPath = "attachments/image.png";
|
|
753
|
-
const newPath = "assets/image.png";
|
|
754
|
-
const replacement = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
|
|
755
|
-
await mkdir(join(testVaultPath, "attachments"), { recursive: true });
|
|
756
|
-
await mkdir(join(testVaultPath, "assets"), { recursive: true });
|
|
757
|
-
await writeFile(join(testVaultPath, oldPath), replacement);
|
|
758
|
-
await writeFile(join(testVaultPath, newPath), Buffer.from([0x00]));
|
|
759
|
-
const result = await fileSystem.moveFile({
|
|
760
|
-
oldPath,
|
|
761
|
-
newPath,
|
|
762
|
-
confirmOldPath: oldPath,
|
|
763
|
-
confirmNewPath: newPath,
|
|
764
|
-
overwrite: true
|
|
765
|
-
});
|
|
766
|
-
expect(result.success).toBe(true);
|
|
767
|
-
const moved = await readFile(join(testVaultPath, newPath));
|
|
768
|
-
expect(Buffer.compare(moved, replacement)).toBe(0);
|
|
769
|
-
});
|
|
770
|
-
test("move_file rejects directory sources", async () => {
|
|
771
|
-
await mkdir(join(testVaultPath, "attachments/folder"), { recursive: true });
|
|
772
|
-
const result = await fileSystem.moveFile({
|
|
773
|
-
oldPath: "attachments/folder",
|
|
774
|
-
newPath: "assets/folder",
|
|
775
|
-
confirmOldPath: "attachments/folder",
|
|
776
|
-
confirmNewPath: "assets/folder"
|
|
777
|
-
});
|
|
778
|
-
expect(result.success).toBe(false);
|
|
779
|
-
expect(result.message).toContain("supports files only");
|
|
780
|
-
});
|
|
781
|
-
test("move_file blocks restricted system paths", async () => {
|
|
782
|
-
const result = await fileSystem.moveFile({
|
|
783
|
-
oldPath: ".obsidian/plugins/data.json",
|
|
784
|
-
newPath: "assets/data.json",
|
|
785
|
-
confirmOldPath: ".obsidian/plugins/data.json",
|
|
786
|
-
confirmNewPath: "assets/data.json"
|
|
787
|
-
});
|
|
788
|
-
expect(result.success).toBe(false);
|
|
789
|
-
expect(result.message).toContain("Access denied");
|
|
790
|
-
});
|
|
791
|
-
test("move_file requires matching confirmation paths", async () => {
|
|
792
|
-
const oldPath = "attachments/check.png";
|
|
793
|
-
const newPath = "assets/check.png";
|
|
794
|
-
await mkdir(join(testVaultPath, "attachments"), { recursive: true });
|
|
795
|
-
await writeFile(join(testVaultPath, oldPath), Buffer.from([0x11, 0x22]));
|
|
796
|
-
const result = await fileSystem.moveFile({
|
|
797
|
-
oldPath,
|
|
798
|
-
newPath,
|
|
799
|
-
confirmOldPath: "attachments/other.png",
|
|
800
|
-
confirmNewPath: newPath
|
|
801
|
-
});
|
|
802
|
-
expect(result.success).toBe(false);
|
|
803
|
-
expect(result.message).toContain("confirmation paths do not match");
|
|
804
|
-
const stillExists = await readFile(join(testVaultPath, oldPath));
|
|
805
|
-
expect(Buffer.compare(stillExists, Buffer.from([0x11, 0x22]))).toBe(0);
|
|
806
|
-
});
|
|
807
|
-
test("patch note with regex special chars in oldString", async () => {
|
|
808
|
-
const testPath = "regex-test.md";
|
|
809
|
-
const content = "Price: $10.50 (discount)";
|
|
810
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
811
|
-
const result = await fileSystem.patchNote({
|
|
812
|
-
path: testPath,
|
|
813
|
-
oldString: "$10.50 (discount)",
|
|
814
|
-
newString: "$15.00 (regular)",
|
|
815
|
-
replaceAll: false
|
|
816
|
-
});
|
|
817
|
-
expect(result.success).toBe(true);
|
|
818
|
-
const note = await fileSystem.readNote(testPath);
|
|
819
|
-
expect(note.content).toContain("$15.00 (regular)");
|
|
820
|
-
});
|
|
821
|
-
// Note: searchNotes is in SearchService, not FileSystemService
|
|
822
|
-
// Search tests with regex special chars should be in search.test.ts
|
|
823
|
-
// ============================================================================
|
|
824
|
-
// UNICODE AND INTERNATIONAL PATHS
|
|
825
|
-
// ============================================================================
|
|
826
|
-
test("handles unicode in file paths", async () => {
|
|
827
|
-
const testPath = "日本語/ノート.md";
|
|
828
|
-
const content = "# Japanese note";
|
|
829
|
-
await mkdir(join(testVaultPath, "日本語"), { recursive: true });
|
|
830
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
831
|
-
const note = await fileSystem.readNote(testPath);
|
|
832
|
-
expect(note.content).toContain("Japanese note");
|
|
833
|
-
});
|
|
834
|
-
test("handles emoji in file paths", async () => {
|
|
835
|
-
const testPath = "📁/🎉.md";
|
|
836
|
-
const content = "# Emoji note";
|
|
837
|
-
await mkdir(join(testVaultPath, "📁"), { recursive: true });
|
|
838
|
-
await writeFile(join(testVaultPath, testPath), content);
|
|
839
|
-
const note = await fileSystem.readNote(testPath);
|
|
840
|
-
expect(note.content).toContain("Emoji note");
|
|
841
|
-
});
|
|
842
|
-
// ============================================================================
|
|
843
|
-
// VAULT STATS TESTS
|
|
844
|
-
// ============================================================================
|
|
845
|
-
test("get vault stats with empty vault", async () => {
|
|
846
|
-
const stats = await fileSystem.getVaultStats();
|
|
847
|
-
expect(stats.totalNotes).toBe(0);
|
|
848
|
-
expect(stats.totalFolders).toBe(0);
|
|
849
|
-
expect(stats.totalSize).toBe(0);
|
|
850
|
-
expect(stats.recentlyModified).toHaveLength(0);
|
|
851
|
-
});
|
|
852
|
-
test("get vault stats counts notes and folders", async () => {
|
|
853
|
-
await mkdir(join(testVaultPath, "folder1"), { recursive: true });
|
|
854
|
-
await mkdir(join(testVaultPath, "folder2/nested"), { recursive: true });
|
|
855
|
-
await writeFile(join(testVaultPath, "note1.md"), "# Note 1");
|
|
856
|
-
await writeFile(join(testVaultPath, "folder1/note2.md"), "# Note 2");
|
|
857
|
-
await writeFile(join(testVaultPath, "folder2/nested/note3.md"), "# Note 3");
|
|
858
|
-
const stats = await fileSystem.getVaultStats();
|
|
859
|
-
expect(stats.totalNotes).toBe(3);
|
|
860
|
-
expect(stats.totalFolders).toBe(3); // folder1, folder2, folder2/nested
|
|
861
|
-
expect(stats.totalSize).toBeGreaterThan(0);
|
|
862
|
-
});
|
|
863
|
-
test("get vault stats returns recently modified files in order", async () => {
|
|
864
|
-
// Create files with slight delays to ensure different modification times
|
|
865
|
-
await writeFile(join(testVaultPath, "old.md"), "# Old");
|
|
866
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
867
|
-
await writeFile(join(testVaultPath, "middle.md"), "# Middle");
|
|
868
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
869
|
-
await writeFile(join(testVaultPath, "recent.md"), "# Recent");
|
|
870
|
-
const stats = await fileSystem.getVaultStats(3);
|
|
871
|
-
expect(stats.recentlyModified).toHaveLength(3);
|
|
872
|
-
expect(stats.recentlyModified[0]?.path).toBe("recent.md");
|
|
873
|
-
expect(stats.recentlyModified[1]?.path).toBe("middle.md");
|
|
874
|
-
expect(stats.recentlyModified[2]?.path).toBe("old.md");
|
|
875
|
-
});
|
|
876
|
-
test("get vault stats respects recentCount limit", async () => {
|
|
877
|
-
await writeFile(join(testVaultPath, "note1.md"), "# Note 1");
|
|
878
|
-
await writeFile(join(testVaultPath, "note2.md"), "# Note 2");
|
|
879
|
-
await writeFile(join(testVaultPath, "note3.md"), "# Note 3");
|
|
880
|
-
const stats = await fileSystem.getVaultStats(2);
|
|
881
|
-
expect(stats.recentlyModified).toHaveLength(2);
|
|
882
|
-
});
|
|
883
|
-
test("get vault stats excludes filtered paths", async () => {
|
|
884
|
-
await mkdir(join(testVaultPath, ".obsidian"), { recursive: true });
|
|
885
|
-
await mkdir(join(testVaultPath, ".git"), { recursive: true });
|
|
886
|
-
await writeFile(join(testVaultPath, ".obsidian/config.json"), "{}");
|
|
887
|
-
await writeFile(join(testVaultPath, ".git/config"), "git config");
|
|
888
|
-
await writeFile(join(testVaultPath, "visible.md"), "# Visible");
|
|
889
|
-
const stats = await fileSystem.getVaultStats();
|
|
890
|
-
expect(stats.totalNotes).toBe(1);
|
|
891
|
-
expect(stats.totalFolders).toBe(0); // .obsidian and .git are filtered
|
|
892
|
-
expect(stats.recentlyModified.map(f => f.path)).toContain("visible.md");
|
|
893
|
-
expect(stats.recentlyModified.map(f => f.path)).not.toContain(".obsidian/config.json");
|
|
894
|
-
});
|
|
895
|
-
test("get vault stats excludes files matched by custom ** ignored patterns", async () => {
|
|
896
|
-
const customFilter = new PathFilter({
|
|
897
|
-
ignoredPatterns: ["ignored/**"]
|
|
898
|
-
});
|
|
899
|
-
const customFileSystem = new FileSystemService(testVaultPath, customFilter);
|
|
900
|
-
await mkdir(join(testVaultPath, "ignored"), { recursive: true });
|
|
901
|
-
await mkdir(join(testVaultPath, "ignored/nested"), { recursive: true });
|
|
902
|
-
await writeFile(join(testVaultPath, "ignored/something.md"), "# Disallowed 1");
|
|
903
|
-
await writeFile(join(testVaultPath, "ignored/nested/something.md"), "# Disallowed 2");
|
|
904
|
-
await writeFile(join(testVaultPath, "visible.md"), "# Visible");
|
|
905
|
-
const stats = await customFileSystem.getVaultStats(10);
|
|
906
|
-
const recentPaths = stats.recentlyModified.map(file => file.path);
|
|
907
|
-
expect(stats.totalNotes).toBe(1);
|
|
908
|
-
expect(recentPaths).toContain("visible.md");
|
|
909
|
-
expect(recentPaths).not.toContain("ignored/something.md");
|
|
910
|
-
expect(recentPaths).not.toContain("ignored/nested/something.md");
|
|
911
|
-
});
|
|
912
|
-
test("get vault stats includes notes inside directories that contain dots", async () => {
|
|
913
|
-
await mkdir(join(testVaultPath, "2026.03"), { recursive: true });
|
|
914
|
-
await writeFile(join(testVaultPath, "2026.03/nested.md"), "# Nested");
|
|
915
|
-
await writeFile(join(testVaultPath, "root.md"), "# Root");
|
|
916
|
-
const stats = await fileSystem.getVaultStats(10);
|
|
917
|
-
const recentPaths = stats.recentlyModified.map(file => file.path);
|
|
918
|
-
expect(stats.totalNotes).toBe(2);
|
|
919
|
-
expect(stats.totalFolders).toBe(1);
|
|
920
|
-
expect(recentPaths).toContain("2026.03/nested.md");
|
|
921
|
-
expect(recentPaths).toContain("root.md");
|
|
922
|
-
});
|
|
923
|
-
test("get vault stats calculates total size correctly", async () => {
|
|
924
|
-
const content1 = "# Note 1 with some content";
|
|
925
|
-
const content2 = "# Note 2 with more content here";
|
|
926
|
-
await writeFile(join(testVaultPath, "note1.md"), content1);
|
|
927
|
-
await writeFile(join(testVaultPath, "note2.md"), content2);
|
|
928
|
-
const stats = await fileSystem.getVaultStats();
|
|
929
|
-
const expectedSize = Buffer.byteLength(content1) + Buffer.byteLength(content2);
|
|
930
|
-
expect(stats.totalSize).toBe(expectedSize);
|
|
931
|
-
});
|
|
932
|
-
// ============================================================================
|
|
933
|
-
// ERROR MESSAGE TESTS
|
|
934
|
-
// ============================================================================
|
|
935
|
-
test("error messages include remediation suggestions for file not found", async () => {
|
|
936
|
-
await expect(fileSystem.readNote("nonexistent.md"))
|
|
937
|
-
.rejects.toThrow(/list_directory/);
|
|
938
|
-
});
|
|
939
|
-
test("error messages include remediation suggestions for access denied", async () => {
|
|
940
|
-
await expect(fileSystem.readNote(".obsidian/config.json"))
|
|
941
|
-
.rejects.toThrow(/restricted/);
|
|
942
|
-
});
|
|
943
|
-
test("error messages include remediation suggestions for path traversal", async () => {
|
|
944
|
-
await expect(fileSystem.readNote("../outside.md"))
|
|
945
|
-
.rejects.toThrow(/within the vault/);
|
|
946
|
-
});
|