kimi-code/packages/agent-core-v2/test/process/processRunnerService.test.ts
haozhe.yang 4d8dd8cd5c refactor(agent-core-v2): extract host process primitives into os/
Add App-scope IHostProcessService in os/interface with a node-local
backend, then make ISessionProcessRunner delegate to it instead of
calling child_process directly. Session/process keeps cwd/env overlay
semantics while os/ owns spawn/kill/cross-platform process-tree cleanup.

- add os/interface/hostProcess.ts and hostProcessService.ts
- update os/interface and os/backends barrels
- migrate session/process/processRunnerService.ts to inject IHostProcessService
- delete session/process/spawnedProcess.ts
- add tests for HostProcessService and update processRunnerService tests
2026-07-03 12:17:50 +08:00

83 lines
2.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Readable } from 'node:stream';
import { InstantiationType } from '#/_base/di/extensions';
import {
LifecycleScope,
_clearScopedRegistryForTests,
registerScopedService,
} from '#/_base/di/scope';
import { createScopedTestHost, stubPair } from '#/_base/di/test';
import { IHostProcessService } from '#/os/interface/hostProcess';
import { HostProcessService } from '#/os/backends/node-local/hostProcessService';
import { createExecContext, IExecContext } from '#/session/execContext';
import { ISessionProcessRunner, SessionProcessRunner } from '#/session/process';
async function collect(stream: Readable): Promise<string> {
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(chunk as Buffer);
}
return Buffer.concat(chunks).toString('utf8');
}
describe('SessionProcessRunner (backed by IExecContext)', () => {
let dir: string;
beforeEach(async () => {
_clearScopedRegistryForTests();
registerScopedService(
LifecycleScope.App,
IHostProcessService,
HostProcessService,
InstantiationType.Delayed,
'hostProcess',
);
registerScopedService(
LifecycleScope.Session,
ISessionProcessRunner,
SessionProcessRunner,
InstantiationType.Delayed,
'process',
);
dir = await mkdtemp(join(tmpdir(), 'procrunner-'));
});
afterEach(async () => {
await rm(dir, { recursive: true, force: true });
});
async function makeRunner(): Promise<ISessionProcessRunner> {
const host = createScopedTestHost();
const session = host.child(
LifecycleScope.Session,
's',
[stubPair(IExecContext, createExecContext(dir))],
);
return session.accessor.get(ISessionProcessRunner);
}
it('exec runs a command and captures stdout + exit code', async () => {
const runner = await makeRunner();
const proc = await runner.exec(['node', '-e', 'process.stdout.write("ok")']);
const out = await collect(proc.stdout);
expect(out).toBe('ok');
expect(await proc.wait()).toBe(0);
expect(proc.exitCode).toBe(0);
});
it('exec overlays per-call env', async () => {
const runner = await makeRunner();
const proc = await runner.exec(
['node', '-e', 'process.stdout.write(process.env.FOO ?? "")'],
{ env: { FOO: 'bar' } },
);
const out = await collect(proc.stdout);
expect(out).toBe('bar');
expect(await proc.wait()).toBe(0);
});
});