fix(invariants): assert runtime relationships, not API shapes
This commit is contained in:
@@ -1,31 +1,55 @@
|
||||
/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tasks`. @module @deepseek-ai/dsh-tasks/invariant */
|
||||
/** Package-owned background-task snapshot invariants. @module @deepseek-ai/dsh-tasks/invariant */
|
||||
|
||||
import type { Context } from 'cordis'
|
||||
import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type { InvariantFailure, InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import type { TaskSnapshot } from './types.ts'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tasks'
|
||||
const TERMINAL_STATUSES = new Set(['completed', 'killed', 'failed'])
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tasks-invariant'
|
||||
/** Services required before the companion can register. */
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** Install checks for this package's active plugin fibers. */
|
||||
const install: InvariantInstaller = (ctx, fail) => {
|
||||
observePluginInvariant(ctx, fail, {
|
||||
name: 'TaskService',
|
||||
effects: [
|
||||
'ctx.provide("tasks")',
|
||||
'tasks teardown',
|
||||
],
|
||||
services: [
|
||||
'tasks',
|
||||
],
|
||||
})
|
||||
/** Validate the cross-field relationships in one registry snapshot. */
|
||||
function validateSnapshot(snapshot: TaskSnapshot, owner: Agent | undefined, fail: InvariantFailure): void {
|
||||
const id = String(snapshot.id)
|
||||
const prefix = `${snapshot.kind}-`
|
||||
const ordinal = Number(id.slice(prefix.length))
|
||||
if (snapshot.kind.length === 0 || !id.startsWith(prefix)
|
||||
|| !Number.isSafeInteger(ordinal) || ordinal < 1) {
|
||||
fail(`task snapshot id ${JSON.stringify(id)} must be ${JSON.stringify(prefix)} followed by a positive ordinal`)
|
||||
}
|
||||
if (snapshot.label.length === 0) fail(`task ${JSON.stringify(id)} label must be non-empty`)
|
||||
if (!Number.isSafeInteger(snapshot.startedAt) || snapshot.startedAt < 0) {
|
||||
fail(`task ${JSON.stringify(id)} startedAt must be a non-negative epoch integer`)
|
||||
}
|
||||
|
||||
const terminal = TERMINAL_STATUSES.has(snapshot.status)
|
||||
if (terminal !== (snapshot.finishedAt !== undefined)) {
|
||||
fail(`task ${JSON.stringify(id)} finishedAt must be present exactly for a terminal status`)
|
||||
}
|
||||
if (snapshot.finishedAt !== undefined
|
||||
&& (!Number.isSafeInteger(snapshot.finishedAt) || snapshot.finishedAt < snapshot.startedAt)) {
|
||||
fail(`task ${JSON.stringify(id)} finishedAt must be an epoch integer no earlier than startedAt`)
|
||||
}
|
||||
|
||||
const expectedOwner = owner?.id
|
||||
if (snapshot.ownerSession !== expectedOwner) {
|
||||
fail(`task ${JSON.stringify(id)} ownerSession does not match its completion owner`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Install checks over current unowned records and every terminal snapshot. */
|
||||
const install: InvariantInstaller = Object.assign((ctx: Context, fail: InvariantFailure) => {
|
||||
for (const snapshot of ctx.tasks.list()) validateSnapshot(snapshot, undefined, fail)
|
||||
ctx.tasks.onTaskDone((snapshot, owner) => { validateSnapshot(snapshot, owner, fail) })
|
||||
}, { inject: ['tasks'] })
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* Register the task-registry invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
|
||||
87
packages/tasks/tasks/tests/invariant.spec.ts
Normal file
87
packages/tasks/tasks/tests/invariant.spec.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
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/)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +1,24 @@
|
||||
/** Package-owned runtime contract checks for `@deepseek-ai/dsh-tool-tasks`. @module @deepseek-ai/dsh-tool-tasks/invariant */
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-tasks`.
|
||||
* @module @deepseek-ai/dsh-tool-tasks/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import { observePluginInvariant, type InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-tasks'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tool-tasks-invariant'
|
||||
/** Services required before the companion can register. */
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/** Install checks for this package's active plugin fibers. */
|
||||
const install: InvariantInstaller = (ctx, fail) => {
|
||||
observePluginInvariant(ctx, fail, {
|
||||
name: 'tool-tasks',
|
||||
inject: [
|
||||
'tools',
|
||||
'tasks',
|
||||
'systemPrompt',
|
||||
],
|
||||
effects: [
|
||||
'tools.register()',
|
||||
],
|
||||
})
|
||||
}
|
||||
/**
|
||||
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution
|
||||
* relations are owned by the capability seam it calls.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
@@ -32,3 +27,4 @@ const install: InvariantInstaller = (ctx, fail) => {
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
Reference in New Issue
Block a user