mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-12 04:01:20 +00:00
107 lines
4.1 KiB
TypeScript
107 lines
4.1 KiB
TypeScript
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterAll, describe, expect, it } from 'vitest'
|
|
import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
|
|
import { launchLinuxScope, probeLinuxScope } from '../src/linux-scope.ts'
|
|
import { bindManagedProcess } from '../src/spawn.ts'
|
|
|
|
const scratch = mkdtempSync(join(tmpdir(), 'dsh-native-containment-'))
|
|
afterAll(() => { rmSync(scratch, { recursive: true, force: true }) })
|
|
|
|
function spec(argv: string[], graceMs = 100): SubprocessSpawnSpec {
|
|
return {
|
|
argv,
|
|
cwd: scratch,
|
|
stdio: {
|
|
stdin: 'ignore',
|
|
stdout: { maxBytes: 64_000 },
|
|
stderr: { maxBytes: 64_000 },
|
|
},
|
|
graceMs,
|
|
}
|
|
}
|
|
|
|
async function waitForPid(path: string): Promise<number> {
|
|
const deadline = Date.now() + 5_000
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
const pid = Number(readFileSync(path, 'utf8').trim())
|
|
if (Number.isSafeInteger(pid) && pid > 0) return pid
|
|
} catch {
|
|
// The target has not written the file yet.
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 20))
|
|
}
|
|
throw new Error(`pid file ${path} was not written`)
|
|
}
|
|
|
|
async function waitGone(pid: number): Promise<void> {
|
|
const deadline = Date.now() + 5_000
|
|
while (Date.now() < deadline) {
|
|
try {
|
|
process.kill(pid, 0)
|
|
try {
|
|
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8')
|
|
const state = stat.slice(stat.lastIndexOf(')') + 2, stat.lastIndexOf(')') + 3)
|
|
if (state === 'Z' || state === 'X') return
|
|
} catch {
|
|
return
|
|
}
|
|
} catch {
|
|
return
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, 20))
|
|
}
|
|
throw new Error(`pid ${pid} remained alive`)
|
|
}
|
|
|
|
const linuxNative = process.platform === 'linux' && probeLinuxScope()
|
|
|
|
describe.skipIf(!linuxNative)('Linux user-systemd native containment', () => {
|
|
it('terminates a setsid descendant and waits for the scope to become empty', async () => {
|
|
const pidFile = join(scratch, `setsid-${Date.now()}.pid`)
|
|
const command = `setsid sh -c 'echo $$ > "$1"; trap "" TERM; while :; do sleep 60; done' sh ${JSON.stringify(pidFile)} & wait`
|
|
const handle = bindManagedProcess(spec(['bash', '-c', command], 80), launchLinuxScope(spec(['bash', '-c', command], 80)))
|
|
const descendant = await waitForPid(pidFile)
|
|
handle.terminate()
|
|
await handle.done
|
|
await expect(handle.waitForExit()).resolves.toBe(true)
|
|
await waitGone(descendant)
|
|
})
|
|
|
|
it('keeps direct outcome separate from a double-fork descendant, then reaps the range', async () => {
|
|
const pidFile = join(scratch, `double-fork-${Date.now()}.pid`)
|
|
const script = [
|
|
'import os, signal, time',
|
|
'if os.fork() > 0: os._exit(0)',
|
|
'os.setsid()',
|
|
'if os.fork() > 0: os._exit(0)',
|
|
`open(${JSON.stringify(pidFile)}, 'w').write(str(os.getpid()))`,
|
|
'signal.signal(signal.SIGTERM, signal.SIG_IGN)',
|
|
'while True: time.sleep(60)',
|
|
].join('\n')
|
|
const request = spec(['python3', '-c', script], 80)
|
|
const handle = bindManagedProcess(request, launchLinuxScope(request))
|
|
const descendant = await waitForPid(pidFile)
|
|
await expect(handle.done).resolves.toEqual({ exitCode: 0, signal: null })
|
|
await expect(handle.waitForExit(AbortSignal.timeout(30))).resolves.toBe(false)
|
|
handle.terminate()
|
|
await expect(handle.waitForExit()).resolves.toBe(true)
|
|
await waitGone(descendant)
|
|
})
|
|
|
|
it('preserves Node-shaped ENOENT and EACCES spawn failures without replay', async () => {
|
|
const missing = spec([`missing-native-target-${Date.now()}`])
|
|
const missingHandle = bindManagedProcess(missing, launchLinuxScope(missing))
|
|
await expect(missingHandle.done).rejects.toMatchObject({ code: 'ENOENT' })
|
|
|
|
const deniedPath = join(scratch, `not-executable-${Date.now()}`)
|
|
writeFileSync(deniedPath, '#!/bin/sh\nexit 0\n', { mode: 0o600 })
|
|
chmodSync(deniedPath, 0o600)
|
|
const denied = spec([deniedPath])
|
|
const deniedHandle = bindManagedProcess(denied, launchLinuxScope(denied))
|
|
await expect(deniedHandle.done).rejects.toMatchObject({ code: 'EACCES' })
|
|
})
|
|
})
|