115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
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<void>((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<void>((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<void>((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 })
|
|
}
|
|
})
|
|
})
|