Skip to content

Commit 6f69c87

Browse files
prosdevclaude
andcommitted
test(core): comprehensive tests for all 10 AST patterns
10 positive (exact counts), 10 negative (one per query), 3 language routing (TSX fixture, JSX→javascript, unsupported), 3 edge cases (empty, malformed, invalid query), 1 performance sanity (552 lines + 10 queries in 35ms), 5 resolveLanguage extension tests. 32 tests total. All S-expressions verified against real tree-sitter parsing — no mocks. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d513b91 commit 6f69c87

1 file changed

Lines changed: 345 additions & 0 deletions

File tree

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
/**
2+
* WasmPatternMatcher Tests
3+
*
4+
* Tests AST-based pattern detection using real tree-sitter parsing.
5+
* 10 positive (exact counts), 10 negative (count === 0), 3 routing,
6+
* 3 edge cases, 1 performance sanity check.
7+
*/
8+
9+
import * as fs from 'node:fs/promises';
10+
import * as path from 'node:path';
11+
import { beforeAll, describe, expect, it } from 'vitest';
12+
import {
13+
ALL_QUERIES,
14+
ERROR_HANDLING_QUERIES,
15+
IMPORT_STYLE_QUERIES,
16+
TYPE_COVERAGE_QUERIES,
17+
} from '../rules';
18+
import { createPatternMatcher, type PatternMatcher, resolveLanguage } from '../wasm-matcher';
19+
20+
describe('WasmPatternMatcher', () => {
21+
let matcher: PatternMatcher;
22+
23+
beforeAll(() => {
24+
matcher = createPatternMatcher();
25+
});
26+
27+
// ========================================================================
28+
// Positive cases (10 tests — assert exact match counts)
29+
// ========================================================================
30+
31+
describe('positive matches (exact counts)', () => {
32+
it('detects try/catch — count === 1', async () => {
33+
const results = await matcher.match(
34+
'try { x(); } catch (e) { }',
35+
'typescript',
36+
ERROR_HANDLING_QUERIES
37+
);
38+
expect(results.get('try-catch')).toBe(1);
39+
});
40+
41+
it('detects throw — count === 1', async () => {
42+
const results = await matcher.match(
43+
'throw new Error("bad");',
44+
'typescript',
45+
ERROR_HANDLING_QUERIES
46+
);
47+
expect(results.get('throw')).toBe(1);
48+
});
49+
50+
it('detects promise.catch — count === 1', async () => {
51+
const results = await matcher.match(
52+
'fetch("/api").catch(handleError);',
53+
'typescript',
54+
ERROR_HANDLING_QUERIES
55+
);
56+
expect(results.get('promise-catch')).toBe(1);
57+
});
58+
59+
it('detects await-in-try (const declaration) — count === 1', async () => {
60+
const results = await matcher.match(
61+
'async function f() { try { const x = await fetch("/api"); } catch (e) {} }',
62+
'typescript',
63+
ERROR_HANDLING_QUERIES
64+
);
65+
expect(results.get('await-in-try')).toBe(1);
66+
});
67+
68+
it('detects error-class with extends — count === 1', async () => {
69+
const results = await matcher.match(
70+
'class HttpError extends BaseError { constructor(m: string) { super(m); } }',
71+
'typescript',
72+
ERROR_HANDLING_QUERIES
73+
);
74+
expect(results.get('error-class')).toBe(1);
75+
});
76+
77+
it('detects dynamic import — count === 1', async () => {
78+
const results = await matcher.match(
79+
'const m = await import("./mod");',
80+
'typescript',
81+
IMPORT_STYLE_QUERIES
82+
);
83+
expect(results.get('dynamic-import')).toBe(1);
84+
});
85+
86+
it('detects re-export — count === 1', async () => {
87+
const results = await matcher.match(
88+
'export { foo } from "./bar";',
89+
'typescript',
90+
IMPORT_STYLE_QUERIES
91+
);
92+
expect(results.get('re-export')).toBe(1);
93+
});
94+
95+
it('detects require — count === 1', async () => {
96+
const results = await matcher.match(
97+
'const fs = require("fs");',
98+
'typescript',
99+
IMPORT_STYLE_QUERIES
100+
);
101+
expect(results.get('require')).toBe(1);
102+
});
103+
104+
it('detects arrow function return type — count === 1', async () => {
105+
const results = await matcher.match(
106+
'const add = (a: number, b: number): number => a + b;',
107+
'typescript',
108+
TYPE_COVERAGE_QUERIES
109+
);
110+
expect(results.get('arrow-return-type')).toBe(1);
111+
});
112+
113+
it('detects function return type — count === 1', async () => {
114+
const results = await matcher.match(
115+
'function greet(name: string): string { return name; }',
116+
'typescript',
117+
TYPE_COVERAGE_QUERIES
118+
);
119+
expect(results.get('function-return-type')).toBe(1);
120+
});
121+
});
122+
123+
// ========================================================================
124+
// Negative cases (10 tests — one per query, count === 0)
125+
// ========================================================================
126+
127+
describe('negative matches (count === 0)', () => {
128+
it('no try/catch in source', async () => {
129+
const results = await matcher.match('const x = 1;', 'typescript', ERROR_HANDLING_QUERIES);
130+
expect(results.get('try-catch')).toBe(0);
131+
});
132+
133+
it('no throw in source', async () => {
134+
const results = await matcher.match(
135+
'function safe() { return 1; }',
136+
'typescript',
137+
ERROR_HANDLING_QUERIES
138+
);
139+
expect(results.get('throw')).toBe(0);
140+
});
141+
142+
it('.then() but not .catch()', async () => {
143+
const results = await matcher.match(
144+
'fetch("/api").then(handle);',
145+
'typescript',
146+
ERROR_HANDLING_QUERIES
147+
);
148+
expect(results.get('promise-catch')).toBe(0);
149+
});
150+
151+
it('bare await in try — documents narrowness', async () => {
152+
// await-in-try only matches const/let declarations with await.
153+
// Bare await as expression statement is intentionally not matched.
154+
const results = await matcher.match(
155+
'async function f() { try { await fetch("/api"); } catch (e) {} }',
156+
'typescript',
157+
ERROR_HANDLING_QUERIES
158+
);
159+
expect(results.get('await-in-try')).toBe(0);
160+
});
161+
162+
it('class without extends', async () => {
163+
const results = await matcher.match(
164+
'class AppService { run() {} }',
165+
'typescript',
166+
ERROR_HANDLING_QUERIES
167+
);
168+
expect(results.get('error-class')).toBe(0);
169+
});
170+
171+
it('static import only — no dynamic import', async () => {
172+
const results = await matcher.match(
173+
'import { foo } from "./bar";',
174+
'typescript',
175+
IMPORT_STYLE_QUERIES
176+
);
177+
expect(results.get('dynamic-import')).toBe(0);
178+
});
179+
180+
it('named export without from — not a re-export', async () => {
181+
const results = await matcher.match('export { foo };', 'typescript', IMPORT_STYLE_QUERIES);
182+
expect(results.get('re-export')).toBe(0);
183+
});
184+
185+
it('ESM only — no require', async () => {
186+
const results = await matcher.match(
187+
'import fs from "fs";',
188+
'typescript',
189+
IMPORT_STYLE_QUERIES
190+
);
191+
expect(results.get('require')).toBe(0);
192+
});
193+
194+
it('arrow without return type', async () => {
195+
const results = await matcher.match(
196+
'const add = (a: number, b: number) => a + b;',
197+
'typescript',
198+
TYPE_COVERAGE_QUERIES
199+
);
200+
expect(results.get('arrow-return-type')).toBe(0);
201+
});
202+
203+
it('function without return type', async () => {
204+
const results = await matcher.match(
205+
'function greet(name: string) { return name; }',
206+
'typescript',
207+
TYPE_COVERAGE_QUERIES
208+
);
209+
expect(results.get('function-return-type')).toBe(0);
210+
});
211+
});
212+
213+
// ========================================================================
214+
// Language routing (3 tests)
215+
// ========================================================================
216+
217+
describe('language routing', () => {
218+
it('parses TSX with try/catch in JSX component', async () => {
219+
// Use the real fixture file for realistic TSX
220+
const fixturePath = path.join(__dirname, '../../services/__fixtures__/react-component.tsx');
221+
let source: string;
222+
try {
223+
source = await fs.readFile(fixturePath, 'utf-8');
224+
} catch {
225+
// Fixture not available — use inline TSX
226+
source = `
227+
function App() {
228+
try { const data = JSON.parse("{}"); } catch (e) { console.error(e); }
229+
return <div>hello</div>;
230+
}`;
231+
}
232+
233+
const results = await matcher.match(source, 'tsx', ERROR_HANDLING_QUERIES);
234+
expect(results.get('try-catch')).toBeGreaterThan(0);
235+
});
236+
237+
it('routes .jsx to javascript grammar', async () => {
238+
const results = await matcher.match(
239+
'const App = () => { try { x(); } catch (e) {} return null; };',
240+
'javascript',
241+
ERROR_HANDLING_QUERIES
242+
);
243+
expect(results.get('try-catch')).toBe(1);
244+
});
245+
246+
it('returns empty map for unsupported language', async () => {
247+
const results = await matcher.match('def hello(): pass', 'python', ERROR_HANDLING_QUERIES);
248+
expect(results.size).toBe(0);
249+
});
250+
});
251+
252+
// ========================================================================
253+
// Edge cases (3 tests)
254+
// ========================================================================
255+
256+
describe('edge cases', () => {
257+
it('empty source — no matches, no crash', async () => {
258+
const results = await matcher.match('', 'typescript', ALL_QUERIES);
259+
// Empty source returns empty map (short-circuits before parsing)
260+
expect(results.size).toBe(0);
261+
});
262+
263+
it('malformed TypeScript — no crash', async () => {
264+
const results = await matcher.match(
265+
'function { { { const = ;; }}',
266+
'typescript',
267+
ERROR_HANDLING_QUERIES
268+
);
269+
// Parser handles gracefully — may return partial results or empty
270+
expect(results).toBeInstanceOf(Map);
271+
});
272+
273+
it('invalid S-expression — returns 0 for that query', async () => {
274+
const badRule = [{ id: 'bad', category: 'test', query: '(((invalid_garbage @@@' }];
275+
const results = await matcher.match('const x = 1;', 'typescript', badRule);
276+
expect(results.get('bad')).toBe(0);
277+
});
278+
});
279+
280+
// ========================================================================
281+
// Performance sanity (1 test — soft assertion)
282+
// ========================================================================
283+
284+
describe('performance', () => {
285+
it('parses ~500 lines + 10 queries in reasonable time', async () => {
286+
// Generate a realistic ~500-line TypeScript file
287+
const lines: string[] = ['import { foo } from "./bar";', ''];
288+
for (let i = 0; i < 50; i++) {
289+
lines.push(`export function fn${i}(x: number): number {`);
290+
lines.push(' try {');
291+
lines.push(` const result = await fetch("/api/${i}");`);
292+
lines.push(' if (!result.ok) throw new Error("failed");');
293+
lines.push(' return result.json();');
294+
lines.push(' } catch (e) {');
295+
lines.push(' console.error(e);');
296+
lines.push(' throw e;');
297+
lines.push(' }');
298+
lines.push('}');
299+
lines.push('');
300+
}
301+
const source = lines.join('\n');
302+
303+
const start = Date.now();
304+
const results = await matcher.match(source, 'typescript', ALL_QUERIES);
305+
const duration = Date.now() - start;
306+
307+
// Soft assertion — log timing, generous threshold
308+
console.log(`Performance: ${source.split('\n').length} lines, 10 queries, ${duration}ms`);
309+
expect(duration).toBeLessThan(500);
310+
311+
// Sanity: should find patterns in the generated code
312+
expect(results.get('try-catch')).toBeGreaterThan(0);
313+
expect(results.get('throw')).toBeGreaterThan(0);
314+
expect(results.get('function-return-type')).toBeGreaterThan(0);
315+
});
316+
});
317+
});
318+
319+
// ========================================================================
320+
// resolveLanguage (extension routing)
321+
// ========================================================================
322+
323+
describe('resolveLanguage', () => {
324+
it('maps .ts to typescript', () => {
325+
expect(resolveLanguage('src/auth.ts')).toBe('typescript');
326+
});
327+
328+
it('maps .tsx to tsx', () => {
329+
expect(resolveLanguage('components/App.tsx')).toBe('tsx');
330+
});
331+
332+
it('maps .js to javascript', () => {
333+
expect(resolveLanguage('lib/utils.js')).toBe('javascript');
334+
});
335+
336+
it('maps .jsx to javascript', () => {
337+
expect(resolveLanguage('components/App.jsx')).toBe('javascript');
338+
});
339+
340+
it('returns undefined for unsupported extensions', () => {
341+
expect(resolveLanguage('main.py')).toBeUndefined();
342+
expect(resolveLanguage('main.go')).toBeUndefined(); // Go has scanner, not pattern matcher
343+
expect(resolveLanguage('README.md')).toBeUndefined();
344+
});
345+
});

0 commit comments

Comments
 (0)