Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 37 additions & 7 deletions __tests__/arkts-resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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');
Expand All @@ -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;
});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
});
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand All @@ -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;
});
Expand Down Expand Up @@ -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');
Expand All @@ -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;
});
Expand Down Expand Up @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions __tests__/frameworks-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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();
});
});

Expand Down
19 changes: 17 additions & 2 deletions __tests__/mcp-daemon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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 () => {
Expand Down
19 changes: 15 additions & 4 deletions __tests__/mcp-initialize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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 () => {
Expand Down
21 changes: 16 additions & 5 deletions __tests__/mcp-roots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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 () => {
Expand Down
19 changes: 15 additions & 4 deletions __tests__/mcp-subproject-adoption.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>((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 () => {
Expand Down
28 changes: 24 additions & 4 deletions __tests__/resolution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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
Expand All @@ -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 });
}
});
Expand Down Expand Up @@ -3684,6 +3689,7 @@ class Both : public Base<char>, 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 });
Expand All @@ -3701,7 +3707,7 @@ class Both : public Base<char>, 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
Expand All @@ -3716,11 +3722,16 @@ class Both : public Base<char>, 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 });
Expand All @@ -3735,7 +3746,7 @@ class Both : public Base<char>, 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
Expand All @@ -3750,11 +3761,16 @@ class Both : public Base<char>, 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
Expand All @@ -3774,7 +3790,7 @@ class Both : public Base<char>, 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
Expand All @@ -3789,6 +3805,10 @@ class Both : public Base<char>, 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 });
}
});
Expand Down