Merge origin/master into xtr/agent-setup-hook

This commit is contained in:
_Kerman committed 2026-09-09 17:31:17 +08:00
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 })