118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
|
|
import { spawn, resolveShellConfig } from '../src/comm/child_process.js';
|
|
|
|
describe('process spawn options', () => {
|
|
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform');
|
|
const originalEnv = process.env;
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
process.env = originalEnv;
|
|
if (originalPlatform) {
|
|
Object.defineProperty(process, 'platform', originalPlatform);
|
|
}
|
|
tempDirs.splice(0).forEach(dir => fs.rmSync(dir, { recursive: true, force: true }));
|
|
});
|
|
|
|
it('spawn automatically injects windowsHide: true', () => {
|
|
const spawnMock = jest.fn().mockReturnValue(createFakeChild());
|
|
|
|
jest.resetModules();
|
|
jest.doMock('child_process', () => ({
|
|
...jest.requireActual('child_process'),
|
|
spawn: spawnMock,
|
|
}));
|
|
|
|
let wrappedSpawn: typeof spawn;
|
|
jest.isolateModules(() => {
|
|
({ spawn: wrappedSpawn } = require('../src/comm/child_process.js'));
|
|
});
|
|
|
|
wrappedSpawn!('echo', ['hello'], { stdio: 'ignore' });
|
|
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
'echo',
|
|
['hello'],
|
|
expect.objectContaining({ stdio: 'ignore', windowsHide: true })
|
|
);
|
|
});
|
|
|
|
it('uses Git Bash before cmd.exe on Windows when available', () => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32' });
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'neta-git-bash-'));
|
|
tempDirs.push(root);
|
|
const gitBash = path.join(root, 'Git', 'bin', 'bash.exe');
|
|
fs.mkdirSync(path.dirname(gitBash), { recursive: true });
|
|
fs.writeFileSync(gitBash, '', 'utf8');
|
|
process.env = {
|
|
...originalEnv,
|
|
ProgramFiles: root,
|
|
'ProgramFiles(x86)': '',
|
|
ComSpec: 'C:\\Windows\\System32\\cmd.exe',
|
|
};
|
|
|
|
const config = resolveShellConfig();
|
|
|
|
expect(config).toEqual({
|
|
shell: gitBash,
|
|
args: ['-lc'],
|
|
});
|
|
});
|
|
|
|
it('hides subagent worker process windows', async () => {
|
|
const fakeChild = createFakeChild();
|
|
const spawnMock = jest.fn().mockReturnValue(fakeChild);
|
|
|
|
jest.resetModules();
|
|
jest.doMock('child_process', () => ({
|
|
spawn: spawnMock,
|
|
}));
|
|
|
|
let runner: any;
|
|
let protocolVersion: number;
|
|
await jest.isolateModulesAsync(async () => {
|
|
({ SubagentProcessRunner: runner } = await import('../src/modules/netaclaw/subagent/process_runner.js'));
|
|
({ SUBAGENT_WORKER_PROTOCOL_VERSION: protocolVersion } = await import('../src/modules/netaclaw/subagent/process_protocol.js'));
|
|
});
|
|
|
|
const instance = new runner({ workerPath: 'worker.js' });
|
|
void instance.run({
|
|
protocolVersion,
|
|
runId: 'run-hidden',
|
|
sessionId: 'session-1',
|
|
parentMessageId: 1,
|
|
parentToolCallId: 'tool-1',
|
|
parentAgentId: 1,
|
|
task: { goal: 'test' },
|
|
agentConfig: { name: 'agent', systemPrompt: 'test', model: 'openai:gpt-5.4', apiKey: 'test-key' },
|
|
toolNames: [],
|
|
}).catch(() => undefined);
|
|
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
expect.any(String),
|
|
expect.any(Array),
|
|
expect.objectContaining({ windowsHide: true })
|
|
);
|
|
});
|
|
});
|
|
|
|
function createFakeChild() {
|
|
const { PassThrough } = require('stream');
|
|
const { EventEmitter } = require('events');
|
|
const child = new EventEmitter();
|
|
child.stdin = new PassThrough();
|
|
child.stdout = new PassThrough();
|
|
child.stderr = new PassThrough();
|
|
child.killed = false;
|
|
child.kill = jest.fn(() => {
|
|
child.killed = true;
|
|
child.emit('close', 0);
|
|
return true;
|
|
});
|
|
return child;
|
|
}
|