88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import type { Agent } from '@deepseek-ai/dsh-agent'
|
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
import TaskService, { TaskId } from '@deepseek-ai/dsh-tasks'
|
|
import type { TaskDoneListener, TaskSnapshot } from '@deepseek-ai/dsh-tasks'
|
|
import * as TasksInvariant from '@deepseek-ai/dsh-tasks/invariant'
|
|
import InvariantService from '@deepseek-ai/dsh-invariants'
|
|
|
|
const BASE: TaskSnapshot = {
|
|
id: TaskId('bash-1'),
|
|
kind: 'bash',
|
|
label: 'compile',
|
|
status: 'completed',
|
|
startedAt: 10,
|
|
finishedAt: 20,
|
|
reported: false,
|
|
}
|
|
|
|
const RUNNING: TaskSnapshot = {
|
|
id: TaskId('bash-1'),
|
|
kind: 'bash',
|
|
label: 'compile',
|
|
status: 'running',
|
|
startedAt: 10,
|
|
reported: false,
|
|
}
|
|
|
|
const TERMINAL_WITHOUT_FINISH: TaskSnapshot = {
|
|
id: TaskId('bash-1'),
|
|
kind: 'bash',
|
|
label: 'compile',
|
|
status: 'completed',
|
|
startedAt: 10,
|
|
reported: false,
|
|
}
|
|
|
|
async function setup(seed: TaskSnapshot[] = []): Promise<(snapshot: unknown, owner?: Agent) => void> {
|
|
const ctx = new Context()
|
|
let listener: TaskDoneListener | undefined
|
|
const probe = {
|
|
list: () => seed,
|
|
onTaskDone(value: TaskDoneListener) {
|
|
listener = value
|
|
return () => { listener = undefined }
|
|
},
|
|
} as unknown as TaskService
|
|
await ctx.plugin(InvariantService)
|
|
await ctx.plugin({
|
|
name: 'task-invariant-probe',
|
|
apply(child: Context) { child.provide('tasks', probe) },
|
|
})
|
|
await ctx.plugin(TasksInvariant)
|
|
if (listener === undefined) throw new Error('task invariant did not subscribe to terminal snapshots')
|
|
return (snapshot, owner) => { listener!(snapshot as TaskSnapshot, owner) }
|
|
}
|
|
|
|
describe('task-registry invariants', () => {
|
|
it('accepts coherent current and terminal snapshots', async () => {
|
|
const notify = await setup([RUNNING])
|
|
expect(() => { notify(BASE) }).not.toThrow()
|
|
const owner = { id: SessionId('owner') } as Agent
|
|
expect(() => { notify({ ...BASE, id: TaskId('subagent-2'), kind: 'subagent', ownerSession: owner.id }, owner) })
|
|
.not.toThrow()
|
|
})
|
|
|
|
it.each([
|
|
[{ ...BASE, id: TaskId('-1'), kind: '' }, undefined, /positive ordinal/],
|
|
[{ ...BASE, id: TaskId('other-1') }, undefined, /must be "bash-" followed by a positive ordinal/],
|
|
[{ ...BASE, id: TaskId('bash-x') }, undefined, /positive ordinal/],
|
|
[{ ...BASE, id: TaskId('bash-0') }, undefined, /positive ordinal/],
|
|
[{ ...BASE, startedAt: -1 }, undefined, /startedAt must be a non-negative epoch integer/],
|
|
[{ ...BASE, startedAt: 0.5 }, undefined, /startedAt must be a non-negative epoch integer/],
|
|
[{ ...BASE, status: 'running' }, undefined, /finishedAt must be present exactly for a terminal status/],
|
|
[TERMINAL_WITHOUT_FINISH, undefined, /finishedAt must be present exactly for a terminal status/],
|
|
[{ ...BASE, finishedAt: 9 }, undefined, /no earlier than startedAt/],
|
|
[{ ...BASE, finishedAt: 20.5 }, undefined, /no earlier than startedAt/],
|
|
[{ ...BASE, ownerSession: SessionId('recorded') }, { id: SessionId('actual') } as Agent, /does not match its completion owner/],
|
|
] as const)('rejects an incoherent registry snapshot', async (snapshot, owner, message) => {
|
|
const notify = await setup()
|
|
expect(() => { notify(snapshot, owner) }).toThrow(message)
|
|
})
|
|
|
|
it('rejects an incoherent record already present at installation', async () => {
|
|
await expect(setup([{ ...BASE, label: '' }])).rejects.toThrow(/label must be non-empty/)
|
|
})
|
|
})
|