windows-inspector.spec.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. import { describe, expect, it } from 'vitest'
  2. import {
  3. createWindowsProcessInspector,
  4. isInvalidHandle,
  5. windowsProcessTree,
  6. WindowsProcessInspector,
  7. } from '@deepseek-ai/dsh-subprocess-local/src/windows-inspector.ts'
  8. import type {
  9. NativePtr,
  10. ProcessEntry,
  11. WindowsProcessInspectorInternals,
  12. WindowsProcessState,
  13. } from '@deepseek-ai/dsh-subprocess-local/src/windows-inspector.ts'
  14. function fakeInternals() {
  15. const entries: ProcessEntry[] = []
  16. const states = new Map<number, WindowsProcessState>()
  17. const kills: Array<[number, boolean]> = []
  18. return {
  19. internals: {
  20. snapshot: () => [...entries],
  21. processState: pid => states.get(pid),
  22. taskkill: (pid: number, force: boolean) => { kills.push([pid, force]) },
  23. } satisfies WindowsProcessInspectorInternals,
  24. add(entry: ProcessEntry, started?: string, active = true): void {
  25. entries.push(entry)
  26. if (started !== undefined) states.set(entry.pid, { started, active })
  27. },
  28. kills,
  29. }
  30. }
  31. describe('windowsProcessTree', () => {
  32. it('walks a table children-first with readable identities only', () => {
  33. const started = (pid: number): string | undefined => pid === 12 ? undefined : `t${pid}`
  34. expect(windowsProcessTree([
  35. { pid: 10, parentPid: 0 },
  36. { pid: 11, parentPid: 10 },
  37. { pid: 12, parentPid: 11 },
  38. { pid: 13, parentPid: 11 },
  39. { pid: 14, parentPid: 10 },
  40. ], 10, started)).toEqual([
  41. { pid: 13, started: 't13' },
  42. { pid: 11, started: 't11' },
  43. { pid: 14, started: 't14' },
  44. { pid: 10, started: 't10' },
  45. ])
  46. })
  47. it('returns an empty walk for an absent root', () => {
  48. expect(windowsProcessTree([{ pid: 10, parentPid: 0 }], 99, () => 't')).toEqual([])
  49. })
  50. it('terminates on a parent cycle instead of recursing forever', () => {
  51. const entries = [
  52. { pid: 10, parentPid: 11 },
  53. { pid: 11, parentPid: 10 },
  54. ]
  55. expect(windowsProcessTree(entries, 10, () => 't')).toHaveLength(2)
  56. })
  57. })
  58. describe('WindowsProcessInspector (injected internals)', () => {
  59. it('exposes the shell pid as the pseudo foreground group and never proves stdin waits', () => {
  60. const fake = fakeInternals()
  61. const inspector = new WindowsProcessInspector(fake.internals)
  62. expect(inspector.foregroundPgid(77)).toBe(77)
  63. expect(inspector.isStdinWaiting(77)).toBe(false)
  64. expect(inspector.processSession(77)).toEqual([])
  65. })
  66. it('delegates tree walks and identity checks to the internals', () => {
  67. const fake = fakeInternals()
  68. fake.add({ pid: 10, parentPid: 0 }, 't10')
  69. fake.add({ pid: 11, parentPid: 10 }, 't11')
  70. const inspector = new WindowsProcessInspector(fake.internals)
  71. expect(inspector.processTree(10)).toEqual([
  72. { pid: 11, started: 't11' },
  73. { pid: 10, started: 't10' },
  74. ])
  75. expect(inspector.isAlive({ pid: 11, started: 't11' })).toBe(true)
  76. expect(inspector.isAlive({ pid: 11, started: 'stale' })).toBe(false)
  77. expect(inspector.isAlive({ pid: 99, started: 't99' })).toBe(false)
  78. fake.add({ pid: 12, parentPid: 10 }, 't12', false)
  79. expect(inspector.isAlive({ pid: 12, started: 't12' })).toBe(false)
  80. })
  81. it('maps SIGKILL to a forced taskkill and other signals to the grace form', () => {
  82. const fake = fakeInternals()
  83. const inspector = new WindowsProcessInspector(fake.internals)
  84. inspector.signalGroup(77, 'SIGKILL')
  85. inspector.signalGroup(77, 'SIGTERM')
  86. inspector.signalGroup(0, 'SIGKILL')
  87. expect(fake.kills).toEqual([[77, true], [77, false], [0, true]])
  88. })
  89. it('signals a process only while its start identity matches', () => {
  90. const fake = fakeInternals()
  91. fake.add({ pid: 10, parentPid: 0 }, 't10')
  92. fake.add({ pid: 11, parentPid: 10 }, 't11', false)
  93. const inspector = new WindowsProcessInspector(fake.internals)
  94. inspector.signalProcess({ pid: 10, started: 't10' }, 'SIGKILL')
  95. inspector.signalProcess({ pid: 11, started: 't11' }, 'SIGKILL')
  96. inspector.signalProcess({ pid: 10, started: 'stale' }, 'SIGTERM')
  97. expect(fake.kills).toEqual([[10, true]])
  98. })
  99. it('accepts an injected internals factory through the creator', () => {
  100. const fake = fakeInternals()
  101. expect(createWindowsProcessInspector(fake.internals)).toBeInstanceOf(WindowsProcessInspector)
  102. expect(createWindowsProcessInspector()).toBeInstanceOf(WindowsProcessInspector)
  103. })
  104. })
  105. describe('isInvalidHandle', () => {
  106. it('rejects null, zero, and the all-ones INVALID_HANDLE_VALUE forms', () => {
  107. const ptr = (value: bigint): NativePtr => value as NativePtr
  108. expect(isInvalidHandle(null)).toBe(true)
  109. expect(isInvalidHandle(undefined)).toBe(true)
  110. expect(isInvalidHandle(ptr(0n))).toBe(true)
  111. expect(isInvalidHandle(ptr(0xFFFFFFFFFFFFFFFFn))).toBe(true)
  112. expect(isInvalidHandle(ptr(-1n))).toBe(true)
  113. expect(isInvalidHandle(ptr(1234n))).toBe(false)
  114. })
  115. })
  116. const win32 = process.platform === 'win32' ? describe : describe.skip
  117. win32('WindowsProcessInspector over the real koffi bindings', () => {
  118. it('walks the live process table from the test runner itself', () => {
  119. const inspector = createWindowsProcessInspector()
  120. const tree = inspector.processTree(process.pid)
  121. const self = tree.find(member => member.pid === process.pid)
  122. expect(self).toBeDefined()
  123. expect(inspector.isAlive(self!)).toBe(true)
  124. expect(inspector.foregroundPgid(process.pid)).toBe(process.pid)
  125. })
  126. it('reports unreadable identities for absent processes and no-ops tree signalling', () => {
  127. const inspector = createWindowsProcessInspector()
  128. expect(inspector.isAlive({ pid: 0x7FFFFFFF, started: 'absent' })).toBe(false)
  129. expect(() => { inspector.signalGroup(0x7FFFFFFF, 'SIGKILL') }).not.toThrow()
  130. expect(() => { inspector.signalGroup(0x7FFFFFFF, 'SIGTERM') }).not.toThrow()
  131. expect(() => { inspector.signalGroup(0, 'SIGKILL') }).not.toThrow()
  132. expect(() => { inspector.signalProcess({ pid: 0x7FFFFFFF, started: 'absent' }, 'SIGKILL') }).not.toThrow()
  133. })
  134. })