Files
Genarrative/apps/ai-game-creator-shell/tests/processSessionRealE2eFixture.test.ts
kdletters 15c227cebf 修复AGC Windows运行与全量测试稳定性
完善Codex CLI、App Server、ConPTY与进程会话在Windows下的发现、启动、恢复和退出行为
修复Agent Runtime、Provider重试、项目写锁及工具交接账本的并发与跨测试串线问题
补齐配置目录、路径脱敏、原子写入、浏览器探测和本地Provider smoke的跨平台兼容
增强Goal Contract、自动策略、资源生成及运行态恢复的契约和回归测试
更新AI游戏创作智能体App技术文档中的Windows稳定性说明
验证AGC开发态、Release打包、打包后GUI运行及完整agc:check门禁
2026-08-13 13:36:21 +08:00

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);
});