Files
Genarrative/scripts/dev-windows-process.test.ts
T
suzmii c5219f7ad5
Project CI / Repository checks (pull_request) Successful in 2m56s
Project CI / Frontend tests (pull_request) Successful in 3m45s
Project CI / Backend tests (pull_request) Successful in 7m8s
Project CI / Native shell tests (pull_request) Successful in 18m40s
修复 AGC Ctrl+C 残留上个工作树后端导致切换工作树复用旧后端
closes #314

新增 scripts/dev-windows-process.mjs:提供按根 PID 遍历与按身份匹配(api-server.exe 绝对路径、SpacetimeDB --data-dir)两条独立清理路径
scripts/dev.mjs:直接子进程(cmd.exe 包装层)已退出时仍按记录 PID 清理后代,不再提前 return
scripts/dev.mjs:退出时按身份兜底清扫本工作树 api-server 与自建 SpacetimeDB,复用他人 standalone 时跳过
scripts/dev.mjs:启动前清理旧 api-server 保留 Wait-Process 等待语义,避免 cargo 报 failed to remove file
apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:复用配套后端前校验端口监听进程归属,无法证明归属则改为启动本工作树后端并允许端口漂移
apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:收到信号与 finally 各兜底清扫一次本工作树 api-server.exe,taskkill 失败时降级为按 PID 遍历
apps/ai-game-creator-shell/scripts/start-dev-stack.mjs:等待后端就绪时输出归属校验未通过的具体原因,避免只表现为 600 秒超时
新增 scripts/dev-windows-process.test.ts 并扩充 AGC 复用门禁用例:覆盖断链遍历、身份匹配、归属判定与探测不可用退化
同步 docs/project-memory/shared-memory/pitfalls.md 与本地开发运维文档的进程清理与复用归属口径
2026-09-09 19:37:54 +08:00

181 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, test } from 'vitest';
import {
normalizeWindowsPath,
parseWindowsProcessSnapshot,
selectProcessTreeIds,
selectWorktreeOwnedProcessIds,
} from './dev-windows-process.mjs';
function processEntry(overrides) {
return {
ProcessId: 1,
ParentProcessId: 0,
Name: 'node.exe',
ExecutablePath: null,
CommandLine: null,
...overrides,
};
}
describe('Windows 进程快照解析', () => {
test('单个进程对象也归一为数组', () => {
const single = parseWindowsProcessSnapshot(
JSON.stringify({ ProcessId: 42, ParentProcessId: 1 }),
);
expect(single).toHaveLength(1);
expect(single[0].ProcessId).toBe(42);
});
test('空输出和非法 JSON 返回空数组', () => {
expect(parseWindowsProcessSnapshot('')).toEqual([]);
expect(parseWindowsProcessSnapshot('null')).toEqual([]);
expect(parseWindowsProcessSnapshot('not json')).toEqual([]);
});
test('路径归一化去掉 \\\\?\\ 前缀、尾部分隔符并忽略大小写', () => {
expect(
normalizeWindowsPath('\\\\?\\C:\\Repo\\target\\debug\\api-server.exe'),
).toBe('c:\\repo\\target\\debug\\api-server.exe');
expect(normalizeWindowsPath('C:\\Repo\\data\\')).toBe('c:\\repo\\data');
expect(normalizeWindowsPath(null)).toBe('');
});
});
describe('按根 PID 遍历进程树', () => {
test('中间层进程已从快照消失时无法再到达更深的后代', () => {
// cmd(10) -> wrapper(20) -> api-server(30)。Ctrl+C 先杀掉 10 和 20
// 快照里只剩 30ParentProcessId 仍指向已消失的 20),父链断开后
// 按根 PID 遍历只能拿到根自己,这正是后端被漏杀的原因。
const snapshot = [
processEntry({
ProcessId: 30,
ParentProcessId: 20,
Name: 'api-server.exe',
}),
];
expect(selectProcessTreeIds(snapshot, 10)).toEqual([10]);
});
test('根已退出但中间层仍在快照里时仍可收全后代', () => {
const snapshot = [
processEntry({ ProcessId: 20, ParentProcessId: 10, Name: 'cargo.exe' }),
processEntry({
ProcessId: 30,
ParentProcessId: 20,
Name: 'api-server.exe',
}),
];
expect(selectProcessTreeIds(snapshot, 10).sort((a, b) => a - b)).toEqual([
10, 20, 30,
]);
});
test('父链完整时能收全后代', () => {
const snapshot = [
processEntry({ ProcessId: 10, ParentProcessId: 1, Name: 'cmd.exe' }),
processEntry({ ProcessId: 20, ParentProcessId: 10, Name: 'cargo.exe' }),
processEntry({
ProcessId: 30,
ParentProcessId: 20,
Name: 'api-server.exe',
}),
processEntry({ ProcessId: 40, ParentProcessId: 1, Name: 'other.exe' }),
];
expect(selectProcessTreeIds(snapshot, 10).sort((a, b) => a - b)).toEqual([
10, 20, 30,
]);
});
});
describe('按身份匹配本工作树后端进程', () => {
const apiServerExePath = 'C:\\Repo\\server-rs\\target\\debug\\api-server.exe';
const spacetimeDataDir =
'C:\\Repo\\server-rs\\.spacetimedb\\ai-game-creator\\data';
test('只收本工作树的 api-server.exe 与同一 data-dir 的 SpacetimeDB', () => {
const snapshot = [
processEntry({
ProcessId: 100,
Name: 'api-server.exe',
ExecutablePath: apiServerExePath,
CommandLine: '"server-rs\\target\\debug\\api-server.exe"',
}),
processEntry({
ProcessId: 101,
Name: 'api-server.exe',
ExecutablePath: 'C:\\Other\\server-rs\\target\\debug\\api-server.exe',
}),
processEntry({
ProcessId: 102,
Name: 'spacetimedb-standalone.exe',
ExecutablePath:
'C:\\Users\\me\\SpacetimeDB\\spacetimedb-standalone.exe',
CommandLine: `spacetimedb-standalone.exe start --data-dir ${spacetimeDataDir} --listen-addr 127.0.0.1:3101`,
}),
processEntry({
ProcessId: 103,
Name: 'spacetimedb-standalone.exe',
CommandLine:
'spacetimedb-standalone.exe start --data-dir C:\\Other\\data --listen-addr 127.0.0.1:3101',
}),
processEntry({
ProcessId: 104,
Name: 'node.exe',
CommandLine: `node dev.mjs --spacetime-data-dir ${spacetimeDataDir}`,
}),
];
expect(
selectWorktreeOwnedProcessIds(snapshot, {
apiServerExePath,
spacetimeDataDir,
}).sort((a, b) => a - b),
).toEqual([100, 102]);
});
test('只给 api-server 路径时不会误伤 SpacetimeDB', () => {
const snapshot = [
processEntry({
ProcessId: 100,
Name: 'api-server.exe',
ExecutablePath: apiServerExePath,
}),
processEntry({
ProcessId: 102,
Name: 'spacetimedb-standalone.exe',
CommandLine: `--data-dir ${spacetimeDataDir}`,
}),
];
expect(
selectWorktreeOwnedProcessIds(snapshot, { apiServerExePath }),
).toEqual([100]);
});
test('排除自身进程且路径大小写不敏感', () => {
const snapshot = [
processEntry({
ProcessId: 200,
Name: 'api-server.exe',
ExecutablePath: apiServerExePath.toUpperCase(),
}),
];
expect(
selectWorktreeOwnedProcessIds(snapshot, {
apiServerExePath,
selfPid: 200,
}),
).toEqual([]);
expect(
selectWorktreeOwnedProcessIds(snapshot, { apiServerExePath }),
).toEqual([200]);
});
test('没有可匹配身份时返回空数组', () => {
const snapshot = [processEntry({ ProcessId: 100, Name: 'api-server.exe' })];
expect(selectWorktreeOwnedProcessIds(snapshot, {})).toEqual([]);
});
});