Files
deepseek-harness/packages/bash/bash/tests/service.spec.ts
imccyu ec601ca13d build(vendor): rescope the vendored Cordis packages into @deepseek-ai
Machine-produced by `pnpm run rescope-vendor --apply` plus the regeneration it
prints: `pnpm install` for the lockfile, `pnpm run gen-third-party-notices`,
`verify-translation-pairing --write` for the touched bilingual pairs,
`gen-doc-graphs`, and one typert snapshot whose ids embed character offsets.
`pnpm run rescope-vendor --check` verifies the result.

Renames nine vendored packages (cordis, cosmokit, schemastery and the six
@cordisjs plugins) and every reference that resolves them: manifest names and
dependency keys, module specifiers including declare-module merges, cordis.yml
plugin names, tsconfig paths, every Markdown fence, and `docs/` prose.
Directory names, upstream versions, and dependency ranges are unchanged, so
vendor/README.md still reads as an upstream snapshot; its manifest table gains
an upstream-name column so THIRD_PARTY_NOTICES keeps MIT attribution pointed
at each fork's origin.

The tutorial tier follows the rename end to end: its yaml fences named plugins
the Loader can no longer resolve, its `ts ignore-check` fences disagreed with
the compiled fences beside them, and its prose quoted both. The contracts that
told readers to keep upstream names — the root convention and the vendoring
cookbook's tree comment and manifest invariant — now say to rescope instead.

Two rules read `@deepseek-ai/` as "another workspace plugin": the client bundle
purity gate now names the vendored libraries a browser bundle inlines, and the
files where a bare `cordis` is an agent-preset id keep that product data.
2026-08-10 22:04:13 +08:00

85 lines
2.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import { BashExecutor } from '@deepseek-ai/dsh-bash'
import type { BashExecRequest, BashExecSpec, BashProcess, BashProcessRead, BashRunResult } from '@deepseek-ai/dsh-bash'
/**
* Minimal concrete executor: canned foreground results, a hand-built process
* handle. The seam is TASK-FREE (start returns a {@link BashProcess} handle;
* task semantics live in `ctx.tasks`), so this stub is all an implementation
* owes the abstract class.
*/
class StubExecutor extends BashExecutor {
resolve(request: BashExecRequest): BashExecSpec {
return {
command: request.command,
workdir: request.workdir ?? '/stub',
timeoutMs: request.timeoutMs ?? 1000,
stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000,
...request.signal ? { signal: request.signal } : {},
sandboxPolicy: request.sandboxPolicy,
}
}
async run(spec: BashExecSpec): Promise<BashRunResult> {
return {
exitCode: 0,
signal: null,
timedOut: false,
aborted: false,
timeoutMs: spec.timeoutMs,
stdout: { text: 'ok', truncated: false },
stderr: { text: '', truncated: false },
}
}
start(): BashProcess {
const proc: BashProcess = {
status: 'running',
exitCode: null,
signal: null,
done: Promise.resolve(),
readOutput: (): BashProcessRead => ({ delta: '', lossy: false }),
kill: (): boolean => {
if (proc.status !== 'running') return false
proc.status = 'killed'
return true
},
}
return proc
}
}
describe('BashExecutor service seam', () => {
it('a concrete subclass registers as ctx.bash and serves the abstract API', async () => {
const ctx = new Context()
await ctx.plugin(StubExecutor)
const spec = ctx.bash.resolve({ command: 'echo hi' })
expect(spec).toEqual({ command: 'echo hi', workdir: '/stub', timeoutMs: 1000, stdoutMaxBytes: 64_000, sandboxPolicy: undefined })
const result = await ctx.bash.run(spec)
expect(result.exitCode).toBe(0)
expect(result.stdout.text).toBe('ok')
const proc = ctx.bash.start(spec)
expect(proc.status).toBe('running')
expect(proc.readOutput()).toEqual({ delta: '', lossy: false })
expect(proc.kill()).toBe(true)
expect(proc.kill()).toBe(false) // already settled → no-op
await proc.done
})
it('reports no default sandbox mode from the task-free base seam', async () => {
const ctx = new Context()
await ctx.plugin(StubExecutor)
expect(ctx.bash.sandboxMode).toBeUndefined()
})
it('loading a second implementation throws (one bash service per context — cordis standard)', async () => {
const ctx = new Context()
await ctx.plugin(StubExecutor)
class SecondExecutor extends StubExecutor {}
await expect(ctx.plugin(SecondExecutor)).rejects.toThrow(/service "bash" has been registered/)
})
})