mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
Review found the shared observation defeated the very fence it fed: it carries the original PID-to-start-time pairing forward, so a recycled PID still matches it and takes a signal meant for the process that exited. Capturing it outside the per-member try also let one failed read abort a whole teardown round, breaking the synchronous host-exit contract, and an empty round paid a read for no members. signalProcess now reads ProcessInspector.isAlive immediately before delivering, from the narrowest per-identity source each platform offers; signalMembers and waitForMembers return before capturing when a round has no members. snapshot() keeps serving the readiness poll, whose per-poll table read stays at one. Windows enumerates Toolhelp32 lazily on the first tree question, so a snapshot asked only for liveness — the 25 ms teardown poll — performs no table walk at all.
174 lines
6.9 KiB
TypeScript
174 lines
6.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
createWindowsProcessInspector,
|
|
isInvalidHandle,
|
|
windowsProcessTree,
|
|
WindowsProcessInspector,
|
|
} from '@deepseek-ai/dsh-subprocess-local/src/windows-inspector.ts'
|
|
import type {
|
|
NativePtr,
|
|
ProcessEntry,
|
|
WindowsProcessInspectorInternals,
|
|
WindowsProcessState,
|
|
} from '@deepseek-ai/dsh-subprocess-local/src/windows-inspector.ts'
|
|
|
|
function fakeInternals() {
|
|
const entries: ProcessEntry[] = []
|
|
const states = new Map<number, WindowsProcessState>()
|
|
const kills: Array<[number, boolean]> = []
|
|
const counts = { enumerations: 0, stateReads: 0 }
|
|
return {
|
|
counts,
|
|
internals: {
|
|
snapshot: () => { counts.enumerations += 1; return [...entries] },
|
|
processState: (pid) => { counts.stateReads += 1; return states.get(pid) },
|
|
taskkill: (pid: number, force: boolean) => { kills.push([pid, force]) },
|
|
} satisfies WindowsProcessInspectorInternals,
|
|
add(entry: ProcessEntry, started?: string, active = true): void {
|
|
entries.push(entry)
|
|
if (started !== undefined) states.set(entry.pid, { started, active })
|
|
},
|
|
kills,
|
|
}
|
|
}
|
|
|
|
describe('WindowsProcessInspector table enumeration', () => {
|
|
it('enumerates the process table only for questions that need it', () => {
|
|
const fake = fakeInternals()
|
|
fake.add({ pid: 10, parentPid: 0 }, 't10')
|
|
fake.add({ pid: 11, parentPid: 10 }, 't11')
|
|
const inspector = new WindowsProcessInspector(fake.internals)
|
|
|
|
// Liveness is a per-handle question on Windows, so a snapshot asked only
|
|
// for liveness must not pay a Toolhelp32 walk. The terminal's Windows
|
|
// teardown polls exactly this way, every 25 ms.
|
|
const observed = inspector.snapshot()
|
|
expect(observed.alive({ pid: 11, started: 't11' })).toBe(true)
|
|
expect(fake.counts.enumerations).toBe(0)
|
|
|
|
expect(observed.tree(10)).toHaveLength(2)
|
|
expect(fake.counts.enumerations).toBe(1)
|
|
|
|
// A second tree question reuses the same observation.
|
|
observed.tree(10)
|
|
expect(fake.counts.enumerations).toBe(1)
|
|
})
|
|
})
|
|
|
|
describe('windowsProcessTree', () => {
|
|
it('walks a table children-first with readable identities only', () => {
|
|
const started = (pid: number): string | undefined => pid === 12 ? undefined : `t${pid}`
|
|
expect(windowsProcessTree([
|
|
{ pid: 10, parentPid: 0 },
|
|
{ pid: 11, parentPid: 10 },
|
|
{ pid: 12, parentPid: 11 },
|
|
{ pid: 13, parentPid: 11 },
|
|
{ pid: 14, parentPid: 10 },
|
|
], 10, started)).toEqual([
|
|
{ pid: 13, started: 't13' },
|
|
{ pid: 11, started: 't11' },
|
|
{ pid: 14, started: 't14' },
|
|
{ pid: 10, started: 't10' },
|
|
])
|
|
})
|
|
|
|
it('returns an empty walk for an absent root', () => {
|
|
expect(windowsProcessTree([{ pid: 10, parentPid: 0 }], 99, () => 't')).toEqual([])
|
|
})
|
|
|
|
it('terminates on a parent cycle instead of recursing forever', () => {
|
|
const entries = [
|
|
{ pid: 10, parentPid: 11 },
|
|
{ pid: 11, parentPid: 10 },
|
|
]
|
|
expect(windowsProcessTree(entries, 10, () => 't')).toHaveLength(2)
|
|
})
|
|
})
|
|
|
|
describe('WindowsProcessInspector (injected internals)', () => {
|
|
it('exposes the shell pid as the pseudo foreground group and never proves stdin waits', () => {
|
|
const fake = fakeInternals()
|
|
const inspector = new WindowsProcessInspector(fake.internals)
|
|
expect(inspector.foregroundPgid(77)).toBe(77)
|
|
expect(inspector.isStdinWaiting(77, 10)).toBe(false)
|
|
expect(inspector.snapshot().session(77)).toEqual([])
|
|
})
|
|
|
|
it('delegates tree walks and identity checks to the internals', () => {
|
|
const fake = fakeInternals()
|
|
fake.add({ pid: 10, parentPid: 0 }, 't10')
|
|
fake.add({ pid: 11, parentPid: 10 }, 't11')
|
|
const inspector = new WindowsProcessInspector(fake.internals)
|
|
expect(inspector.snapshot().tree(10)).toEqual([
|
|
{ pid: 11, started: 't11' },
|
|
{ pid: 10, started: 't10' },
|
|
])
|
|
expect(inspector.isAlive({ pid: 11, started: 't11' })).toBe(true)
|
|
expect(inspector.isAlive({ pid: 11, started: 'stale' })).toBe(false)
|
|
expect(inspector.isAlive({ pid: 99, started: 't99' })).toBe(false)
|
|
|
|
fake.add({ pid: 12, parentPid: 10 }, 't12', false)
|
|
expect(inspector.isAlive({ pid: 12, started: 't12' })).toBe(false)
|
|
})
|
|
|
|
it('maps SIGKILL to a forced taskkill and other signals to the grace form', () => {
|
|
const fake = fakeInternals()
|
|
const inspector = new WindowsProcessInspector(fake.internals)
|
|
inspector.signalGroup(77, 'SIGKILL')
|
|
inspector.signalGroup(77, 'SIGTERM')
|
|
inspector.signalGroup(0, 'SIGKILL')
|
|
expect(fake.kills).toEqual([[77, true], [77, false], [0, true]])
|
|
})
|
|
|
|
it('signals a process only while its start identity matches', () => {
|
|
const fake = fakeInternals()
|
|
fake.add({ pid: 10, parentPid: 0 }, 't10')
|
|
fake.add({ pid: 11, parentPid: 10 }, 't11', false)
|
|
const inspector = new WindowsProcessInspector(fake.internals)
|
|
inspector.signalProcess({ pid: 10, started: 't10' }, 'SIGKILL')
|
|
inspector.signalProcess({ pid: 11, started: 't11' }, 'SIGKILL')
|
|
inspector.signalProcess({ pid: 10, started: 'stale' }, 'SIGTERM')
|
|
expect(fake.kills).toEqual([[10, true]])
|
|
})
|
|
|
|
it('accepts an injected internals factory through the creator', () => {
|
|
const fake = fakeInternals()
|
|
expect(createWindowsProcessInspector(fake.internals)).toBeInstanceOf(WindowsProcessInspector)
|
|
expect(createWindowsProcessInspector()).toBeInstanceOf(WindowsProcessInspector)
|
|
})
|
|
})
|
|
|
|
describe('isInvalidHandle', () => {
|
|
it('rejects null, zero, and the all-ones INVALID_HANDLE_VALUE forms', () => {
|
|
const ptr = (value: bigint): NativePtr => value as NativePtr
|
|
expect(isInvalidHandle(null)).toBe(true)
|
|
expect(isInvalidHandle(undefined)).toBe(true)
|
|
expect(isInvalidHandle(ptr(0n))).toBe(true)
|
|
expect(isInvalidHandle(ptr(0xFFFFFFFFFFFFFFFFn))).toBe(true)
|
|
expect(isInvalidHandle(ptr(-1n))).toBe(true)
|
|
expect(isInvalidHandle(ptr(1234n))).toBe(false)
|
|
})
|
|
})
|
|
|
|
const win32 = process.platform === 'win32' ? describe : describe.skip
|
|
|
|
win32('WindowsProcessInspector over the real koffi bindings', () => {
|
|
it('walks the live process table from the test runner itself', () => {
|
|
const inspector = createWindowsProcessInspector()
|
|
const tree = inspector.snapshot().tree(process.pid)
|
|
const self = tree.find(member => member.pid === process.pid)
|
|
expect(self).toBeDefined()
|
|
expect(inspector.snapshot().alive(self!)).toBe(true)
|
|
expect(inspector.foregroundPgid(process.pid)).toBe(process.pid)
|
|
})
|
|
|
|
it('reports unreadable identities for absent processes and no-ops tree signalling', () => {
|
|
const inspector = createWindowsProcessInspector()
|
|
expect(inspector.isAlive({ pid: 0x7FFFFFFF, started: 'absent' })).toBe(false)
|
|
expect(() => { inspector.signalGroup(0x7FFFFFFF, 'SIGKILL') }).not.toThrow()
|
|
expect(() => { inspector.signalGroup(0x7FFFFFFF, 'SIGTERM') }).not.toThrow()
|
|
expect(() => { inspector.signalGroup(0, 'SIGKILL') }).not.toThrow()
|
|
expect(() => { inspector.signalProcess({ pid: 0x7FFFFFFF, started: 'absent' }, 'SIGKILL') }).not.toThrow()
|
|
})
|
|
})
|