Address reliability and consistency gaps across Agent Runtimes, persistence, settings, Knowledge, Magic Notes, Smart Heartbeat, and the download site. Runtime processes now have bounded lifecycle cleanup and atomic configuration rollback, while model packages and persisted mutations recover safely. Add a configurable global shortcut, protect unsaved work, improve modal and keyboard behavior, localize the built-in project without rewriting stored data, and lazy-load heavy renderer routes under enforced bundle budgets. Align project forms and disabled controls with shared typography and interaction states, and strengthen website release metadata validation and navigation accessibility. Release note: 修复 Runtime、设置、知识库、魔法笔记与智能心跳中的可靠性和交互一致性问题;新增可配置全局快捷键,改进无障碍与加载性能,并强化官网下载校验。
196 lines
5.0 KiB
TypeScript
196 lines
5.0 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
requestProcessTreeTermination,
|
|
terminateProcessTreeAndWait,
|
|
waitForProcessExit,
|
|
type WaitableProcessTreeChild
|
|
} from './child-process-termination'
|
|
|
|
function fakeChild(
|
|
pid = 42
|
|
): WaitableProcessTreeChild & EventEmitter {
|
|
const child =
|
|
new EventEmitter() as WaitableProcessTreeChild & EventEmitter
|
|
child.exitCode = null
|
|
child.pid = pid
|
|
child.kill = vi.fn()
|
|
return child
|
|
}
|
|
|
|
describe('child process tree termination', () => {
|
|
it('uses taskkill /T /F on Windows and bounds both exit waits', async () => {
|
|
vi.useFakeTimers()
|
|
try {
|
|
const child = fakeChild(314)
|
|
const killer = fakeChild(315)
|
|
killer.unref = vi.fn()
|
|
const spawnMock = vi.fn(() => killer)
|
|
|
|
const termination = terminateProcessTreeAndWait(child, {
|
|
platform: 'win32',
|
|
spawn: spawnMock,
|
|
waitMs: 25
|
|
})
|
|
await vi.advanceTimersByTimeAsync(25)
|
|
await vi.advanceTimersByTimeAsync(25)
|
|
await expect(termination).resolves.toBeUndefined()
|
|
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
'taskkill.exe',
|
|
['/PID', '314', '/T', '/F'],
|
|
{
|
|
shell: false,
|
|
stdio: 'ignore',
|
|
windowsHide: true
|
|
}
|
|
)
|
|
expect(killer.unref).toHaveBeenCalledOnce()
|
|
expect(child.kill).toHaveBeenCalledWith('SIGTERM')
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
|
|
it('falls back to direct termination when Windows taskkill fails', async () => {
|
|
const child = fakeChild(314)
|
|
const killer = fakeChild(315)
|
|
const spawnMock = vi.fn(() => killer)
|
|
const termination = terminateProcessTreeAndWait(child, {
|
|
platform: 'win32',
|
|
spawn: spawnMock,
|
|
waitMs: 1_000
|
|
})
|
|
|
|
killer.exitCode = 1
|
|
killer.emit('close', 1, null)
|
|
await Promise.resolve()
|
|
expect(child.kill).toHaveBeenCalledWith('SIGTERM')
|
|
child.exitCode = 0
|
|
child.emit('close', 0, null)
|
|
|
|
await expect(termination).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('terminates a detached POSIX process group with the requested signal', () => {
|
|
const child = fakeChild(2718)
|
|
const killProcess = vi.fn()
|
|
|
|
requestProcessTreeTermination(child, {
|
|
platform: 'linux',
|
|
processGroup: true,
|
|
signal: 'SIGKILL',
|
|
killProcess
|
|
})
|
|
|
|
expect(killProcess).toHaveBeenCalledWith(-2718, 'SIGKILL')
|
|
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
|
|
})
|
|
|
|
it('falls back to the direct child when POSIX group termination fails', () => {
|
|
const child = fakeChild(2718)
|
|
|
|
requestProcessTreeTermination(child, {
|
|
platform: 'linux',
|
|
processGroup: true,
|
|
killProcess: vi.fn(() => {
|
|
throw new Error('not a group leader')
|
|
})
|
|
})
|
|
|
|
expect(child.kill).toHaveBeenCalledWith('SIGTERM')
|
|
})
|
|
|
|
it('resolves exit waiting immediately on close', async () => {
|
|
const child = fakeChild()
|
|
const waiting = waitForProcessExit(child, 1_000)
|
|
child.emit('close', 0, null)
|
|
await expect(waiting).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('does not terminate a child already marked as killed', () => {
|
|
const child = fakeChild()
|
|
child.killed = true
|
|
const spawnMock = vi.fn()
|
|
const killProcess = vi.fn()
|
|
|
|
expect(
|
|
requestProcessTreeTermination(child, {
|
|
platform: 'win32',
|
|
spawn: spawnMock,
|
|
killProcess
|
|
})
|
|
).toBeUndefined()
|
|
expect(spawnMock).not.toHaveBeenCalled()
|
|
expect(killProcess).not.toHaveBeenCalled()
|
|
expect(child.kill).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('supports utility-process handles without an exitCode', () => {
|
|
const child = {
|
|
killed: false,
|
|
pid: 99,
|
|
kill: vi.fn()
|
|
}
|
|
const killer = fakeChild(100)
|
|
const spawnMock = vi.fn(() => killer)
|
|
|
|
expect(
|
|
requestProcessTreeTermination(child, {
|
|
platform: 'win32',
|
|
spawn: spawnMock
|
|
})
|
|
).toBe(killer)
|
|
expect(spawnMock).toHaveBeenCalledWith(
|
|
'taskkill.exe',
|
|
['/PID', '99', '/T', '/F'],
|
|
{
|
|
shell: false,
|
|
stdio: 'ignore',
|
|
windowsHide: true
|
|
}
|
|
)
|
|
})
|
|
|
|
it('falls back asynchronously for a synchronous Windows caller', async () => {
|
|
vi.useFakeTimers()
|
|
try {
|
|
const child = {
|
|
pid: 99,
|
|
kill: vi.fn()
|
|
}
|
|
const killer = fakeChild(100)
|
|
|
|
requestProcessTreeTermination(child, {
|
|
platform: 'win32',
|
|
spawn: vi.fn(() => killer),
|
|
signal: 'SIGKILL',
|
|
waitMs: 25
|
|
})
|
|
|
|
await vi.advanceTimersByTimeAsync(25)
|
|
expect(child.kill).toHaveBeenCalledOnce()
|
|
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
|
|
it('does not directly kill after successful Windows tree termination', () => {
|
|
const child = {
|
|
pid: 99,
|
|
kill: vi.fn()
|
|
}
|
|
const killer = fakeChild(100)
|
|
|
|
requestProcessTreeTermination(child, {
|
|
platform: 'win32',
|
|
spawn: vi.fn(() => killer)
|
|
})
|
|
killer.exitCode = 0
|
|
killer.emit('close', 0, null)
|
|
|
|
expect(child.kill).not.toHaveBeenCalled()
|
|
})
|
|
})
|