Files
deepseek-harness/packages/subprocess/subprocess/tests/service.spec.ts
T
Huanqi Cao b2838fb2be fix(pwsh): close the remaining review threads
- scrubbedParentEnv folds case before the DSH_ prefix check (Windows env
  names are case-insensitive; a parent dsh_* entry read back as \*
  in the child) and the service spec pins the lowercase probe.
- The acp.snapshot.ts pwsh probe follows resolvePwshPath() like the package
  suites, so a Windows host with only an install-location pwsh still runs
  the scenario.
- pwsh-tool-turn is re-recorded around [Console]::Out.Write('PWSH_OK'):
  the fixture carries no platform newline, so one recording replays on
  Windows and POSIX alike (record + refresh; replay-verified keyless).
- The pwsh-local Known Limitations bullet drops the self-defeating no-op
  advice: & { } is scoped to param(...), using/#requires scripts run from a
  file (both languages, pairing re-recorded).
- The capability-seams graph moves ctx.bashEnv ownership to bash-env and
  lists pwsh-local/tool-pwsh on the ctx.bash seam (source updated,
  docs regenerated).
- The tool-bash presenter fixture retires the stale 'command aborted'
  literal for the shipped 'tool call aborted' message.
2026-08-02 21:46:53 +08:00

78 lines
3.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import { scrubbedParentEnv, SubprocessService } from '@deepseek-ai/dsh-subprocess'
import type { SubprocessHandle, SubprocessOutputRead, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess'
/**
* Minimal concrete service: a hand-built handle. The seam is spawn-only —
* defaulting, shell semantics, and deadlines belong to callers — so this stub
* is all an implementation owes the abstract class.
*/
class StubSubprocessService extends SubprocessService {
spawn(spec: SubprocessSpawnSpec): SubprocessHandle {
const read: SubprocessOutputRead = { text: '', nextOffset: 0, lossy: false }
const collected = spec.stdio.stdout !== 'pipe' && spec.stdio.stdout !== 'inherit'
? { stdout: { readFrom: () => read } }
: {}
return {
pid: spec.argv.length,
stdin: undefined,
stdout: undefined,
stderr: undefined,
collected,
done: Promise.resolve({ exitCode: 0, signal: null }),
terminate: () => {},
waitForExit: () => Promise.resolve(true),
}
}
}
describe('SubprocessService seam', () => {
it('a concrete subclass registers as ctx.subprocess and serves the abstract API', async () => {
const ctx = new Context()
await ctx.plugin(StubSubprocessService)
const handle = ctx.subprocess.spawn({
argv: ['true'],
cwd: '/stub',
stdio: { stdin: 'ignore', stdout: { maxBytes: 1 }, stderr: 'inherit' },
graceMs: 1,
})
expect(handle.pid).toBe(1)
expect(handle.collected.stdout!.readFrom(0)).toEqual({ text: '', nextOffset: 0, lossy: false })
handle.terminate()
await expect(handle.waitForExit()).resolves.toBe(true)
const outcome = await handle.done
expect(outcome.exitCode).toBe(0)
})
it('loading a second implementation throws (one subprocess service per context — cordis standard)', async () => {
const ctx = new Context()
await ctx.plugin(StubSubprocessService)
class SecondService extends StubSubprocessService {}
await expect(ctx.plugin(SecondService)).rejects.toThrow(/service "subprocess" has been registered/)
})
it('scrubbedParentEnv drops credential-shaped and DSH_ names (case-insensitively) but keeps PATH', () => {
process.env.DSH_SCRUB_PROBE = 'stale'
process.env.dsh_scrub_probe_lower = 'stale'
process.env.SCRUB_PROBE_TOKEN = 'secret'
process.env.SCRUB_PROBE_PASSWORD = 'secret'
process.env.SCRUB_PROBE_PLAIN = 'visible'
try {
const env = scrubbedParentEnv()
expect(env.DSH_SCRUB_PROBE).toBeUndefined()
expect(env.dsh_scrub_probe_lower).toBeUndefined()
expect(env.SCRUB_PROBE_TOKEN).toBeUndefined()
expect(env.SCRUB_PROBE_PASSWORD).toBeUndefined()
expect(env.SCRUB_PROBE_PLAIN).toBe('visible')
expect(env.PATH).toBeDefined()
} finally {
delete process.env.DSH_SCRUB_PROBE
delete process.env.dsh_scrub_probe_lower
delete process.env.SCRUB_PROBE_TOKEN
delete process.env.SCRUB_PROBE_PASSWORD
delete process.env.SCRUB_PROBE_PLAIN
}
})
})