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' }); }); });