Merge origin/master into xtr/agent-setup-hook
This commit is contained in:
commit
2f7a02022a
243 files changed
+3931
-2186
No files matched your search
@@ -13,7 +13,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSyn
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { afterAll, afterEach, describe, expect, it, onTestFinished } from 'vitest'
|
||||
import { afterAll, afterEach, describe, expect, it } from 'vitest'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import { PwshLocalExecutor, ENCODING_PREAMBLE, candidatePwshPaths, resolvePwshPath } from '@deepseek-ai/dsh-pwsh-local'
|
||||
import LocalSubprocessRuntime from '@deepseek-ai/dsh-subprocess-local'
|
||||
@@ -40,6 +40,24 @@ afterEach(async () => {
|
||||
if (failures.length > 0) throw new AggregateError(failures, 'PowerShell fixture cleanup failed')
|
||||
})
|
||||
|
||||
function createContext(): Context {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
return ctx
|
||||
}
|
||||
|
||||
/** A private file barrier keeps the command alive until the test releases it. */
|
||||
function commandBarrier() {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-barrier-'))
|
||||
tempDirs.push(dir)
|
||||
const path = join(dir, 'release')
|
||||
return {
|
||||
command: 'while (-not (Test-Path -LiteralPath $env:DSH_TEST_RELEASE)) { Start-Sleep -Milliseconds 20 }',
|
||||
env: { DSH_TEST_RELEASE: path },
|
||||
release: () => { writeFileSync(path, '') },
|
||||
}
|
||||
}
|
||||
|
||||
// The probe follows the executor's own resolution (Program Files installs on
|
||||
// Windows are found even when bare `pwsh` is not on PATH).
|
||||
const hasPwsh = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0
|
||||
@@ -56,8 +74,7 @@ function samePath(actual: string, expected: string): boolean {
|
||||
}
|
||||
|
||||
async function setup(config: ConstructorParameters<typeof PwshLocalExecutor>[1] = {}) {
|
||||
const ctx = new Context()
|
||||
contexts.push(ctx)
|
||||
const ctx = createContext()
|
||||
await ctx.plugin(LocalSubprocessRuntime)
|
||||
;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir }
|
||||
// A short kill grace via the REAL config path, so escalation tests stay fast.
|
||||
@@ -67,9 +84,8 @@ async function setup(config: ConstructorParameters<typeof PwshLocalExecutor>[1]
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll a handle's consuming readOutput until the ACCUMULATED delta contains
|
||||
* `expected`; returns the accumulation (reads never re-deliver, so the caller
|
||||
* gets everything produced up to the match).
|
||||
* Accumulate consuming reads until the marker arrives, using the current test's
|
||||
* budget. Callers keep the child at a barrier when later output must remain unread.
|
||||
*/
|
||||
async function readUntil(proc: ShellProcess, expected: string, timeoutMs: number): Promise<string> {
|
||||
let all = ''
|
||||
@@ -202,7 +218,7 @@ describe('spawn construction (pure, every platform)', () => {
|
||||
}
|
||||
|
||||
it('runs every command as ONE argv element under the UTF-8 encoding preamble', async () => {
|
||||
const ctx = new Context()
|
||||
const ctx = createContext()
|
||||
const subprocess = new CapturingSubprocessRuntime(ctx)
|
||||
await ctx.plugin(PwshLocalExecutor)
|
||||
await ctx.shell.run(ctx.shell.resolve({ command: 'Write-Output 你好' }))
|
||||
@@ -215,7 +231,7 @@ describe('spawn construction (pure, every platform)', () => {
|
||||
})
|
||||
|
||||
it('reports both unread stderr and an asynchronous provider rejection exactly once', async () => {
|
||||
const ctx = new Context()
|
||||
const ctx = createContext()
|
||||
const subprocess = new CapturingSubprocessRuntime(ctx)
|
||||
await ctx.plugin(PwshLocalExecutor)
|
||||
subprocess.stderrText = 'target stderr'
|
||||
@@ -232,7 +248,7 @@ describe('spawn construction (pure, every platform)', () => {
|
||||
})
|
||||
|
||||
it('settles an unprintable provider rejection instead of rejecting done', async () => {
|
||||
const ctx = new Context()
|
||||
const ctx = createContext()
|
||||
const subprocess = new CapturingSubprocessRuntime(ctx)
|
||||
await ctx.plugin(PwshLocalExecutor)
|
||||
const providerError = new Error('unprintable provider error')
|
||||
@@ -249,7 +265,7 @@ describe('spawn construction (pure, every platform)', () => {
|
||||
})
|
||||
|
||||
it('preserves an explicit kill stamp and maps an aborted direct outcome to killed', async () => {
|
||||
const ctx = new Context()
|
||||
const ctx = createContext()
|
||||
const subprocess = new CapturingSubprocessRuntime(ctx)
|
||||
await ctx.plugin(PwshLocalExecutor)
|
||||
|
||||
@@ -406,20 +422,25 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.run', () => {
|
||||
})
|
||||
|
||||
describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)', () => {
|
||||
it('start returns immediately with a running handle that settles as completed', async () => {
|
||||
it('start returns immediately with a running handle that settles as completed', async ({ task }) => {
|
||||
const { bash } = await setup()
|
||||
const before = Date.now()
|
||||
// The sleep outlasts any realistic spawn latency, so returning while the
|
||||
// child still sleeps proves start() does not wait for completion.
|
||||
const proc = bash.start(bash.resolve({ command: 'Start-Sleep -Milliseconds 2000; Write-Output done' }))
|
||||
expect(Date.now() - before).toBeLessThan(1000)
|
||||
const barrier = commandBarrier()
|
||||
const proc = bash.start(bash.resolve({
|
||||
command: `Write-Output ready; [Console]::Out.Flush(); ${barrier.command}; Write-Output done`,
|
||||
env: barrier.env,
|
||||
}))
|
||||
expect(proc.status).toBe('running')
|
||||
expect(await readUntil(proc, 'ready\n', task.timeout)).toBe('ready\n')
|
||||
expect(proc.status).toBe('running')
|
||||
barrier.release()
|
||||
await proc.done
|
||||
expect(proc.status).toBe('completed')
|
||||
expect(proc.signal).toBeNull()
|
||||
expect(proc.exitCode).toBe(0)
|
||||
expect(lf(proc.readOutput().delta)).toBe('done\n')
|
||||
})
|
||||
|
||||
it('threads stdin and extra env into a background process', async ({ task }) => {
|
||||
it('threads stdin and extra env into a background process', async () => {
|
||||
const { bash } = await setup()
|
||||
const proc = bash.start(bash.resolve({
|
||||
command: '$s = ([Console]::In.ReadToEnd()).TrimEnd(); Write-Output $s; Write-Output "[$env:BG_VAR][$env:DSH_BG_VAR]"',
|
||||
@@ -427,32 +448,25 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)'
|
||||
env: { BG_VAR: 'bg-env' },
|
||||
dshEnv: { DSH_BG_VAR: 'bg-dsh-env' },
|
||||
}))
|
||||
const partialOutput = await readUntil(proc, '[bg-env][bg-dsh-env]', task.timeout)
|
||||
await proc.done
|
||||
expect(proc.status).toBe('completed')
|
||||
const output = partialOutput + lf(proc.readOutput().delta)
|
||||
expect(output).toBe('bg-stdin\n[bg-env][bg-dsh-env]\n')
|
||||
expect(proc.signal).toBeNull()
|
||||
expect(proc.exitCode).toBe(0)
|
||||
expect(lf(proc.readOutput().delta)).toBe('bg-stdin\n[bg-env][bg-dsh-env]\n')
|
||||
})
|
||||
|
||||
it('readOutput is consuming: increments are never re-delivered, and reads stay valid after exit', async ({ task }) => {
|
||||
const { ctx, bash } = await setup()
|
||||
const dir = mkdtempSync(join(tmpdir(), 'dsh-pwsh-read-'))
|
||||
onTestFinished(async () => {
|
||||
await ctx.fiber.dispose()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
const releasePath = join(dir, 'release')
|
||||
// The second write cannot race the host's first consuming read.
|
||||
const { bash } = await setup()
|
||||
const barrier = commandBarrier()
|
||||
const proc = bash.start(bash.resolve({
|
||||
command: 'Write-Output first; [Console]::Out.Flush(); while (-not [System.IO.File]::Exists($env:DSH_PWSH_RELEASE)) { Start-Sleep -Milliseconds 20 }; Write-Output second',
|
||||
env: { DSH_PWSH_RELEASE: releasePath },
|
||||
command: `Write-Output first; [Console]::Out.Flush(); ${barrier.command}; Write-Output second`,
|
||||
env: barrier.env,
|
||||
}))
|
||||
const first = await readUntil(proc, 'first\n', task.timeout)
|
||||
expect(lf(first)).toBe('first\n')
|
||||
expect(first).toBe('first\n')
|
||||
expect(proc.status).toBe('running')
|
||||
expect(proc.readOutput().delta).toBe('')
|
||||
writeFileSync(releasePath, '')
|
||||
barrier.release()
|
||||
await proc.done
|
||||
expect(proc.status).toBe('completed')
|
||||
expect(proc.exitCode).toBe(0)
|
||||
@@ -552,7 +566,7 @@ describe.skipIf(!hasPwsh)('PwshLocalExecutor.start (background process handles)'
|
||||
|
||||
describe.skipIf(!hasPwsh)('process lifecycle ownership (the subprocess service, not the executor)', () => {
|
||||
it('a background process survives executor-fiber disposal and dies with the subprocess service', async ({ task }) => {
|
||||
const ctx = new Context()
|
||||
const ctx = createContext()
|
||||
const managerFiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir }
|
||||
const executorFiber = await ctx.plugin(PwshLocalExecutor, { graceMs: 200 })
|
||||
@@ -584,7 +598,7 @@ describe.skipIf(!hasPwsh)('process lifecycle ownership (the subprocess service,
|
||||
})
|
||||
|
||||
it('service disposal settles running handles and leaves settled ones untouched', async () => {
|
||||
const ctx = new Context()
|
||||
const ctx = createContext()
|
||||
const managerFiber = await ctx.plugin(LocalSubprocessRuntime)
|
||||
;(ctx.subprocess as LocalSubprocessRuntime).internals = { spillDir }
|
||||
await ctx.plugin(PwshLocalExecutor, { graceMs: 200 })
|
||||
|
||||
Reference in new issue
Block a user