Kaynağa Gözat

test(subprocess): cover source-only control launches and refresh generated references

Tianyi Cui 6 gün önce
ebeveyn
işleme
96e12ddaf4

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
 #   pnpm run verify-translation-pairing --write docs/module-graph.md
-module-graph.md: d2f7fd72e9026a7f7eb248a1c4ae234125f3571a
-module-graph.zh.md: cc85a23a7866411aa69edf47a1b437f05270a930
+module-graph.md: 1fe166cfbe4a5c8a8d214cf5cd8438cbff32e5f7
+module-graph.zh.md: a2adf21e35734e8cd28a325cd3398c3638c963b0

+ 2 - 1
docs/module-graph.md

@@ -423,6 +423,7 @@ flowchart TD
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_launch_environment
   pkg_credentials_local --> pkg_launch_environment
+  pkg_sandbox_windows_acl --> pkg_subprocess
   pkg_subprocess_local --> pkg_subprocess
   pkg_subprocess_local --> pkg_subprocess
   pkg_subprocess_local --> pkg_timeout
   pkg_subprocess_local --> pkg_timeout
   pkg_skill_badge --> pkg_skill
   pkg_skill_badge --> pkg_skill
@@ -1262,7 +1263,6 @@ flowchart TD
 | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — |
 | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — |
 | [`host-webserver`](../packages/host/webserver) | `host` | — |
 | [`host-webserver`](../packages/host/webserver) | `host` | — |
 | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — |
 | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — |
-| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | — |
 | [`session-format`](../packages/session/session-format) | `session` | — |
 | [`session-format`](../packages/session/session-format) | `session` | — |
 | [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — |
 | [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — |
 | [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — |
 | [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — |
@@ -1301,6 +1301,7 @@ flowchart TD
 | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) |
 | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
+| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`subprocess`](../packages/subprocess/subprocess) |
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |

+ 2 - 1
docs/module-graph.zh.md

@@ -425,6 +425,7 @@ flowchart TD
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_credentials
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_home_paths
   pkg_credentials_local --> pkg_launch_environment
   pkg_credentials_local --> pkg_launch_environment
+  pkg_sandbox_windows_acl --> pkg_subprocess
   pkg_subprocess_local --> pkg_subprocess
   pkg_subprocess_local --> pkg_subprocess
   pkg_subprocess_local --> pkg_timeout
   pkg_subprocess_local --> pkg_timeout
   pkg_skill_badge --> pkg_skill
   pkg_skill_badge --> pkg_skill
@@ -1264,7 +1265,6 @@ flowchart TD
 | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — |
 | [`host-open-in-app`](../packages/host/open-in-app) | `host` | — |
 | [`host-webserver`](../packages/host/webserver) | `host` | — |
 | [`host-webserver`](../packages/host/webserver) | `host` | — |
 | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — |
 | [`invariants`](../packages/runtime-diagnostics/invariants) | `runtime-diagnostics` | — |
-| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | — |
 | [`session-format`](../packages/session/session-format) | `session` | — |
 | [`session-format`](../packages/session/session-format) | `session` | — |
 | [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — |
 | [`session-format-v0-to-v1`](../packages/session/session-format-v0-to-v1) | `session` | — |
 | [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — |
 | [`session-format-v1-to-v2`](../packages/session/session-format-v1-to-v2) | `session` | — |
@@ -1303,6 +1303,7 @@ flowchart TD
 | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) |
 | [`client-file-upload`](../packages/client/file-upload) | `client` | [`scope`](../packages/core/scope) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`authorization`](../packages/credentials/authorization) | `credentials` | [`credentials`](../packages/credentials/credentials), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
 | [`credentials-local`](../packages/credentials/credentials-local) | `credentials` | [`atomic-write`](../packages/util/atomic-write), [`credentials`](../packages/credentials/credentials), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment) |
+| [`sandbox-windows-acl`](../packages/sandbox/sandbox-windows-acl) | `sandbox` | [`subprocess`](../packages/subprocess/subprocess) |
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`subprocess-local`](../packages/subprocess/subprocess-local) | `subprocess` | [`subprocess`](../packages/subprocess/subprocess), [`timeout`](../packages/util/timeout) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`skill-badge`](../packages/skill/skill-badge) | `skill` | [`skill`](../packages/skill/skill) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`spill`](../packages/spill/spill) | `spill` | [`brand`](../packages/util/brand), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |

+ 1 - 1
packages/sandbox/sandbox-windows-acl/tests/control.spec.ts

@@ -23,7 +23,7 @@ describe.skipIf(process.platform !== 'win32')('managed Windows ACL control pipe'
     ctx = new Context()
     ctx = new Context()
     await ctx.plugin(LocalSubprocessRuntime)
     await ctx.plugin(LocalSubprocessRuntime)
     const runner = fileURLToPath(new URL('../src/runner.ts', import.meta.url))
     const runner = fileURLToPath(new URL('../src/runner.ts', import.meta.url))
-    const helper = import.meta.resolve('@deepseek-ai/dsh-subprocess/control')
+    const helper = import.meta.resolve('@deepseek-ai/dsh-subprocess/src/control.ts')
     const program = `
     const program = `
       const { openInheritedControlChannel } = await import(process.argv[1]);
       const { openInheritedControlChannel } = await import(process.argv[1]);
       const { writeFileSync } = await import('node:fs');
       const { writeFileSync } = await import('node:fs');

+ 17 - 0
packages/sandbox/sandbox-windows-acl/tests/index-failure-paths.spec.ts

@@ -358,6 +358,23 @@ describe('AclSandbox init', () => {
 })
 })
 
 
 describe('AclSandbox spawn', () => {
 describe('AclSandbox spawn', () => {
+  it('refuses control with piped stdio before starting a restricted process', async () => {
+    const sandbox = new AclSandbox({ writableDirs: [], tempDir: null, mode: 'read-only' })
+    await sandbox.init()
+    expect(() => sandbox.spawn({ command: 'probe.exe', controlFileDescriptor: 7 })).toThrow('control pipe requires inherited stdio')
+    sandbox.dispose()
+  })
+
+  it('forwards the inherited control pipe to the restricted child', async () => {
+    const { api } = state.stubs as HappyStubs
+    Object.assign(api, { uvGetOsfhandle: vi.fn(() => 107n), getFileType: vi.fn(() => 3) })
+    const sandbox = new AclSandbox({ writableDirs: [], tempDir: null, mode: 'read-only' })
+    await sandbox.init()
+    const child = sandbox.spawn({ command: 'probe.exe', stdio: 'inherit', controlFileDescriptor: 7 })
+    await expect(child.wait()).resolves.toEqual({ stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), exitCode: 42 })
+    sandbox.dispose()
+  })
+
   it('refuses to spawn before init', () => {
   it('refuses to spawn before init', () => {
     const workspace = scratch()
     const workspace = scratch()
     const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-11', mode: 'workspace-write' })
     const sandbox = new AclSandbox({ writableDirs: [workspace], tempDir: null, writeSid: 'S-1-4-9000-11', mode: 'workspace-write' })

+ 8 - 4
packages/subprocess/subprocess-local/tests/control.spec.ts

@@ -5,9 +5,10 @@ import { fileURLToPath } from 'node:url'
 import { once } from 'node:events'
 import { once } from 'node:events'
 import { afterEach, describe, expect, it } from 'vitest'
 import { afterEach, describe, expect, it } from 'vitest'
 import { Context } from '@deepseek-ai/cordis'
 import { Context } from '@deepseek-ai/cordis'
-import type { SubprocessHandle } from '@deepseek-ai/dsh-subprocess'
+import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
 import { SUBPROCESS_CONTROL_ENV } from '@deepseek-ai/dsh-subprocess/control'
 import { SUBPROCESS_CONTROL_ENV } from '@deepseek-ai/dsh-subprocess/control'
 import { LocalSubprocessRuntime } from '../src/index.ts'
 import { LocalSubprocessRuntime } from '../src/index.ts'
+import { spawnSubprocess } from '../src/spawn.ts'
 
 
 const fixture = fileURLToPath(new URL('./fixtures/control-child.ts', import.meta.url))
 const fixture = fileURLToPath(new URL('./fixtures/control-child.ts', import.meta.url))
 const helper = fileURLToPath(new URL('../../subprocess/src/control.ts', import.meta.url))
 const helper = fileURLToPath(new URL('../../subprocess/src/control.ts', import.meta.url))
@@ -16,6 +17,8 @@ let root: string | undefined
 let handle: SubprocessHandle | undefined
 let handle: SubprocessHandle | undefined
 
 
 afterEach(async () => {
 afterEach(async () => {
+  handle?.terminate()
+  await handle?.waitForExit()
   handle?.control?.destroy()
   handle?.control?.destroy()
   await ctx?.fiber.dispose()
   await ctx?.fiber.dispose()
   if (root !== undefined) await rm(root, { recursive: true, force: true })
   if (root !== undefined) await rm(root, { recursive: true, force: true })
@@ -58,18 +61,19 @@ describe('managed subprocess control pipe', () => {
     expect(await handle.waitForExit()).toBe(true)
     expect(await handle.waitForExit()).toBe(true)
   })
   })
 
 
-  it('returns exact binary control bytes independently of stdout and stderr', async () => {
+  it.each(['managed', 'fallback'] as const)('returns exact binary control bytes through %s independently of stdio', async (backend) => {
     root = await mkdtemp(join(tmpdir(), 'dsh-control-'))
     root = await mkdtemp(join(tmpdir(), 'dsh-control-'))
     ctx = new Context()
     ctx = new Context()
     await ctx.plugin(LocalSubprocessRuntime)
     await ctx.plugin(LocalSubprocessRuntime)
     const input = Buffer.alloc(256 * 1024)
     const input = Buffer.alloc(256 * 1024)
     for (let index = 0; index < input.length; index++) input[index] = index % 256
     for (let index = 0; index < input.length; index++) input[index] = index % 256
-    handle = ctx.subprocess.spawn({
+    const request: SubprocessSpawnSpec = {
       argv: [process.execPath, fixture, helper, String(input.length)],
       argv: [process.execPath, fixture, helper, String(input.length)],
       cwd: root,
       cwd: root,
       stdio: { stdin: 'ignore', stdout: { maxBytes: 1024 }, stderr: { maxBytes: 1024 }, control: 'pipe' },
       stdio: { stdin: 'ignore', stdout: { maxBytes: 1024 }, stderr: { maxBytes: 1024 }, control: 'pipe' },
       graceMs: 1000,
       graceMs: 1000,
-    })
+    }
+    handle = backend === 'managed' ? ctx.subprocess.spawn(request) : spawnSubprocess(request)
     const channel = handle.control
     const channel = handle.control
     if (channel === undefined) throw new Error('requested control pipe is absent')
     if (channel === undefined) throw new Error('requested control pipe is absent')
     const received = (async () => {
     const received = (async () => {

+ 4 - 4
packages/subprocess/subprocess-local/tests/spawn-runner.spec.ts

@@ -543,8 +543,8 @@ describe('Linux one-shot exec bootstrap', () => {
     })
     })
   })
   })
 
 
-  it('retries ENOEXEC through /bin/sh with the resolved file and original arguments', async () => {
-    const files = track(createLinuxLaunchFiles({ cwd: '/work', env: { PATH: 'bin' } }))
+  it.each([undefined, 'pipe'] as const)('retries ENOEXEC through /bin/sh with control %s', async (control) => {
+    const files = track(createLinuxLaunchFiles({ cwd: '/work', env: { PATH: 'bin' }, ...control === undefined ? {} : { control } }))
     const execve = vi.fn()
     const execve = vi.fn()
       .mockImplementationOnce(() => { throw Object.assign(new Error('exec format'), { code: 'ENOEXEC' }) })
       .mockImplementationOnce(() => { throw Object.assign(new Error('exec format'), { code: 'ENOEXEC' }) })
       .mockImplementationOnce(() => { throw Object.assign(new Error('shell failed'), { code: 'EIO' }) })
       .mockImplementationOnce(() => { throw Object.assign(new Error('shell failed'), { code: 'EIO' }) })
@@ -555,8 +555,8 @@ describe('Linux one-shot exec bootstrap', () => {
       internals({ execve: execve as never }),
       internals({ execve: execve as never }),
     )
     )
     expect(execve.mock.calls).toEqual([
     expect(execve.mock.calls).toEqual([
-      ['/work/bin/tool', ['tool', 'literal arg'], { PATH: 'bin' }],
-      ['/bin/sh', ['/bin/sh', '/work/bin/tool', 'literal arg'], { PATH: 'bin' }],
+      ['/work/bin/tool', ['tool', 'literal arg'], { PATH: 'bin' }, ...control === undefined ? [] : [control]],
+      ['/bin/sh', ['/bin/sh', '/work/bin/tool', 'literal arg'], { PATH: 'bin' }, ...control === undefined ? [] : [control]],
     ])
     ])
     expect(readLinuxStartupError(files.startupErrorPath)).toMatchObject({
     expect(readLinuxStartupError(files.startupErrorPath)).toMatchObject({
       type: 'error', error: { code: 'EIO', path: 'tool' },
       type: 'error', error: { code: 'EIO', path: 'tool' },

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

@@ -138,6 +138,20 @@ describe('Windows Job capability', () => {
 })
 })
 
 
 describe('Windows parent runner contract', () => {
 describe('Windows parent runner contract', () => {
+  it('keeps the requested control endpoint separate from runner IPC and ordinary output', () => {
+    const child = new FakeChild()
+    const control = new PassThrough()
+    child.stdio.push(control)
+    const request = { ...spec, stdio: { ...spec.stdio, control: 'pipe' as const } }
+    const { result, spawn } = launch(child, request)
+    expect(result.control).toBe(control)
+    expect(child.sent).toEqual([{ type: 'start', cwd: 'C:\\target', env: { TARGET: 'yes' }, control: 'pipe' }])
+    expect(spawn).toHaveBeenCalledWith('C:\\node.exe', expect.any(Array), expect.objectContaining({
+      stdio: ['ignore', 'ignore', 'ignore', 'ipc', 'pipe', 'pipe', 2, 'overlapped'],
+    }))
+    control.destroy()
+  })
+
   it('isolates runner stdio, carries target stdio on fd 4 through fd 6, and sends cwd/env', () => {
   it('isolates runner stdio, carries target stdio on fd 4 through fd 6, and sends cwd/env', () => {
     const { child, result, spawn } = launch()
     const { child, result, spawn } = launch()
     expect(spawn).toHaveBeenCalledWith('C:\\node.exe', [
     expect(spawn).toHaveBeenCalledWith('C:\\node.exe', [

+ 10 - 0
packages/subprocess/win32-process/tests/process.spec.ts

@@ -70,6 +70,16 @@ function inheritedApi(overrides: Partial<Win32ProcessBindings> = {}): {
 describe('spawnInheritedJobProcess', () => {
 describe('spawnInheritedJobProcess', () => {
   const token = 70n as NativePtr
   const token = 70n as NativePtr
 
 
+  it('inherits the control carrier into a restricted child before resume', () => {
+    const descriptorHandle = vi.fn(() => 107n as NativePtr)
+    const { api, events } = inheritedApi({ uvGetOsfhandle: descriptorHandle, getFileType: vi.fn(() => 3) })
+    const child = spawnInheritedJobProcess(api, { command: 'node.exe', args: [], cwd: 'C:\\work', token, controlFileDescriptor: 7 })
+    expect(child.pid).toBe(1234)
+    expect(descriptorHandle).toHaveBeenCalledExactlyOnceWith(7)
+    expect(events.filter(event => event === 'inherit')).toHaveLength(4)
+    expect(events.indexOf('assign')).toBeLessThan(events.indexOf('resume'))
+  })
+
   it('creates suspended, assigns the Job, then resumes the restricted child', () => {
   it('creates suspended, assigns the Job, then resumes the restricted child', () => {
     const {
     const {
       api,
       api,

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
snapshots/session/cordis-inspect-jsdoc/session.v3.jsonl


Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor