ソースを参照

fix(subprocess): preserve post-commit runner errors

pku-xht 1 ヶ月 前
親
コミット
14c16012fb

+ 14 - 3
packages/subprocess/subprocess-local/src/windows-job.ts

@@ -11,6 +11,7 @@ import {
 } from '@deepseek-ai/dsh-win32-process'
 import type { BoundProcessOwner, ManagedProcessLaunch } from './managed-owner.ts'
 import {
+  type SerializedRunnerError,
   type WindowsRunnerResult,
   deserializeRunnerError,
   parseWindowsRunnerResult,
@@ -39,6 +40,14 @@ type RunnerProcess = Omit<ReturnType<typeof spawn>, 'send' | 'stdio'> & {
   stdio: Array<Readable | Writable | null>
 }
 
+function isWindowsStartCancellationError(error: SerializedRunnerError): boolean {
+  return error.name === 'Error'
+    && error.message === 'subprocess target start was cancelled'
+    && error.code === undefined
+    && error.syscall === undefined
+    && error.path === undefined
+}
+
 /**
  * Re-check the runner entry, bindings, and current Job capability for every spawn.
  * @param internals - optional runner and Win32 capability seams used by tests.
@@ -90,8 +99,10 @@ class WindowsJobOwner implements BoundProcessOwner {
     }
   }
 
-  mapStartFailure(failure: unknown): unknown {
-    return this.cancellationReasonSet ? this.cancellationReason : failure
+  mapStartFailure(failure: unknown, serialized: SerializedRunnerError): unknown {
+    return this.cancellationReasonSet && isWindowsStartCancellationError(serialized)
+      ? this.cancellationReason
+      : failure
   }
 
   async waitForExit(): Promise<void> {
@@ -168,7 +179,7 @@ export function launchWindowsJob(
     if (result.type === 'target-exit') {
       direct.resolve({ exitCode: result.exitCode, signal: null })
     } else {
-      direct.reject(owner.mapStartFailure(deserializeRunnerError(result.error)))
+      direct.reject(owner.mapStartFailure(deserializeRunnerError(result.error), result.error))
     }
   })
   child.once('spawn', () => {

+ 26 - 0
packages/subprocess/subprocess-local/tests/windows-job.spec.ts

@@ -266,6 +266,32 @@ describe('Windows parent runner contract', () => {
     await expect(implicit.result.owner.waitForExit()).resolves.toBeUndefined()
   })
 
+  it('preserves a strict provider error after a termination request', async () => {
+    const spawned = launch()
+    const localReason = new Error('caller aborted after target commit')
+    spawned.result.owner.signal('SIGTERM', localReason)
+    spawned.child.emit('message', {
+      type: 'error',
+      error: {
+        name: 'Error',
+        message: 'poll failed',
+        code: 'EIO',
+        syscall: 'QueryInformationJobObject',
+      },
+    })
+
+    const failure = await spawned.result.direct.catch((error: unknown) => error)
+    expect(failure).not.toBe(localReason)
+    expect(failure).toMatchObject({
+      message: 'poll failed',
+      code: 'EIO',
+      syscall: 'QueryInformationJobObject',
+    })
+    spawned.child.connected = false
+    spawned.child.emit('close', 127, null)
+    await expect(spawned.result.owner.waitForExit()).rejects.toThrow('exit code 127')
+  })
+
   it('rejects direct and wait for runner error or abnormal runner exit', async () => {
     const failed = launch()
     failed.child.emit('message', {