@noteplanco/noteplan-mcp 1.1.6 → 1.1.8
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/dist/noteplan/embeddings.d.ts +8 -0
- package/dist/noteplan/embeddings.d.ts.map +1 -1
- package/dist/noteplan/embeddings.js +3 -3
- package/dist/noteplan/embeddings.js.map +1 -1
- package/dist/noteplan/file-reader.d.ts +6 -0
- package/dist/noteplan/file-reader.d.ts.map +1 -1
- package/dist/noteplan/file-reader.js +15 -0
- package/dist/noteplan/file-reader.js.map +1 -1
- package/dist/noteplan/file-writer.d.ts.map +1 -1
- package/dist/noteplan/file-writer.js +86 -17
- package/dist/noteplan/file-writer.js.map +1 -1
- package/dist/noteplan/file-writer.test.d.ts +2 -0
- package/dist/noteplan/file-writer.test.d.ts.map +1 -0
- package/dist/noteplan/file-writer.test.js +896 -0
- package/dist/noteplan/file-writer.test.js.map +1 -0
- package/dist/noteplan/filter-store.d.ts.map +1 -1
- package/dist/noteplan/filter-store.js +13 -1
- package/dist/noteplan/filter-store.js.map +1 -1
- package/dist/noteplan/frontmatter-parser.d.ts +10 -1
- package/dist/noteplan/frontmatter-parser.d.ts.map +1 -1
- package/dist/noteplan/frontmatter-parser.js +59 -6
- package/dist/noteplan/frontmatter-parser.js.map +1 -1
- package/dist/noteplan/frontmatter-parser.test.js +576 -1
- package/dist/noteplan/frontmatter-parser.test.js.map +1 -1
- package/dist/noteplan/markdown-parser.d.ts +6 -1
- package/dist/noteplan/markdown-parser.d.ts.map +1 -1
- package/dist/noteplan/markdown-parser.js +25 -46
- package/dist/noteplan/markdown-parser.js.map +1 -1
- package/dist/noteplan/markdown-parser.test.d.ts +2 -0
- package/dist/noteplan/markdown-parser.test.d.ts.map +1 -0
- package/dist/noteplan/markdown-parser.test.js +690 -0
- package/dist/noteplan/markdown-parser.test.js.map +1 -0
- package/dist/noteplan/template-docs.d.ts +35 -0
- package/dist/noteplan/template-docs.d.ts.map +1 -0
- package/dist/noteplan/template-docs.js +184 -0
- package/dist/noteplan/template-docs.js.map +1 -0
- package/dist/noteplan/unified-store.d.ts +2 -0
- package/dist/noteplan/unified-store.d.ts.map +1 -1
- package/dist/noteplan/unified-store.js +22 -6
- package/dist/noteplan/unified-store.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +661 -241
- package/dist/server.js.map +1 -1
- package/dist/tools/attachments.d.ts +151 -0
- package/dist/tools/attachments.d.ts.map +1 -0
- package/dist/tools/attachments.js +421 -0
- package/dist/tools/attachments.js.map +1 -0
- package/dist/tools/attachments.test.d.ts +2 -0
- package/dist/tools/attachments.test.d.ts.map +1 -0
- package/dist/tools/attachments.test.js +561 -0
- package/dist/tools/attachments.test.js.map +1 -0
- package/dist/tools/calendar.d.ts +7 -7
- package/dist/tools/notes.d.ts +148 -48
- package/dist/tools/notes.d.ts.map +1 -1
- package/dist/tools/notes.js +366 -29
- package/dist/tools/notes.js.map +1 -1
- package/dist/tools/notes.test.d.ts +2 -0
- package/dist/tools/notes.test.d.ts.map +1 -0
- package/dist/tools/notes.test.js +800 -0
- package/dist/tools/notes.test.js.map +1 -0
- package/dist/tools/plugins.d.ts.map +1 -1
- package/dist/tools/plugins.js +1 -0
- package/dist/tools/plugins.js.map +1 -1
- package/dist/tools/reminders.d.ts +4 -4
- package/dist/tools/search.d.ts +2 -2
- package/dist/tools/search.d.ts.map +1 -1
- package/dist/tools/search.js +32 -4
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/tasks.d.ts +10 -10
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +14 -27
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/templates.d.ts +130 -0
- package/dist/tools/templates.d.ts.map +1 -0
- package/dist/tools/templates.js +217 -0
- package/dist/tools/templates.js.map +1 -0
- package/dist/tools/templates.test.d.ts +2 -0
- package/dist/tools/templates.test.d.ts.map +1 -0
- package/dist/tools/templates.test.js +48 -0
- package/dist/tools/templates.test.js.map +1 -0
- package/dist/tools/ui.d.ts +2 -0
- package/dist/tools/ui.d.ts.map +1 -1
- package/dist/tools/ui.js +24 -0
- package/dist/tools/ui.js.map +1 -1
- package/dist/utils/applescript.d.ts.map +1 -1
- package/dist/utils/applescript.js +21 -0
- package/dist/utils/applescript.js.map +1 -1
- package/dist/utils/confirmation-tokens.test.d.ts +2 -0
- package/dist/utils/confirmation-tokens.test.d.ts.map +1 -0
- package/dist/utils/confirmation-tokens.test.js +159 -0
- package/dist/utils/confirmation-tokens.test.js.map +1 -0
- package/dist/utils/version.d.ts +3 -0
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +71 -23
- package/dist/utils/version.js.map +1 -1
- package/docs/templates.db.gz +0 -0
- package/docs/x-callback-url.md +318 -0
- package/package.json +1 -1
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Copies of private helper functions from notes.ts
|
|
4
|
+
// These are verbatim copies so we can unit-test them without exporting.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function toBoundedInt(value, defaultValue, min, max) {
|
|
7
|
+
const numeric = typeof value === 'number' ? value : Number(value);
|
|
8
|
+
if (!Number.isFinite(numeric))
|
|
9
|
+
return defaultValue;
|
|
10
|
+
return Math.min(max, Math.max(min, Math.floor(numeric)));
|
|
11
|
+
}
|
|
12
|
+
function isDebugTimingsEnabled(value) {
|
|
13
|
+
if (typeof value === 'boolean')
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value === 'string') {
|
|
16
|
+
const normalized = value.trim().toLowerCase();
|
|
17
|
+
return normalized === 'true' || normalized === '1';
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
function toOptionalBoolean(value) {
|
|
22
|
+
if (value === undefined || value === null || value === '')
|
|
23
|
+
return undefined;
|
|
24
|
+
if (typeof value === 'boolean')
|
|
25
|
+
return value;
|
|
26
|
+
if (typeof value === 'string') {
|
|
27
|
+
const normalized = value.trim().toLowerCase();
|
|
28
|
+
if (normalized === 'true')
|
|
29
|
+
return true;
|
|
30
|
+
if (normalized === 'false')
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function normalizeIndentationStyle(value) {
|
|
36
|
+
if (value === 'preserve')
|
|
37
|
+
return 'preserve';
|
|
38
|
+
return 'tabs';
|
|
39
|
+
}
|
|
40
|
+
function retabListIndentation(content) {
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
let linesRetabbed = 0;
|
|
43
|
+
const normalized = lines.map((line) => {
|
|
44
|
+
const match = line.match(/^( +)(?=(?:[*+-]|\d+[.)])(?:\s|\t|\[))/);
|
|
45
|
+
if (!match)
|
|
46
|
+
return line;
|
|
47
|
+
const spaceCount = match[1].length;
|
|
48
|
+
if (spaceCount < 2)
|
|
49
|
+
return line;
|
|
50
|
+
const tabs = '\t'.repeat(Math.floor(spaceCount / 2));
|
|
51
|
+
linesRetabbed += 1;
|
|
52
|
+
return `${tabs}${line.slice(spaceCount)}`;
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
content: normalized.join('\n'),
|
|
56
|
+
linesRetabbed,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function extractAttachmentReferences(text) {
|
|
60
|
+
const matches = text.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g);
|
|
61
|
+
const refs = new Set();
|
|
62
|
+
for (const match of matches) {
|
|
63
|
+
const ref = (match[1] || '').trim();
|
|
64
|
+
if (!ref)
|
|
65
|
+
continue;
|
|
66
|
+
refs.add(ref);
|
|
67
|
+
}
|
|
68
|
+
return Array.from(refs);
|
|
69
|
+
}
|
|
70
|
+
function getRemovedAttachmentReferences(beforeText, afterText) {
|
|
71
|
+
const before = new Set(extractAttachmentReferences(beforeText));
|
|
72
|
+
const after = new Set(extractAttachmentReferences(afterText));
|
|
73
|
+
return Array.from(before).filter((ref) => !after.has(ref));
|
|
74
|
+
}
|
|
75
|
+
function buildLineWindow(allLines, options) {
|
|
76
|
+
const totalLineCount = allLines.length;
|
|
77
|
+
const requestedStartLine = toBoundedInt(options.startLine, 1, 1, Math.max(1, totalLineCount));
|
|
78
|
+
const requestedEndLine = toBoundedInt(options.endLine, totalLineCount, requestedStartLine, Math.max(requestedStartLine, totalLineCount));
|
|
79
|
+
const rangeStartIndex = requestedStartLine - 1;
|
|
80
|
+
const rangeEndIndexExclusive = requestedEndLine;
|
|
81
|
+
const rangeLines = allLines.slice(rangeStartIndex, rangeEndIndexExclusive);
|
|
82
|
+
const offset = toBoundedInt(options.cursor ?? options.offset, 0, 0, Number.MAX_SAFE_INTEGER);
|
|
83
|
+
const limit = toBoundedInt(options.limit, options.defaultLimit, 1, options.maxLimit);
|
|
84
|
+
const page = rangeLines.slice(offset, offset + limit);
|
|
85
|
+
const hasMore = offset + page.length < rangeLines.length;
|
|
86
|
+
const nextCursor = hasMore ? String(offset + page.length) : null;
|
|
87
|
+
return {
|
|
88
|
+
lineCount: totalLineCount,
|
|
89
|
+
rangeStartLine: requestedStartLine,
|
|
90
|
+
rangeEndLine: requestedEndLine,
|
|
91
|
+
rangeLineCount: rangeLines.length,
|
|
92
|
+
returnedLineCount: page.length,
|
|
93
|
+
offset,
|
|
94
|
+
limit,
|
|
95
|
+
hasMore,
|
|
96
|
+
nextCursor,
|
|
97
|
+
content: page.join('\n'),
|
|
98
|
+
lines: page.map((content, index) => ({
|
|
99
|
+
line: requestedStartLine + offset + index,
|
|
100
|
+
lineIndex: rangeStartIndex + offset + index,
|
|
101
|
+
content,
|
|
102
|
+
})),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function normalizeDateToken(value) {
|
|
106
|
+
if (!value)
|
|
107
|
+
return null;
|
|
108
|
+
const digits = value.replace(/\D/g, '');
|
|
109
|
+
return digits.length === 8 ? digits : null;
|
|
110
|
+
}
|
|
111
|
+
function noteMatchScore(note, query, queryDateToken) {
|
|
112
|
+
const queryLower = query.toLowerCase();
|
|
113
|
+
const idLower = (note.id || '').toLowerCase();
|
|
114
|
+
const titleLower = (note.title || '').toLowerCase();
|
|
115
|
+
const filenameLower = (note.filename || '').toLowerCase();
|
|
116
|
+
const path_basename = filenameLower.split('/').pop() || '';
|
|
117
|
+
const path_extname_idx = path_basename.lastIndexOf('.');
|
|
118
|
+
const basenameLower = path_extname_idx > 0 ? path_basename.slice(0, path_extname_idx) : path_basename;
|
|
119
|
+
const noteDateToken = normalizeDateToken(note.date);
|
|
120
|
+
if (idLower && idLower === queryLower)
|
|
121
|
+
return 1.0;
|
|
122
|
+
if (filenameLower === queryLower)
|
|
123
|
+
return 0.99;
|
|
124
|
+
if (basenameLower === queryLower)
|
|
125
|
+
return 0.97;
|
|
126
|
+
if (titleLower === queryLower)
|
|
127
|
+
return 0.96;
|
|
128
|
+
if (queryDateToken && noteDateToken && queryDateToken === noteDateToken)
|
|
129
|
+
return 0.95;
|
|
130
|
+
if (titleLower.startsWith(queryLower))
|
|
131
|
+
return 0.9;
|
|
132
|
+
if (basenameLower.startsWith(queryLower))
|
|
133
|
+
return 0.88;
|
|
134
|
+
if (filenameLower.includes(`/${queryLower}`) || filenameLower.includes(queryLower))
|
|
135
|
+
return 0.83;
|
|
136
|
+
if (`${titleLower} ${filenameLower}`.includes(queryLower))
|
|
137
|
+
return 0.76;
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
function findParagraphBounds(lines, lineIndex) {
|
|
141
|
+
let startIndex = lineIndex;
|
|
142
|
+
while (startIndex > 0 && lines[startIndex - 1].trim() !== '') {
|
|
143
|
+
startIndex -= 1;
|
|
144
|
+
}
|
|
145
|
+
let endIndex = lineIndex;
|
|
146
|
+
while (endIndex < lines.length - 1 && lines[endIndex + 1].trim() !== '') {
|
|
147
|
+
endIndex += 1;
|
|
148
|
+
}
|
|
149
|
+
return { startIndex, endIndex };
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Tests
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
describe('toBoundedInt', () => {
|
|
155
|
+
it('returns default for NaN', () => {
|
|
156
|
+
expect(toBoundedInt(NaN, 10, 0, 100)).toBe(10);
|
|
157
|
+
});
|
|
158
|
+
it('returns default for undefined', () => {
|
|
159
|
+
expect(toBoundedInt(undefined, 10, 0, 100)).toBe(10);
|
|
160
|
+
});
|
|
161
|
+
it('returns clamped value for null (Number(null) === 0)', () => {
|
|
162
|
+
// Number(null) === 0 which is finite, so it gets clamped to [min, max]
|
|
163
|
+
expect(toBoundedInt(null, 10, 0, 100)).toBe(0);
|
|
164
|
+
expect(toBoundedInt(null, 10, 5, 100)).toBe(5);
|
|
165
|
+
});
|
|
166
|
+
it('returns default for Infinity', () => {
|
|
167
|
+
expect(toBoundedInt(Infinity, 10, 0, 100)).toBe(10);
|
|
168
|
+
});
|
|
169
|
+
it('returns default for -Infinity', () => {
|
|
170
|
+
expect(toBoundedInt(-Infinity, 10, 0, 100)).toBe(10);
|
|
171
|
+
});
|
|
172
|
+
it('returns default for non-numeric strings', () => {
|
|
173
|
+
expect(toBoundedInt('hello', 10, 0, 100)).toBe(10);
|
|
174
|
+
});
|
|
175
|
+
it('returns clamped value for empty string (Number("") === 0)', () => {
|
|
176
|
+
// Number('') === 0 which is finite, so it gets clamped to [min, max]
|
|
177
|
+
expect(toBoundedInt('', 10, 0, 100)).toBe(0);
|
|
178
|
+
expect(toBoundedInt('', 10, 5, 100)).toBe(5);
|
|
179
|
+
});
|
|
180
|
+
it('floors decimal numbers', () => {
|
|
181
|
+
expect(toBoundedInt(3.7, 10, 0, 100)).toBe(3);
|
|
182
|
+
expect(toBoundedInt(3.2, 10, 0, 100)).toBe(3);
|
|
183
|
+
expect(toBoundedInt(9.999, 10, 0, 100)).toBe(9);
|
|
184
|
+
});
|
|
185
|
+
it('clamps below min', () => {
|
|
186
|
+
expect(toBoundedInt(-5, 10, 0, 100)).toBe(0);
|
|
187
|
+
expect(toBoundedInt(3, 10, 5, 100)).toBe(5);
|
|
188
|
+
});
|
|
189
|
+
it('clamps above max', () => {
|
|
190
|
+
expect(toBoundedInt(200, 10, 0, 100)).toBe(100);
|
|
191
|
+
expect(toBoundedInt(50, 10, 0, 20)).toBe(20);
|
|
192
|
+
});
|
|
193
|
+
it('accepts numeric strings', () => {
|
|
194
|
+
expect(toBoundedInt('42', 10, 0, 100)).toBe(42);
|
|
195
|
+
expect(toBoundedInt('7', 10, 0, 100)).toBe(7);
|
|
196
|
+
});
|
|
197
|
+
it('accepts actual numbers within range', () => {
|
|
198
|
+
expect(toBoundedInt(42, 10, 0, 100)).toBe(42);
|
|
199
|
+
expect(toBoundedInt(0, 10, 0, 100)).toBe(0);
|
|
200
|
+
expect(toBoundedInt(100, 10, 0, 100)).toBe(100);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('isDebugTimingsEnabled', () => {
|
|
204
|
+
it('returns true for boolean true', () => {
|
|
205
|
+
expect(isDebugTimingsEnabled(true)).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
it('returns false for boolean false', () => {
|
|
208
|
+
expect(isDebugTimingsEnabled(false)).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
it('returns true for string "true" (case-insensitive)', () => {
|
|
211
|
+
expect(isDebugTimingsEnabled('true')).toBe(true);
|
|
212
|
+
expect(isDebugTimingsEnabled('TRUE')).toBe(true);
|
|
213
|
+
expect(isDebugTimingsEnabled('True')).toBe(true);
|
|
214
|
+
expect(isDebugTimingsEnabled(' true ')).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
it('returns true for string "1"', () => {
|
|
217
|
+
expect(isDebugTimingsEnabled('1')).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
it('returns false for string "false", "0", and random strings', () => {
|
|
220
|
+
expect(isDebugTimingsEnabled('false')).toBe(false);
|
|
221
|
+
expect(isDebugTimingsEnabled('0')).toBe(false);
|
|
222
|
+
expect(isDebugTimingsEnabled('random')).toBe(false);
|
|
223
|
+
expect(isDebugTimingsEnabled('yes')).toBe(false);
|
|
224
|
+
});
|
|
225
|
+
it('returns false for undefined, null, and number', () => {
|
|
226
|
+
expect(isDebugTimingsEnabled(undefined)).toBe(false);
|
|
227
|
+
expect(isDebugTimingsEnabled(null)).toBe(false);
|
|
228
|
+
expect(isDebugTimingsEnabled(1)).toBe(false);
|
|
229
|
+
expect(isDebugTimingsEnabled(0)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe('toOptionalBoolean', () => {
|
|
233
|
+
it('returns undefined for undefined', () => {
|
|
234
|
+
expect(toOptionalBoolean(undefined)).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
it('returns undefined for null', () => {
|
|
237
|
+
expect(toOptionalBoolean(null)).toBeUndefined();
|
|
238
|
+
});
|
|
239
|
+
it('returns undefined for empty string', () => {
|
|
240
|
+
expect(toOptionalBoolean('')).toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
it('returns true for boolean true', () => {
|
|
243
|
+
expect(toOptionalBoolean(true)).toBe(true);
|
|
244
|
+
});
|
|
245
|
+
it('returns false for boolean false', () => {
|
|
246
|
+
expect(toOptionalBoolean(false)).toBe(false);
|
|
247
|
+
});
|
|
248
|
+
it('returns true for string "true" (case-insensitive, trimmed)', () => {
|
|
249
|
+
expect(toOptionalBoolean('true')).toBe(true);
|
|
250
|
+
expect(toOptionalBoolean('TRUE')).toBe(true);
|
|
251
|
+
expect(toOptionalBoolean('True')).toBe(true);
|
|
252
|
+
expect(toOptionalBoolean(' true ')).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
it('returns false for string "false" (case-insensitive, trimmed)', () => {
|
|
255
|
+
expect(toOptionalBoolean('false')).toBe(false);
|
|
256
|
+
expect(toOptionalBoolean('FALSE')).toBe(false);
|
|
257
|
+
expect(toOptionalBoolean('False')).toBe(false);
|
|
258
|
+
expect(toOptionalBoolean(' false ')).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
it('returns undefined for unrecognized string', () => {
|
|
261
|
+
expect(toOptionalBoolean('yes')).toBeUndefined();
|
|
262
|
+
expect(toOptionalBoolean('no')).toBeUndefined();
|
|
263
|
+
expect(toOptionalBoolean('1')).toBeUndefined();
|
|
264
|
+
expect(toOptionalBoolean('0')).toBeUndefined();
|
|
265
|
+
expect(toOptionalBoolean('random')).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
describe('normalizeIndentationStyle', () => {
|
|
269
|
+
it('returns "preserve" for "preserve"', () => {
|
|
270
|
+
expect(normalizeIndentationStyle('preserve')).toBe('preserve');
|
|
271
|
+
});
|
|
272
|
+
it('returns "tabs" for "tabs"', () => {
|
|
273
|
+
expect(normalizeIndentationStyle('tabs')).toBe('tabs');
|
|
274
|
+
});
|
|
275
|
+
it('returns "tabs" for anything else', () => {
|
|
276
|
+
expect(normalizeIndentationStyle(undefined)).toBe('tabs');
|
|
277
|
+
expect(normalizeIndentationStyle(null)).toBe('tabs');
|
|
278
|
+
expect(normalizeIndentationStyle('spaces')).toBe('tabs');
|
|
279
|
+
expect(normalizeIndentationStyle(42)).toBe('tabs');
|
|
280
|
+
expect(normalizeIndentationStyle('')).toBe('tabs');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe('retabListIndentation', () => {
|
|
284
|
+
it('converts 2 spaces to 1 tab for * item', () => {
|
|
285
|
+
const result = retabListIndentation(' * item');
|
|
286
|
+
expect(result.content).toBe('\t* item');
|
|
287
|
+
expect(result.linesRetabbed).toBe(1);
|
|
288
|
+
});
|
|
289
|
+
it('converts 4 spaces to 2 tabs for - item', () => {
|
|
290
|
+
const result = retabListIndentation(' - item');
|
|
291
|
+
expect(result.content).toBe('\t\t- item');
|
|
292
|
+
expect(result.linesRetabbed).toBe(1);
|
|
293
|
+
});
|
|
294
|
+
it('converts 6 spaces to 3 tabs for + item', () => {
|
|
295
|
+
const result = retabListIndentation(' + item');
|
|
296
|
+
expect(result.content).toBe('\t\t\t+ item');
|
|
297
|
+
expect(result.linesRetabbed).toBe(1);
|
|
298
|
+
});
|
|
299
|
+
it('works for numbered lists', () => {
|
|
300
|
+
const result = retabListIndentation(' 1. item');
|
|
301
|
+
expect(result.content).toBe('\t1. item');
|
|
302
|
+
expect(result.linesRetabbed).toBe(1);
|
|
303
|
+
});
|
|
304
|
+
it('works for checkbox tasks', () => {
|
|
305
|
+
const result = retabListIndentation(' * [x] done');
|
|
306
|
+
expect(result.content).toBe('\t* [x] done');
|
|
307
|
+
expect(result.linesRetabbed).toBe(1);
|
|
308
|
+
});
|
|
309
|
+
it('handles odd spaces (3) with floor(3/2)=1 tab', () => {
|
|
310
|
+
const result = retabListIndentation(' * item');
|
|
311
|
+
expect(result.content).toBe('\t* item');
|
|
312
|
+
expect(result.linesRetabbed).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
it('does NOT convert 1 space (spaceCount < 2)', () => {
|
|
315
|
+
const result = retabListIndentation(' * item');
|
|
316
|
+
expect(result.content).toBe(' * item');
|
|
317
|
+
expect(result.linesRetabbed).toBe(0);
|
|
318
|
+
});
|
|
319
|
+
it('does NOT convert plain text with leading spaces', () => {
|
|
320
|
+
const result = retabListIndentation(' some plain text');
|
|
321
|
+
expect(result.content).toBe(' some plain text');
|
|
322
|
+
expect(result.linesRetabbed).toBe(0);
|
|
323
|
+
});
|
|
324
|
+
it('does NOT convert lines starting with tabs (already tabbed)', () => {
|
|
325
|
+
const result = retabListIndentation('\t* already tabbed');
|
|
326
|
+
expect(result.content).toBe('\t* already tabbed');
|
|
327
|
+
expect(result.linesRetabbed).toBe(0);
|
|
328
|
+
});
|
|
329
|
+
it('preserves lines with no leading spaces', () => {
|
|
330
|
+
const result = retabListIndentation('* top level item');
|
|
331
|
+
expect(result.content).toBe('* top level item');
|
|
332
|
+
expect(result.linesRetabbed).toBe(0);
|
|
333
|
+
});
|
|
334
|
+
it('handles multi-line content, only retabbing list lines', () => {
|
|
335
|
+
const input = [
|
|
336
|
+
'# Heading',
|
|
337
|
+
' * nested item',
|
|
338
|
+
'Some text',
|
|
339
|
+
' - deep item',
|
|
340
|
+
' plain text not a list',
|
|
341
|
+
].join('\n');
|
|
342
|
+
const result = retabListIndentation(input);
|
|
343
|
+
const expected = [
|
|
344
|
+
'# Heading',
|
|
345
|
+
'\t* nested item',
|
|
346
|
+
'Some text',
|
|
347
|
+
'\t\t- deep item',
|
|
348
|
+
' plain text not a list',
|
|
349
|
+
].join('\n');
|
|
350
|
+
expect(result.content).toBe(expected);
|
|
351
|
+
expect(result.linesRetabbed).toBe(2);
|
|
352
|
+
});
|
|
353
|
+
it('returns correct linesRetabbed count', () => {
|
|
354
|
+
const input = [
|
|
355
|
+
' * one',
|
|
356
|
+
' - two',
|
|
357
|
+
'* three',
|
|
358
|
+
' + four',
|
|
359
|
+
].join('\n');
|
|
360
|
+
const result = retabListIndentation(input);
|
|
361
|
+
expect(result.linesRetabbed).toBe(3);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
describe('extractAttachmentReferences', () => {
|
|
365
|
+
it('extracts a single image reference', () => {
|
|
366
|
+
expect(extractAttachmentReferences('')).toEqual(['path.png']);
|
|
367
|
+
});
|
|
368
|
+
it('extracts multiple references', () => {
|
|
369
|
+
const text = ' some text ';
|
|
370
|
+
expect(extractAttachmentReferences(text)).toEqual(['one.png', 'two.jpg']);
|
|
371
|
+
});
|
|
372
|
+
it('deduplicates identical references', () => {
|
|
373
|
+
const text = ' and ';
|
|
374
|
+
expect(extractAttachmentReferences(text)).toEqual(['same.png']);
|
|
375
|
+
});
|
|
376
|
+
it('returns empty array for no references', () => {
|
|
377
|
+
expect(extractAttachmentReferences('no images here')).toEqual([]);
|
|
378
|
+
expect(extractAttachmentReferences('')).toEqual([]);
|
|
379
|
+
});
|
|
380
|
+
it('handles alt text with spaces and folder paths', () => {
|
|
381
|
+
const text = '';
|
|
382
|
+
expect(extractAttachmentReferences(text)).toEqual(['folder/file.jpg']);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
describe('getRemovedAttachmentReferences', () => {
|
|
386
|
+
it('returns refs present in before but not in after', () => {
|
|
387
|
+
const before = ' ';
|
|
388
|
+
const after = '';
|
|
389
|
+
expect(getRemovedAttachmentReferences(before, after)).toEqual(['two.png']);
|
|
390
|
+
});
|
|
391
|
+
it('returns empty when no refs removed', () => {
|
|
392
|
+
const before = '';
|
|
393
|
+
const after = ' ';
|
|
394
|
+
expect(getRemovedAttachmentReferences(before, after)).toEqual([]);
|
|
395
|
+
});
|
|
396
|
+
it('returns empty when before has no refs', () => {
|
|
397
|
+
const before = 'no images';
|
|
398
|
+
const after = '';
|
|
399
|
+
expect(getRemovedAttachmentReferences(before, after)).toEqual([]);
|
|
400
|
+
});
|
|
401
|
+
it('works with multiple removals', () => {
|
|
402
|
+
const before = '  ';
|
|
403
|
+
const after = '';
|
|
404
|
+
const removed = getRemovedAttachmentReferences(before, after);
|
|
405
|
+
expect(removed).toContain('one.png');
|
|
406
|
+
expect(removed).toContain('three.png');
|
|
407
|
+
expect(removed).not.toContain('two.png');
|
|
408
|
+
expect(removed).toHaveLength(2);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
describe('buildLineWindow', () => {
|
|
412
|
+
const sampleLines = ['line1', 'line2', 'line3', 'line4', 'line5'];
|
|
413
|
+
it('returns full content with defaults', () => {
|
|
414
|
+
const result = buildLineWindow(sampleLines, { defaultLimit: 100, maxLimit: 200 });
|
|
415
|
+
expect(result.lineCount).toBe(5);
|
|
416
|
+
expect(result.rangeStartLine).toBe(1);
|
|
417
|
+
expect(result.rangeEndLine).toBe(5);
|
|
418
|
+
expect(result.rangeLineCount).toBe(5);
|
|
419
|
+
expect(result.returnedLineCount).toBe(5);
|
|
420
|
+
expect(result.content).toBe('line1\nline2\nline3\nline4\nline5');
|
|
421
|
+
expect(result.hasMore).toBe(false);
|
|
422
|
+
expect(result.nextCursor).toBeNull();
|
|
423
|
+
});
|
|
424
|
+
it('selects a range with startLine/endLine (1-indexed)', () => {
|
|
425
|
+
const result = buildLineWindow(sampleLines, {
|
|
426
|
+
startLine: 2,
|
|
427
|
+
endLine: 4,
|
|
428
|
+
defaultLimit: 100,
|
|
429
|
+
maxLimit: 200,
|
|
430
|
+
});
|
|
431
|
+
expect(result.rangeStartLine).toBe(2);
|
|
432
|
+
expect(result.rangeEndLine).toBe(4);
|
|
433
|
+
expect(result.rangeLineCount).toBe(3);
|
|
434
|
+
expect(result.content).toBe('line2\nline3\nline4');
|
|
435
|
+
});
|
|
436
|
+
it('caps output with limit', () => {
|
|
437
|
+
const result = buildLineWindow(sampleLines, {
|
|
438
|
+
defaultLimit: 2,
|
|
439
|
+
maxLimit: 200,
|
|
440
|
+
});
|
|
441
|
+
expect(result.returnedLineCount).toBe(2);
|
|
442
|
+
expect(result.content).toBe('line1\nline2');
|
|
443
|
+
expect(result.hasMore).toBe(true);
|
|
444
|
+
});
|
|
445
|
+
it('skips lines with offset', () => {
|
|
446
|
+
const result = buildLineWindow(sampleLines, {
|
|
447
|
+
offset: 2,
|
|
448
|
+
defaultLimit: 100,
|
|
449
|
+
maxLimit: 200,
|
|
450
|
+
});
|
|
451
|
+
expect(result.offset).toBe(2);
|
|
452
|
+
expect(result.returnedLineCount).toBe(3);
|
|
453
|
+
expect(result.content).toBe('line3\nline4\nline5');
|
|
454
|
+
});
|
|
455
|
+
it('cursor acts as offset', () => {
|
|
456
|
+
const result = buildLineWindow(sampleLines, {
|
|
457
|
+
cursor: '3',
|
|
458
|
+
offset: 0,
|
|
459
|
+
defaultLimit: 100,
|
|
460
|
+
maxLimit: 200,
|
|
461
|
+
});
|
|
462
|
+
expect(result.offset).toBe(3);
|
|
463
|
+
expect(result.returnedLineCount).toBe(2);
|
|
464
|
+
expect(result.content).toBe('line4\nline5');
|
|
465
|
+
});
|
|
466
|
+
it('hasMore=true when more lines available', () => {
|
|
467
|
+
const result = buildLineWindow(sampleLines, {
|
|
468
|
+
limit: 3,
|
|
469
|
+
defaultLimit: 100,
|
|
470
|
+
maxLimit: 200,
|
|
471
|
+
});
|
|
472
|
+
expect(result.hasMore).toBe(true);
|
|
473
|
+
expect(result.returnedLineCount).toBe(3);
|
|
474
|
+
});
|
|
475
|
+
it('nextCursor is string of next offset', () => {
|
|
476
|
+
const result = buildLineWindow(sampleLines, {
|
|
477
|
+
limit: 2,
|
|
478
|
+
defaultLimit: 100,
|
|
479
|
+
maxLimit: 200,
|
|
480
|
+
});
|
|
481
|
+
expect(result.nextCursor).toBe('2');
|
|
482
|
+
});
|
|
483
|
+
it('hasMore=false and nextCursor=null at end', () => {
|
|
484
|
+
const result = buildLineWindow(sampleLines, {
|
|
485
|
+
defaultLimit: 100,
|
|
486
|
+
maxLimit: 200,
|
|
487
|
+
});
|
|
488
|
+
expect(result.hasMore).toBe(false);
|
|
489
|
+
expect(result.nextCursor).toBeNull();
|
|
490
|
+
});
|
|
491
|
+
it('lines array has correct 1-indexed line numbers', () => {
|
|
492
|
+
const result = buildLineWindow(sampleLines, {
|
|
493
|
+
startLine: 2,
|
|
494
|
+
endLine: 4,
|
|
495
|
+
defaultLimit: 100,
|
|
496
|
+
maxLimit: 200,
|
|
497
|
+
});
|
|
498
|
+
expect(result.lines).toEqual([
|
|
499
|
+
{ line: 2, lineIndex: 1, content: 'line2' },
|
|
500
|
+
{ line: 3, lineIndex: 2, content: 'line3' },
|
|
501
|
+
{ line: 4, lineIndex: 3, content: 'line4' },
|
|
502
|
+
]);
|
|
503
|
+
});
|
|
504
|
+
it('handles empty content', () => {
|
|
505
|
+
const result = buildLineWindow([], { defaultLimit: 100, maxLimit: 200 });
|
|
506
|
+
expect(result.lineCount).toBe(0);
|
|
507
|
+
expect(result.rangeLineCount).toBe(0);
|
|
508
|
+
expect(result.returnedLineCount).toBe(0);
|
|
509
|
+
expect(result.content).toBe('');
|
|
510
|
+
expect(result.hasMore).toBe(false);
|
|
511
|
+
expect(result.nextCursor).toBeNull();
|
|
512
|
+
expect(result.lines).toEqual([]);
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
describe('normalizeDateToken', () => {
|
|
516
|
+
it('normalizes "2024-01-15" to "20240115"', () => {
|
|
517
|
+
expect(normalizeDateToken('2024-01-15')).toBe('20240115');
|
|
518
|
+
});
|
|
519
|
+
it('passes through "20240115" unchanged', () => {
|
|
520
|
+
expect(normalizeDateToken('20240115')).toBe('20240115');
|
|
521
|
+
});
|
|
522
|
+
it('returns null for non-8-digit result', () => {
|
|
523
|
+
expect(normalizeDateToken('2024')).toBeNull();
|
|
524
|
+
expect(normalizeDateToken('abc')).toBeNull();
|
|
525
|
+
expect(normalizeDateToken('123456789')).toBeNull();
|
|
526
|
+
});
|
|
527
|
+
it('returns null for undefined/empty', () => {
|
|
528
|
+
expect(normalizeDateToken(undefined)).toBeNull();
|
|
529
|
+
expect(normalizeDateToken('')).toBeNull();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
describe('noteMatchScore', () => {
|
|
533
|
+
it('returns 1.0 for exact id match', () => {
|
|
534
|
+
const note = { id: 'abc-123', title: 'Something', filename: 'something.md' };
|
|
535
|
+
expect(noteMatchScore(note, 'abc-123', null)).toBe(1.0);
|
|
536
|
+
});
|
|
537
|
+
it('returns 0.99 for exact filename match', () => {
|
|
538
|
+
const note = { id: 'xyz', title: 'Something', filename: 'notes/my-note.md' };
|
|
539
|
+
expect(noteMatchScore(note, 'notes/my-note.md', null)).toBe(0.99);
|
|
540
|
+
});
|
|
541
|
+
it('returns 0.97 for exact basename match', () => {
|
|
542
|
+
const note = { id: 'xyz', title: 'Something Else', filename: 'folder/my-note.md' };
|
|
543
|
+
expect(noteMatchScore(note, 'my-note', null)).toBe(0.97);
|
|
544
|
+
});
|
|
545
|
+
it('returns 0.96 for exact title match', () => {
|
|
546
|
+
const note = { id: 'xyz', title: 'My Note Title', filename: 'folder/different.md' };
|
|
547
|
+
expect(noteMatchScore(note, 'My Note Title', null)).toBe(0.96);
|
|
548
|
+
});
|
|
549
|
+
it('returns 0.95 for date token match', () => {
|
|
550
|
+
const note = { id: 'xyz', title: 'Daily', filename: 'cal.md', date: '2024-01-15' };
|
|
551
|
+
const queryDateToken = normalizeDateToken('2024-01-15');
|
|
552
|
+
expect(noteMatchScore(note, '2024-01-15', queryDateToken)).toBe(0.95);
|
|
553
|
+
});
|
|
554
|
+
it('returns 0.9 for title starts with query', () => {
|
|
555
|
+
const note = { id: 'xyz', title: 'Meeting notes for project X', filename: 'other.md' };
|
|
556
|
+
expect(noteMatchScore(note, 'meeting notes', null)).toBe(0.9);
|
|
557
|
+
});
|
|
558
|
+
it('returns 0.88 for basename starts with query', () => {
|
|
559
|
+
const note = { id: 'xyz', title: 'Something Else', filename: 'folder/meeting-recap.md' };
|
|
560
|
+
expect(noteMatchScore(note, 'meeting', null)).toBe(0.88);
|
|
561
|
+
});
|
|
562
|
+
it('returns 0.83 for filename contains query', () => {
|
|
563
|
+
const note = { id: 'xyz', title: 'Unrelated', filename: 'projects/quarterly-review.md' };
|
|
564
|
+
expect(noteMatchScore(note, 'quarterly-review.md', null)).toBe(0.83);
|
|
565
|
+
});
|
|
566
|
+
it('returns 0.76 for combined title+filename contains query', () => {
|
|
567
|
+
const note = { id: 'xyz', title: 'Project', filename: 'folder/notes.md' };
|
|
568
|
+
// The query must span title and filename combined: "project folder/notes.md"
|
|
569
|
+
// actually let's test with something that only matches across the combination
|
|
570
|
+
expect(noteMatchScore(note, 'project folder', null)).toBe(0.76);
|
|
571
|
+
});
|
|
572
|
+
it('returns 0 for no match', () => {
|
|
573
|
+
const note = { id: 'xyz', title: 'Something', filename: 'folder/note.md' };
|
|
574
|
+
expect(noteMatchScore(note, 'completely-unrelated-query', null)).toBe(0);
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
describe('findParagraphBounds', () => {
|
|
578
|
+
it('handles single paragraph (no blank lines)', () => {
|
|
579
|
+
const lines = ['line one', 'line two', 'line three'];
|
|
580
|
+
expect(findParagraphBounds(lines, 1)).toEqual({ startIndex: 0, endIndex: 2 });
|
|
581
|
+
});
|
|
582
|
+
it('handles paragraph bounded by blanks', () => {
|
|
583
|
+
const lines = ['first para', '', 'second', 'para', '', 'third para'];
|
|
584
|
+
expect(findParagraphBounds(lines, 2)).toEqual({ startIndex: 2, endIndex: 3 });
|
|
585
|
+
expect(findParagraphBounds(lines, 3)).toEqual({ startIndex: 2, endIndex: 3 });
|
|
586
|
+
});
|
|
587
|
+
it('handles first paragraph (starts at line 0)', () => {
|
|
588
|
+
const lines = ['first', 'paragraph', '', 'second'];
|
|
589
|
+
expect(findParagraphBounds(lines, 0)).toEqual({ startIndex: 0, endIndex: 1 });
|
|
590
|
+
expect(findParagraphBounds(lines, 1)).toEqual({ startIndex: 0, endIndex: 1 });
|
|
591
|
+
});
|
|
592
|
+
it('handles last paragraph (extends to end)', () => {
|
|
593
|
+
const lines = ['first', '', 'last', 'paragraph'];
|
|
594
|
+
expect(findParagraphBounds(lines, 2)).toEqual({ startIndex: 2, endIndex: 3 });
|
|
595
|
+
expect(findParagraphBounds(lines, 3)).toEqual({ startIndex: 2, endIndex: 3 });
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
// matchesFrontmatterProperties — exported from unified-store
|
|
600
|
+
// ---------------------------------------------------------------------------
|
|
601
|
+
import { matchesFrontmatterProperties, normalizeFrontmatterScalar } from '../noteplan/unified-store.js';
|
|
602
|
+
describe('normalizeFrontmatterScalar', () => {
|
|
603
|
+
it('strips surrounding double quotes', () => {
|
|
604
|
+
expect(normalizeFrontmatterScalar('"book"', false)).toBe('book');
|
|
605
|
+
});
|
|
606
|
+
it('strips surrounding single quotes', () => {
|
|
607
|
+
expect(normalizeFrontmatterScalar("'book'", false)).toBe('book');
|
|
608
|
+
});
|
|
609
|
+
it('lowercases when caseSensitive is false', () => {
|
|
610
|
+
expect(normalizeFrontmatterScalar('Book', false)).toBe('book');
|
|
611
|
+
});
|
|
612
|
+
it('preserves case when caseSensitive is true', () => {
|
|
613
|
+
expect(normalizeFrontmatterScalar('Book', true)).toBe('Book');
|
|
614
|
+
});
|
|
615
|
+
it('trims whitespace', () => {
|
|
616
|
+
expect(normalizeFrontmatterScalar(' hello ', false)).toBe('hello');
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
describe('matchesFrontmatterProperties', () => {
|
|
620
|
+
const makeNote = (content) => ({
|
|
621
|
+
id: 'test-id',
|
|
622
|
+
title: 'Test Note',
|
|
623
|
+
filename: 'test.md',
|
|
624
|
+
type: 'note',
|
|
625
|
+
source: 'local',
|
|
626
|
+
folder: '',
|
|
627
|
+
content,
|
|
628
|
+
modifiedAt: new Date(),
|
|
629
|
+
createdAt: new Date(),
|
|
630
|
+
spaceId: undefined,
|
|
631
|
+
date: undefined,
|
|
632
|
+
});
|
|
633
|
+
it('matches a single property filter', () => {
|
|
634
|
+
const note = makeNote('---\ntype: book\n---\nSome content');
|
|
635
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book']], false)).toBe(true);
|
|
636
|
+
});
|
|
637
|
+
it('rejects when property value does not match', () => {
|
|
638
|
+
const note = makeNote('---\ntype: article\n---\nSome content');
|
|
639
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book']], false)).toBe(false);
|
|
640
|
+
});
|
|
641
|
+
it('rejects when property key is missing', () => {
|
|
642
|
+
const note = makeNote('---\ntitle: My Note\n---\nSome content');
|
|
643
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book']], false)).toBe(false);
|
|
644
|
+
});
|
|
645
|
+
it('returns false when there is no frontmatter', () => {
|
|
646
|
+
const note = makeNote('Just some text without frontmatter');
|
|
647
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book']], false)).toBe(false);
|
|
648
|
+
});
|
|
649
|
+
it('matches case-insensitively by default', () => {
|
|
650
|
+
const note = makeNote('---\nType: Book\n---\nContent');
|
|
651
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book']], false)).toBe(true);
|
|
652
|
+
});
|
|
653
|
+
it('matches comma-separated list values', () => {
|
|
654
|
+
const note = makeNote('---\ntags: fiction, book, novel\n---\nContent');
|
|
655
|
+
expect(matchesFrontmatterProperties(note, [['tags', 'book']], false)).toBe(true);
|
|
656
|
+
});
|
|
657
|
+
it('requires all filters to match', () => {
|
|
658
|
+
const note = makeNote('---\ntype: book\nstatus: reading\n---\nContent');
|
|
659
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book'], ['status', 'reading']], false)).toBe(true);
|
|
660
|
+
expect(matchesFrontmatterProperties(note, [['type', 'book'], ['status', 'done']], false)).toBe(false);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
// ── Regression: read-write line number consistency with frontmatter ──
|
|
664
|
+
// These tests verify that line numbers from buildLineWindow (used by get_notes
|
|
665
|
+
// and getParagraphs) can be directly used in delete/edit/replace operations
|
|
666
|
+
// without any frontmatter offset mismatch. This was a real user-reported bug
|
|
667
|
+
// where delete_lines previewed wrong content because it applied a frontmatter
|
|
668
|
+
// offset while get_notes used absolute line numbers.
|
|
669
|
+
describe('frontmatter line number consistency (regression)', () => {
|
|
670
|
+
// A note with 3 lines of frontmatter (lines 1-3) and 5 content lines (4-8)
|
|
671
|
+
const noteWithFM = [
|
|
672
|
+
'---', // line 1
|
|
673
|
+
'title: Wishlist', // line 2
|
|
674
|
+
'---', // line 3
|
|
675
|
+
'# Wishlist', // line 4
|
|
676
|
+
'', // line 5
|
|
677
|
+
'## AI', // line 6
|
|
678
|
+
'- Feature A', // line 7
|
|
679
|
+
'- Choose AI provider', // line 8 ← user wants to delete this
|
|
680
|
+
].join('\n');
|
|
681
|
+
const allLines = noteWithFM.split('\n');
|
|
682
|
+
it('buildLineWindow reports absolute line numbers including frontmatter', () => {
|
|
683
|
+
const window = buildLineWindow(allLines, {
|
|
684
|
+
startLine: 1,
|
|
685
|
+
endLine: 8,
|
|
686
|
+
defaultLimit: 100,
|
|
687
|
+
maxLimit: 100,
|
|
688
|
+
});
|
|
689
|
+
expect(window.lines[0]).toEqual({ line: 1, lineIndex: 0, content: '---' });
|
|
690
|
+
expect(window.lines[3]).toEqual({ line: 4, lineIndex: 3, content: '# Wishlist' });
|
|
691
|
+
expect(window.lines[7]).toEqual({ line: 8, lineIndex: 7, content: '- Choose AI provider' });
|
|
692
|
+
});
|
|
693
|
+
it('line number from buildLineWindow can be used directly for array splice', () => {
|
|
694
|
+
// Simulate: user reads note, gets line 8 = "- Choose AI provider"
|
|
695
|
+
const window = buildLineWindow(allLines, {
|
|
696
|
+
startLine: 1,
|
|
697
|
+
endLine: 8,
|
|
698
|
+
defaultLimit: 100,
|
|
699
|
+
maxLimit: 100,
|
|
700
|
+
});
|
|
701
|
+
const targetLine = window.lines.find(l => l.content === '- Choose AI provider');
|
|
702
|
+
expect(targetLine).toBeDefined();
|
|
703
|
+
expect(targetLine.line).toBe(8);
|
|
704
|
+
// Now simulate delete: splice at (line - 1) with no frontmatter offset
|
|
705
|
+
const splicedLines = [...allLines];
|
|
706
|
+
splicedLines.splice(targetLine.line - 1, 1);
|
|
707
|
+
expect(splicedLines).toEqual([
|
|
708
|
+
'---', 'title: Wishlist', '---', '# Wishlist', '', '## AI', '- Feature A',
|
|
709
|
+
]);
|
|
710
|
+
// The deleted line should be "- Choose AI provider", not something else
|
|
711
|
+
expect(splicedLines).not.toContain('- Choose AI provider');
|
|
712
|
+
});
|
|
713
|
+
it('searching for content returns line numbers usable for deletion', () => {
|
|
714
|
+
// Simulate searchParagraphs: find "Choose AI provider", get line number
|
|
715
|
+
const window = buildLineWindow(allLines, {
|
|
716
|
+
defaultLimit: allLines.length,
|
|
717
|
+
maxLimit: allLines.length,
|
|
718
|
+
});
|
|
719
|
+
const match = window.lines.find(l => l.content.includes('Choose AI provider'));
|
|
720
|
+
expect(match).toBeDefined();
|
|
721
|
+
expect(match.line).toBe(8);
|
|
722
|
+
// Use that line number for deletion (absolute, no offset)
|
|
723
|
+
const startIndex = match.line - 1; // 0-indexed
|
|
724
|
+
expect(allLines[startIndex]).toBe('- Choose AI provider');
|
|
725
|
+
});
|
|
726
|
+
it('startLine/endLine window with frontmatter uses absolute numbering', () => {
|
|
727
|
+
// Read lines 6-8 (the AI section)
|
|
728
|
+
const window = buildLineWindow(allLines, {
|
|
729
|
+
startLine: 6,
|
|
730
|
+
endLine: 8,
|
|
731
|
+
defaultLimit: 100,
|
|
732
|
+
maxLimit: 100,
|
|
733
|
+
});
|
|
734
|
+
expect(window.lines).toEqual([
|
|
735
|
+
{ line: 6, lineIndex: 5, content: '## AI' },
|
|
736
|
+
{ line: 7, lineIndex: 6, content: '- Feature A' },
|
|
737
|
+
{ line: 8, lineIndex: 7, content: '- Choose AI provider' },
|
|
738
|
+
]);
|
|
739
|
+
});
|
|
740
|
+
it('note without frontmatter: line 1 is first line of content', () => {
|
|
741
|
+
const noFM = ['# Title', 'Body 1', 'Body 2'].join('\n');
|
|
742
|
+
const noFMLines = noFM.split('\n');
|
|
743
|
+
const window = buildLineWindow(noFMLines, {
|
|
744
|
+
defaultLimit: 100,
|
|
745
|
+
maxLimit: 100,
|
|
746
|
+
});
|
|
747
|
+
expect(window.lines[0]).toEqual({ line: 1, lineIndex: 0, content: '# Title' });
|
|
748
|
+
expect(window.lines[1]).toEqual({ line: 2, lineIndex: 1, content: 'Body 1' });
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
// ── Manual test procedure for frontmatter line consistency ──
|
|
752
|
+
// Run this against a live NotePlan MCP server to verify read/write consistency.
|
|
753
|
+
//
|
|
754
|
+
// SETUP: Create a test note with frontmatter:
|
|
755
|
+
// noteplan_manage_note(action="create", title="FM Line Test", content=`
|
|
756
|
+
// ---
|
|
757
|
+
// test: true
|
|
758
|
+
// ---
|
|
759
|
+
// # FM Line Test
|
|
760
|
+
//
|
|
761
|
+
// Line A
|
|
762
|
+
// Line B
|
|
763
|
+
// Line C
|
|
764
|
+
// `)
|
|
765
|
+
//
|
|
766
|
+
// TEST 1: Read and verify line numbers
|
|
767
|
+
// noteplan_get_notes(filename="...", includeContent=true)
|
|
768
|
+
// → Verify: "---" is line 1, "# FM Line Test" is line 4, "Line A" is line 6
|
|
769
|
+
//
|
|
770
|
+
// TEST 2: Search and verify line numbers match
|
|
771
|
+
// noteplan_paragraphs(action="search", filename="...", query="Line B")
|
|
772
|
+
// → Verify: result.line matches get_notes line for "Line B" (should be 7)
|
|
773
|
+
//
|
|
774
|
+
// TEST 3: Delete using the line number from search
|
|
775
|
+
// noteplan_edit_content(action="delete_lines", filename="...", startLine=7, endLine=7, dryRun=true)
|
|
776
|
+
// → Verify: dry run preview shows "Line B", NOT a different line
|
|
777
|
+
// → If preview shows wrong content, the frontmatter offset bug has regressed!
|
|
778
|
+
//
|
|
779
|
+
// TEST 4: Edit a line using absolute number
|
|
780
|
+
// noteplan_edit_content(action="edit_line", filename="...", line=6, content="Line A (edited)")
|
|
781
|
+
// → Verify: "Line A" was changed, not a different line
|
|
782
|
+
//
|
|
783
|
+
// TEST 5: Replace lines using absolute numbers
|
|
784
|
+
// noteplan_edit_content(action="replace_lines", filename="...", startLine=6, endLine=8,
|
|
785
|
+
// content="Replaced A\nReplaced B\nReplaced C", dryRun=true)
|
|
786
|
+
// → Verify: preview shows "Line A", "Line B", "Line C" being replaced
|
|
787
|
+
//
|
|
788
|
+
// TEST 6: Insert at absolute line
|
|
789
|
+
// noteplan_edit_content(action="insert", filename="...", position="at-line", line=6,
|
|
790
|
+
// content="Inserted before Line A")
|
|
791
|
+
// → Verify: new line appears at line 6, "Line A" moves to line 7
|
|
792
|
+
//
|
|
793
|
+
// TEST 7: Frontmatter protection
|
|
794
|
+
// noteplan_edit_content(action="delete_lines", filename="...", startLine=1, endLine=3)
|
|
795
|
+
// → Verify: returns error about frontmatter protection, does NOT delete
|
|
796
|
+
//
|
|
797
|
+
// CLEANUP:
|
|
798
|
+
// noteplan_manage_note(action="delete", filename="...")
|
|
799
|
+
//
|
|
800
|
+
//# sourceMappingURL=notes.test.js.map
|