import { spawn } from 'node:child_process' import { existsSync, mkdtempSync, rmSync } from 'node:fs' import * as os from 'node:os' import * as path from 'node:path' import { describe, expect, it } from 'vitest' import { findProcessesUsingWorktree, killProcessesUsingWorktree, resolveWorktreePath } from './worktree' describe('resolveWorktreePath', () => { const workspaceRoot = '/workspace/project' it('绝对路径原样返回', () => { const abs = '/home/user/Sources/worktree/repo/session-foo' expect(resolveWorktreePath(abs, workspaceRoot)).toBe(abs) }) it('~/... 展开为 home 下路径', () => { const home = os.homedir() expect(resolveWorktreePath('~/Sources/worktree/repo/session-foo', workspaceRoot)) .toBe(path.join(home, 'Sources/worktree/repo/session-foo')) }) it('相对路径拼到 workspaceRoot 下', () => { expect(resolveWorktreePath('.claude/worktrees/abc123', workspaceRoot)) .toBe(path.join(workspaceRoot, '.claude/worktrees/abc123')) }) }) // 检测靠扫 /proc,只有 Linux 有;别的平台整组跳过(函数本身也会返回空)。 describe.runIf(existsSync('/proc'))('findProcessesUsingWorktree', () => { it('抓到 cwd 落在 worktree 目录下的常驻进程', async () => { const dir = mkdtempSync(path.join(os.tmpdir(), 'wt-proc-')) // cwd 设进 dir 的 sleep,模拟仍在 worktree 里跑的 dev server const child = spawn('sleep', ['30'], { cwd: dir, stdio: 'ignore' }) try { await new Promise((resolve, reject) => { child.on('spawn', () => resolve()) child.on('error', reject) }) const hits = findProcessesUsingWorktree(dir) expect(hits.some(h => h.pid === String(child.pid))).toBe(true) } finally { child.kill('SIGKILL') rmSync(dir, { recursive: true, force: true }) } }) it('没进程占用的目录返回空数组', () => { const dir = mkdtempSync(path.join(os.tmpdir(), 'wt-empty-')) try { expect(findProcessesUsingWorktree(dir)).toEqual([]) } finally { rmSync(dir, { recursive: true, force: true }) } }) }) // 杀占用进程:与 find 同依赖 /proc,非 Linux 跳过。 describe.runIf(existsSync('/proc'))('killProcessesUsingWorktree', () => { it('杀掉 cwd 落在 worktree 下的进程,find 结果变空', async () => { const dir = mkdtempSync(path.join(os.tmpdir(), 'wt-kill-')) const child = spawn('sleep', ['30'], { cwd: dir, stdio: 'ignore' }) try { await new Promise((resolve, reject) => { child.on('spawn', () => resolve()) child.on('error', reject) }) expect(findProcessesUsingWorktree(dir).some(h => h.pid === String(child.pid))).toBe(true) const killed = killProcessesUsingWorktree(dir) expect(killed.some(h => h.pid === String(child.pid))).toBe(true) // 等子进程真正退出(kill 函数已等 300–500ms,这里再兜底) await new Promise((resolve) => { if (child.killed || child.exitCode !== null) resolve() else child.on('exit', () => resolve()) setTimeout(resolve, 1000) }) expect(findProcessesUsingWorktree(dir).some(h => h.pid === String(child.pid))).toBe(false) } finally { try { child.kill('SIGKILL') } catch {} rmSync(dir, { recursive: true, force: true }) } }) it('不杀 process.pid / process.ppid(命中时跳过)', () => { // chdir 到独占临时目录,让 find 能命中本进程 cwd,又不会扫到 vitest/项目其它进程。 const dir = mkdtempSync(path.join(os.tmpdir(), 'wt-self-')) const prev = process.cwd() const selfPid = String(process.pid) const parentPid = String(process.ppid) try { process.chdir(dir) // find 应能看到自身(cwd=dir);kill 必须跳过 self/ppid expect(findProcessesUsingWorktree(dir).some(h => h.pid === selfPid)).toBe(true) const killed = killProcessesUsingWorktree(dir) expect(killed.some(h => h.pid === selfPid)).toBe(false) expect(killed.some(h => h.pid === parentPid)).toBe(false) expect(process.kill(process.pid, 0)).toBe(true) } finally { process.chdir(prev) rmSync(dir, { recursive: true, force: true }) } }) })