opencode-snippets 1.4.2 → 1.4.3
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/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/src/arg-parser.d.ts +16 -0
- package/dist/src/arg-parser.d.ts.map +1 -0
- package/dist/src/arg-parser.js +94 -0
- package/dist/src/arg-parser.js.map +1 -0
- package/dist/src/commands.d.ts +30 -0
- package/dist/src/commands.d.ts.map +1 -0
- package/dist/src/commands.js +315 -0
- package/dist/src/commands.js.map +1 -0
- package/dist/src/constants.d.ts +26 -0
- package/dist/src/constants.d.ts.map +1 -0
- package/dist/src/constants.js +28 -0
- package/dist/src/constants.js.map +1 -0
- package/dist/src/expander.d.ts +36 -0
- package/dist/src/expander.d.ts.map +1 -0
- package/dist/src/expander.js +187 -0
- package/dist/src/expander.js.map +1 -0
- package/dist/src/loader.d.ts +46 -0
- package/dist/src/loader.d.ts.map +1 -0
- package/dist/src/loader.js +223 -0
- package/dist/src/loader.js.map +1 -0
- package/dist/src/logger.d.ts +15 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +107 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/notification.d.ts +11 -0
- package/dist/src/notification.d.ts.map +1 -0
- package/dist/src/notification.js +26 -0
- package/dist/src/notification.js.map +1 -0
- package/dist/src/shell.d.ts +18 -0
- package/dist/src/shell.d.ts.map +1 -0
- package/dist/src/shell.js +30 -0
- package/dist/src/shell.js.map +1 -0
- package/dist/src/types.d.ts +65 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/package.json +8 -5
- package/index.ts +0 -81
- package/src/arg-parser.test.ts +0 -177
- package/src/arg-parser.ts +0 -87
- package/src/commands.test.ts +0 -188
- package/src/commands.ts +0 -414
- package/src/constants.ts +0 -32
- package/src/expander.test.ts +0 -846
- package/src/expander.ts +0 -225
- package/src/loader.test.ts +0 -352
- package/src/loader.ts +0 -268
- package/src/logger.test.ts +0 -136
- package/src/logger.ts +0 -121
- package/src/notification.ts +0 -30
- package/src/shell.ts +0 -50
- package/src/types.ts +0 -71
package/src/expander.test.ts
DELETED
|
@@ -1,846 +0,0 @@
|
|
|
1
|
-
import { assembleMessage, expandHashtags, parseSnippetBlocks } from "../src/expander.js";
|
|
2
|
-
import type { SnippetInfo, SnippetRegistry } from "../src/types.js";
|
|
3
|
-
|
|
4
|
-
/** Helper to create a SnippetInfo from just content */
|
|
5
|
-
function snippet(content: string, name = "test"): SnippetInfo {
|
|
6
|
-
return { name, content, aliases: [], filePath: "", source: "global" };
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/** Helper to create a registry from [key, content] pairs */
|
|
10
|
-
function createRegistry(entries: [string, string][]): SnippetRegistry {
|
|
11
|
-
return new Map(entries.map(([key, content]) => [key, snippet(content, key)]));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe("expandHashtags - Recursive Includes and Loop Detection", () => {
|
|
15
|
-
describe("Basic expansion", () => {
|
|
16
|
-
it("should expand a single hashtag", () => {
|
|
17
|
-
const registry = createRegistry([["greeting", "Hello, World!"]]);
|
|
18
|
-
|
|
19
|
-
const result = expandHashtags("Say #greeting", registry);
|
|
20
|
-
|
|
21
|
-
expect(result.text).toBe("Say Hello, World!");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("should expand multiple hashtags in one text", () => {
|
|
25
|
-
const registry = createRegistry([
|
|
26
|
-
["greeting", "Hello"],
|
|
27
|
-
["name", "Alice"],
|
|
28
|
-
]);
|
|
29
|
-
|
|
30
|
-
const result = expandHashtags("#greeting, #name!", registry);
|
|
31
|
-
|
|
32
|
-
expect(result.text).toBe("Hello, Alice!");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should leave unknown hashtags unchanged", () => {
|
|
36
|
-
const registry = createRegistry([["known", "content"]]);
|
|
37
|
-
|
|
38
|
-
const result = expandHashtags("This is #known and #unknown", registry);
|
|
39
|
-
|
|
40
|
-
expect(result.text).toBe("This is content and #unknown");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("should handle empty text", () => {
|
|
44
|
-
const registry = createRegistry([["test", "content"]]);
|
|
45
|
-
|
|
46
|
-
const result = expandHashtags("", registry);
|
|
47
|
-
|
|
48
|
-
expect(result.text).toBe("");
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("should handle text with no hashtags", () => {
|
|
52
|
-
const registry = createRegistry([["test", "content"]]);
|
|
53
|
-
|
|
54
|
-
const result = expandHashtags("No hashtags here", registry);
|
|
55
|
-
|
|
56
|
-
expect(result.text).toBe("No hashtags here");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("should handle case-insensitive hashtags", () => {
|
|
60
|
-
const registry = createRegistry([["greeting", "Hello"]]);
|
|
61
|
-
|
|
62
|
-
const result = expandHashtags("#Greeting #GREETING #greeting", registry);
|
|
63
|
-
|
|
64
|
-
expect(result.text).toBe("Hello Hello Hello");
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
describe("Recursive expansion", () => {
|
|
69
|
-
it("should expand nested hashtags one level deep", () => {
|
|
70
|
-
const registry = createRegistry([
|
|
71
|
-
["outer", "Start #inner End"],
|
|
72
|
-
["inner", "Middle"],
|
|
73
|
-
]);
|
|
74
|
-
|
|
75
|
-
const result = expandHashtags("#outer", registry);
|
|
76
|
-
|
|
77
|
-
expect(result.text).toBe("Start Middle End");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("should expand nested hashtags multiple levels deep", () => {
|
|
81
|
-
const registry = createRegistry([
|
|
82
|
-
["level1", "L1 #level2"],
|
|
83
|
-
["level2", "L2 #level3"],
|
|
84
|
-
["level3", "L3 #level4"],
|
|
85
|
-
["level4", "L4"],
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
const result = expandHashtags("#level1", registry);
|
|
89
|
-
|
|
90
|
-
expect(result.text).toBe("L1 L2 L3 L4");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("should expand multiple nested hashtags in one snippet", () => {
|
|
94
|
-
const registry = createRegistry([
|
|
95
|
-
["main", "Start #a and #b End"],
|
|
96
|
-
["a", "Content A"],
|
|
97
|
-
["b", "Content B"],
|
|
98
|
-
]);
|
|
99
|
-
|
|
100
|
-
const result = expandHashtags("#main", registry);
|
|
101
|
-
|
|
102
|
-
expect(result.text).toBe("Start Content A and Content B End");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("should expand complex nested structure", () => {
|
|
106
|
-
const registry = createRegistry([
|
|
107
|
-
["greeting", "#hello #name"],
|
|
108
|
-
["hello", "Hello"],
|
|
109
|
-
["name", "#firstname #lastname"],
|
|
110
|
-
["firstname", "John"],
|
|
111
|
-
["lastname", "Doe"],
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
const result = expandHashtags("#greeting", registry);
|
|
115
|
-
|
|
116
|
-
expect(result.text).toBe("Hello John Doe");
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe("Loop detection - Direct cycles", () => {
|
|
121
|
-
it("should detect and prevent simple self-reference", { timeout: 100 }, () => {
|
|
122
|
-
const registry = createRegistry([["self", "I reference #self"]]);
|
|
123
|
-
|
|
124
|
-
const result = expandHashtags("#self", registry);
|
|
125
|
-
|
|
126
|
-
// Loop detected after 15 expansions, #self left as-is
|
|
127
|
-
const expected = `${"I reference ".repeat(15)}#self`;
|
|
128
|
-
expect(result.text).toBe(expected);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("should detect and prevent two-way circular reference", () => {
|
|
132
|
-
const registry = createRegistry([
|
|
133
|
-
["a", "A references #b"],
|
|
134
|
-
["b", "B references #a"],
|
|
135
|
-
]);
|
|
136
|
-
|
|
137
|
-
const result = expandHashtags("#a", registry);
|
|
138
|
-
|
|
139
|
-
// Should expand alternating A and B 15 times then stop
|
|
140
|
-
const expected = `${"A references B references ".repeat(15)}#a`;
|
|
141
|
-
expect(result.text).toBe(expected);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("should detect and prevent three-way circular reference", () => {
|
|
145
|
-
const registry = createRegistry([
|
|
146
|
-
["a", "A -> #b"],
|
|
147
|
-
["b", "B -> #c"],
|
|
148
|
-
["c", "C -> #a"],
|
|
149
|
-
]);
|
|
150
|
-
|
|
151
|
-
const result = expandHashtags("#a", registry);
|
|
152
|
-
|
|
153
|
-
// Should expand cycling through A, B, C 15 times then stop
|
|
154
|
-
const expected = `${"A -> B -> C -> ".repeat(15)}#a`;
|
|
155
|
-
expect(result.text).toBe(expected);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("should detect loops in longer chains", () => {
|
|
159
|
-
const registry = createRegistry([
|
|
160
|
-
["a", "#b"],
|
|
161
|
-
["b", "#c"],
|
|
162
|
-
["c", "#d"],
|
|
163
|
-
["d", "#e"],
|
|
164
|
-
["e", "#b"], // Loop back to b
|
|
165
|
-
]);
|
|
166
|
-
|
|
167
|
-
const result = expandHashtags("#a", registry);
|
|
168
|
-
|
|
169
|
-
// Should expand until loop detected
|
|
170
|
-
expect(result.text).toBe("#b");
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe("Loop detection - Complex scenarios", () => {
|
|
175
|
-
it("should allow same snippet in different branches", () => {
|
|
176
|
-
const registry = createRegistry([
|
|
177
|
-
["main", "#branch1 and #branch2"],
|
|
178
|
-
["branch1", "B1 uses #shared"],
|
|
179
|
-
["branch2", "B2 uses #shared"],
|
|
180
|
-
["shared", "Shared content"],
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
const result = expandHashtags("#main", registry);
|
|
184
|
-
|
|
185
|
-
// #shared should be expanded in both branches
|
|
186
|
-
expect(result.text).toBe("B1 uses Shared content and B2 uses Shared content");
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("should handle partial loops with valid branches", () => {
|
|
190
|
-
const registry = createRegistry([
|
|
191
|
-
["main", "#valid and #loop"],
|
|
192
|
-
["valid", "Valid content"],
|
|
193
|
-
["loop", "Loop #loop"],
|
|
194
|
-
]);
|
|
195
|
-
|
|
196
|
-
const result = expandHashtags("#main", registry);
|
|
197
|
-
|
|
198
|
-
// Valid expands once, loop expands 15 times
|
|
199
|
-
const expected = `Valid content and ${"Loop ".repeat(15)}#loop`;
|
|
200
|
-
expect(result.text).toBe(expected);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("should handle multiple independent loops", () => {
|
|
204
|
-
const registry = createRegistry([
|
|
205
|
-
["main", "#loop1 and #loop2"],
|
|
206
|
-
["loop1", "L1 #loop1"],
|
|
207
|
-
["loop2", "L2 #loop2"],
|
|
208
|
-
]);
|
|
209
|
-
|
|
210
|
-
const result = expandHashtags("#main", registry);
|
|
211
|
-
|
|
212
|
-
// Each loop expands 15 times independently
|
|
213
|
-
const expected = `${"L1 ".repeat(15)}#loop1 and ${"L2 ".repeat(15)}#loop2`;
|
|
214
|
-
expect(result.text).toBe(expected);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it("should handle nested loops", () => {
|
|
218
|
-
const registry = createRegistry([
|
|
219
|
-
["outer", "Outer #inner"],
|
|
220
|
-
["inner", "Inner #outer and #self"],
|
|
221
|
-
["self", "Self #self"],
|
|
222
|
-
]);
|
|
223
|
-
|
|
224
|
-
const result = expandHashtags("#outer", registry);
|
|
225
|
-
|
|
226
|
-
// Complex nested loop - outer/inner cycle 15 times, plus self cycles
|
|
227
|
-
// This is complex expansion behavior, just verify it doesn't hang
|
|
228
|
-
expect(result.text).toContain("Outer");
|
|
229
|
-
expect(result.text).toContain("Inner");
|
|
230
|
-
expect(result.text).toContain("#outer");
|
|
231
|
-
expect(result.text).toContain("#self");
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("should handle diamond pattern (same snippet reached via multiple paths)", () => {
|
|
235
|
-
const registry = createRegistry([
|
|
236
|
-
["top", "#left #right"],
|
|
237
|
-
["left", "Left #bottom"],
|
|
238
|
-
["right", "Right #bottom"],
|
|
239
|
-
["bottom", "Bottom"],
|
|
240
|
-
]);
|
|
241
|
-
|
|
242
|
-
const result = expandHashtags("#top", registry);
|
|
243
|
-
|
|
244
|
-
// Diamond: top -> left -> bottom, top -> right -> bottom
|
|
245
|
-
expect(result.text).toBe("Left Bottom Right Bottom");
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("should handle loop after valid expansion", () => {
|
|
249
|
-
const registry = createRegistry([
|
|
250
|
-
["a", "#b #c"],
|
|
251
|
-
["b", "Valid B"],
|
|
252
|
-
["c", "#d"],
|
|
253
|
-
["d", "#c"], // Loop back
|
|
254
|
-
]);
|
|
255
|
-
|
|
256
|
-
const result = expandHashtags("#a", registry);
|
|
257
|
-
|
|
258
|
-
expect(result.text).toBe("Valid B #c");
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe("Edge cases", () => {
|
|
263
|
-
it("should handle empty registry", () => {
|
|
264
|
-
const registry: SnippetRegistry = new Map();
|
|
265
|
-
|
|
266
|
-
const result = expandHashtags("#anything", registry);
|
|
267
|
-
|
|
268
|
-
expect(result.text).toBe("#anything");
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("should handle snippet with empty content", () => {
|
|
272
|
-
const registry = createRegistry([["empty", ""]]);
|
|
273
|
-
|
|
274
|
-
const result = expandHashtags("Before #empty After", registry);
|
|
275
|
-
|
|
276
|
-
expect(result.text).toBe("Before After");
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("should handle snippet containing only hashtags", () => {
|
|
280
|
-
const registry = createRegistry([
|
|
281
|
-
["only-refs", "#a #b"],
|
|
282
|
-
["a", "A"],
|
|
283
|
-
["b", "B"],
|
|
284
|
-
]);
|
|
285
|
-
|
|
286
|
-
const result = expandHashtags("#only-refs", registry);
|
|
287
|
-
|
|
288
|
-
expect(result.text).toBe("A B");
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("should handle hashtags at start, middle, and end", () => {
|
|
292
|
-
const registry = createRegistry([
|
|
293
|
-
["start", "Start"],
|
|
294
|
-
["middle", "Middle"],
|
|
295
|
-
["end", "End"],
|
|
296
|
-
]);
|
|
297
|
-
|
|
298
|
-
const result = expandHashtags("#start text #middle text #end", registry);
|
|
299
|
-
|
|
300
|
-
expect(result.text).toBe("Start text Middle text End");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("should handle consecutive hashtags", () => {
|
|
304
|
-
const registry = createRegistry([
|
|
305
|
-
["a", "A"],
|
|
306
|
-
["b", "B"],
|
|
307
|
-
["c", "C"],
|
|
308
|
-
]);
|
|
309
|
-
|
|
310
|
-
const result = expandHashtags("#a#b#c", registry);
|
|
311
|
-
|
|
312
|
-
expect(result.text).toBe("ABC");
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("should handle hashtags with hyphens and underscores", () => {
|
|
316
|
-
const registry = createRegistry([
|
|
317
|
-
["my-snippet", "Hyphenated"],
|
|
318
|
-
["my_snippet", "Underscored"],
|
|
319
|
-
["my-complex_name", "Mixed"],
|
|
320
|
-
]);
|
|
321
|
-
|
|
322
|
-
const result = expandHashtags("#my-snippet #my_snippet #my-complex_name", registry);
|
|
323
|
-
|
|
324
|
-
expect(result.text).toBe("Hyphenated Underscored Mixed");
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
it("should handle hashtags with numbers", () => {
|
|
328
|
-
const registry = createRegistry([
|
|
329
|
-
["test123", "Test with numbers"],
|
|
330
|
-
["123test", "Numbers first"],
|
|
331
|
-
]);
|
|
332
|
-
|
|
333
|
-
const result = expandHashtags("#test123 #123test", registry);
|
|
334
|
-
|
|
335
|
-
expect(result.text).toBe("Test with numbers Numbers first");
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
it("should not expand hashtags in URLs", () => {
|
|
339
|
-
const registry = createRegistry([["issue", "ISSUE"]]);
|
|
340
|
-
|
|
341
|
-
// Note: The current implementation WILL expand #issue in URLs
|
|
342
|
-
// This test documents current behavior
|
|
343
|
-
const result = expandHashtags("See https://github.com/user/repo/issues/#issue", registry);
|
|
344
|
-
|
|
345
|
-
expect(result.text).toBe("See https://github.com/user/repo/issues/ISSUE");
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
it("should handle multiline content", () => {
|
|
349
|
-
const registry = createRegistry([["multiline", "Line 1\nLine 2\nLine 3"]]);
|
|
350
|
-
|
|
351
|
-
const result = expandHashtags("Start\n#multiline\nEnd", registry);
|
|
352
|
-
|
|
353
|
-
expect(result.text).toBe("Start\nLine 1\nLine 2\nLine 3\nEnd");
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it("should handle nested multiline content", () => {
|
|
357
|
-
const registry = createRegistry([
|
|
358
|
-
["outer", "Outer start\n#inner\nOuter end"],
|
|
359
|
-
["inner", "Inner line 1\nInner line 2"],
|
|
360
|
-
]);
|
|
361
|
-
|
|
362
|
-
const result = expandHashtags("#outer", registry);
|
|
363
|
-
|
|
364
|
-
expect(result.text).toBe("Outer start\nInner line 1\nInner line 2\nOuter end");
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe("Real-world scenarios", () => {
|
|
369
|
-
it("should expand code review template with nested snippets", () => {
|
|
370
|
-
const registry = createRegistry([
|
|
371
|
-
["review", "Code Review Checklist:\n#security\n#performance\n#tests"],
|
|
372
|
-
["security", "- Check for SQL injection\n- Validate input"],
|
|
373
|
-
["performance", "- Check for N+1 queries\n- Review algorithm complexity"],
|
|
374
|
-
["tests", "- Unit tests present\n- Edge cases covered"],
|
|
375
|
-
]);
|
|
376
|
-
|
|
377
|
-
const result = expandHashtags("#review", registry);
|
|
378
|
-
|
|
379
|
-
expect(result.text).toContain("Code Review Checklist:");
|
|
380
|
-
expect(result.text).toContain("Check for SQL injection");
|
|
381
|
-
expect(result.text).toContain("Check for N+1 queries");
|
|
382
|
-
expect(result.text).toContain("Unit tests present");
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("should expand documentation template with shared components", () => {
|
|
386
|
-
const registry = createRegistry([
|
|
387
|
-
["doc", "# Documentation\n#header\n#body\n#footer"],
|
|
388
|
-
["header", "Author: #author\nDate: 2024-01-01"],
|
|
389
|
-
["author", "John Doe"],
|
|
390
|
-
["body", "Main content here"],
|
|
391
|
-
["footer", "Contact: #author"],
|
|
392
|
-
]);
|
|
393
|
-
|
|
394
|
-
const result = expandHashtags("#doc", registry);
|
|
395
|
-
|
|
396
|
-
// #author should be expanded in both header and footer
|
|
397
|
-
expect(result.text).toContain("Author: John Doe");
|
|
398
|
-
expect(result.text).toContain("Contact: John Doe");
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("should handle instruction composition", () => {
|
|
402
|
-
const registry = createRegistry([
|
|
403
|
-
["careful", "Think step by step. #verify"],
|
|
404
|
-
["verify", "Double-check your work."],
|
|
405
|
-
["complete", "Be thorough. #careful"],
|
|
406
|
-
]);
|
|
407
|
-
|
|
408
|
-
const result = expandHashtags("Instructions: #complete", registry);
|
|
409
|
-
|
|
410
|
-
expect(result.text).toBe(
|
|
411
|
-
"Instructions: Be thorough. Think step by step. Double-check your work.",
|
|
412
|
-
);
|
|
413
|
-
});
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
describe("Performance and stress tests", () => {
|
|
417
|
-
it("should handle deep nesting without stack overflow", () => {
|
|
418
|
-
const registry: SnippetRegistry = new Map();
|
|
419
|
-
const depth = 50;
|
|
420
|
-
|
|
421
|
-
// Create a chain: level0 -> level1 -> level2 -> ... -> level49 -> "End"
|
|
422
|
-
for (let i = 0; i < depth - 1; i++) {
|
|
423
|
-
registry.set(`level${i}`, snippet(`L${i} #level${i + 1}`, `level${i}`));
|
|
424
|
-
}
|
|
425
|
-
registry.set(`level${depth - 1}`, snippet("End", `level${depth - 1}`));
|
|
426
|
-
|
|
427
|
-
const result = expandHashtags("#level0", registry);
|
|
428
|
-
|
|
429
|
-
expect(result.text).toContain("L0");
|
|
430
|
-
expect(result.text).toContain("End");
|
|
431
|
-
expect(result.text.split(" ").length).toBe(depth);
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it("should handle many snippets in one text", () => {
|
|
435
|
-
const registry: SnippetRegistry = new Map();
|
|
436
|
-
const count = 100;
|
|
437
|
-
|
|
438
|
-
for (let i = 0; i < count; i++) {
|
|
439
|
-
registry.set(`snippet${i}`, snippet(`Content${i}`, `snippet${i}`));
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const hashtags = Array.from({ length: count }, (_, i) => `#snippet${i}`).join(" ");
|
|
443
|
-
const result = expandHashtags(hashtags, registry);
|
|
444
|
-
|
|
445
|
-
expect(result.text.split(" ").length).toBe(count);
|
|
446
|
-
expect(result.text).toContain("Content0");
|
|
447
|
-
expect(result.text).toContain(`Content${count - 1}`);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it("should handle wide branching (many children)", () => {
|
|
451
|
-
const registry: SnippetRegistry = new Map();
|
|
452
|
-
const branches = 20;
|
|
453
|
-
|
|
454
|
-
const children = Array.from({ length: branches }, (_, i) => `#child${i}`).join(" ");
|
|
455
|
-
registry.set("parent", snippet(children, "parent"));
|
|
456
|
-
|
|
457
|
-
for (let i = 0; i < branches; i++) {
|
|
458
|
-
registry.set(`child${i}`, snippet(`Child${i}`, `child${i}`));
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const result = expandHashtags("#parent", registry);
|
|
462
|
-
|
|
463
|
-
for (let i = 0; i < branches; i++) {
|
|
464
|
-
expect(result.text).toContain(`Child${i}`);
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
describe("parseSnippetBlocks", () => {
|
|
471
|
-
describe("Basic parsing", () => {
|
|
472
|
-
it("should return full content as inline when no blocks present", () => {
|
|
473
|
-
const result = parseSnippetBlocks("Just some content");
|
|
474
|
-
|
|
475
|
-
expect(result).toEqual({
|
|
476
|
-
inline: "Just some content",
|
|
477
|
-
prepend: [],
|
|
478
|
-
append: [],
|
|
479
|
-
});
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it("should extract append block and inline content", () => {
|
|
483
|
-
const result = parseSnippetBlocks("Inline text\n<append>\nAppend content\n</append>");
|
|
484
|
-
|
|
485
|
-
expect(result).toEqual({
|
|
486
|
-
inline: "Inline text",
|
|
487
|
-
prepend: [],
|
|
488
|
-
append: ["Append content"],
|
|
489
|
-
});
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it("should extract prepend block and inline content", () => {
|
|
493
|
-
const result = parseSnippetBlocks("<prepend>\nPrepend content\n</prepend>\nInline text");
|
|
494
|
-
|
|
495
|
-
expect(result).toEqual({
|
|
496
|
-
inline: "Inline text",
|
|
497
|
-
prepend: ["Prepend content"],
|
|
498
|
-
append: [],
|
|
499
|
-
});
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
it("should extract both prepend and append blocks", () => {
|
|
503
|
-
const content = `<prepend>
|
|
504
|
-
Before content
|
|
505
|
-
</prepend>
|
|
506
|
-
Inline text
|
|
507
|
-
<append>
|
|
508
|
-
After content
|
|
509
|
-
</append>`;
|
|
510
|
-
|
|
511
|
-
const result = parseSnippetBlocks(content);
|
|
512
|
-
|
|
513
|
-
expect(result).toEqual({
|
|
514
|
-
inline: "Inline text",
|
|
515
|
-
prepend: ["Before content"],
|
|
516
|
-
append: ["After content"],
|
|
517
|
-
});
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
it("should handle multiple blocks of the same type", () => {
|
|
521
|
-
const content = `<append>
|
|
522
|
-
First append
|
|
523
|
-
</append>
|
|
524
|
-
Inline
|
|
525
|
-
<append>
|
|
526
|
-
Second append
|
|
527
|
-
</append>`;
|
|
528
|
-
|
|
529
|
-
const result = parseSnippetBlocks(content);
|
|
530
|
-
|
|
531
|
-
expect(result).toEqual({
|
|
532
|
-
inline: "Inline",
|
|
533
|
-
prepend: [],
|
|
534
|
-
append: ["First append", "Second append"],
|
|
535
|
-
});
|
|
536
|
-
});
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
describe("Edge cases", () => {
|
|
540
|
-
it("should handle empty inline (only blocks)", () => {
|
|
541
|
-
const content = `<append>
|
|
542
|
-
Only append content
|
|
543
|
-
</append>`;
|
|
544
|
-
|
|
545
|
-
const result = parseSnippetBlocks(content);
|
|
546
|
-
|
|
547
|
-
expect(result).toEqual({
|
|
548
|
-
inline: "",
|
|
549
|
-
prepend: [],
|
|
550
|
-
append: ["Only append content"],
|
|
551
|
-
});
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
it("should handle unclosed tag leniently (rest is block content)", () => {
|
|
555
|
-
const content = "Inline\n<append>\nUnclosed append content";
|
|
556
|
-
|
|
557
|
-
const result = parseSnippetBlocks(content);
|
|
558
|
-
|
|
559
|
-
expect(result).toEqual({
|
|
560
|
-
inline: "Inline",
|
|
561
|
-
prepend: [],
|
|
562
|
-
append: ["Unclosed append content"],
|
|
563
|
-
});
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it("should return null for nested tags (different types)", () => {
|
|
567
|
-
const content = "<append>\n<prepend>\nnested\n</prepend>\n</append>";
|
|
568
|
-
|
|
569
|
-
const result = parseSnippetBlocks(content);
|
|
570
|
-
|
|
571
|
-
expect(result).toBeNull();
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("should return null for nested tags (same type)", () => {
|
|
575
|
-
const content = "<prepend>\n<prepend>\nnested\n</prepend>\n</prepend>";
|
|
576
|
-
|
|
577
|
-
const result = parseSnippetBlocks(content);
|
|
578
|
-
|
|
579
|
-
expect(result).toBeNull();
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
it("should trim content inside blocks", () => {
|
|
583
|
-
const content = "<append>\n \n Content with whitespace \n \n</append>";
|
|
584
|
-
|
|
585
|
-
const result = parseSnippetBlocks(content);
|
|
586
|
-
|
|
587
|
-
expect(result?.append[0]).toBe("Content with whitespace");
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it("should trim inline content", () => {
|
|
591
|
-
const content = " \n Inline with whitespace \n ";
|
|
592
|
-
|
|
593
|
-
const result = parseSnippetBlocks(content);
|
|
594
|
-
|
|
595
|
-
expect(result?.inline).toBe("Inline with whitespace");
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it("should be case-insensitive for tags", () => {
|
|
599
|
-
const content = "<APPEND>\nContent\n</APPEND>";
|
|
600
|
-
|
|
601
|
-
const result = parseSnippetBlocks(content);
|
|
602
|
-
|
|
603
|
-
expect(result).toEqual({
|
|
604
|
-
inline: "",
|
|
605
|
-
prepend: [],
|
|
606
|
-
append: ["Content"],
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it("should handle empty blocks", () => {
|
|
611
|
-
const content = "Inline<append></append>";
|
|
612
|
-
|
|
613
|
-
const result = parseSnippetBlocks(content);
|
|
614
|
-
|
|
615
|
-
expect(result).toEqual({
|
|
616
|
-
inline: "Inline",
|
|
617
|
-
prepend: [],
|
|
618
|
-
append: [],
|
|
619
|
-
});
|
|
620
|
-
});
|
|
621
|
-
});
|
|
622
|
-
|
|
623
|
-
describe("Real-world content", () => {
|
|
624
|
-
it("should parse Jira MCP example", () => {
|
|
625
|
-
const content = `Jira MCP server
|
|
626
|
-
<append>
|
|
627
|
-
## Jira MCP Usage
|
|
628
|
-
|
|
629
|
-
Use these custom field mappings when creating issues:
|
|
630
|
-
- customfield_16570 => Acceptance Criteria
|
|
631
|
-
- customfield_11401 => Team
|
|
632
|
-
</append>`;
|
|
633
|
-
|
|
634
|
-
const result = parseSnippetBlocks(content);
|
|
635
|
-
|
|
636
|
-
expect(result?.inline).toBe("Jira MCP server");
|
|
637
|
-
expect(result?.append).toHaveLength(1);
|
|
638
|
-
expect(result?.append[0]).toContain("Jira MCP Usage");
|
|
639
|
-
expect(result?.append[0]).toContain("customfield_16570");
|
|
640
|
-
});
|
|
641
|
-
});
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
describe("assembleMessage", () => {
|
|
645
|
-
it("should assemble text only", () => {
|
|
646
|
-
const result = assembleMessage({
|
|
647
|
-
text: "Main content",
|
|
648
|
-
prepend: [],
|
|
649
|
-
append: [],
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
expect(result).toBe("Main content");
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("should assemble with append blocks", () => {
|
|
656
|
-
const result = assembleMessage({
|
|
657
|
-
text: "Main content",
|
|
658
|
-
prepend: [],
|
|
659
|
-
append: ["Appended section"],
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
expect(result).toBe("Main content\n\nAppended section");
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
it("should assemble with prepend blocks", () => {
|
|
666
|
-
const result = assembleMessage({
|
|
667
|
-
text: "Main content",
|
|
668
|
-
prepend: ["Prepended section"],
|
|
669
|
-
append: [],
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
expect(result).toBe("Prepended section\n\nMain content");
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
it("should assemble with both prepend and append", () => {
|
|
676
|
-
const result = assembleMessage({
|
|
677
|
-
text: "Main content",
|
|
678
|
-
prepend: ["Before"],
|
|
679
|
-
append: ["After"],
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
expect(result).toBe("Before\n\nMain content\n\nAfter");
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it("should join multiple prepend blocks", () => {
|
|
686
|
-
const result = assembleMessage({
|
|
687
|
-
text: "Main",
|
|
688
|
-
prepend: ["First", "Second"],
|
|
689
|
-
append: [],
|
|
690
|
-
});
|
|
691
|
-
|
|
692
|
-
expect(result).toBe("First\n\nSecond\n\nMain");
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it("should join multiple append blocks", () => {
|
|
696
|
-
const result = assembleMessage({
|
|
697
|
-
text: "Main",
|
|
698
|
-
prepend: [],
|
|
699
|
-
append: ["First", "Second"],
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
expect(result).toBe("Main\n\nFirst\n\nSecond");
|
|
703
|
-
});
|
|
704
|
-
|
|
705
|
-
it("should handle empty text with blocks", () => {
|
|
706
|
-
const result = assembleMessage({
|
|
707
|
-
text: "",
|
|
708
|
-
prepend: ["Before"],
|
|
709
|
-
append: ["After"],
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
expect(result).toBe("Before\n\nAfter");
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
it("should handle whitespace-only text with blocks", () => {
|
|
716
|
-
const result = assembleMessage({
|
|
717
|
-
text: " ",
|
|
718
|
-
prepend: ["Before"],
|
|
719
|
-
append: ["After"],
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
expect(result).toBe("Before\n\nAfter");
|
|
723
|
-
});
|
|
724
|
-
});
|
|
725
|
-
|
|
726
|
-
describe("Prepend/Append integration with expandHashtags", () => {
|
|
727
|
-
it("should collect append blocks during expansion", () => {
|
|
728
|
-
const registry = createRegistry([
|
|
729
|
-
["jira", "Jira MCP server\n<append>\nJira reference docs\n</append>"],
|
|
730
|
-
]);
|
|
731
|
-
|
|
732
|
-
const result = expandHashtags("Create a ticket in #jira", registry);
|
|
733
|
-
|
|
734
|
-
expect(result.text).toBe("Create a ticket in Jira MCP server");
|
|
735
|
-
expect(result.append).toEqual(["Jira reference docs"]);
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
it("should collect prepend blocks during expansion", () => {
|
|
739
|
-
const registry = createRegistry([
|
|
740
|
-
["context", "<prepend>\nImportant context\n</prepend>\nUse the context"],
|
|
741
|
-
]);
|
|
742
|
-
|
|
743
|
-
const result = expandHashtags("#context please", registry);
|
|
744
|
-
|
|
745
|
-
expect(result.text).toBe("Use the context please");
|
|
746
|
-
expect(result.prepend).toEqual(["Important context"]);
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
it("should collect blocks from nested snippets", () => {
|
|
750
|
-
const registry = createRegistry([
|
|
751
|
-
["outer", "Outer #inner text"],
|
|
752
|
-
["inner", "Inner\n<append>\nInner's append\n</append>"],
|
|
753
|
-
]);
|
|
754
|
-
|
|
755
|
-
const result = expandHashtags("#outer", registry);
|
|
756
|
-
|
|
757
|
-
expect(result.text).toBe("Outer Inner text");
|
|
758
|
-
expect(result.append).toEqual(["Inner's append"]);
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
it("should collect blocks from multiple snippets", () => {
|
|
762
|
-
const registry = createRegistry([
|
|
763
|
-
["a", "A text\n<append>\nA's append\n</append>"],
|
|
764
|
-
["b", "B text\n<append>\nB's append\n</append>"],
|
|
765
|
-
]);
|
|
766
|
-
|
|
767
|
-
const result = expandHashtags("#a and #b", registry);
|
|
768
|
-
|
|
769
|
-
expect(result.text).toBe("A text and B text");
|
|
770
|
-
expect(result.append).toEqual(["A's append", "B's append"]);
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
it("should handle empty inline with only blocks", () => {
|
|
774
|
-
const registry = createRegistry([["ref", "<append>\nReference material\n</append>"]]);
|
|
775
|
-
|
|
776
|
-
const result = expandHashtags("Use #ref here", registry);
|
|
777
|
-
|
|
778
|
-
expect(result.text).toBe("Use here");
|
|
779
|
-
expect(result.append).toEqual(["Reference material"]);
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
it("should assemble full message correctly", () => {
|
|
783
|
-
const registry = createRegistry([
|
|
784
|
-
["jira", "Jira MCP server\n<append>\n## Jira Usage\n- Field mappings here\n</append>"],
|
|
785
|
-
]);
|
|
786
|
-
|
|
787
|
-
const result = expandHashtags("Create a bug ticket in #jira about the memory leak", registry);
|
|
788
|
-
const assembled = assembleMessage(result);
|
|
789
|
-
|
|
790
|
-
expect(assembled).toBe(
|
|
791
|
-
"Create a bug ticket in Jira MCP server about the memory leak\n\n## Jira Usage\n- Field mappings here",
|
|
792
|
-
);
|
|
793
|
-
});
|
|
794
|
-
|
|
795
|
-
it("should collect multiple append blocks from single snippet", () => {
|
|
796
|
-
const registry = createRegistry([
|
|
797
|
-
["multi", "Inline\n<append>\nFirst append\n</append>\n<append>\nSecond append\n</append>"],
|
|
798
|
-
]);
|
|
799
|
-
|
|
800
|
-
const result = expandHashtags("#multi", registry);
|
|
801
|
-
|
|
802
|
-
expect(result.text).toBe("Inline");
|
|
803
|
-
expect(result.append).toEqual(["First append", "Second append"]);
|
|
804
|
-
});
|
|
805
|
-
|
|
806
|
-
it("should collect multiple prepend blocks from single snippet", () => {
|
|
807
|
-
const registry = createRegistry([
|
|
808
|
-
[
|
|
809
|
-
"multi",
|
|
810
|
-
"<prepend>\nFirst prepend\n</prepend>\n<prepend>\nSecond prepend\n</prepend>\nInline",
|
|
811
|
-
],
|
|
812
|
-
]);
|
|
813
|
-
|
|
814
|
-
const result = expandHashtags("#multi", registry);
|
|
815
|
-
|
|
816
|
-
expect(result.text).toBe("Inline");
|
|
817
|
-
expect(result.prepend).toEqual(["First prepend", "Second prepend"]);
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
it("should assemble multiple prepends and appends in correct order", () => {
|
|
821
|
-
const registry = createRegistry([
|
|
822
|
-
["a", "<prepend>\nA prepend\n</prepend>\nA inline\n<append>\nA append\n</append>"],
|
|
823
|
-
["b", "<prepend>\nB prepend\n</prepend>\nB inline\n<append>\nB append\n</append>"],
|
|
824
|
-
]);
|
|
825
|
-
|
|
826
|
-
const result = expandHashtags("#a then #b", registry);
|
|
827
|
-
const assembled = assembleMessage(result);
|
|
828
|
-
|
|
829
|
-
// Prepends first (in order), then inline, then appends (in order)
|
|
830
|
-
expect(assembled).toBe(
|
|
831
|
-
"A prepend\n\nB prepend\n\nA inline then B inline\n\nA append\n\nB append",
|
|
832
|
-
);
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
it("should handle mix of snippets with and without blocks", () => {
|
|
836
|
-
const registry = createRegistry([
|
|
837
|
-
["plain", "Plain content"],
|
|
838
|
-
["withblocks", "Block inline\n<append>\nBlock append\n</append>"],
|
|
839
|
-
]);
|
|
840
|
-
|
|
841
|
-
const result = expandHashtags("#plain and #withblocks", registry);
|
|
842
|
-
const assembled = assembleMessage(result);
|
|
843
|
-
|
|
844
|
-
expect(assembled).toBe("Plain content and Block inline\n\nBlock append");
|
|
845
|
-
});
|
|
846
|
-
});
|