Files
Genarrative/apps/ai-game-creator-shell/tests/start-tauri-dev.test.ts
menghao 53e37ea361
Project CI / Repository checks (pull_request) Failing after 10s
Project CI / Backend tests (pull_request) Failing after 10s
Project CI / Frontend tests (pull_request) Successful in 3m28s
Project CI / Native shell tests (pull_request) Failing after 9m27s
支持App Run并行开发并修复资源画布
新增App Run独立开发profile并隔离端口、数据库、Tauri身份与AppData
常驻资源排列切换并避免布局状态挤压操作入口
显式扩展资源画布横向滚动范围并收敛依赖图可见连线
补充启动器、资源布局、界面回归测试与项目文档
2026-08-07 10:13:25 +08:00

273 lines
7.8 KiB
TypeScript

import { EventEmitter } from 'node:events';
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, test, vi } from 'vitest';
import {
isProcessGroupAlive,
spawnChild,
terminateChildTree,
} from '../scripts/start-dev-stack.mjs';
import {
buildTauriArguments,
parseLauncherArguments,
runTauriDev,
} from '../scripts/start-tauri-dev.mjs';
async function waitForFile(path: string, timeoutMs = 5000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (existsSync(path)) {
return;
}
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
}
throw new Error(`等待测试进程标记超时: ${path}`);
}
describe('AI 游戏创作 Tauri dev 启动参数', () => {
test('普通 dev 参数原样交给 Tauri CLI', () => {
expect(buildTauriArguments(['--no-watch'])).toEqual(['dev', '--no-watch']);
});
test('app-run 使用独立 Tauri 配置并保留 CLI 参数', () => {
expect(buildTauriArguments(['--app-run', '--no-watch'])).toEqual([
'dev',
'--config',
'src-tauri/tauri.app-run-dev.conf.json',
'--no-watch',
]);
expect(parseLauncherArguments(['--app-run'])).toEqual({
appRun: true,
gameChat: false,
args: [],
});
});
test('app-run 与 game-chat 不允许混用', () => {
expect(() =>
buildTauriArguments(['--app-run', '--game-chat']),
).toThrow('app-run profile 不能与 game-chat 入口同时使用');
});
test('game-chat 参数进入应用参数区且保留项目参数', () => {
expect(
buildTauriArguments([
'--game-chat',
'--project-path',
'/tmp/example-game',
]),
).toEqual([
'dev',
'--',
'--',
'--game-chat',
'--project-path',
'/tmp/example-game',
]);
});
});
describe('AI 游戏创作 Tauri dev 生命周期', () => {
test('app-run 预检 3081 profile 并把 profile 传给 Tauri 子进程', async () => {
const child = Object.assign(new EventEmitter(), {
pid: 1234,
exitCode: 0,
signalCode: null,
kill: vi.fn(),
});
const preflight = vi.fn(async () => {});
const spawnCli = vi.fn(() => child);
await expect(
runTauriDev(['--app-run'], {
preflight,
spawnCli,
waitForCli: async () => ({ type: 'exit', code: 0, signal: null }),
terminateTree: async () => ({ stopped: true, forced: false }),
}),
).resolves.toBe(0);
expect(preflight).toHaveBeenCalledWith({
profile: expect.objectContaining({ name: 'app-run', vitePort: 3081 }),
});
expect(spawnCli).toHaveBeenCalledWith(
[
'dev',
'--config',
'src-tauri/tauri.app-run-dev.conf.json',
],
{ profileName: 'app-run' },
);
});
test('3080 预检失败时不启动 Tauri CLI', async () => {
const spawnCli = vi.fn();
await expect(
runTauriDev([], {
preflight: async () => {
throw new Error('stale 3080');
},
spawnCli,
}),
).rejects.toThrow('stale 3080');
expect(spawnCli).not.toHaveBeenCalled();
});
test('预检先于 CLI 启动且 CLI 退出后始终清理进程树', async () => {
const order: string[] = [];
const child = Object.assign(new EventEmitter(), {
pid: 1234,
exitCode: 1,
signalCode: null,
kill: vi.fn(),
});
const result = await runTauriDev([], {
preflight: async () => {
order.push('preflight');
},
spawnCli: () => {
order.push('spawn');
return child;
},
waitForCli: async () => {
order.push('exit');
return { type: 'exit', code: 1, signal: null };
},
terminateTree: async (receivedChild) => {
expect(receivedChild).toBe(child);
order.push('cleanup');
return { stopped: true, forced: false };
},
});
expect(result).toBe(1);
expect(order).toEqual(['preflight', 'spawn', 'exit', 'cleanup']);
});
const posixTest = process.platform === 'win32' ? test.skip : test;
posixTest('Tauri CLI leader 先退出后仍收束同 PGID 的客户端后代', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'agc-tauri-tree-'));
const readyPath = join(tempDir, 'client-ready');
const stoppedPath = join(tempDir, 'client-stopped');
const descendantSource = `
const { writeFileSync } = require('node:fs');
const [readyPath, stoppedPath] = process.argv.slice(1);
process.on('SIGTERM', () => {
writeFileSync(stoppedPath, 'stopped');
process.exit(0);
});
writeFileSync(readyPath, 'ready');
setInterval(() => {}, 1000);
`;
const leaderSource = `
const { existsSync } = require('node:fs');
const { spawn } = require('node:child_process');
const [readyPath, stoppedPath, descendantSource] = process.argv.slice(1);
const descendant = spawn(
process.execPath,
['-e', descendantSource, readyPath, stoppedPath],
{ stdio: 'ignore' },
);
descendant.unref();
const timer = setInterval(() => {
if (existsSync(readyPath)) {
clearInterval(timer);
process.exit(42);
}
}, 10);
`;
let cliChild;
try {
const result = await runTauriDev([], {
preflight: async () => {},
spawnCli: () => {
cliChild = spawnChild(
process.execPath,
['-e', leaderSource, readyPath, stoppedPath, descendantSource],
{ cwd: process.cwd() },
);
return cliChild;
},
});
expect(result).toBe(42);
await waitForFile(stoppedPath);
} finally {
if (Number.isInteger(cliChild?.pid)) {
try {
process.kill(-cliChild.pid, 'SIGKILL');
} catch {
// 进程组已经由启动器收束。
}
}
rmSync(tempDir, { recursive: true, force: true });
}
});
posixTest('客户端后代忽略 TERM 时在有界宽限后升级 KILL', async () => {
const tempDir = mkdtempSync(join(tmpdir(), 'agc-tauri-force-tree-'));
const readyPath = join(tempDir, 'client-ready');
const descendantSource = `
const { writeFileSync } = require('node:fs');
const [readyPath] = process.argv.slice(1);
process.on('SIGTERM', () => {});
writeFileSync(readyPath, 'ready');
setInterval(() => {}, 1000);
`;
const leaderSource = `
const { existsSync } = require('node:fs');
const { spawn } = require('node:child_process');
const [readyPath, descendantSource] = process.argv.slice(1);
const descendant = spawn(
process.execPath,
['-e', descendantSource, readyPath],
{ stdio: 'ignore' },
);
descendant.unref();
const timer = setInterval(() => {
if (existsSync(readyPath)) {
clearInterval(timer);
process.exit(42);
}
}, 10);
`;
let cliChild;
try {
const result = await runTauriDev([], {
preflight: async () => {},
spawnCli: () => {
cliChild = spawnChild(
process.execPath,
['-e', leaderSource, readyPath, descendantSource],
{ cwd: process.cwd() },
);
return cliChild;
},
terminateTree: (child) =>
terminateChildTree(child, {
gracefulTimeoutMs: 50,
forceTimeoutMs: 2000,
}),
});
expect(result).toBe(42);
expect(isProcessGroupAlive(cliChild.pid)).toBe(false);
} finally {
if (Number.isInteger(cliChild?.pid)) {
try {
process.kill(-cliChild.pid, 'SIGKILL');
} catch {
// 进程组已经由启动器强制收束。
}
}
rmSync(tempDir, { recursive: true, force: true });
}
});
});