diff --git a/__tests__/arkts-resolution.test.ts b/__tests__/arkts-resolution.test.ts index b095a4433..53825498a 100644 --- a/__tests__/arkts-resolution.test.ts +++ b/__tests__/arkts-resolution.test.ts @@ -25,7 +25,12 @@ beforeAll(async () => { describe('ArkTS attribute-chain resolution precision', () => { let tmpDir: string | undefined; + let cg: CodeGraph | undefined; afterEach(() => { + // Windows refuses to remove a directory holding an open file, so the + // graph's SQLite handle has to go before the temp project does. + cg?.close(); + cg = undefined; if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); @@ -65,7 +70,7 @@ describe('ArkTS attribute-chain resolution precision', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const fns = cg.getNodesByKind('function'); @@ -99,7 +104,12 @@ describe('ArkTS attribute-chain resolution precision', () => { describe('ArkTS ohpm workspace import resolution', () => { let tmpDir: string | undefined; + let cg: CodeGraph | undefined; afterEach(() => { + // Windows refuses to remove a directory holding an open file, so the + // graph's SQLite handle has to go before the temp project does. + cg?.close(); + cg = undefined; if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); @@ -146,7 +156,7 @@ describe('ArkTS ohpm workspace import resolution', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const classes = cg.getNodesByKind('class'); @@ -168,7 +178,12 @@ describe('ArkTS ohpm workspace import resolution', () => { describe('ArkUI state → build() re-render bridge (assignment-gated)', () => { let tmpDir: string | undefined; + let cg: CodeGraph | undefined; afterEach(() => { + // Windows refuses to remove a directory holding an open file, so the + // graph's SQLite handle has to go before the temp project does. + cg?.close(); + cg = undefined; if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); @@ -201,7 +216,7 @@ describe('ArkUI state → build() re-render bridge (assignment-gated)', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const methods = cg.getNodesByKind('method'); @@ -229,7 +244,12 @@ describe('ArkUI state → build() re-render bridge (assignment-gated)', () => { describe('ArkUI @ohos.events.emitter bridge', () => { let tmpDir: string | undefined; + let cg: CodeGraph | undefined; afterEach(() => { + // Windows refuses to remove a directory holding an open file, so the + // graph's SQLite handle has to go before the temp project does. + cg?.close(); + cg = undefined; if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); @@ -266,7 +286,7 @@ describe('ArkUI @ohos.events.emitter bridge', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const methods = cg.getNodesByKind('method'); @@ -299,7 +319,7 @@ describe('ArkUI @ohos.events.emitter bridge', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const fns = cg.getNodesByKind('function'); @@ -314,7 +334,12 @@ describe('ArkUI @ohos.events.emitter bridge', () => { describe('ArkUI router bridge (pushUrl literal → @Entry struct)', () => { let tmpDir: string | undefined; + let cg: CodeGraph | undefined; afterEach(() => { + // Windows refuses to remove a directory holding an open file, so the + // graph's SQLite handle has to go before the temp project does. + cg?.close(); + cg = undefined; if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); @@ -343,7 +368,7 @@ describe('ArkUI router bridge (pushUrl literal → @Entry struct)', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const methods = cg.getNodesByKind('method'); @@ -362,7 +387,12 @@ describe('ArkUI router bridge (pushUrl literal → @Entry struct)', () => { describe('ohpm main entry (custom barrel + .ts consumer)', () => { let tmpDir: string | undefined; + let cg: CodeGraph | undefined; afterEach(() => { + // Windows refuses to remove a directory holding an open file, so the + // graph's SQLite handle has to go before the temp project does. + cg?.close(); + cg = undefined; if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); tmpDir = undefined; }); @@ -408,7 +438,7 @@ describe('ohpm main entry (custom barrel + .ts consumer)', () => { '}\n' ); - const cg = CodeGraph.initSync(tmpDir); + cg = CodeGraph.initSync(tmpDir); await cg.indexAll(); const classes = cg.getNodesByKind('class'); diff --git a/__tests__/frameworks-integration.test.ts b/__tests__/frameworks-integration.test.ts index 3df4f2d88..533018fb3 100644 --- a/__tests__/frameworks-integration.test.ts +++ b/__tests__/frameworks-integration.test.ts @@ -788,6 +788,8 @@ describe('JVM FQN imports — end-to-end', () => { const edge = cg.getIncomingEdges(util!.id).find((e) => e.kind === 'imports'); expect(edge, 'imports edge should reach the top-level function by FQN').toBeDefined(); + + cg.close(); }); it('resolves cross-language: Kotlin importing a Java class', async () => { @@ -809,6 +811,8 @@ describe('JVM FQN imports — end-to-end', () => { const edge = cg.getIncomingEdges(javaBar!.id).find((e) => e.kind === 'imports'); expect(edge, 'Kotlin caller should resolve its import to the Java class').toBeDefined(); + + cg.close(); }); it('disambiguates a class-name collision across packages', async () => { @@ -854,6 +858,8 @@ describe('JVM FQN imports — end-to-end', () => { edges.map((e) => cg.getNode(e.source)?.filePath).filter(Boolean); expect(sourceFiles(alphaIncoming).some((p) => p?.includes('CallerA.kt'))).toBe(true); expect(sourceFiles(betaIncoming).some((p) => p?.includes('CallerB.kt'))).toBe(true); + + cg.close(); }); }); diff --git a/__tests__/mcp-daemon.test.ts b/__tests__/mcp-daemon.test.ts index c73ac564c..fd74c1d14 100644 --- a/__tests__/mcp-daemon.test.ts +++ b/__tests__/mcp-daemon.test.ts @@ -193,10 +193,25 @@ describe('Shared MCP daemon (issue #411)', () => { const daemonPid = readLockPid(realRoot); if (daemonPid && daemonPid !== process.pid && isAlive(daemonPid)) { try { process.kill(daemonPid, 'SIGKILL'); } catch { /* race */ } + await waitProcessExit(daemonPid, 5000); } - await new Promise((r) => setTimeout(r, 50)); + // A signalled process still holds its file handles until it is actually + // gone, and Windows refuses to remove a directory holding an open file — so + // wait for each exit rather than for a fixed grace period. + await Promise.all( + servers.map(({ child }) => + child.exitCode === null && child.signalCode === null + ? new Promise((r) => child.once('exit', () => r())) + : Promise.resolve() + ) + ); servers.length = 0; - fs.rmSync(tempDir, { recursive: true, force: true }); + // These tests deliberately leave a detached daemon alive, and a losing + // launcher can leave a candidate of its own mid-exit; either still holds + // the directory for a moment after the pid above is gone. Windows will not + // remove a directory holding an open file, so retry for a few seconds + // rather than assume the handles are released the instant the pid is. + fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 60, retryDelay: 100 }); }); it('two invocations share ONE detached daemon; both attach as proxies', async () => { diff --git a/__tests__/mcp-initialize.test.ts b/__tests__/mcp-initialize.test.ts index 0a320773d..9f95cecb0 100644 --- a/__tests__/mcp-initialize.test.ts +++ b/__tests__/mcp-initialize.test.ts @@ -107,12 +107,23 @@ describe('MCP initialize handshake (issue #172)', () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-mcp-init-')); }); - afterEach(() => { - if (child && !child.killed) { - child.kill('SIGKILL'); + afterEach(async () => { + // A signalled child still holds its file handles until it is actually gone, + // and Windows refuses to remove a directory holding an open file — so wait + // for the exit, rather than only sending the signal. + if (child) { + const proc = child; child = null; + if (proc.exitCode === null && proc.signalCode === null) { + await new Promise((resolve) => { + proc.once('exit', () => resolve()); + proc.kill('SIGKILL'); + }); + } } - fs.rmSync(tempDir, { recursive: true, force: true }); + // The server may have started a detached daemon this suite does not track, + // so retry the removal while any straggler releases the directory. + fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }); }); it('responds to initialize quickly when no .codegraph exists in cwd', async () => { diff --git a/__tests__/mcp-roots.test.ts b/__tests__/mcp-roots.test.ts index 8e1d4520d..dd9569884 100644 --- a/__tests__/mcp-roots.test.ts +++ b/__tests__/mcp-roots.test.ts @@ -84,13 +84,24 @@ describe('MCP project resolution via roots/list (issue #196)', () => { projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-mcp-proj-')); }); - afterEach(() => { - if (child && !child.killed) { - child.kill('SIGKILL'); + afterEach(async () => { + // A signalled child still holds its file handles until it is actually gone, + // and Windows refuses to remove a directory holding an open file — so wait + // for the exit, rather than only sending the signal. + if (child) { + const proc = child; child = null; + if (proc.exitCode === null && proc.signalCode === null) { + await new Promise((resolve) => { + proc.once('exit', () => resolve()); + proc.kill('SIGKILL'); + }); + } } - fs.rmSync(cwdDir, { recursive: true, force: true }); - fs.rmSync(projectDir, { recursive: true, force: true }); + // The server may have started a detached daemon this suite does not track, + // so retry the removal while any straggler releases the directory. + fs.rmSync(cwdDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }); + fs.rmSync(projectDir, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }); }); it('resolves the project from the client roots/list when no rootUri is sent', async () => { diff --git a/__tests__/mcp-subproject-adoption.test.ts b/__tests__/mcp-subproject-adoption.test.ts index 39abac038..ca0068da3 100644 --- a/__tests__/mcp-subproject-adoption.test.ts +++ b/__tests__/mcp-subproject-adoption.test.ts @@ -113,12 +113,23 @@ describe('MCP workspace sub-project adoption (#1606) + no-default diagnostics (# ws = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-mcp-ws-')); }); - afterEach(() => { - if (child && !child.killed) { - child.kill('SIGKILL'); + afterEach(async () => { + // A signalled child still holds its file handles until it is actually gone, + // and Windows refuses to remove a directory holding an open file — so wait + // for the exit, rather than only sending the signal. + if (child) { + const proc = child; child = null; + if (proc.exitCode === null && proc.signalCode === null) { + await new Promise((resolve) => { + proc.once('exit', () => resolve()); + proc.kill('SIGKILL'); + }); + } } - fs.rmSync(ws, { recursive: true, force: true }); + // The server may have started a detached daemon this suite does not track, + // so retry the removal while any straggler releases the directory. + fs.rmSync(ws, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 }); }); it('adopts the single indexed sub-project below a workspace root as the default project', async () => { diff --git a/__tests__/resolution.test.ts b/__tests__/resolution.test.ts index decaadee5..ab8c49eb6 100644 --- a/__tests__/resolution.test.ts +++ b/__tests__/resolution.test.ts @@ -3570,6 +3570,7 @@ int run() { // feature can't silently regress to a no-op in the indexing flow. it('connects #include to the real header file via include-dir scan (end-to-end)', async () => { const tempProject = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-cpp-e2e-')); + let db: DatabaseConnection | undefined; try { fs.mkdirSync(path.join(tempProject, 'include'), { recursive: true }); fs.mkdirSync(path.join(tempProject, 'src'), { recursive: true }); @@ -3592,7 +3593,7 @@ int run() { // The `#include "utils.h"` edge should target the real // `include/utils.h` file node — not a floating `import` node // living inside main.cpp. - const db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); + db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); const rows = db.getDb().prepare(` select dst.kind as dstKind, dst.file_path as dstPath from edges e @@ -3612,6 +3613,10 @@ int run() { ); expect(stdlibFile).toBeUndefined(); } finally { + // Windows refuses to remove a directory holding an open file: both the + // graph's handle and this test's own one have to go first. + db?.close(); + cg?.close(); fs.rmSync(tempProject, { recursive: true, force: true }); } }); @@ -3684,6 +3689,7 @@ class Both : public Base, public Plain {}; // templated + plain in one cla }); it('resolves require_once to a file→file imports edge (#660)', async () => { + let db: DatabaseConnection | undefined; const tempProject = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-php-e2e-')); try { fs.mkdirSync(path.join(tempProject, 'src'), { recursive: true }); @@ -3701,7 +3707,7 @@ class Both : public Base, public Plain {}; // templated + plain in one cla // reporter's repro: page.php's `require_once("lib.php")` must resolve // to the real src/lib.php file node — a file→file `imports` edge, so // callers(lib.php) now includes page.php. - const db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); + db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); const rows = db.getDb().prepare(` select dst.kind as dstKind, dst.file_path as dstPath from edges e @@ -3716,11 +3722,16 @@ class Both : public Base, public Plain {}; // templated + plain in one cla ); expect(resolved, 'page.php → src/lib.php imports edge missing').toBeDefined(); } finally { + // Windows refuses to remove a directory holding an open file: both the + // graph's handle and this test's own one have to go first. + db?.close(); + cg?.close(); fs.rmSync(tempProject, { recursive: true, force: true }); } }); it('resolves a subdirectory include path to the correct file (#660)', async () => { + let db: DatabaseConnection | undefined; const tempProject = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-php-subdir-')); try { fs.mkdirSync(path.join(tempProject, 'inc'), { recursive: true }); @@ -3735,7 +3746,7 @@ class Both : public Base, public Plain {}; // templated + plain in one cla cg = await CodeGraph.init(tempProject, { index: true }); - const db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); + db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); const rows = db.getDb().prepare(` select dst.kind as dstKind, dst.file_path as dstPath from edges e @@ -3750,11 +3761,16 @@ class Both : public Base, public Plain {}; // templated + plain in one cla 'index.php → inc/db.php imports edge missing' ).toBeDefined(); } finally { + // Windows refuses to remove a directory holding an open file: both the + // graph's handle and this test's own one have to go first. + db?.close(); + cg?.close(); fs.rmSync(tempProject, { recursive: true, force: true }); } }); it('does not mis-connect an unresolvable include to a same-named file elsewhere (#660)', async () => { + let db: DatabaseConnection | undefined; const tempProject = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-php-misresolve-')); try { // app/page.php's `require "inc/db.php"` resolves relative to app/, where @@ -3774,7 +3790,7 @@ class Both : public Base, public Plain {}; // templated + plain in one cla cg = await CodeGraph.init(tempProject, { index: true }); - const db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); + db = DatabaseConnection.open(path.join(tempProject, '.codegraph', 'codegraph.db')); const rows = db.getDb().prepare(` select dst.kind as dstKind, dst.file_path as dstPath from edges e @@ -3789,6 +3805,10 @@ class Both : public Base, public Plain {}; // templated + plain in one cla 'app/page.php must NOT mis-connect to unrelated lib/inc/db.php' ).toBeUndefined(); } finally { + // Windows refuses to remove a directory holding an open file: both the + // graph's handle and this test's own one have to go first. + db?.close(); + cg?.close(); fs.rmSync(tempProject, { recursive: true, force: true }); } });