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, // 快照里只剩 30(ParentProcessId 仍指向已消失的 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([]); }); });