GPU_GUARD_MONOREPO/packages/backend/test/subagent_worker_tools.test.ts

215 lines
7.5 KiB
TypeScript
Raw Normal View History

2026-05-20 21:39:12 +08:00
import { resolveWorkerTools } from '../src/modules/netaclaw/subagent/worker_tools.js';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
describe('resolveWorkerTools', () => {
const cleanupTasks: Array<() => void> = [];
afterEach(() => {
cleanupTasks.splice(0).forEach(cleanup => cleanup());
});
const createTempDir = () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-worker-policy-'));
cleanupTasks.push(() => fs.rmSync(dir, { recursive: true, force: true }));
return dir;
};
it('resolves only worker-local evidence tools', () => {
const result = resolveWorkerTools([
{ name: 'bash', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true, requiresShell: true },
{ name: 'read_file', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true },
{ name: 'list_dir', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true },
{ name: 'find_files', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true },
{ name: 'grep', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true },
{ name: 'write_file', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'sequential', supportedInWorker: false, requiresWrite: true },
{ name: 'delegate_task', visibility: 'tool', capability: 'text', kind: 'delegation', executionMode: 'sequential', supportedInWorker: false },
{ name: 'bash', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true, requiresShell: true },
]);
expect(result.tools.map(tool => tool.name)).toEqual([
'bash',
'read_file',
'list_dir',
'find_files',
'grep',
]);
expect(result.unsupportedToolNames).toEqual([
'write_file',
'delegate_task',
]);
});
it('blocks file tools when no workspace root is configured', async () => {
const result = resolveWorkerTools([
{ name: 'read_file', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true },
]);
const readFile = result.tools[0];
await expect(readFile.execute('tool-1', { path: __filename })).rejects.toThrow(
'Subagent subprocess policy requires at least one workspace root'
);
});
it('blocks file tools outside configured workspace roots', async () => {
const root = createTempDir();
const outside = path.join(os.tmpdir(), `neta-outside-${Date.now()}.txt`);
fs.writeFileSync(outside, 'outside', 'utf8');
cleanupTasks.push(() => fs.rmSync(outside, { force: true }));
const result = resolveWorkerTools([{
name: 'read_file',
visibility: 'tool',
capability: 'text',
kind: 'builtin',
executionMode: 'parallel',
supportedInWorker: true,
}], {
workspaceRoots: [root],
});
const readFile = result.tools[0];
await expect(readFile.execute('tool-1', { path: outside })).rejects.toThrow(
'Subagent subprocess policy blocks path outside workspace roots'
);
});
it('allows file tools inside configured workspace roots', async () => {
const root = createTempDir();
const filePath = path.join(root, 'note.txt');
fs.writeFileSync(filePath, 'inside-root', 'utf8');
const result = resolveWorkerTools([{
name: 'read_file',
visibility: 'tool',
capability: 'text',
kind: 'builtin',
executionMode: 'parallel',
supportedInWorker: true,
}], {
workspaceRoots: [root],
});
const readFile = result.tools[0];
await expect(readFile.execute('tool-1', { path: filePath })).resolves.toEqual({
type: 'text',
text: 'inside-root',
});
});
it('passes read_file offset and limit through worker policy wrapper', async () => {
const root = createTempDir();
const filePath = path.join(root, 'note.txt');
fs.writeFileSync(filePath, 'line 1\nline 2\nline 3', 'utf8');
const result = resolveWorkerTools([{
name: 'read_file',
visibility: 'tool',
capability: 'text',
kind: 'builtin',
executionMode: 'parallel',
supportedInWorker: true,
}], {
workspaceRoots: [root],
});
const readFile = result.tools[0];
await expect(readFile.execute('tool-1', {
path: filePath,
offset: 2,
limit: 1,
})).resolves.toMatchObject({
type: 'text',
text: expect.stringContaining('line 2'),
});
});
it('blocks shell execution unless explicitly allowed', async () => {
const root = createTempDir();
const result = resolveWorkerTools([{
name: 'bash',
visibility: 'tool',
capability: 'text',
kind: 'builtin',
executionMode: 'parallel',
supportedInWorker: true,
requiresShell: true,
}], {
workspaceRoots: [root],
});
const bash = result.tools[0];
await expect(bash.execute('tool-1', {
command: 'echo blocked',
cwd: root,
})).rejects.toThrow('Subagent subprocess policy blocks shell execution');
});
it('allows shell execution only with allowShell and workspace cwd', async () => {
const root = createTempDir();
const result = resolveWorkerTools([{
name: 'bash',
visibility: 'tool',
capability: 'text',
kind: 'builtin',
executionMode: 'parallel',
supportedInWorker: true,
requiresShell: true,
}], {
workspaceRoots: [root],
allowShell: true,
});
const bash = result.tools[0];
await expect(bash.execute('tool-1', {
command: 'node -e "process.stdout.write(\'inside-root\')"',
cwd: root,
timeout: 5000,
})).resolves.toEqual({
type: 'text',
text: 'inside-root',
});
});
it('builds worker-local tools through injected default operations', async () => {
const root = createTempDir();
const readFile = jest.fn().mockResolvedValue(Buffer.from('worker-injected', 'utf8'));
let resolveWorkerToolsWithMock: typeof resolveWorkerTools;
jest.isolateModules(() => {
jest.doMock('../src/modules/netaclaw/tools/operations/index.js', () => ({
getDefaultOperations: () => ({
file: {
readFile,
writeFile: jest.fn(),
appendFile: jest.fn(),
access: jest.fn().mockResolvedValue(undefined),
isDirectory: jest.fn().mockResolvedValue(false),
readDir: jest.fn().mockResolvedValue([]),
mkdir: jest.fn().mockResolvedValue(undefined),
realpath: jest.fn().mockImplementation(async (p: string) => p),
},
process: {
exec: jest.fn(),
},
search: {
ripgrep: jest.fn(),
fd: jest.fn(),
},
}),
}));
({ resolveWorkerTools: resolveWorkerToolsWithMock } = require('../src/modules/netaclaw/subagent/worker_tools.js'));
});
const result = resolveWorkerToolsWithMock!([
{ name: 'read_file', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true },
], {
workspaceRoots: [root],
});
const readFileTool = result.tools[0];
const toolResult = await readFileTool.execute('tool-1', { path: path.join(root, 'demo.txt') });
expect(readFile).toHaveBeenCalledWith(path.join(root, 'demo.txt'));
expect(toolResult).toEqual({ type: 'text', text: 'worker-injected' });
});
});