| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- import { describe, expect, it, vi } from 'vitest'
- import { Context } from 'cordis'
- import { BashExecutor } from '@deepseek-ai/dsh-bash'
- import type { BashExecRequest, BashExecSpec, BashRunResult, BashTask, BashTaskRead } from '@deepseek-ai/dsh-bash'
- /** Minimal concrete executor: records calls, lets tests drive completions. */
- class StubExecutor extends BashExecutor {
- tasks = new Map<string, BashTask>()
- resolve(request: BashExecRequest): BashExecSpec {
- return {
- command: request.command,
- workdir: request.workdir ?? '/stub',
- timeoutMs: request.timeoutMs ?? 1000,
- ...request.signal ? { signal: request.signal } : {},
- }
- }
- async run(_spec: BashExecSpec): Promise<BashRunResult> {
- return {
- exitCode: 0,
- signal: null,
- timedOut: false,
- aborted: false,
- timeoutMs: 1000,
- stdout: { text: 'ok', truncated: false },
- stderr: { text: '', truncated: false },
- }
- }
- start(spec: BashExecSpec): BashTask {
- const task: BashTask = {
- id: `stub-${this.tasks.size + 1}`,
- command: spec.command,
- status: 'running',
- exitCode: null,
- signal: null,
- done: Promise.resolve(),
- }
- this.tasks.set(task.id, task)
- return task
- }
- get(id: string): BashTask | undefined {
- return this.tasks.get(id)
- }
- list(): BashTask[] {
- return [...this.tasks.values()]
- }
- readOutput(id: string): BashTaskRead {
- const task = this.tasks.get(id)
- if (!task) throw new Error(`unknown bash task "${id}"`)
- return { task, delta: '', lossy: false }
- }
- kill(id: string): boolean {
- const task = this.tasks.get(id)
- if (!task) throw new Error(`unknown bash task "${id}"`)
- if (task.status !== 'running') return false
- task.status = 'killed'
- return true
- }
- /** Expose the protected notifier for tests. */
- fire(task: BashTask): void {
- this.notifyTaskDone(task)
- }
- }
- async function setup() {
- const ctx = new Context()
- await ctx.plugin(StubExecutor)
- // ctx.bash resolves to the registered implementation.
- const bash = ctx.bash as StubExecutor
- return { ctx, bash }
- }
- describe('BashExecutor service seam', () => {
- it('registers as ctx.bash and serves the abstract API', async () => {
- const { bash } = await setup()
- const task = bash.start(bash.resolve({ command: 'sleep 1' }))
- expect(bash.get(task.id)).toBe(task)
- expect(bash.list()).toEqual([task])
- expect(bash.kill(task.id)).toBe(true)
- expect(bash.kill(task.id)).toBe(false)
- const result = await bash.run(bash.resolve({ command: 'true' }))
- expect(result.exitCode).toBe(0)
- })
- it('onTaskDone delivers completions to registered listeners', async () => {
- const { bash } = await setup()
- const seen: string[] = []
- bash.onTaskDone(task => void seen.push(task.id))
- const task = bash.start(bash.resolve({ command: 'x' }))
- bash.fire(task)
- expect(seen).toEqual([task.id])
- })
- it('onTaskDone disposer unsubscribes the listener', async () => {
- const { bash } = await setup()
- const listener = vi.fn()
- const dispose = bash.onTaskDone(listener)
- dispose()
- bash.fire(bash.start(bash.resolve({ command: 'x' })))
- expect(listener).not.toHaveBeenCalled()
- })
- it('listeners registered from a fiber are removed on dispose (HMR safety)', async () => {
- const { ctx, bash } = await setup()
- const listener = vi.fn()
- const fiber = await ctx.plugin(Object.assign((inner: Context) => {
- inner.bash.onTaskDone(listener)
- }, { inject: ['bash'] }))
- bash.fire(bash.start(bash.resolve({ command: 'one' })))
- expect(listener).toHaveBeenCalledTimes(1)
- await fiber.dispose()
- bash.fire(bash.start(bash.resolve({ command: 'two' })))
- expect(listener).toHaveBeenCalledTimes(1)
- })
- it('silences listeners once the service fiber is disposed', async () => {
- const ctx = new Context()
- const fiber = await ctx.plugin(Object.assign(async (inner: Context) => {
- await inner.plugin(StubExecutor)
- }, {}))
- const bash = ctx.bash as StubExecutor
- const listener = vi.fn()
- bash.onTaskDone(listener)
- const task = bash.start(bash.resolve({ command: 'x' }))
- await fiber.dispose()
- bash.fire(task)
- expect(listener).not.toHaveBeenCalled()
- })
- })
|