vault.test.mjs file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:6 feat(calendar): enforce agent context tiers in retrieval API (Phase 1E)… · aaronrene · Jun 18, 2026
1 /**
2 * Vault tests: listMarkdownFiles, readNote, parseFrontmatterAndBody, resolveVaultRelativePath, noteFileExistsInVault, normalizeSlug, normalizeTags.
3 */
4 import { describe, it } from 'node:test';
5 import assert from 'node:assert';
6 import fs from 'fs';
7 import os from 'os';
8 import path from 'path';
9 import { fileURLToPath } from 'url';
10 import {
11 listMarkdownFiles,
12 readNote,
13 parseFrontmatterAndBody,
14 resolveVaultRelativePath,
15 normalizeSlug,
16 normalizeTags,
17 noteFileExistsInVault,
18 listVaultFolderOptions,
19 effectiveProjectSlug,
20 } from '../lib/vault.mjs';
21
22 const __dirname = path.dirname(fileURLToPath(import.meta.url));
23 const vaultPath = path.join(__dirname, 'fixtures', 'vault-fs');
24
25 describe('vault', () => {
26 describe('listMarkdownFiles', () => {
27 it('returns vault-relative paths for all .md files', () => {
28 const paths = listMarkdownFiles(vaultPath);
29 assert(Array.isArray(paths));
30 assert(paths.length >= 3);
31 assert(paths.some((p) => p === 'inbox/one.md'));
32 assert(paths.some((p) => p === 'inbox/two.md'));
33 assert(paths.some((p) => p === 'projects/foo/note.md'));
34 paths.forEach((p) => assert(!p.includes('\\')));
35 });
36
37 it('respects ignore option', () => {
38 const paths = listMarkdownFiles(vaultPath, { ignore: ['inbox'] });
39 assert(!paths.some((p) => p.startsWith('inbox/')));
40 assert(paths.some((p) => p === 'projects/foo/note.md'));
41 });
42 });
43
44 describe('readNote', () => {
45 it('returns parsed note with path, project, tags, date', () => {
46 const note = readNote(vaultPath, 'inbox/one.md');
47 assert.strictEqual(note.path, 'inbox/one.md');
48 assert.strictEqual(note.project, 'foo');
49 assert.deepStrictEqual(note.tags, ['a', 'b']);
50 assert(note.date && note.date.startsWith('2025'));
51 assert(note.body && note.body.includes('Inbox one'));
52 });
53
54 it('throws for path that escapes vault', () => {
55 assert.throws(
56 () => readNote(vaultPath, '../../../etc/passwd'),
57 /Invalid path|escapes vault/
58 );
59 });
60
61 it('throws for non-existent file', () => {
62 assert.throws(
63 () => readNote(vaultPath, 'inbox/nonexistent.md'),
64 /Note not found/
65 );
66 });
67
68 it('security: rejects symlinks that resolve outside the vault', () => {
69 const root = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-vault-symlink-root-'));
70 const outside = fs.mkdtempSync(path.join(os.tmpdir(), 'knowtation-vault-symlink-outside-'));
71 try {
72 fs.mkdirSync(path.join(root, 'inbox'), { recursive: true });
73 fs.writeFileSync(path.join(outside, 'secret.md'), '# Secret\n\noutside vault');
74 fs.symlinkSync(path.join(outside, 'secret.md'), path.join(root, 'inbox', 'linked.md'));
75
76 assert.throws(
77 () => readNote(root, 'inbox/linked.md'),
78 /Invalid path|escapes vault|Note not found/
79 );
80 assert.strictEqual(noteFileExistsInVault(root, 'inbox/linked.md'), false);
81 } finally {
82 fs.rmSync(root, { recursive: true, force: true });
83 fs.rmSync(outside, { recursive: true, force: true });
84 }
85 });
86 });
87
88 describe('parseFrontmatterAndBody', () => {
89 it('parses YAML frontmatter and body', () => {
90 const content = '---\ntitle: Hi\ndate: 2025-01-01\n---\n\n# Hello';
91 const { frontmatter, body } = parseFrontmatterAndBody(content);
92 assert.strictEqual(frontmatter.title, 'Hi');
93 const dateStr = frontmatter.date instanceof Date ? frontmatter.date.toISOString().slice(0, 10) : String(frontmatter.date);
94 assert.strictEqual(dateStr.slice(0, 10), '2025-01-01');
95 assert.strictEqual(body.trim(), '# Hello');
96 });
97
98 it('returns empty frontmatter when no fence', () => {
99 const { frontmatter, body } = parseFrontmatterAndBody('# No frontmatter');
100 assert.deepStrictEqual(frontmatter, {});
101 assert.strictEqual(body.trim(), '# No frontmatter');
102 });
103 });
104
105 describe('resolveVaultRelativePath', () => {
106 it('normalizes and returns vault-relative path', () => {
107 const out = resolveVaultRelativePath(vaultPath, 'inbox/foo.md');
108 assert.strictEqual(out, 'inbox/foo.md');
109 });
110
111 it('rejects path that escapes vault', () => {
112 assert.throws(
113 () => resolveVaultRelativePath(vaultPath, '../other/foo.md'),
114 /Invalid path|escapes vault/
115 );
116 });
117
118 it('rejects absolute path', () => {
119 assert.throws(
120 () => resolveVaultRelativePath(vaultPath, '/tmp/foo.md'),
121 /Invalid path/
122 );
123 });
124 });
125
126 describe('noteFileExistsInVault', () => {
127 it('returns true for an existing note path', () => {
128 assert.strictEqual(noteFileExistsInVault(vaultPath, 'inbox/one.md'), true);
129 });
130
131 it('returns false for missing file', () => {
132 assert.strictEqual(noteFileExistsInVault(vaultPath, 'inbox/ghost.md'), false);
133 });
134
135 it('returns false for empty or invalid input', () => {
136 assert.strictEqual(noteFileExistsInVault(vaultPath, ''), false);
137 assert.strictEqual(noteFileExistsInVault(vaultPath, ' '), false);
138 assert.strictEqual(noteFileExistsInVault(vaultPath, null), false);
139 });
140
141 it('returns false for escape paths without throwing', () => {
142 assert.strictEqual(noteFileExistsInVault(vaultPath, '../../../etc/passwd'), false);
143 });
144 });
145
146 describe('listVaultFolderOptions', () => {
147 it('returns inbox first then top-level and projects/* subdirs sorted', () => {
148 const root = fs.mkdtempSync(path.join(__dirname, 'fixtures', 'tmp-vault-folders-'));
149 try {
150 fs.mkdirSync(path.join(root, 'inbox'));
151 fs.mkdirSync(path.join(root, 'media'));
152 fs.mkdirSync(path.join(root, 'projects', 'born-free'), { recursive: true });
153 const o = listVaultFolderOptions(root);
154 assert.deepStrictEqual(o, ['inbox', 'media', 'projects', 'projects/born-free']);
155 } finally {
156 try {
157 fs.rmSync(root, { recursive: true });
158 } catch (_) {}
159 }
160 });
161
162 it('returns at least inbox for missing path', () => {
163 assert.deepStrictEqual(listVaultFolderOptions('/no/such/vault/path'), ['inbox']);
164 });
165 });
166
167 describe('normalizeSlug', () => {
168 it('lowercases and keeps only a-z0-9 and hyphen', () => {
169 assert.strictEqual(normalizeSlug('Foo Bar'), 'foo-bar');
170 assert.strictEqual(normalizeSlug(' xYz '), 'xyz');
171 });
172 });
173
174 describe('normalizeTags', () => {
175 it('accepts array and returns normalized array', () => {
176 assert.deepStrictEqual(normalizeTags(['A', 'b']), ['a', 'b']);
177 });
178 it('accepts comma-sep string', () => {
179 assert.deepStrictEqual(normalizeTags('x, Y'), ['x', 'y']);
180 });
181 });
182
183 describe('effectiveProjectSlug', () => {
184 it('uses normalized frontmatter project when set', () => {
185 assert.strictEqual(effectiveProjectSlug('inbox/x.md', { project: 'My Acme' }), 'my-acme');
186 });
187 it('infers from projects/<slug>/ when frontmatter project absent', () => {
188 assert.strictEqual(effectiveProjectSlug('projects/born-free/inbox/n.md', {}), 'born-free');
189 });
190 it('returns undefined when no fm project and path not under projects/', () => {
191 assert.strictEqual(effectiveProjectSlug('inbox/x.md', {}), undefined);
192 });
193 });
194 });