GPU_GUARD_MONOREPO/packages/backend/test/subagent_worker.test.ts

242 lines
7.8 KiB
TypeScript
Raw Permalink Normal View History

2026-05-20 21:39:12 +08:00
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']);
});
});