15c227cebf
完善Codex CLI、App Server、ConPTY与进程会话在Windows下的发现、启动、恢复和退出行为 修复Agent Runtime、Provider重试、项目写锁及工具交接账本的并发与跨测试串线问题 补齐配置目录、路径脱敏、原子写入、浏览器探测和本地Provider smoke的跨平台兼容 增强Goal Contract、自动策略、资源生成及运行态恢复的契约和回归测试 更新AI游戏创作智能体App技术文档中的Windows稳定性说明 验证AGC开发态、Release打包、打包后GUI运行及完整agc:check门禁
123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
import { spawn } from 'node:child_process';
|
|
import { once } from 'node:events';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import readline from 'node:readline';
|
|
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
|
|
import { buildProcessSessionFixtureSource } from '../scripts/process-session-real-e2e-fixture.mjs';
|
|
|
|
const readyPrefix = 'GENARRATIVE_PROCESS_READY';
|
|
const echoPrefix = 'GENARRATIVE_PROCESS_ECHO';
|
|
const stoppedMarker = 'GENARRATIVE_PROCESS_STOPPED';
|
|
const temporaryRoots = new Set<string>();
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
[...temporaryRoots].map((root) =>
|
|
fs.rm(root, { recursive: true, force: true }),
|
|
),
|
|
);
|
|
temporaryRoots.clear();
|
|
});
|
|
|
|
describe('process-session real E2E fixture', () => {
|
|
it('runs as a pure PTY-style protocol without project control files or sockets', async () => {
|
|
const root = await fs.mkdtemp(
|
|
path.join(os.tmpdir(), 'genarrative-process-fixture-test-'),
|
|
);
|
|
temporaryRoots.add(root);
|
|
const fixturePath = path.join(root, 'fixture.mjs');
|
|
await fs.writeFile(
|
|
fixturePath,
|
|
buildProcessSessionFixtureSource({
|
|
readyPrefix,
|
|
echoPrefix,
|
|
stoppedMarker,
|
|
}),
|
|
);
|
|
|
|
const child = spawn(process.execPath, [fixturePath], {
|
|
cwd: root,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
});
|
|
const exitPromise = once(child, 'exit');
|
|
const output = readline.createInterface({ input: child.stdout });
|
|
const lines: string[] = [];
|
|
const waiters = new Set<{
|
|
predicate: (line: string) => boolean;
|
|
resolve: (line: string) => void;
|
|
}>();
|
|
output.on('line', (line) => {
|
|
const normalized = line.endsWith('\r') ? line.slice(0, -1) : line;
|
|
lines.push(normalized);
|
|
for (const waiter of [...waiters]) {
|
|
if (waiter.predicate(normalized)) waiter.resolve(normalized);
|
|
}
|
|
});
|
|
|
|
const waitForLine = (
|
|
predicate: (line: string) => boolean,
|
|
label: string,
|
|
): Promise<string> => {
|
|
const existing = lines.find(predicate);
|
|
if (existing) return Promise.resolve(existing);
|
|
return new Promise((resolve, reject) => {
|
|
const waiter = {
|
|
predicate,
|
|
resolve: (line) => {
|
|
clearTimeout(timer);
|
|
waiters.delete(waiter);
|
|
resolve(line);
|
|
},
|
|
};
|
|
const timer = setTimeout(() => {
|
|
waiters.delete(waiter);
|
|
reject(new Error(`process fixture did not emit ${label}`));
|
|
}, 3_000);
|
|
waiters.add(waiter);
|
|
});
|
|
};
|
|
|
|
try {
|
|
const readyLine = await waitForLine(
|
|
(line) => line.startsWith(`${readyPrefix} challenge=`),
|
|
'readiness',
|
|
);
|
|
const match = readyLine.match(
|
|
/^GENARRATIVE_PROCESS_READY challenge=([0-9a-f]{36})$/u,
|
|
);
|
|
expect(match).not.toBeNull();
|
|
if (!match?.[1]) throw new Error('process fixture challenge is missing');
|
|
expect(
|
|
await fs.stat(path.join(root, '.agent')).catch(() => null),
|
|
).toBeNull();
|
|
|
|
const challenge = match[1];
|
|
child.stdin.write(`${challenge}\n`);
|
|
await expect(
|
|
waitForLine((line) => line === `${echoPrefix} ${challenge}`, 'echo'),
|
|
).resolves.toBe(`${echoPrefix} ${challenge}`);
|
|
|
|
if (process.platform === 'win32') {
|
|
child.stdin.write(`${challenge}:stop\n`);
|
|
} else {
|
|
expect(child.kill('SIGTERM')).toBe(true);
|
|
}
|
|
await expect(
|
|
waitForLine((line) => line === stoppedMarker, 'stopped marker'),
|
|
).resolves.toBe(stoppedMarker);
|
|
const [exitCode, signal] = await exitPromise;
|
|
expect({ exitCode, signal }).toEqual({ exitCode: 0, signal: null });
|
|
expect(await fs.readdir(root)).toEqual(['fixture.mjs']);
|
|
} finally {
|
|
output.close();
|
|
child.stdin.destroy();
|
|
if (child.exitCode === null && child.signalCode === null)
|
|
child.kill('SIGKILL');
|
|
}
|
|
}, 10_000);
|
|
});
|