refactor(subprocess): rename the process seam to subprocess and address review
Review feedback (tianyicui): 'process' is a poor service name. The family is now packages/subprocess/ — @deepseek-ai/dsh-subprocess (ctx.subprocess, abstract SubprocessService, Subprocess* vocabulary) and @deepseek-ai/dsh-subprocess-local (LocalSubprocessService) — renamed throughout code, compositions, docs (en+zh, pairs re-recorded), catalogs, and gates. 'subprocess' is the precise term for managed OS children (the Python-stdlib sense), avoids colliding with Node's global process object, and reads as one system beside dsh-subagent-subprocess. ds-review-bot findings addressed: - kill() on a settled handle is now a no-op (no signal to a possibly-reused pgid, no referenced grace timer delaying exit); pinned by a spy test. - The moved DshEnvironmentKey/DshEnvironment/CollectedOutput types get drift-checked type-equiv blocks on the new subprocess.md page, restoring their manifest registration. - subprocess.md is registered in the core.md sub-page index (en+zh).
This commit is contained in:
55
packages/subprocess/subprocess/tests/service.spec.ts
Normal file
55
packages/subprocess/subprocess/tests/service.spec.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { 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 }
|
||||
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('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',
|
||||
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(StubSubprocessService)
|
||||
class SecondManager extends StubSubprocessService {}
|
||||
await expect(ctx.plugin(SecondManager)).rejects.toThrow(/service "subprocess" has been registered/)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user