Files
deepseek-harness/packages/subprocess/subprocess-local/tests/linux-scope.spec.ts
T

619 lines
26 KiB
TypeScript

import { EventEmitter } from 'node:events'
import { existsSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'
import { PassThrough } from 'node:stream'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
launchLinuxScope,
prepareLinuxTerminalScope,
probeLinuxBootstrap,
probeLinuxManager,
probeLinuxNative,
probeLinuxScope,
} from '../src/linux-scope.ts'
import type { LinuxScopeInternals } from '../src/linux-scope.ts'
import {
consumeLinuxLaunchRequest,
linuxLaunchFilesFromLocator,
writeLinuxStartupError,
} from '../src/runner-protocol.ts'
import { SUBPROCESS_RUNNER_ENV } from '../src/runner-launch.ts'
const childProcessMocks = vi.hoisted(() => ({
execFile: vi.fn(),
spawn: vi.fn(),
spawnSync: vi.fn(),
}))
vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:child_process')>()
return {
...actual,
execFile: childProcessMocks.execFile as unknown as typeof actual.execFile,
spawn: childProcessMocks.spawn as typeof actual.spawn,
spawnSync: childProcessMocks.spawnSync as typeof actual.spawnSync,
}
})
class FakeChild extends EventEmitter {
pid: number | undefined = 321
exitCode: number | null = null
signalCode: NodeJS.Signals | null = null
stdin = new PassThrough()
stdout = new PassThrough()
stderr = new PassThrough()
kills: NodeJS.Signals[] = []
kill(signal: NodeJS.Signals): boolean {
this.kills.push(signal)
return true
}
exit(exitCode: number | null, signal: NodeJS.Signals | null): void {
this.exitCode = exitCode
this.signalCode = signal
this.emit('exit', exitCode, signal)
}
}
const directories: string[] = []
afterEach(() => {
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
vi.restoreAllMocks()
childProcessMocks.execFile.mockReset()
childProcessMocks.spawn.mockReset()
childProcessMocks.spawnSync.mockReset()
})
function missingUnit() {
return { status: 1, stdout: '', stderr: 'Unit dsh.scope could not be found.' }
}
function activeUnit(state = 'active') {
return { status: 0, stdout: `LoadState=loaded\nActiveState=${state}\n`, stderr: '' }
}
function unloadedUnit() {
return { status: 0, stdout: 'LoadState=not-found\nActiveState=inactive\n', stderr: '' }
}
function unitState(loadState: string, activeState: string) {
return { status: 0, stdout: `LoadState=${loadState}\nActiveState=${activeState}\n`, stderr: '' }
}
function spec() {
return {
argv: ['tool', 'literal arg'],
cwd: '/target',
stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
graceMs: 100,
env: { TARGET: 'yes' },
} as const
}
function launch(
query: LinuxScopeInternals['systemctlQuery'],
overrides: LinuxScopeInternals = {},
) {
const child = new FakeChild()
let options: { env?: NodeJS.ProcessEnv; cwd?: string; detached?: boolean } | undefined
const spawn = vi.fn((_command: string, _args: readonly string[], received: typeof options) => {
options = received
return child
})
const spawnSync = vi.fn(() => ({ status: 0, stdout: '', stderr: '' }))
const systemctlQuery = overrides.systemctlQuery ?? query
const result = launchLinuxScope(spec(), { TARGET: 'yes' }, {
spawn: overrides.spawn ?? spawn as never,
spawnSync: overrides.spawnSync ?? spawnSync as never,
...systemctlQuery === undefined ? {} : { systemctlQuery },
systemdRun: overrides.systemdRun ?? '/bin/systemd-run',
systemctl: overrides.systemctl ?? '/bin/systemctl',
runnerInvocation: overrides.runnerInvocation ?? ['/usr/bin/node', '/runner.js'],
...overrides.runnerAvailable === undefined ? {} : { runnerAvailable: overrides.runnerAvailable },
...overrides.loadLinuxExecve === undefined ? {} : { loadLinuxExecve: overrides.loadLinuxExecve },
...overrides.sleep === undefined ? {} : { sleep: overrides.sleep },
})
const requestPath = options?.env?.[SUBPROCESS_RUNNER_ENV]
if (requestPath === undefined) throw new Error('launch did not publish a request locator')
directories.push(linuxLaunchFilesFromLocator(requestPath).directory)
return { child, result, requestPath, spawn, spawnSync, options }
}
describe('Linux native capability selection', () => {
it('rechecks bootstrap and literal transient-scope support', () => {
const spawnSync = vi.fn(() => ({ status: 0, error: undefined }))
const runnerAvailable = vi.fn(() => true)
const loadLinuxExecve = vi.fn(() => vi.fn() as never)
const inputs = {
spawnSync: spawnSync as never,
runnerAvailable,
runnerInvocation: ['/usr/bin/node', '/runner.js'] as [string, ...string[]],
loadLinuxExecve,
systemdRun: '/bin/systemd-run',
systemctl: '/bin/systemctl',
}
expect(probeLinuxNative(inputs)).toBe(true)
expect(probeLinuxNative(inputs)).toBe(true)
expect(runnerAvailable).toHaveBeenCalledTimes(2)
expect(loadLinuxExecve).toHaveBeenCalledTimes(2)
expect(spawnSync).toHaveBeenCalledTimes(2)
expect(probeLinuxBootstrap({
...inputs,
loadLinuxExecve: () => { throw new Error('libc execve missing') },
})).toBe(false)
})
it('reports each failed dynamic prerequisite without executing a target', () => {
expect(probeLinuxScope({
spawnSync: vi.fn(() => ({ status: null, error: new Error('missing') })) as never,
})).toBe(false)
expect(probeLinuxBootstrap({
loadLinuxExecve: () => vi.fn() as never,
runnerInvocation: ['/missing'],
runnerAvailable: () => false,
})).toBe(false)
expect(probeLinuxBootstrap({
loadLinuxExecve: () => vi.fn() as never,
resolveRunnerInvocation: () => { throw new Error('runner resolution failed') },
})).toBe(false)
})
it('uses the default command adapters and runner resolution', () => {
childProcessMocks.spawnSync.mockReturnValue({ status: 0, error: undefined })
expect(probeLinuxScope()).toBe(true)
expect(probeLinuxManager()).toBe(true)
expect(childProcessMocks.spawnSync).toHaveBeenCalledTimes(2)
expect(probeLinuxBootstrap({ loadLinuxExecve: () => vi.fn() as never })).toBe(true)
expect(probeLinuxBootstrap({
runnerInvocation: [process.execPath],
runnerAvailable: () => true,
})).toBe(process.platform !== 'win32')
})
it('keeps quieting on the transient-scope probe but preserves manager diagnostics', () => {
const spawnSync = vi.fn((
_command: string,
_args: readonly string[],
_options: unknown,
) => ({ status: 0, error: undefined }))
expect(probeLinuxScope({ spawnSync: spawnSync as never })).toBe(true)
expect(probeLinuxManager({ spawnSync: spawnSync as never })).toBe(true)
const scopeOptions = spawnSync.mock.calls[0]?.[2] as { env: NodeJS.ProcessEnv }
const managerOptions = spawnSync.mock.calls[1]?.[2] as { env: NodeJS.ProcessEnv }
expect(scopeOptions.env).toMatchObject({ LC_ALL: 'C', SYSTEMD_LOG_TARGET: 'null' })
expect(managerOptions.env).toMatchObject({ LC_ALL: 'C' })
expect(managerOptions.env).not.toHaveProperty('SYSTEMD_LOG_TARGET')
expect(probeLinuxManager({
spawnSync: vi.fn(() => ({ status: 1, error: undefined })) as never,
})).toBe(false)
expect(probeLinuxManager({
spawnSync: vi.fn(() => ({ status: null, error: new Error('missing') })) as never,
})).toBe(false)
})
})
describe('Linux scope establishment and quiescence', () => {
it('does not mistake pre-establishment unit absence for quiescence and settles an empty range after cancellation', async () => {
const { child, result, requestPath, spawnSync } = launch(async () => missingUnit())
const waiting = result.owner.waitForExit()
result.owner.signal('SIGTERM')
expect(child.kills).toEqual(['SIGTERM'])
expect(spawnSync).toHaveBeenCalledWith('/bin/systemctl', expect.arrayContaining([
'kill', '--kill-whom=all', '--signal=SIGTERM',
]), expect.anything())
const direct = expect(result.direct).rejects.toThrow('before its bootstrap consumed')
child.exit(null, 'SIGTERM')
await direct
await expect(waiting).resolves.toBeUndefined()
expect(existsSync(requestPath)).toBe(true)
result.owner.cleanup?.()
})
it('accepts request consumption followed by rapid --collect unload as stopped', async () => {
const states = [activeUnit(), unloadedUnit()]
const { child, result, requestPath } = launch(async () => states.shift() ?? missingUnit())
expect(consumeLinuxLaunchRequest(requestPath)).toEqual({ cwd: '/target', env: { TARGET: 'yes' } })
const waiting = result.owner.waitForExit()
child.exit(0, null)
await expect(result.direct).resolves.toEqual({ exitCode: 0, signal: null })
await expect(waiting).resolves.toBeUndefined()
result.owner.signal('SIGKILL')
result.owner.cleanup?.()
expect(existsSync(linuxLaunchFilesFromLocator(requestPath).directory)).toBe(false)
})
it('uses the scope alone after establishment and the direct range only when scope signalling fails', async () => {
const spawnSync = vi.fn()
.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
.mockReturnValueOnce({ status: 1, stdout: '', stderr: 'scope signal failed' })
const { child, result, requestPath } = launch(async () => activeUnit(), {
spawnSync: spawnSync as never,
})
consumeLinuxLaunchRequest(requestPath)
const processKill = vi.spyOn(process, 'kill').mockReturnValue(true)
result.owner.signal('SIGTERM')
expect(processKill).not.toHaveBeenCalled()
result.owner.signal('SIGKILL')
expect(processKill).toHaveBeenCalledExactlyOnceWith(-321, 'SIGKILL')
expect(spawnSync).toHaveBeenCalledTimes(2)
child.exit(null, 'SIGKILL')
await expect(result.direct).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
result.owner.cleanup?.()
})
it('uses manager-observed unit existence as establishment proof', async () => {
const { child, result } = launch(async () => activeUnit('inactive'))
await expect(result.owner.waitForExit()).resolves.toBeUndefined()
child.exit(1, null)
await expect(result.direct).rejects.toThrow('before its bootstrap consumed')
result.owner.cleanup?.()
})
it('keeps waiting while the unit is absent and the direct launcher is still running', async () => {
const states = [missingUnit(), activeUnit('inactive')]
const { child, result } = launch(async () => states.shift() ?? activeUnit('inactive'))
await expect(result.owner.waitForExit()).resolves.toBeUndefined()
child.exit(1, null)
await expect(result.direct).rejects.toThrow('before its bootstrap consumed')
result.owner.cleanup?.()
})
it('treats status-zero not-found as pending until the direct launcher proves the range was never created', async () => {
const state: { child?: FakeChild } = {}
const launched = launch(async () => unloadedUnit(), {
sleep: async () => { state.child?.exit(127, null) },
})
state.child = launched.child
await expect(launched.result.owner.waitForExit()).resolves.toBeUndefined()
await expect(launched.result.direct).rejects.toThrow('before its bootstrap consumed')
launched.result.owner.cleanup?.()
})
it('polls promptly before establishment and backs off established active scopes', async () => {
const delays: number[] = []
const states = [
missingUnit(),
activeUnit(),
activeUnit(),
activeUnit(),
activeUnit(),
activeUnit(),
activeUnit(),
activeUnit(),
activeUnit(),
activeUnit('inactive'),
]
const launched = launch(
async () => states.shift() ?? activeUnit('inactive'),
{ sleep: async (delayMs) => { delays.push(delayMs) } },
)
await expect(launched.result.owner.waitForExit()).resolves.toBeUndefined()
expect(delays).toEqual([50, 50, 100, 200, 400, 800, 1_600, 3_200, 5_000])
launched.result.owner.cleanup?.()
})
it('keeps reloading scopes active and lets terminate wake a backed-off observation', async () => {
const states = [activeUnit('reloading'), activeUnit('inactive')]
const sleeping = Promise.withResolvers<undefined>()
const sleep = vi.fn(async (_delayMs: number, signal?: AbortSignal) => {
sleeping.resolve(undefined)
if (signal === undefined) throw new Error('missing sleep cancellation signal')
await new Promise<void>((resolve) => { signal.addEventListener('abort', () => { resolve() }, { once: true }) })
})
const launched = launch(async () => states.shift() ?? activeUnit('inactive'), { sleep })
consumeLinuxLaunchRequest(launched.requestPath)
const waiting = launched.result.owner.waitForExit()
await sleeping.promise
launched.result.owner.signal('SIGTERM')
await expect(waiting).resolves.toBeUndefined()
expect(sleep).toHaveBeenCalledOnce()
expect(sleep.mock.calls[0]?.[0]).toBe(50)
expect(sleep.mock.calls[0]?.[1]?.aborted).toBe(true)
expect(launched.spawnSync).toHaveBeenCalledOnce()
launched.result.owner.cleanup?.()
})
it('skips the next poll delay when terminate arrives during a manager query', async () => {
const firstQuery = Promise.withResolvers<ReturnType<typeof activeUnit>>()
const query = vi.fn()
.mockImplementationOnce(async () => await firstQuery.promise)
.mockResolvedValueOnce(activeUnit('inactive'))
const sleep = vi.fn(async () => {})
const launched = launch(query, { sleep })
consumeLinuxLaunchRequest(launched.requestPath)
const waiting = launched.result.owner.waitForExit()
launched.result.owner.signal('SIGTERM')
firstQuery.resolve(activeUnit())
await expect(waiting).resolves.toBeUndefined()
expect(sleep).not.toHaveBeenCalled()
launched.result.owner.cleanup?.()
})
it('reports child termination before request consumption to the direct result and settles the empty range', async () => {
const { child, result } = launch(async () => missingUnit())
child.exit(127, null)
await expect(result.direct).rejects.toThrow('before its bootstrap consumed')
await expect(result.owner.waitForExit()).resolves.toBeUndefined()
result.owner.cleanup?.()
})
it('reconstructs a pre-exec startup error instead of exposing bootstrap exit 127', async () => {
const { child, result, requestPath } = launch(async () => missingUnit())
const files = linuxLaunchFilesFromLocator(requestPath)
unlinkSync(requestPath)
writeLinuxStartupError(files, {
type: 'error',
error: { name: 'Error', message: 'spawn tool ENOENT', code: 'ENOENT' },
})
child.exit(127, null)
await expect(result.direct).rejects.toMatchObject({ code: 'ENOENT' })
result.owner.cleanup?.()
})
it('retries a failed state query and rejects unknown states or failed final kills', async () => {
const query = vi.fn()
.mockResolvedValueOnce({ status: null, stdout: '', stderr: '', error: new Error('query failed') })
.mockResolvedValueOnce(missingUnit())
const { result, requestPath } = launch(query)
unlinkSync(requestPath)
await expect(result.owner.waitForExit()).rejects.toThrow('query failed')
await expect(result.owner.waitForExit()).resolves.toBeUndefined()
result.owner.cleanup?.()
const unknown = launch(async () => activeUnit('mystery'))
await expect(unknown.result.owner.waitForExit()).rejects.toThrow('unknown ActiveState')
unknown.result.owner.cleanup?.()
const unknownLoad = launch(async () => unitState('masked', 'inactive'))
await expect(unknownLoad.result.owner.waitForExit()).rejects.toThrow('unknown state')
unknownLoad.result.owner.cleanup?.()
const killFailed = launch(async () => activeUnit(), {
spawnSync: vi.fn(() => ({ status: 1, stdout: '', stderr: 'permission denied' })) as never,
})
killFailed.result.owner.signal('SIGKILL')
await expect(killFailed.result.owner.waitForExit()).rejects.toThrow('could not signal')
killFailed.result.owner.cleanup?.()
})
it('reports command-query failures from the default systemctl adapter', async () => {
childProcessMocks.execFile.mockImplementationOnce((...args: unknown[]) => {
const callback = args.at(-1) as (error: Error | null, stdout: string, stderr: string) => void
const options = args[2] as { env: NodeJS.ProcessEnv }
expect(options.env).toMatchObject({ LC_ALL: 'C' })
expect(options.env).not.toHaveProperty('SYSTEMD_LOG_TARGET')
callback(null, 'LoadState=loaded\nActiveState=inactive\n', 'manager diagnostic remains readable')
return new EventEmitter()
})
const stopped = launch(undefined)
await expect(stopped.result.owner.waitForExit()).resolves.toBeUndefined()
stopped.result.owner.cleanup?.()
const queryError = Object.assign(new Error('systemctl execution failed'), { code: 'ENOENT' })
childProcessMocks.execFile.mockImplementationOnce((...args: unknown[]) => {
const callback = args.at(-1) as (error: Error | null, stdout: string, stderr: string) => void
callback(queryError, '', '')
return new EventEmitter()
})
const failed = launch(undefined)
await expect(failed.result.owner.waitForExit()).rejects.toBe(queryError)
failed.result.owner.cleanup?.()
})
it('rejects malformed, duplicate, incomplete, and extra manager state fields', async () => {
for (const [stdout, message] of [
['loaded\nActiveState=active\n', 'malformed state'],
['LoadState=loaded\nLoadState=loaded\nActiveState=active\n', 'duplicate LoadState'],
['LoadState=loaded\n', 'incomplete state'],
['LoadState=loaded\nActiveState=inactive\nOther=value\n', 'incomplete state'],
] as const) {
const launched = launch(async () => ({ status: 0, stdout, stderr: '' }))
await expect(launched.result.owner.waitForExit()).rejects.toThrow(message)
launched.result.owner.cleanup?.()
}
})
it('keeps signal failures scoped to final kill proof and stays idempotent after stop', async () => {
const spawnSync = vi.fn()
.mockReturnValueOnce({ status: 1, stdout: '', stderr: '' })
.mockReturnValueOnce({ status: 1, stderr: 'Unit dsh.scope could not be found.' })
.mockReturnValueOnce({ status: 1, stdout: '', stderr: '' })
.mockReturnValueOnce({ status: 0, stdout: '', stderr: '' })
const states = [activeUnit(), activeUnit('failed')]
const launched = launch(async () => states.shift() ?? missingUnit(), {
spawnSync: spawnSync as never,
})
launched.child.pid = undefined
unlinkSync(launched.requestPath)
launched.result.owner.signal('SIGTERM')
launched.result.owner.signal('SIGKILL')
launched.result.owner.signal('SIGKILL')
launched.result.owner.signal('SIGKILL')
await expect(launched.result.owner.waitForExit()).resolves.toBeUndefined()
await expect(launched.result.owner.waitForExit()).resolves.toBeUndefined()
launched.result.owner.terminateForHostExit()
expect(spawnSync).toHaveBeenCalledTimes(4)
launched.result.owner.cleanup?.()
})
it('reports unreadable manager output and a failed kill before establishment', async () => {
const withOutput = launch(async () => ({
status: 5, stdout: '', stderr: 'permission denied',
}))
await expect(withOutput.result.owner.waitForExit()).rejects.toThrow('permission denied')
withOutput.result.owner.cleanup?.()
const withoutOutput = launch(async () => ({ status: null, stdout: '', stderr: '' }))
await expect(withoutOutput.result.owner.waitForExit()).rejects.toThrow('exit null')
withoutOutput.result.owner.cleanup?.()
const killFailed = launch(async () => missingUnit(), {
spawnSync: vi.fn(() => ({ status: 1, stdout: '', stderr: 'kill denied' })) as never,
})
vi.spyOn(process, 'kill').mockImplementation(() => { throw new Error('missing process group') })
killFailed.result.owner.signal('SIGKILL')
await expect(killFailed.result.owner.waitForExit()).rejects.toThrow('kill denied')
killFailed.result.owner.cleanup?.()
})
it('settles direct outcomes once and reports malformed startup errors', async () => {
const childError = launch(async () => missingUnit())
const spawnError = new Error('systemd-run failed')
childError.child.emit('error', spawnError)
childError.child.exit(1, null)
await expect(childError.result.direct).rejects.toBe(spawnError)
childError.result.owner.cleanup?.()
const lateError = launch(async () => missingUnit())
consumeLinuxLaunchRequest(lateError.requestPath)
lateError.child.exit(0, null)
lateError.child.emit('error', new Error('late child error'))
await expect(lateError.result.direct).resolves.toEqual({ exitCode: 0, signal: null })
lateError.result.owner.cleanup?.()
const malformed = launch(async () => missingUnit())
const files = linuxLaunchFilesFromLocator(malformed.requestPath)
unlinkSync(malformed.requestPath)
writeFileSync(files.startupErrorPath, '{', { mode: 0o600 })
malformed.child.exit(127, null)
await expect(malformed.result.direct).rejects.toBeInstanceOf(SyntaxError)
malformed.result.owner.cleanup?.()
})
it('does not signal a direct group before the launcher publishes a pid', async () => {
const launched = launch(async () => activeUnit('inactive'))
launched.child.pid = undefined
const processKill = vi.spyOn(process, 'kill')
launched.result.owner.signal('SIGTERM')
expect(processKill).not.toHaveBeenCalled()
expect(launched.child.kills).toEqual([])
consumeLinuxLaunchRequest(launched.requestPath)
launched.child.exit(0, null)
await expect(launched.result.direct).resolves.toEqual({ exitCode: 0, signal: null })
launched.result.owner.cleanup?.()
})
it('does not signal the direct group after the launcher exits', async () => {
const { child, result, requestPath, spawnSync } = launch(async () => activeUnit())
consumeLinuxLaunchRequest(requestPath)
child.exit(0, null)
await expect(result.direct).resolves.toEqual({ exitCode: 0, signal: null })
const processKill = vi.spyOn(process, 'kill')
result.owner.signal('SIGTERM')
result.owner.terminateForHostExit()
expect(processKill).not.toHaveBeenCalled()
expect(child.kills).toEqual([])
expect(spawnSync).toHaveBeenCalledTimes(2)
result.owner.cleanup?.()
})
it('runs direct fallback before the exact synchronous scope kill on host exit', () => {
const events: string[] = []
const { child, result } = launch(async () => missingUnit(), {
spawnSync: vi.fn(() => { events.push('scope'); return { status: 0 } }) as never,
})
child.kill = vi.fn(() => { events.push('direct'); return true })
vi.spyOn(process, 'kill').mockImplementation(() => { events.push('direct'); return true })
result.owner.terminateForHostExit()
expect(events).toEqual(['direct', 'scope'])
result.owner.cleanup?.()
})
})
describe('Linux PTY bootstrap reuse', () => {
const terminalSpec = {
argv: ['bash', '--noprofile'],
cwd: '/target',
env: { TARGET: 'yes' },
rows: 24,
cols: 80,
graceMs: 100,
} as const
it('uses the same request/bootstrap, preserves argv, and cleans after owner settlement', async () => {
const scope = prepareLinuxTerminalScope(terminalSpec, { TARGET: 'yes' }, {
systemdRun: '/bin/systemd-run',
systemctl: '/bin/systemctl',
runnerInvocation: ['/usr/bin/node', '/runner.js'],
spawnSync: vi.fn(() => ({ status: 0 })) as never,
systemctlQuery: async () => missingUnit(),
})
const requestPath = scope.env[SUBPROCESS_RUNNER_ENV]
if (requestPath === undefined) throw new Error('missing PTY request')
expect(scope.args.slice(-3)).toEqual(['--', 'bash', '--noprofile'])
expect(consumeLinuxLaunchRequest(requestPath)).toEqual({ cwd: '/target', env: { TARGET: 'yes' } })
const owner = scope.bindOwner({ running: () => false, signal: vi.fn() })
await expect(owner.waitForExit()).resolves.toBeUndefined()
expect(scope.resolveOutcome({ exitCode: 0, signal: null })).toEqual({ exitCode: 0, signal: null })
scope.cleanup()
expect(existsSync(linuxLaunchFilesFromLocator(requestPath).directory)).toBe(false)
})
it('surfaces PTY pre-exec errors instead of launcher outcomes', () => {
const scope = prepareLinuxTerminalScope(terminalSpec, { TARGET: 'yes' })
const requestPath = scope.env[SUBPROCESS_RUNNER_ENV]
if (requestPath === undefined) throw new Error('missing PTY request')
const files = linuxLaunchFilesFromLocator(requestPath)
unlinkSync(requestPath)
writeLinuxStartupError(files, {
type: 'error', error: { name: 'Error', message: 'bad cwd', code: 'ENOENT' },
})
expect(() => scope.resolveOutcome({ exitCode: 127, signal: null })).toThrow('bad cwd')
scope.cleanup()
})
it('uses default owner dependencies and rejects an unconsumed request', () => {
const scope = prepareLinuxTerminalScope(terminalSpec, { TARGET: 'yes' })
const requestPath = scope.env[SUBPROCESS_RUNNER_ENV]
if (requestPath === undefined) throw new Error('missing PTY request')
directories.push(linuxLaunchFilesFromLocator(requestPath).directory)
scope.bindOwner({ running: () => true, signal: vi.fn() })
expect(() => scope.resolveOutcome({ exitCode: 1, signal: null })).toThrow(
'before its bootstrap consumed',
)
scope.cleanup()
})
})
describe('Linux ordinary launch adapters', () => {
it('uses the default launch dependencies without changing the target request', async () => {
const child = new FakeChild()
childProcessMocks.spawn.mockReturnValue(child)
const result = launchLinuxScope(spec(), { TARGET: 'yes' })
const call = childProcessMocks.spawn.mock.calls[0]
const options = call?.[2] as { env?: NodeJS.ProcessEnv } | undefined
const requestPath = options?.env?.[SUBPROCESS_RUNNER_ENV]
if (requestPath === undefined) throw new Error('launch did not publish a request locator')
directories.push(linuxLaunchFilesFromLocator(requestPath).directory)
expect(call?.[0]).toBe('systemd-run')
expect(consumeLinuxLaunchRequest(requestPath)).toEqual({ cwd: '/target', env: { TARGET: 'yes' } })
child.exit(0, null)
await expect(result.direct).resolves.toEqual({ exitCode: 0, signal: null })
result.owner.cleanup?.()
})
it('removes the private launch directory when spawn throws synchronously', () => {
const spawnError = new Error('synchronous spawn failure')
let requestPath: string | undefined
expect(() => launchLinuxScope(spec(), { TARGET: 'yes' }, {
runnerInvocation: ['/usr/bin/node', '/runner.js'],
spawn: vi.fn((_command: string, _args: readonly string[], options: { env?: NodeJS.ProcessEnv }) => {
requestPath = options.env?.[SUBPROCESS_RUNNER_ENV]
throw spawnError
}) as never,
})).toThrow(spawnError)
if (requestPath === undefined) throw new Error('spawn did not receive a request locator')
expect(existsSync(linuxLaunchFilesFromLocator(requestPath).directory)).toBe(false)
})
})