New process/ capability family: @deepseek-ai/dsh-process owns ctx.processes — abstract ProcessManager.spawn(spec) over a fully-explicit ProcessSpawnSpec — plus the shared DSH_* managed-environment and CollectedOutput vocabulary; @deepseek-ai/dsh-process-local carries the former bash-local run.ts plumbing (detached groups, tail-keep spill-backed output, credential scrub, kill escalation, kill-and-join disposal) with no config of its own. dsh-bash-local becomes a consumer: it keeps command defaulting, the fused deadline timedOut/aborted classification, the model-friendly terminal env (now merged through the ordinary env channel), and the [stderr]-marked background read merge, and spawns through ctx.processes. Background-process lifetime moves to the manager, so an executor reload no longer kills live background work; a background spawn failure is injected once into the read path instead of being buffered as fake stderr. dsh-bash re-exports the moved vocabulary so bash consumers keep one import root; dsh-bash-sandbox only redeclares the inherited inject. Every composition loading a bash executor now loads dsh-process-local (CLI, examples, python bundled runtime, create-sdk bash feature, inline test configs).
56 lines
1.9 KiB
TypeScript
56 lines
1.9 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import { ProcessManager } from '@deepseek-ai/dsh-process'
|
|
import type { ProcessHandle, ProcessOutputRead, ProcessSpawnSpec } from '@deepseek-ai/dsh-process'
|
|
|
|
/**
|
|
* Minimal concrete manager: 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 StubProcessManager extends ProcessManager {
|
|
spawn(spec: ProcessSpawnSpec): ProcessHandle {
|
|
const read: ProcessOutputRead = { text: '', nextOffset: 0, lossy: false }
|
|
let killed = false
|
|
return {
|
|
pid: spec.argv.length,
|
|
stdout: { readFrom: () => read },
|
|
stderr: { readFrom: () => read },
|
|
done: Promise.resolve({
|
|
exitCode: killed ? null : 0,
|
|
signal: null,
|
|
stdout: { text: 'ok', truncated: false },
|
|
stderr: { text: '', truncated: false },
|
|
}),
|
|
kill: () => { killed = true },
|
|
}
|
|
}
|
|
}
|
|
|
|
describe('ProcessManager seam', () => {
|
|
it('a concrete subclass registers as ctx.processes and serves the abstract API', async () => {
|
|
const ctx = new Context()
|
|
await ctx.plugin(StubProcessManager)
|
|
const handle = ctx.processes.spawn({
|
|
argv: ['true'],
|
|
cwd: '/stub',
|
|
stdoutMaxBytes: 1,
|
|
stderrMaxBytes: 1,
|
|
maxSpillBytes: 1,
|
|
graceMs: 1,
|
|
})
|
|
expect(handle.pid).toBe(1)
|
|
expect(handle.stdout.readFrom(0)).toEqual({ text: '', nextOffset: 0, lossy: false })
|
|
handle.kill()
|
|
const outcome = await handle.done
|
|
expect(outcome.stdout.text).toBe('ok')
|
|
})
|
|
|
|
it('loading a second implementation throws (one processes service per context — cordis standard)', async () => {
|
|
const ctx = new Context()
|
|
await ctx.plugin(StubProcessManager)
|
|
class SecondManager extends StubProcessManager {}
|
|
await expect(ctx.plugin(SecondManager)).rejects.toThrow(/service "processes" has been registered/)
|
|
})
|
|
})
|