From 7918552f0007c45bdfcc588369a192e60b07e80f Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 6 Jun 2026 02:21:43 -0600 Subject: [PATCH 1/4] fix(test): prevent EBUSY on Windows from failing embedding-regression cleanup SQLite WAL checkpoint holds OS-level file locks for hundreds of ms after db.close() returns on Windows. The plain rmSync in afterAll has no retry budget and fails intermittently with EBUSY: resource busy or locked. Three-part fix: - Call flushDeferredClose() to close any pending deferred DB handles first - Register process.once('exit') safety net so the temp dir is cleaned up even if the immediate attempt is blocked by a WAL lock - Wrap rmSync in try/catch with maxRetries:10/retryDelay:200 so a transient EBUSY never propagates as a test failure (all assertions already passed) Fixes #1353 --- tests/search/embedding-regression.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/search/embedding-regression.test.ts b/tests/search/embedding-regression.test.ts index ea08c5b7b..e0cbfd632 100644 --- a/tests/search/embedding-regression.test.ts +++ b/tests/search/embedding-regression.test.ts @@ -11,6 +11,7 @@ import os from 'node:os'; import path from 'node:path'; import Database from 'better-sqlite3'; import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { flushDeferredClose } from '../../src/db/index.js'; // Detect whether transformers is available (optional dep) let hasTransformers = false; @@ -69,7 +70,22 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { }, 240_000); afterAll(() => { - if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); + if (!tmpDir) return; + flushDeferredClose(); + const dir = tmpDir; + // On Windows, SQLite WAL checkpoint holds OS-level file locks for hundreds + // of ms after db.close() returns. Register a process.once('exit') safety + // net so a lingering EBUSY never surfaces as a test failure. + process.once('exit', () => { + try { + fs.rmSync(dir, { recursive: true, force: true }); + } catch {} + }); + try { + fs.rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); + } catch { + // Swallow — all assertions already passed; exit handler above cleans up. + } }); describe('smoke tests', () => { From d8bd50aba8d10cba8fafa6feec9d7151c84b50dc Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 6 Jun 2026 03:37:49 -0600 Subject: [PATCH 2/4] fix(test): remove bare catch, add rate-limit skip and TestContext types (#1356) --- tests/search/embedding-regression.test.ts | 61 +++++++++++++---------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/tests/search/embedding-regression.test.ts b/tests/search/embedding-regression.test.ts index e0cbfd632..aedf8b499 100644 --- a/tests/search/embedding-regression.test.ts +++ b/tests/search/embedding-regression.test.ts @@ -10,7 +10,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import Database from 'better-sqlite3'; -import { afterAll, beforeAll, describe, expect, test } from 'vitest'; +import { afterAll, beforeAll, describe, expect, type TestContext, test } from 'vitest'; import { flushDeferredClose } from '../../src/db/index.js'; // Detect whether transformers is available (optional dep) @@ -53,6 +53,8 @@ export function main() { }; let tmpDir: string, dbPath: string; +// Set to true when the model download is rate-limited (HTTP 429) so all tests skip. +let rateLimited = false; describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { beforeAll(async () => { @@ -65,38 +67,41 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { await buildGraph(tmpDir, { skipRegistry: true }); dbPath = path.join(tmpDir, '.codegraph', 'graph.db'); - // Build embeddings with the smallest/fastest model - await buildEmbeddings(tmpDir, 'minilm', dbPath); + // Build embeddings with the smallest/fastest model. + // Skip gracefully when HuggingFace rate-limits the model download (HTTP 429). + try { + await buildEmbeddings(tmpDir, 'minilm', dbPath); + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + if (msg.includes('429')) { + rateLimited = true; + return; + } + throw err; + } }, 240_000); afterAll(() => { if (!tmpDir) return; + // Flush any deferred DB closes before deleting the temp directory. + // On Windows, SQLite WAL files can remain locked briefly after db.close(), + // causing intermittent EBUSY errors. Node's built-in maxRetries handles + // retrying EBUSY/EMFILE automatically with retryDelay ms between attempts. flushDeferredClose(); - const dir = tmpDir; - // On Windows, SQLite WAL checkpoint holds OS-level file locks for hundreds - // of ms after db.close() returns. Register a process.once('exit') safety - // net so a lingering EBUSY never surfaces as a test failure. - process.once('exit', () => { - try { - fs.rmSync(dir, { recursive: true, force: true }); - } catch {} - }); - try { - fs.rmSync(dir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); - } catch { - // Swallow — all assertions already passed; exit handler above cleans up. - } + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); }); describe('smoke tests', () => { - test('stored at least 6 embeddings', () => { + test('stored at least 6 embeddings', (ctx: TestContext) => { + if (rateLimited) ctx.skip(); const db = new Database(dbPath, { readonly: true }); const count = db.prepare('SELECT COUNT(*) as c FROM embeddings').get().c; db.close(); expect(count).toBeGreaterThanOrEqual(6); }); - test('metadata records correct model and dimension', () => { + test('metadata records correct model and dimension', (ctx: TestContext) => { + if (rateLimited) ctx.skip(); const db = new Database(dbPath, { readonly: true }); const model = db.prepare("SELECT value FROM embedding_meta WHERE key = 'model'").get().value; const dim = db.prepare("SELECT value FROM embedding_meta WHERE key = 'dim'").get().value; @@ -105,7 +110,8 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { expect(Number(dim)).toBe(384); }); - test('search returns results with positive similarity', async () => { + test('search returns results with positive similarity', async (ctx: TestContext) => { + if (rateLimited) ctx.skip(); const data = await searchData('add numbers', dbPath, { minScore: 0.01 }); expect(data).not.toBeNull(); expect(data.results.length).toBeGreaterThan(0); @@ -127,23 +133,28 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { expect(names).toContain(expectedName); } - test('"add two numbers together" finds add in top 3', async () => { + test('"add two numbers together" finds add in top 3', async (ctx: TestContext) => { + if (rateLimited) ctx.skip(); await expectInTopN('add two numbers together', 'add', 3); }); - test('"multiply values" finds multiply in top 3', async () => { + test('"multiply values" finds multiply in top 3', async (ctx: TestContext) => { + if (rateLimited) ctx.skip(); await expectInTopN('multiply values', 'multiply', 3); }); - test('"compute the square of a number" finds square in top 3', async () => { + test('"compute the square of a number" finds square in top 3', async (ctx: TestContext) => { + if (rateLimited) ctx.skip(); await expectInTopN('compute the square of a number', 'square', 3); }); - test('"sum of squares calculation" finds sumOfSquares in top 3', async () => { + test('"sum of squares calculation" finds sumOfSquares in top 3', async (ctx: TestContext) => { + if (rateLimited) ctx.skip(); await expectInTopN('sum of squares calculation', 'sumOfSquares', 3); }); - test('"main entry point function" finds main in top 5', async () => { + test('"main entry point function" finds main in top 5', async (ctx: TestContext) => { + if (rateLimited) ctx.skip(); await expectInTopN('main entry point function', 'main', 5); }); }); From d7ac7ccb0592d788c3b3f7c16075c1b216d7e029 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 6 Jun 2026 04:04:36 -0600 Subject: [PATCH 3/4] fix(test): catch only EBUSY/EPERM in afterAll cleanup, re-throw other errors (#1356) --- tests/search/embedding-regression.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/search/embedding-regression.test.ts b/tests/search/embedding-regression.test.ts index aedf8b499..cd4b2ffc3 100644 --- a/tests/search/embedding-regression.test.ts +++ b/tests/search/embedding-regression.test.ts @@ -88,7 +88,14 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { // causing intermittent EBUSY errors. Node's built-in maxRetries handles // retrying EBUSY/EMFILE automatically with retryDelay ms between attempts. flushDeferredClose(); - fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); + try { + fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); + } catch (err: unknown) { + // Only swallow EBUSY / EPERM — Windows WAL locks that outlast the retry budget. + // Any other error (permission denied, quota, path corruption) surfaces normally. + const code = (err as NodeJS.ErrnoException).code; + if (code !== 'EBUSY' && code !== 'EPERM') throw err; + } }); describe('smoke tests', () => { From 16ab06f143432e576d2dd004a3598b26ec9a2175 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Sat, 6 Jun 2026 18:28:23 -0600 Subject: [PATCH 4/4] fix(test): add process.once exit safety net for leaked temp dirs on Windows (#1356) --- tests/search/embedding-regression.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/search/embedding-regression.test.ts b/tests/search/embedding-regression.test.ts index cd4b2ffc3..82f2767fa 100644 --- a/tests/search/embedding-regression.test.ts +++ b/tests/search/embedding-regression.test.ts @@ -88,6 +88,16 @@ describe.skipIf(!hasTransformers)('embedding regression (real model)', () => { // causing intermittent EBUSY errors. Node's built-in maxRetries handles // retrying EBUSY/EMFILE automatically with retryDelay ms between attempts. flushDeferredClose(); + // Safety net: if the WAL lock outlasts the retry budget, clean up at process exit. + // This prevents leaked codegraph-embed-regression-* directories on Windows CI. + const capturedDir = tmpDir; + process.once('exit', () => { + try { + fs.rmSync(capturedDir, { recursive: true, force: true }); + } catch { + // best-effort — OS will eventually reclaim at reboot + } + }); try { fs.rmSync(tmpDir, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 }); } catch (err: unknown) {