| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import { spawn, spawnSync } from 'node:child_process'
- import { describe, expect, it, vi } from 'vitest'
- import type { SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
- import { launchLinuxScope, probeLinuxScope } from '../src/linux-scope.ts'
- import { spawnRunnerInvocation } from '../src/runner-launch.ts'
- function spec(argv: string[]): SubprocessSpawnSpec {
- return {
- argv,
- cwd: process.cwd(),
- stdio: { stdin: 'ignore', stdout: { maxBytes: 1024 }, stderr: { maxBytes: 1024 } },
- graceMs: 100,
- env: { LITERAL_VALUE: '$HOME ${UNCHANGED}' },
- }
- }
- describe('Linux systemd scope adapter', () => {
- it('requires a readable user manager and literal-argument systemd support', () => {
- const calls: string[][] = []
- const runSync = vi.fn((command: string, args: readonly string[]) => {
- calls.push([command, ...args])
- return { status: 0, error: undefined }
- }) as unknown as typeof spawnSync
- expect(probeLinuxScope({ spawnSync: runSync, systemdRun: 'systemd-run', systemctl: 'systemctl' })).toBe(true)
- expect(calls[1]).toContain('--expand-environment=no')
- expect(calls[1]).not.toContain('--wait')
- const oldSystemd = vi.fn((command: string) => ({
- status: command === 'systemctl' ? 0 : 1,
- error: undefined,
- })) as unknown as typeof spawnSync
- expect(probeLinuxScope({ spawnSync: oldSystemd })).toBe(false)
- const managerError = new Error('missing user manager')
- expect(probeLinuxScope({
- spawnSync: vi.fn(() => ({ error: managerError })) as unknown as typeof spawnSync,
- })).toBe(false)
- expect(probeLinuxScope({
- spawnSync: vi.fn(() => ({ status: 1, error: undefined })) as unknown as typeof spawnSync,
- })).toBe(false)
- })
- it('keeps user argv out of systemd-run and reports the direct target outcome', async () => {
- let wrapper: ReturnType<typeof spawn> | undefined
- let systemdArgs: readonly string[] = []
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- systemdArgs = args
- const separator = args.indexOf('--')
- const command = args[separator + 1] as string
- wrapper = spawn(command, args.slice(separator + 2), options)
- return wrapper
- }) as unknown as typeof spawn
- const runSyncMock = vi.fn((command: string, args: readonly string[]) => {
- if (command === 'systemctl' && args[1] === 'show') {
- const active = wrapper?.exitCode === null && wrapper.signalCode === null
- return { status: 0, stdout: active ? 'active\n' : 'inactive\n', stderr: '', error: undefined }
- }
- return { status: 0, stdout: '', stderr: '', error: undefined }
- })
- const runSync = runSyncMock as unknown as typeof spawnSync
- const launch = launchLinuxScope(spec([process.execPath, '-e', 'process.exit(9)', 'literal $VALUE']), {
- spawn: run,
- spawnSync: runSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(launch.direct).resolves.toEqual({ exitCode: 9, signal: null })
- await expect(launch.owner.waitForExit()).resolves.toBe(true)
- const callsBeforeStaleSignal = runSyncMock.mock.calls.length
- launch.owner.signal('SIGKILL')
- expect(runSyncMock).toHaveBeenCalledTimes(callsBeforeStaleSignal)
- expect(systemdArgs).toContain('--expand-environment=no')
- expect(systemdArgs).not.toContain('--wait')
- expect(systemdArgs).not.toContain('literal $VALUE')
- })
- it('uses the authoritative SIGKILL scope signal when the runner cannot report after force kill', async () => {
- let wrapper: ReturnType<typeof spawn> | undefined
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- const command = args[separator + 1] as string
- wrapper = spawn(command, args.slice(separator + 2), { ...options, detached: true })
- return wrapper
- }) as unknown as typeof spawn
- const runSync = vi.fn((command: string, args: readonly string[]) => {
- if (command === 'systemctl' && args[1] === 'kill' && wrapper?.pid !== undefined) {
- process.kill(-wrapper.pid, 'SIGKILL')
- }
- if (command === 'systemctl' && args[1] === 'show') {
- const active = wrapper?.exitCode === null && wrapper.signalCode === null
- return { status: 0, stdout: active ? 'active\n' : 'inactive\n', stderr: '', error: undefined }
- }
- return { status: 0, stdout: '', stderr: '', error: undefined }
- }) as unknown as typeof spawnSync
- const launch = launchLinuxScope(spec([process.execPath, '-e', 'setInterval(() => {}, 1000)']), {
- spawn: run,
- spawnSync: runSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- launch.owner.signal('SIGKILL')
- await expect(launch.direct).resolves.toEqual({ exitCode: null, signal: 'SIGKILL' })
- await expect(launch.owner.waitForExit()).resolves.toBe(true)
- })
- it('rejects wait when the selected native owner becomes unreadable', async () => {
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- return spawn(args[separator + 1] as string, args.slice(separator + 2), options)
- }) as unknown as typeof spawn
- const runSync = vi.fn(() => ({
- status: 1,
- stdout: '',
- stderr: 'Failed to connect to bus',
- error: undefined,
- })) as unknown as typeof spawnSync
- const launch = launchLinuxScope(spec([process.execPath, '-e', 'process.exit(0)']), {
- spawn: run,
- spawnSync: runSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(launch.direct).resolves.toEqual({ exitCode: 0, signal: null })
- await expect(launch.owner.waitForExit()).rejects.toThrow('Failed to connect to bus')
- })
- it('propagates systemctl execution failures and unknown active states', async () => {
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- return spawn(args[separator + 1] as string, args.slice(separator + 2), options)
- }) as unknown as typeof spawn
- const failure = new Error('systemctl execution failed')
- const failedRead = launchLinuxScope(spec([process.execPath, '-e', 'process.exit(0)']), {
- spawn: run,
- spawnSync: vi.fn(() => ({ error: failure })) as unknown as typeof spawnSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(failedRead.owner.waitForExit()).rejects.toBe(failure)
- await expect(failedRead.direct).resolves.toEqual({ exitCode: 0, signal: null })
- const unknownState = launchLinuxScope(spec([process.execPath, '-e', 'process.exit(0)']), {
- spawn: run,
- spawnSync: vi.fn(() => ({ status: 0, stdout: 'reloading\n', stderr: '', error: undefined })) as unknown as typeof spawnSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(unknownState.owner.waitForExit()).rejects.toThrow('unknown ActiveState')
- await expect(unknownState.direct).resolves.toEqual({ exitCode: 0, signal: null })
- const blankFailure = launchLinuxScope(spec([process.execPath, '-e', 'process.exit(0)']), {
- spawn: run,
- spawnSync: vi.fn(() => ({ status: 1, stdout: '', stderr: '', error: undefined })) as unknown as typeof spawnSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(blankFailure.owner.waitForExit()).rejects.toThrow('exit 1')
- await expect(blankFailure.direct).resolves.toEqual({ exitCode: 0, signal: null })
- })
- it.each(['activating', 'deactivating', 'failed'])(
- 'recognizes the %s scope state',
- async (initialState) => {
- let wrapper: ReturnType<typeof spawn> | undefined
- let reads = 0
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- wrapper = spawn(args[separator + 1] as string, args.slice(separator + 2), options)
- return wrapper
- }) as unknown as typeof spawn
- const runSync = vi.fn(() => {
- reads += 1
- return {
- status: 0,
- stdout: reads === 1 ? `${initialState}\n` : 'inactive\n',
- stderr: '',
- error: undefined,
- }
- }) as unknown as typeof spawnSync
- const launch = launchLinuxScope(spec([process.execPath, '-e', 'process.exit(0)']), {
- spawn: run,
- spawnSync: runSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(launch.owner.waitForExit()).resolves.toBe(true)
- await expect(launch.direct).resolves.toEqual({ exitCode: 0, signal: null })
- },
- )
- it('uses runner liveness when systemd has already forgotten the scope', async () => {
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- return spawn(args[separator + 1] as string, args.slice(separator + 2), options)
- }) as unknown as typeof spawn
- const runSyncMock = vi.fn(() => ({
- status: 1,
- stdout: '',
- stderr: 'Unit could not be found',
- error: undefined,
- }))
- const runSync = runSyncMock as unknown as typeof spawnSync
- const launch = launchLinuxScope(spec([process.execPath, '-e', 'setTimeout(() => {}, 40)']), {
- spawn: run,
- spawnSync: runSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- await expect(launch.owner.waitForExit()).resolves.toBe(true)
- await expect(launch.direct).resolves.toEqual({ exitCode: 0, signal: null })
- expect(runSyncMock.mock.calls.length).toBeGreaterThan(1)
- })
- it('does not fabricate a direct outcome after a non-forced scope signal', async () => {
- let wrapper: ReturnType<typeof spawn> | undefined
- const run = vi.fn((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- wrapper = spawn(args[separator + 1] as string, args.slice(separator + 2), { ...options, detached: true })
- return wrapper
- }) as unknown as typeof spawn
- const runSync = vi.fn((command: string, args: readonly string[]) => {
- if (command === 'systemctl' && args[1] === 'kill' && wrapper?.pid !== undefined) {
- process.kill(-wrapper.pid, 'SIGKILL')
- }
- if (command === 'systemctl' && args[1] === 'show') {
- const active = wrapper?.exitCode === null && wrapper.signalCode === null
- return { status: 0, stdout: active ? 'active\n' : 'inactive\n', stderr: '', error: undefined }
- }
- return { status: 0, stdout: '', stderr: '', error: undefined }
- }) as unknown as typeof spawnSync
- const launch = launchLinuxScope(spec([process.execPath, '-e', 'setInterval(() => {}, 1000)']), {
- spawn: run,
- spawnSync: runSync,
- runnerInvocation: spawnRunnerInvocation(),
- })
- launch.owner.signal('SIGTERM')
- await expect(launch.direct).rejects.toThrow('exited without a direct-command result')
- await expect(launch.owner.waitForExit()).resolves.toBe(true)
- })
- it('uses the production command defaults when no Linux internals are supplied', async () => {
- let wrapper: ReturnType<typeof spawn> | undefined
- const run = vi.fn()
- const runSync = vi.fn()
- vi.resetModules()
- vi.doMock('node:child_process', async (importOriginal) => {
- const actual = await importOriginal<typeof import('node:child_process')>()
- run.mockImplementation((_command: string, args: readonly string[], options: Parameters<typeof spawn>[2]) => {
- const separator = args.indexOf('--')
- wrapper = actual.spawn(args[separator + 1] as string, args.slice(separator + 2), options)
- return wrapper
- })
- runSync.mockImplementation((command: string, args: readonly string[]) => {
- if (command === 'systemctl' && args[1] === 'show') {
- const active = wrapper?.exitCode === null && wrapper.signalCode === null
- return { status: 0, stdout: active ? 'active\n' : 'inactive\n', stderr: '', error: undefined }
- }
- return { status: 0, stdout: '', stderr: '', error: undefined }
- })
- return { ...actual, spawn: run, spawnSync: runSync }
- })
- try {
- const defaults = await import('../src/linux-scope.ts')
- expect(defaults.probeLinuxScope()).toBe(true)
- const launch = defaults.launchLinuxScope(spec([process.execPath, '-e', 'process.exit(0)']))
- await expect(launch.direct).resolves.toEqual({ exitCode: 0, signal: null })
- await expect(launch.owner.waitForExit()).resolves.toBe(true)
- expect(run).toHaveBeenCalledWith('systemd-run', expect.any(Array), expect.any(Object))
- expect(runSync).toHaveBeenCalledWith('systemctl', expect.any(Array), expect.any(Object))
- } finally {
- vi.doUnmock('node:child_process')
- vi.resetModules()
- }
- })
- })
|