Files
deepseek-harness/packages/tasks/tasks/tests/service.spec.ts
Tianyi Cui 3b91135923 fix(tasks): fail loud when the abstract seam is mounted directly
Review finding (Codex round 1): abstract erases at runtime and
@deepseek-ai/dsh-tasks used to be the mountable registry, so a stale
composition row would register a ctx.tasks with no method implementations
and fail far from the misconfiguration. The seam constructor now rejects
direct mounts with a load-time pointer at dsh-tasks-local; the seam suite
pins the fence, the Agent Note cost paragraph records the actual behavior,
and the stale tool-pty README requirement line names the implementation
package.
2026-07-26 07:25:47 +08:00

89 lines
2.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { Context } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import { TaskId, TaskService } from '@deepseek-ai/dsh-tasks'
import type { TaskDoneListener, TaskRead, TaskSnapshot, TaskStart } from '@deepseek-ai/dsh-tasks'
/**
* Minimal concrete registry: one canned record. The seam owns the contract
* only (ids, snapshots, authorization-shaped signatures); the registry
* behavior suite lives with `@deepseek-ai/dsh-tasks-local`.
*/
class StubTaskService extends TaskService {
snapshotOf(id: TaskId): TaskSnapshot {
return {
id,
kind: 'bash',
label: 'sleep 60',
status: 'running',
startedAt: 0,
reported: false,
}
}
start(spec: TaskStart): TaskId {
spec.run()
return TaskId(`${spec.kind}-1`)
}
list(): TaskSnapshot[] {
return [this.snapshotOf(TaskId('bash-1'))]
}
get(id: TaskId): TaskSnapshot {
return this.snapshotOf(id)
}
read(id: TaskId): TaskRead {
return { text: '', snapshot: this.snapshotOf(id) }
}
kill(): 'requested' | 'already-finished' {
return 'requested'
}
wait(id: TaskId, _timeoutMs: number, _caller?: Agent, _signal?: AbortSignal): Promise<TaskSnapshot> {
return Promise.resolve(this.snapshotOf(id))
}
onTaskDone(_listener: TaskDoneListener): () => void {
return () => {}
}
attachSurface(_name: string): () => void {
return () => {}
}
}
describe('TaskService seam', () => {
it('a concrete subclass registers as ctx.tasks and serves the abstract API', async () => {
const ctx = new Context()
await ctx.plugin(StubTaskService)
const detachSurface = ctx.tasks.attachSurface('seam-test')
const id = ctx.tasks.start({ kind: 'bash', label: 'sleep 60', run: () => ({ cancel() {}, done: new Promise(() => {}) }) })
expect(id).toBe('bash-1')
expect(ctx.tasks.list()).toHaveLength(1)
expect(ctx.tasks.get(id).status).toBe('running')
expect(ctx.tasks.read(id).text).toBe('')
expect(ctx.tasks.kill(id)).toBe('requested')
await expect(ctx.tasks.wait(id, 5)).resolves.toMatchObject({ id })
const detachListener = ctx.tasks.onTaskDone(() => {})
detachListener()
detachSurface()
})
it('loading a second implementation throws (one tasks service per context — cordis standard)', async () => {
const ctx = new Context()
await ctx.plugin(StubTaskService)
class SecondTaskService extends StubTaskService {}
await expect(ctx.plugin(SecondTaskService)).rejects.toThrow(/service "tasks" has been registered/)
})
it('mounting the abstract seam directly fails loudly at load (stale-composition fence)', async () => {
const ctx = new Context()
await expect(ctx.plugin(TaskService as unknown as typeof StubTaskService))
.rejects.toThrow(/abstract task registry seam; load an implementation such as @deepseek-ai\/dsh-tasks-local/)
})
})