import { describe, expect, it } from 'vitest' import { Context } from 'cordis' import SessionStore, { Session, SessionId } from '@deepseek-ai/dsh-session' import * as HookInvariant from '@deepseek-ai/dsh-hook-protocol/invariant' import InvariantService from '@deepseek-ai/dsh-invariants' async function setup(): Promise { const ctx = new Context() await ctx.plugin(SessionStore) await ctx.plugin(InvariantService) await ctx.plugin(HookInvariant) return ctx } const invoked = (overrides: Record = {}) => ({ turn: 1, point: 'PreToolUse', dialect: 'claude' as const, handlerId: 'hook-1', ...overrides, }) const result = (overrides: Record = {}) => ({ turn: 1, point: 'PreToolUse', handlerId: 'hook-1', decision: 'pass', durationMs: 3, ...overrides, }) describe('hook-protocol invariants', () => { it('pairs serial and repeated handler invocations', async () => { const ctx = await setup() const session = ctx.sessions.create() session.append('hook/invoked', invoked()) session.append('hook/invoked', invoked()) session.append('hook/result', result()) session.append('hook/result', result()) }) it('rebuilds pending hook provenance from an existing session', async () => { const ctx = new Context() await ctx.plugin(SessionStore) const session = ctx.sessions.create() session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } }) session.append('hook/invoked', invoked()) await ctx.plugin(InvariantService) await ctx.plugin(HookInvariant) expect(() => session.append('hook/result', result())).not.toThrow() session.append('turn/end', { turn: 1, reason: { kind: 'completed' } }) }) it('adopts a bare session first observed through publication', async () => { const ctx = await setup() const session = new Session(SessionId('bare-hook-session')) expect(() => { ctx.emit('session/event', session, { type: 'hook/invoked', seq: 0, time: 0, data: invoked(), }) ctx.emit('session/event', session, { type: 'hook/result', seq: 1, time: 1, data: result(), }) }).not.toThrow() }) it.each([ [invoked({ point: '' }), /point and handlerId must be non-empty/], [invoked({ handlerId: '' }), /point and handlerId must be non-empty/], [invoked({ dialect: 'other' }), /unknown dialect/], ])('rejects malformed hook invocation %#', async (data, message) => { const ctx = await setup() expect(() => ctx.sessions.create().append('hook/invoked', data as never)).toThrow(message) }) it('rejects unmatched and malformed results', async () => { const ctx = await setup() const session = ctx.sessions.create() expect(() => session.append('hook/result', result())).toThrow(/no matching hook\/invoked/) session.append('hook/invoked', invoked()) expect(() => session.append('hook/result', result({ durationMs: -1 }))) .toThrow(/durationMs must be a non-negative finite number/) expect(() => session.append('hook/result', result({ point: 'Stop' }))) .toThrow(/no matching hook\/invoked/) }) })