Skip to content

Commit 20bfbe0

Browse files
committed
wip
1 parent e379449 commit 20bfbe0

File tree

6 files changed

+783
-156
lines changed

6 files changed

+783
-156
lines changed

packages/mcp-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@sveltejs/mcp-schema": "workspace:^",
2424
"@tmcp/adapter-valibot": "^0.1.4",
2525
"@typescript-eslint/parser": "^8.44.0",
26+
"commander": "^13.1.0",
2627
"eslint": "^9.36.0",
2728
"eslint-plugin-svelte": "^3.12.3",
2829
"svelte": "^5.39.2",
Lines changed: 384 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import { writeFile, mkdir, rm, readFile } from 'node:fs/promises';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import type { SummaryData } from '../src/lib/schemas.ts';
6+
7+
const current_filename = fileURLToPath(import.meta.url);
8+
const current_dirname = path.dirname(current_filename);
9+
const test_output_dir = path.join(current_dirname, '../test-output');
10+
const test_use_cases_path = path.join(test_output_dir, 'use_cases.json');
11+
12+
function create_summary_data(
13+
summaries: Record<string, string>,
14+
total_sections: number = Object.keys(summaries).length,
15+
): SummaryData {
16+
return {
17+
generated_at: new Date().toISOString(),
18+
model: 'claude-sonnet-4-5-20250929',
19+
total_sections,
20+
successful_summaries: Object.keys(summaries).length,
21+
failed_summaries: 0,
22+
summaries,
23+
};
24+
}
25+
26+
describe('generate-summaries incremental processing', () => {
27+
beforeEach(async () => {
28+
// Create test output directory
29+
await mkdir(test_output_dir, { recursive: true });
30+
});
31+
32+
afterEach(async () => {
33+
// Clean up test output directory
34+
try {
35+
await rm(test_output_dir, { recursive: true, force: true });
36+
} catch {
37+
// Ignore cleanup errors
38+
}
39+
});
40+
41+
describe('file operations', () => {
42+
it('should create use_cases.json if it does not exist', async () => {
43+
const initial_data = create_summary_data({
44+
'svelte/overview': 'always, any svelte project',
45+
});
46+
47+
await writeFile(test_use_cases_path, JSON.stringify(initial_data, null, 2), 'utf-8');
48+
49+
const content = await readFile(test_use_cases_path, 'utf-8');
50+
const data = JSON.parse(content);
51+
52+
expect(data.summaries).toHaveProperty('svelte/overview');
53+
expect(data.total_sections).toBe(1);
54+
});
55+
56+
it('should load existing use_cases.json', async () => {
57+
const existing_data = create_summary_data({
58+
'svelte/overview': 'always, any svelte project',
59+
'svelte/$state': 'reactivity, state management',
60+
});
61+
62+
await writeFile(test_use_cases_path, JSON.stringify(existing_data, null, 2), 'utf-8');
63+
64+
const content = await readFile(test_use_cases_path, 'utf-8');
65+
const data = JSON.parse(content);
66+
67+
expect(Object.keys(data.summaries)).toHaveLength(2);
68+
expect(data.summaries).toHaveProperty('svelte/overview');
69+
expect(data.summaries).toHaveProperty('svelte/$state');
70+
});
71+
72+
it('should handle malformed use_cases.json gracefully', async () => {
73+
await writeFile(test_use_cases_path, '{ invalid json', 'utf-8');
74+
75+
// Should throw when trying to parse
76+
const content = await readFile(test_use_cases_path, 'utf-8');
77+
expect(() => JSON.parse(content)).toThrow();
78+
});
79+
});
80+
81+
describe('change detection', () => {
82+
it('should detect new sections', () => {
83+
const existing_summaries = {
84+
'svelte/overview': 'always, any svelte project',
85+
};
86+
87+
const current_sections = [
88+
{ slug: 'svelte/overview', title: 'Overview' },
89+
{ slug: 'svelte/$state', title: '$state' }, // New section
90+
];
91+
92+
const existing_slugs = new Set(Object.keys(existing_summaries));
93+
const new_sections = current_sections.filter((s) => !existing_slugs.has(s.slug));
94+
95+
expect(new_sections).toHaveLength(1);
96+
expect(new_sections[0]?.slug).toBe('svelte/$state');
97+
});
98+
99+
it('should detect removed sections', () => {
100+
const existing_summaries = {
101+
'svelte/overview': 'always, any svelte project',
102+
'svelte/$state': 'reactivity, state management',
103+
'svelte/old-api': 'deprecated',
104+
};
105+
106+
const current_sections = [
107+
{ slug: 'svelte/overview', title: 'Overview' },
108+
{ slug: 'svelte/$state', title: '$state' },
109+
];
110+
111+
const current_slugs = new Set(current_sections.map((s) => s.slug));
112+
const removed_sections = Object.keys(existing_summaries).filter(
113+
(slug) => !current_slugs.has(slug),
114+
);
115+
116+
expect(removed_sections).toHaveLength(1);
117+
expect(removed_sections[0]).toBe('svelte/old-api');
118+
});
119+
120+
it('should detect no changes when sections are identical', () => {
121+
const existing_summaries = {
122+
'svelte/overview': 'always, any svelte project',
123+
'svelte/$state': 'reactivity, state management',
124+
};
125+
126+
const current_sections = [
127+
{ slug: 'svelte/overview', title: 'Overview' },
128+
{ slug: 'svelte/$state', title: '$state' },
129+
];
130+
131+
const existing_slugs = new Set(Object.keys(existing_summaries));
132+
const current_slugs = new Set(current_sections.map((s) => s.slug));
133+
134+
const new_sections = current_sections.filter((s) => !existing_slugs.has(s.slug));
135+
const removed_sections = Object.keys(existing_summaries).filter(
136+
(slug) => !current_slugs.has(slug),
137+
);
138+
139+
expect(new_sections).toHaveLength(0);
140+
expect(removed_sections).toHaveLength(0);
141+
});
142+
});
143+
144+
describe('merging summaries', () => {
145+
it('should merge new summaries with existing ones', async () => {
146+
const existing_data = create_summary_data({
147+
'svelte/overview': 'always, any svelte project',
148+
'svelte/$state': 'reactivity, state management',
149+
});
150+
151+
const new_summaries = {
152+
'kit/introduction': 'sveltekit, getting started',
153+
};
154+
155+
const merged = { ...existing_data.summaries, ...new_summaries };
156+
157+
expect(Object.keys(merged)).toHaveLength(3);
158+
expect(merged).toHaveProperty('svelte/overview');
159+
expect(merged).toHaveProperty('svelte/$state');
160+
expect(merged).toHaveProperty('kit/introduction');
161+
});
162+
163+
it('should override existing summaries when updating', () => {
164+
const existing_summaries = {
165+
'svelte/overview': 'old description',
166+
};
167+
168+
const new_summaries = {
169+
'svelte/overview': 'new description',
170+
};
171+
172+
const merged = { ...existing_summaries, ...new_summaries };
173+
174+
expect(merged['svelte/overview']).toBe('new description');
175+
});
176+
177+
it('should remove deleted sections from summaries', () => {
178+
const existing_summaries = {
179+
'svelte/overview': 'always, any svelte project',
180+
'svelte/$state': 'reactivity, state management',
181+
'svelte/old-api': 'deprecated',
182+
};
183+
184+
const to_remove = ['svelte/old-api'];
185+
const merged = { ...existing_summaries };
186+
187+
for (const slug of to_remove) {
188+
delete merged[slug];
189+
}
190+
191+
expect(Object.keys(merged)).toHaveLength(2);
192+
expect(merged).not.toHaveProperty('svelte/old-api');
193+
expect(merged).toHaveProperty('svelte/overview');
194+
expect(merged).toHaveProperty('svelte/$state');
195+
});
196+
});
197+
198+
describe('CLI argument parsing', () => {
199+
it('should parse --force flag', () => {
200+
const args = ['--force'];
201+
const has_force = args.includes('--force');
202+
203+
expect(has_force).toBe(true);
204+
});
205+
206+
it('should parse --dry-run flag', () => {
207+
const args = ['--dry-run'];
208+
const has_dry_run = args.includes('--dry-run');
209+
210+
expect(has_dry_run).toBe(true);
211+
});
212+
213+
it('should parse --sections flag with section names', () => {
214+
const args = ['--sections', 'svelte/overview', 'svelte/$state'];
215+
const sections_index = args.indexOf('--sections');
216+
217+
const sections: string[] = [];
218+
if (sections_index !== -1) {
219+
for (let i = sections_index + 1; i < args.length; i++) {
220+
if (args[i]?.startsWith('--')) break;
221+
sections.push(args[i]!);
222+
}
223+
}
224+
225+
expect(sections).toHaveLength(2);
226+
expect(sections).toContain('svelte/overview');
227+
expect(sections).toContain('svelte/$state');
228+
});
229+
230+
it('should handle multiple flags together', () => {
231+
const args = ['--force', '--dry-run', '--sections', 'svelte/overview'];
232+
233+
expect(args.includes('--force')).toBe(true);
234+
expect(args.includes('--dry-run')).toBe(true);
235+
expect(args.includes('--sections')).toBe(true);
236+
});
237+
});
238+
239+
describe('summary data validation', () => {
240+
it('should create valid summary data structure', () => {
241+
const data = create_summary_data({
242+
'svelte/overview': 'always, any svelte project',
243+
});
244+
245+
expect(data).toHaveProperty('generated_at');
246+
expect(data).toHaveProperty('model');
247+
expect(data).toHaveProperty('total_sections');
248+
expect(data).toHaveProperty('successful_summaries');
249+
expect(data).toHaveProperty('failed_summaries');
250+
expect(data).toHaveProperty('summaries');
251+
});
252+
253+
it('should track failed summaries', () => {
254+
const data: SummaryData = {
255+
generated_at: new Date().toISOString(),
256+
model: 'claude-sonnet-4-5-20250929',
257+
total_sections: 3,
258+
successful_summaries: 2,
259+
failed_summaries: 1,
260+
summaries: {
261+
'svelte/overview': 'always',
262+
'svelte/$state': 'reactivity',
263+
},
264+
errors: [{ section: 'svelte/broken', error: 'Failed to fetch' }],
265+
};
266+
267+
expect(data.failed_summaries).toBe(1);
268+
expect(data.errors).toHaveLength(1);
269+
});
270+
});
271+
272+
describe('specific section processing', () => {
273+
it('should only process specified sections when --sections flag is used', () => {
274+
const all_sections = [
275+
{ slug: 'svelte/overview', title: 'Overview' },
276+
{ slug: 'svelte/$state', title: '$state' },
277+
{ slug: 'kit/introduction', title: 'Introduction' },
278+
];
279+
280+
const specified_sections = ['svelte/$state'];
281+
const to_process = all_sections.filter((s) => specified_sections.includes(s.slug));
282+
283+
expect(to_process).toHaveLength(1);
284+
expect(to_process[0]?.slug).toBe('svelte/$state');
285+
});
286+
287+
it('should process multiple specified sections', () => {
288+
const all_sections = [
289+
{ slug: 'svelte/overview', title: 'Overview' },
290+
{ slug: 'svelte/$state', title: '$state' },
291+
{ slug: 'kit/introduction', title: 'Introduction' },
292+
];
293+
294+
const specified_sections = ['svelte/overview', 'kit/introduction'];
295+
const to_process = all_sections.filter((s) => specified_sections.includes(s.slug));
296+
297+
expect(to_process).toHaveLength(2);
298+
});
299+
});
300+
301+
describe('force regeneration', () => {
302+
it('should process all sections when --force is used', () => {
303+
const existing_summaries = {
304+
'svelte/overview': 'always, any svelte project',
305+
};
306+
307+
const all_sections = [
308+
{ slug: 'svelte/overview', title: 'Overview' },
309+
{ slug: 'svelte/$state', title: '$state' },
310+
];
311+
312+
const force = true;
313+
const to_process = force
314+
? all_sections
315+
: all_sections.filter((s) => !existing_summaries[s.slug]);
316+
317+
expect(to_process).toHaveLength(2); // All sections even though one exists
318+
});
319+
320+
it('should only process new sections when --force is not used', () => {
321+
const existing_summaries = {
322+
'svelte/overview': 'always, any svelte project',
323+
};
324+
325+
const all_sections = [
326+
{ slug: 'svelte/overview', title: 'Overview' },
327+
{ slug: 'svelte/$state', title: '$state' },
328+
];
329+
330+
const force = false;
331+
const existing_slugs = new Set(Object.keys(existing_summaries));
332+
const to_process = force
333+
? all_sections
334+
: all_sections.filter((s) => !existing_slugs.has(s.slug));
335+
336+
expect(to_process).toHaveLength(1); // Only new section
337+
expect(to_process[0]?.slug).toBe('svelte/$state');
338+
});
339+
});
340+
341+
describe('edge cases', () => {
342+
it('should handle empty existing summaries', () => {
343+
const existing_summaries: Record<string, string> = {};
344+
const all_sections = [
345+
{ slug: 'svelte/overview', title: 'Overview' },
346+
{ slug: 'svelte/$state', title: '$state' },
347+
];
348+
349+
const existing_slugs = new Set(Object.keys(existing_summaries));
350+
const new_sections = all_sections.filter((s) => !existing_slugs.has(s.slug));
351+
352+
expect(new_sections).toHaveLength(2);
353+
});
354+
355+
it('should handle empty current sections', () => {
356+
const existing_summaries = {
357+
'svelte/overview': 'always, any svelte project',
358+
'svelte/$state': 'reactivity, state management',
359+
};
360+
361+
const current_sections: Array<{ slug: string; title: string }> = [];
362+
363+
const current_slugs = new Set(current_sections.map((s) => s.slug));
364+
const removed_sections = Object.keys(existing_summaries).filter(
365+
(slug) => !current_slugs.has(slug),
366+
);
367+
368+
expect(removed_sections).toHaveLength(2); // All existing should be removed
369+
});
370+
371+
it('should handle sections with special characters in slugs', () => {
372+
const sections = [
373+
{ slug: 'svelte/$state', title: '$state' },
374+
{ slug: 'svelte/@html', title: '@html' },
375+
];
376+
377+
const existing_slugs = new Set(['svelte/$state']);
378+
const new_sections = sections.filter((s) => !existing_slugs.has(s.slug));
379+
380+
expect(new_sections).toHaveLength(1);
381+
expect(new_sections[0]?.slug).toBe('svelte/@html');
382+
});
383+
});
384+
});

0 commit comments

Comments
 (0)