diff --git a/__tests__/context.test.ts b/__tests__/context.test.ts index 52dae1fe..9a0614aa 100644 --- a/__tests__/context.test.ts +++ b/__tests__/context.test.ts @@ -210,6 +210,19 @@ export function validateEmail(email: string): boolean { expect(result.nodes.size).toBeLessThanOrEqual(5); }); + + it('should clamp absurd searchLimit/maxNodes values to safe upper bounds', async () => { + // Without clamping, the internal `findNodesByExactName` query would + // request `searchLimit * 5` rows — passing 1e9 here would blow out + // memory. The call should complete in normal time and not return more + // than the hard cap on maxNodes (1000). + const result = await cg.findRelevantContext('function', { + searchLimit: 1_000_000_000, + maxNodes: 1_000_000_000, + traversalDepth: 1_000, + }); + expect(result.nodes.size).toBeLessThanOrEqual(1000); + }); }); describe('buildContext()', () => { diff --git a/__tests__/extraction.test.ts b/__tests__/extraction.test.ts index 8a70ffed..a6fd7687 100644 --- a/__tests__/extraction.test.ts +++ b/__tests__/extraction.test.ts @@ -3079,3 +3079,72 @@ describe('Directory Exclusion', () => { expect(files.every((f) => !f.includes('vendor'))).toBe(true); }); }); + +// ============================================================================= +// Svelte line-number regressions (audit fix) +// ============================================================================= + +describe('Svelte line numbering', () => { + it('reports symbol line numbers relative to the .svelte file, not the script content', () => { + // Line 1: + const code = `\n`; + const result = extractFromSource('Comp.svelte', code); + const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'add'); + expect(fn).toBeDefined(); + expect(fn?.startLine).toBe(2); + }); + + it('handles multi-line opening tags (script with attributes wrapped)', () => { + // Line 1: + const code = `\nfunction greet() { return "hi"; }\n\n`; + const result = extractFromSource('Comp.svelte', code); + const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'greet'); + expect(fn).toBeDefined(); + expect(fn?.startLine).toBe(3); + }); + + it('preserves correct line numbers when the script block is offset by template lines', () => { + // Line 1:

Hello

+ // Line 2: + // Line 3: + const code = `

Hello

\n\n\n`; + const result = extractFromSource('Comp.svelte', code); + const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'bottom'); + expect(fn).toBeDefined(); + expect(fn?.startLine).toBe(4); + }); + + it('handles a single-line script block with no internal newline', () => { + // Line 1: + const code = `\n`; + const result = extractFromSource('Comp.svelte', code); + const fn = result.nodes.find((n) => n.kind === 'function' && n.name === 'inline'); + expect(fn).toBeDefined(); + expect(fn?.startLine).toBe(1); + }); + + it('attributes each block correctly when a file has both module and instance scripts', () => { + // Line 1: + // Line 4: + // Line 5: + const code = + `\n` + + `\n\n`; + const result = extractFromSource('Comp.svelte', code); + const moduleFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'moduleHelper'); + const instanceFn = result.nodes.find((n) => n.kind === 'function' && n.name === 'instanceHelper'); + expect(moduleFn?.startLine).toBe(2); + expect(instanceFn?.startLine).toBe(6); + }); +}); diff --git a/__tests__/watcher.test.ts b/__tests__/watcher.test.ts index f3638e6d..ee732df6 100644 --- a/__tests__/watcher.test.ts +++ b/__tests__/watcher.test.ts @@ -218,6 +218,36 @@ describe('FileWatcher', () => { watcher.stop(); }); + + it('should retry pending changes after a sync failure (no events lost)', async () => { + // First call rejects, subsequent calls resolve. After the initial + // failure, the watcher should retry the same batch on its own — without + // this, transient sync failures (DB locked etc.) would silently drop the + // changes until a new file event happened. + let calls = 0; + const syncFn = vi.fn().mockImplementation(() => { + calls++; + if (calls === 1) return Promise.reject(new Error('transient')); + return Promise.resolve({ filesChanged: 1, durationMs: 5 }); + }); + const onSyncError = vi.fn(); + const onSyncComplete = vi.fn(); + const watcher = new FileWatcher(testDir, baseConfig, syncFn, { + debounceMs: 100, + onSyncError, + onSyncComplete, + }); + + watcher.start(); + fs.writeFileSync(path.join(testDir, 'src', 'test.ts'), 'export const z = 3;'); + + await waitFor(() => onSyncComplete.mock.calls.length > 0, 5000); + expect(onSyncError).toHaveBeenCalledTimes(1); + expect(syncFn).toHaveBeenCalledTimes(2); + expect(onSyncComplete).toHaveBeenCalledWith({ filesChanged: 1, durationMs: 5 }); + + watcher.stop(); + }); }); describe('CodeGraph integration', () => { diff --git a/src/context/index.ts b/src/context/index.ts index 94192377..08f25657 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -286,6 +286,14 @@ export class ContextBuilder { options: FindRelevantContextOptions = {} ): Promise { const opts = { ...DEFAULT_FIND_OPTIONS, ...options }; + // Bound user-supplied limits — `searchLimit` is multiplied by 5 in + // findNodesByExactName (line 312) and feeds several other unbounded + // operations below, so a request with `searchLimit: 1_000_000` would + // pull millions of rows before any filtering. 100 is well above the + // largest legitimate use we've seen. + opts.searchLimit = Math.min(Math.max(1, opts.searchLimit), 100); + opts.maxNodes = Math.min(Math.max(1, opts.maxNodes), 1000); + opts.traversalDepth = Math.min(Math.max(0, opts.traversalDepth), 10); // Start with empty subgraph const nodes = new Map(); diff --git a/src/extraction/svelte-extractor.ts b/src/extraction/svelte-extractor.ts index 5586ee34..323cbe80 100644 --- a/src/extraction/svelte-extractor.ts +++ b/src/extraction/svelte-extractor.ts @@ -135,13 +135,17 @@ export class SvelteExtractor { // Detect module script const isModule = /context\s*=\s*["']module["']/.test(attrs); - // Calculate start line of the script content (line after