refactor: simplify webhook detection code

- Fix bug: trigger query matched Webhook→Webhook but pipeline creates
  File→Webhook edges — always returned empty results
- Add missing ensureInitialized() in webhookMap
- Export extractWebhooks so test imports it instead of duplicating 30 lines
- Merge duplicate toLowerCase() into single lowerPath variable
- Replace substring+split line counting with zero-allocation charCodeAt loop
- Fix literal \n in tools.ts description (should be actual newlines)
- Remove accidental QUERIES type addition (belongs to ORM PR)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Test 2026-03-25 22:51:04 +01:00
parent 91845ca400
commit 2fac799c88
4 changed files with 25 additions and 50 deletions

View file

@ -729,6 +729,7 @@ const processBatch = (
toolDefs: [],
ormQueries: [],
webhooks: [],
queuePatterns: [],
constructorBindings: [],
fileScopeBindings: [],
skippedLanguages: {},
@ -1356,10 +1357,18 @@ export function extractORMQueries(
// Webhook/Event Handler Extraction
// ============================================================================
function extractWebhooks(filePath: string, content: string, out: ExtractedWebhook[]): void {
// Skip test files — they often mock webhook patterns
const lp = filePath.toLowerCase();
if (lp.includes('/test') || lp.includes('.test.') || lp.includes('.spec.') || lp.includes('__test')) return;
export function extractWebhooks(filePath: string, content: string, out: ExtractedWebhook[]): void {
const lowerPath = filePath.toLowerCase();
// Skip test files — they often mock webhook patterns (e.g. Stripe constructEvent in test mocks)
if (lowerPath.includes('/test') || lowerPath.includes('.test.') || lowerPath.includes('.spec.') || lowerPath.includes('__test')) return;
// Count newlines before offset without allocating substring + split array
const lineAt = (offset: number): number => {
let n = 0;
for (let i = 0; i < offset; i++) { if (content.charCodeAt(i) === 10) n++; }
return n;
};
if (content.includes('constructEvent') || content.includes('webhooks.constructEvent')) {
const eventTypes: string[] = [];
@ -1368,14 +1377,13 @@ function extractWebhooks(filePath: string, content: string, out: ExtractedWebhoo
while ((m = caseRe.exec(content)) !== null) { eventTypes.push(m[1]); }
const idx = content.indexOf('constructEvent');
out.push({ filePath, name: 'stripe-webhook', kind: 'stripe', eventTypes,
lineNumber: idx > -1 ? content.substring(0, idx).split('\n').length - 1 : 0 });
lineNumber: idx > -1 ? lineAt(idx) : 0 });
return;
}
const edgeFnMatch = filePath.match(/supabase\/functions\/([\w-]+)\/index\.ts$/);
if (edgeFnMatch && content.includes('Deno.serve')) {
const idx = content.indexOf('Deno.serve');
out.push({ filePath, name: edgeFnMatch[1], kind: 'edge-function', eventTypes: [],
lineNumber: content.substring(0, idx).split('\n').length - 1 });
lineNumber: lineAt(content.indexOf('Deno.serve')) });
return;
}
if (content.includes('postgres_changes')) {
@ -1383,10 +1391,9 @@ function extractWebhooks(filePath: string, content: string, out: ExtractedWebhoo
let m;
while ((m = channelRe.exec(content)) !== null) {
out.push({ filePath, name: `realtime:${m[1]}`, kind: 'realtime', eventTypes: [],
lineNumber: content.substring(0, m.index).split('\n').length - 1 });
lineNumber: lineAt(m.index) });
}
}
const lowerPath = filePath.toLowerCase();
if (lowerPath.includes('webhook') || lowerPath.includes('hook')) {
const hasSigVerify = content.includes('x-webhook-signature') || content.includes('x-hub-signature')
|| content.includes('verify');

View file

@ -3222,6 +3222,7 @@ export class LocalBackend {
}
private async webhookMap(repo: RepoHandle, params: { kind?: string; name?: string }): Promise<any> {
await this.ensureInitialized(repo.id);
const kindFilter = params.kind ? `AND n.kind = $kind` : '';
const nameFilter = params.name ? `AND n.name CONTAINS $name` : '';
const queryParams: Record<string, any> = {};
@ -3233,8 +3234,8 @@ export class LocalBackend {
RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.kind AS kind, n.eventTypes AS eventTypes
`, queryParams);
const triggerRows = await executeParameterized(repo.id, `
MATCH (a:Webhook)-[r:CodeRelation {type: 'TRIGGERS'}]->(b:Webhook)
RETURN a.name AS source, b.name AS target, r.reason AS reason
MATCH (f:File)-[r:CodeRelation {type: 'TRIGGERS'}]->(w:Webhook)
RETURN f.filePath AS source, w.name AS target, r.reason AS reason
`, {});
const webhooks = rows.map((r: any) => ({
id: r.id, name: r.name, filePath: r.filePath, kind: r.kind, eventTypes: r.eventTypes || [],

View file

@ -337,7 +337,11 @@ Returns: tool nodes with their handler files and descriptions.`,
},
{
name: 'webhook_map',
description: `Show webhook and event handler endpoints: Stripe webhooks, Supabase Edge Functions, and realtime subscriptions.\n\nWHEN TO USE: Understanding event-driven architecture, finding webhook handlers, tracing event flows.\n\nReturns: webhook nodes with handler files, kind, event types, and TRIGGERS edges.`,
description: `Show webhook and event handler endpoints: Stripe webhooks, Supabase Edge Functions, and realtime subscriptions.
WHEN TO USE: Understanding event-driven architecture, finding webhook handlers, tracing event flows.
Returns: webhook nodes with handler files, kind, event types, and TRIGGERS edges.`,
inputSchema: {
type: 'object',
properties: {

View file

@ -21,7 +21,7 @@ import path from 'path';
import { LocalBackend } from '../../src/mcp/local/local-backend.js';
import { listRegisteredRepos } from '../../src/storage/repo-manager.js';
import { withTestLbugDB } from '../helpers/test-indexed-db.js';
import type { ExtractedWebhook } from '../../src/core/ingestion/workers/parse-worker.js';
import { extractWebhooks, type ExtractedWebhook } from '../../src/core/ingestion/workers/parse-worker.js';
vi.mock('../../src/storage/repo-manager.js', () => ({
listRegisteredRepos: vi.fn().mockResolvedValue([]),
@ -31,11 +31,6 @@ vi.mock('../../src/storage/repo-manager.js', () => ({
const WEBHOOK_REPO = path.resolve(__dirname, '..', 'fixtures', 'webhook-repo');
// ─── Part A: extractWebhooks detection logic against fixture files ────
//
// We import the worker module and call extractWebhooks through a dynamic import
// since it's not exported directly. Instead, we replicate the detection logic
// inline — this is identical to what parse-worker.ts does. The fixture files
// are the canonical proof that the regex patterns match real-world code.
describe('webhook detection — extraction from fixture files', () => {
const stripeContent = fs.readFileSync(
@ -48,38 +43,6 @@ describe('webhook detection — extraction from fixture files', () => {
path.join(WEBHOOK_REPO, 'lib/realtime.ts'), 'utf-8'
);
// Replicate extractWebhooks logic from parse-worker.ts to prove fixture detection
function extractWebhooks(filePath: string, content: string, out: ExtractedWebhook[]): void {
const lp = filePath.toLowerCase();
if (lp.includes('/test') || lp.includes('.test.') || lp.includes('.spec.') || lp.includes('__test')) return;
if (content.includes('constructEvent') || content.includes('webhooks.constructEvent')) {
const eventTypes: string[] = [];
const caseRe = /case\s+['"]([.\w-]+)['"]/g;
let m;
while ((m = caseRe.exec(content)) !== null) { eventTypes.push(m[1]); }
const idx = content.indexOf('constructEvent');
out.push({ filePath, name: 'stripe-webhook', kind: 'stripe', eventTypes,
lineNumber: idx > -1 ? content.substring(0, idx).split('\n').length - 1 : 0 });
return;
}
const edgeFnMatch = filePath.match(/supabase\/functions\/([\w-]+)\/index\.ts$/);
if (edgeFnMatch && content.includes('Deno.serve')) {
const idx = content.indexOf('Deno.serve');
out.push({ filePath, name: edgeFnMatch[1], kind: 'edge-function', eventTypes: [],
lineNumber: content.substring(0, idx).split('\n').length - 1 });
return;
}
if (content.includes('postgres_changes')) {
const channelRe = /\.channel\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
let m;
while ((m = channelRe.exec(content)) !== null) {
out.push({ filePath, name: `realtime:${m[1]}`, kind: 'realtime', eventTypes: [],
lineNumber: content.substring(0, m.index).split('\n').length - 1 });
}
}
}
it('detects Stripe webhook with correct kind and eventTypes', () => {
const out: ExtractedWebhook[] = [];
extractWebhooks('app/api/stripe/webhooks/route.ts', stripeContent, out);