97 lines
4.0 KiB
TypeScript
97 lines
4.0 KiB
TypeScript
import { afterEach, beforeEach, expect, test } from 'bun:test';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { findSymbolTool, jsonQueryTool, tools, toolSetOf } from '../src/tools';
|
|
|
|
let dir: string;
|
|
let origCwd: string;
|
|
|
|
beforeEach(() => {
|
|
origCwd = process.cwd();
|
|
dir = mkdtempSync(join(tmpdir(), 'shiro-nav-'));
|
|
process.chdir(dir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(origCwd);
|
|
rmSync(dir, { recursive: true, force: true });
|
|
});
|
|
|
|
const run = <T>(t: { execute?: (input: T, opts: any) => unknown }, input: T) =>
|
|
Promise.resolve(t.execute!(input, { toolCallId: 't1', messages: [] })) as Promise<string>;
|
|
|
|
test('the new tools are registered and assigned to the nav set', () => {
|
|
expect(tools['find_symbol']).toBeDefined();
|
|
expect(tools['json_query']).toBeDefined();
|
|
expect(toolSetOf('find_symbol')).toBe('nav');
|
|
expect(toolSetOf('json_query')).toBe('nav');
|
|
});
|
|
|
|
test('find_symbol jumps to a function declaration, not its uses', async () => {
|
|
await Bun.write(
|
|
'src/util.ts',
|
|
'export function parseConfig() {\n return {};\n}\n\nconst x = parseConfig();\nparseConfig();\n',
|
|
);
|
|
const out = await run(findSymbolTool, { name: 'parseConfig' });
|
|
// Exactly one hit, at the declaration on line 1, not the two calls below it.
|
|
expect(out).toContain('src/util.ts:1');
|
|
expect(out).toContain('function parseConfig');
|
|
expect(out.split('\n')).toHaveLength(1);
|
|
});
|
|
|
|
test('find_symbol finds a Python def and a class', async () => {
|
|
await Bun.write('app.py', 'class Server:\n pass\n\ndef serve():\n pass\n');
|
|
const cls = await run(findSymbolTool, { name: 'Server' });
|
|
expect(cls).toContain('app.py:1');
|
|
const fn = await run(findSymbolTool, { name: 'serve' });
|
|
expect(fn).toContain('app.py:4');
|
|
});
|
|
|
|
test('find_symbol reports a miss plainly and escapes regex metacharacters', async () => {
|
|
await Bun.write('a.ts', 'const other = 1;\n');
|
|
expect(await run(findSymbolTool, { name: 'missing' })).toBe('No definition of "missing" found.');
|
|
// A name with regex metacharacters must not throw or match wildly.
|
|
expect(await run(findSymbolTool, { name: 'a.b(c)' })).toContain('No definition');
|
|
});
|
|
|
|
test('find_symbol skips comments', async () => {
|
|
await Bun.write('c.ts', '// function fake() {}\nfunction real() {}\n');
|
|
const out = await run(findSymbolTool, { name: 'fake' });
|
|
expect(out).toContain('No definition');
|
|
});
|
|
|
|
test('json_query reads a nested value by dotted path', async () => {
|
|
await Bun.write('package.json', JSON.stringify({ scripts: { build: 'tsc', test: 'vitest' }, name: 'demo' }));
|
|
expect(await run(jsonQueryTool, { path: 'package.json', query: 'scripts.build' })).toBe('scripts.build = tsc');
|
|
});
|
|
|
|
test('json_query walks arrays with numeric segments', async () => {
|
|
await Bun.write('data.json', JSON.stringify({ items: [{ id: 'a' }, { id: 'b' }] }));
|
|
expect(await run(jsonQueryTool, { path: 'data.json', query: 'items.1.id' })).toBe('items.1.id = b');
|
|
});
|
|
|
|
test('json_query renders an object value as JSON', async () => {
|
|
await Bun.write('c.json', JSON.stringify({ deps: { react: '^19' } }));
|
|
const out = await run(jsonQueryTool, { path: 'c.json', query: 'deps' });
|
|
expect(out).toContain('react');
|
|
expect(out).toContain('^19');
|
|
});
|
|
|
|
test('json_query names the keys present when a segment misses', async () => {
|
|
await Bun.write('c.json', JSON.stringify({ scripts: { build: 'x' } }));
|
|
const out = run(jsonQueryTool, { path: 'c.json', query: 'scripts.deploy' });
|
|
await expect(out).rejects.toThrow(/no key "deploy"/);
|
|
await expect(out).rejects.toThrow(/build/);
|
|
});
|
|
|
|
test('json_query refuses a missing file and invalid JSON', async () => {
|
|
await expect(run(jsonQueryTool, { path: 'nope.json', query: 'a' })).rejects.toThrow(/No such file/);
|
|
await Bun.write('bad.json', '{ not json');
|
|
await expect(run(jsonQueryTool, { path: 'bad.json', query: 'a' })).rejects.toThrow(/not valid JSON/);
|
|
});
|
|
|
|
test('json_query stays inside the workspace', async () => {
|
|
await expect(run(jsonQueryTool, { path: '../../etc/passwd', query: 'a' })).rejects.toThrow();
|
|
});
|