Files
codegraph/__tests__/chinese-filename-retrieval.test.ts
Colby Mchenry 5001737df3 fix(explore): retrieve exact Han filenames (#1372) (#2005)
* fix(explore): find files by exact Chinese filenames

Co-Authored-By: GPT-5 <noreply@openai.com>

* fix(explore): retrieve exact Han filenames (#1372)

ASCII-only query preparation discarded Han filename queries before retrieval.
Add exact basename/stem file lookup and prioritize matches before the entry-point cap while respecting node-kind filters.
Preserve shared tokenization and retrieval budgets; cover Lua, Svelte, and TypeScript retrieval.

Co-authored-by: 小为 <1053122090@qq.com>
Co-authored-by: GPT-5 <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 小为 <1053122090@qq.com>
Co-authored-by: GPT-5 <noreply@openai.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 13:03:26 +00:00

154 lines
5.5 KiB
TypeScript

/**
* Chinese filename retrieval through codegraph_explore (#1372).
*
* Exact Chinese filenames are already indexed and searchable through
* `codegraph query`, but explore drops a Han-only filename before it can reach
* the indexed file.
*
* The locked invariant: an exact Chinese filename reaches explore without
* turning partial Chinese terms into broader filename search or overriding
* explicit node-kind filters.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import CodeGraph from '../src/index';
import { ToolHandler } from '../src/mcp/tools';
describe('Chinese filename retrieval (#1372)', () => {
let testDir: string;
let cg: CodeGraph;
let handler: ToolHandler;
beforeEach(async () => {
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1372-'));
fs.writeFileSync(
path.join(testDir, '示例模块.lua'),
'local M = {}\nreturn M\n'
);
fs.writeFileSync(
path.join(testDir, '示例模块扩展.lua'),
'local Extension = {}\nreturn Extension\n'
);
for (const fileName of ['用户Service.lua', '用户模块2.lua', '用户-登录.lua']) {
fs.writeFileSync(
path.join(testDir, fileName),
'local MixedName = {}\nreturn MixedName\n'
);
}
fs.writeFileSync(
path.join(testDir, '用户.v2.lua'),
'local DottedStem = {}\nreturn DottedStem\n'
);
fs.writeFileSync(
path.join(testDir, '深度模块.svelte'),
'<script>const exactLongName = {};</script>\n'
);
for (let i = 0; i < 24; i++) {
fs.writeFileSync(
path.join(testDir, `深度模块扩展${i}.c`),
`int prefix_noise_${i}(void) { return ${i}; }\n`
);
}
fs.writeFileSync(
path.join(testDir, '共享模块.ts'),
'export function sharedEntry() { return 42; }\n'
);
fs.writeFileSync(
path.join(testDir, 'Service.ts'),
'export class Service {}\n'
);
cg = CodeGraph.initSync(testDir, {
config: { include: ['**/*.lua', '**/*.c', '**/*.svelte', '**/*.ts'], exclude: [] },
});
await cg.indexAll();
handler = new ToolHandler(cg);
});
afterEach(() => {
if (cg) cg.destroy();
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true, force: true });
});
it('returns a file whose exact Chinese name is the explore query', async () => {
const result = await handler.execute('codegraph_explore', { query: '示例模块' });
const text = result.content[0]!.text as string;
expect(result.isError).toBeFalsy();
expect(text).toContain('示例模块.lua');
expect(text).not.toContain('示例模块扩展.lua');
expect(text).not.toContain('No relevant code found');
});
it('does not treat a partial Chinese name as an exact filename', async () => {
const result = await handler.execute('codegraph_explore', { query: '示例' });
const text = result.content[0]!.text as string;
expect(text).not.toContain('示例模块.lua');
expect(text).not.toContain('示例模块扩展.lua');
});
it('supports exact Chinese filenames mixed with ASCII, digits, and separators', async () => {
for (const query of ['用户Service', '用户模块2', '用户-登录']) {
const result = await handler.execute('codegraph_explore', { query });
const text = result.content[0]!.text as string;
expect(result.isError).toBeFalsy();
expect(text).toContain(`${query}.lua`);
expect(text).not.toContain('No relevant code found');
}
});
it('matches a dotted stem with or without the final extension', async () => {
for (const query of ['用户.v2', '用户.v2.lua']) {
const result = await handler.execute('codegraph_explore', { query });
const text = result.content[0]!.text as string;
expect(result.isError).toBeFalsy();
expect(text).toContain('用户.v2.lua');
expect(text).not.toContain('No relevant code found');
}
});
it('does not lose an exact stem behind a large prefix candidate set', async () => {
const result = await handler.execute('codegraph_explore', { query: '深度模块' });
const text = result.content[0]!.text as string;
expect(result.isError).toBeFalsy();
expect(text).toContain('深度模块.svelte');
expect(text).not.toContain('No relevant code found');
});
it('does not override an explicit node-kind filter', async () => {
const context = await cg.findRelevantContext('示例模块', {
nodeKinds: ['function'],
});
expect([...context.nodes.values()].some((node) => node.kind === 'file')).toBe(false);
});
it('retrieves a TypeScript file and its source through the same MCP path', async () => {
const result = await handler.execute('codegraph_explore', { query: '共享模块' });
const text = result.content[0]!.text as string;
expect(result.isError).toBeFalsy();
expect(text).toContain('共享模块.ts');
expect(text).toContain('return 42');
});
it('keeps the exact mixed filename ahead of symbol matches under the entry-point cap', async () => {
const context = await cg.findRelevantContext('用户Service', { searchLimit: 1 });
expect(context.roots).toHaveLength(1);
expect(context.nodes.get(context.roots[0]!)?.name).toBe('用户Service.lua');
});
it('admits exact file matches when file nodes are explicitly allowed', async () => {
const context = await cg.findRelevantContext('示例模块', { nodeKinds: ['file'] });
expect(context.roots.map((id) => context.nodes.get(id)?.name)).toContain('示例模块.lua');
});
});