import { EventEmitter } from 'events'; import { SUBAGENT_WORKER_PROTOCOL_VERSION } from '../src/modules/netaclaw/subagent/process_protocol.js'; jest.mock('../src/modules/netaclaw/runtime/model_selection.js', () => ({ initDefaultProviders: jest.fn(), })); jest.mock('../src/modules/netaclaw/runtime/agent.js', () => ({ runAgent: jest.fn(), })); describe('subagent worker', () => { let originalStdin: NodeJS.ReadStream; let originalStdoutWrite: typeof process.stdout.write; let originalExitCode: typeof process.exitCode; let stdoutLines: any[]; beforeEach(() => { jest.resetModules(); stdoutLines = []; originalStdin = process.stdin; originalStdoutWrite = process.stdout.write; originalExitCode = process.exitCode; process.exitCode = undefined; process.stdout.write = jest.fn((chunk: any) => { String(chunk).split(/\r?\n/).filter(Boolean).forEach(line => { stdoutLines.push(JSON.parse(line)); }); return true; }) as any; }); afterEach(() => { Object.defineProperty(process, 'stdin', { configurable: true, value: originalStdin, }); process.stdout.write = originalStdoutWrite; process.exitCode = originalExitCode; }); const installStdin = (payload: unknown) => { const stdin = new EventEmitter() as NodeJS.ReadStream; stdin.setEncoding = jest.fn() as any; Object.defineProperty(process, 'stdin', { configurable: true, value: stdin, }); setImmediate(() => { stdin.emit('data', `${JSON.stringify(payload)}\n`); stdin.emit('end'); }); }; const importWorker = async () => { jest.isolateModules(() => { require('../src/modules/netaclaw/subagent/worker.js'); }); await new Promise(resolve => setImmediate(resolve)); await new Promise(resolve => setImmediate(resolve)); }; const baseEnvelope = { protocolVersion: SUBAGENT_WORKER_PROTOCOL_VERSION, runId: 'run-worker-1', sessionId: 'session-1', task: { goal: 'Summarize architecture.', }, agentConfig: { name: 'worker-agent', systemPrompt: 'Use concise output.', model: 'openai:gpt-5.4', apiKey: 'test-key', }, toolNames: [], toolManifest: [], }; it('executes a no-tool agent run and emits run_end', async () => { const { runAgent } = require('../src/modules/netaclaw/runtime/agent.js'); runAgent.mockImplementation(async (params: any) => { params.onToken('partial'); return { finalContent: 'worker final', usage: { inputTokens: 5, outputTokens: 6 }, toolCallCount: 0, messages: [{ role: 'assistant', content: 'worker final' }], }; }); installStdin(baseEnvelope); await importWorker(); expect(runAgent).toHaveBeenCalledWith( expect.objectContaining({ tools: [], toolNames: [], userMessage: 'Summarize architecture.', history: [], agentConfig: expect.objectContaining({ name: 'worker-agent', }), }) ); expect(stdoutLines.map(line => line.type)).toEqual(['run_start', 'token', 'run_end']); expect(stdoutLines[2].result).toEqual({ finalContent: 'worker final', usage: { inputTokens: 5, outputTokens: 6 }, toolCallCount: 0, metadata: { messageCount: 1, proxiedToolNames: [] }, }); }); it('passes supported worker-local tools into runAgent', async () => { const { runAgent } = require('../src/modules/netaclaw/runtime/agent.js'); runAgent.mockResolvedValue({ finalContent: 'tool-backed final', usage: { inputTokens: 2, outputTokens: 3 }, toolCallCount: 1, messages: [{ role: 'assistant', content: 'tool-backed final' }], }); installStdin({ ...baseEnvelope, toolManifest: [ { 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 }, ], }); await importWorker(); expect(runAgent).toHaveBeenCalledWith( expect.objectContaining({ toolNames: ['bash', 'read_file', 'list_dir'], tools: [ expect.objectContaining({ name: 'bash' }), expect.objectContaining({ name: 'read_file' }), expect.objectContaining({ name: 'list_dir' }), ], }) ); expect(stdoutLines.map(line => line.type)).toEqual(['run_start', 'run_end']); expect(stdoutLines[1].result.finalContent).toBe('tool-backed final'); }); it('rejects unsupported tool execution until worker registry wiring exists', async () => { installStdin({ ...baseEnvelope, toolManifest: [ { 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 }, ], }); await importWorker(); expect(stdoutLines.map(line => line.type)).toEqual(['run_start', 'run_error']); expect(stdoutLines[1].error).toBe( 'Subagent subprocess worker does not support tools: write_file, delegate_task' ); expect(process.exitCode).toBe(1); }); it('falls back to toolNames when manifest is absent for backward compatibility', async () => { const { runAgent } = require('../src/modules/netaclaw/runtime/agent.js'); runAgent.mockResolvedValue({ finalContent: 'legacy fallback final', usage: { inputTokens: 1, outputTokens: 1 }, toolCallCount: 0, messages: [{ role: 'assistant', content: 'legacy fallback final' }], }); installStdin({ ...baseEnvelope, toolManifest: undefined, toolNames: ['read_file'], policy: { workspaceRoots: ['C:\\repo'], }, }); await importWorker(); expect(runAgent).toHaveBeenCalledWith( expect.objectContaining({ toolNames: ['read_file'], tools: [expect.objectContaining({ name: 'read_file' })], }) ); expect(stdoutLines.map(line => line.type)).toEqual(['run_start', 'run_end']); }); it('uses manifest routes as the canonical toolNames passed into runAgent', async () => { const { runAgent } = require('../src/modules/netaclaw/runtime/agent.js'); runAgent.mockResolvedValue({ finalContent: 'manifest canonical final', usage: { inputTokens: 1, outputTokens: 2 }, toolCallCount: 0, messages: [{ role: 'assistant', content: 'manifest canonical final' }], }); installStdin({ ...baseEnvelope, toolNames: ['read_file', 'write_file'], toolManifest: [ { name: 'read_file', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'parallel', supportedInWorker: true, workerRoutingHint: 'local', }, { name: 'write_file', visibility: 'tool', capability: 'text', kind: 'builtin', executionMode: 'sequential', supportedInWorker: false, workerRoutingHint: 'main-process-proxy', requiresWrite: true, }, ], }); await importWorker(); expect(runAgent).toHaveBeenCalledWith( expect.objectContaining({ toolNames: ['read_file', 'write_file'], tools: [ expect.objectContaining({ name: 'read_file' }), expect.objectContaining({ name: 'write_file' }), ], }) ); expect(stdoutLines.map(line => line.type)).toEqual(['run_start', 'log', 'run_end']); }); });