invariant.spec.ts 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import { describe, expect, it } from 'vitest'
  2. import { Context } from 'cordis'
  3. import SessionStore, { Session, SessionId, type SessionEvent } from '@deepseek-ai/dsh-session'
  4. import * as PlanModeInvariant from '@deepseek-ai/dsh-plan-mode/invariant'
  5. import InvariantService from '@deepseek-ai/dsh-invariants'
  6. async function setup(): Promise<Context> {
  7. const ctx = new Context()
  8. await ctx.plugin(SessionStore)
  9. await ctx.plugin(InvariantService, { enabled: true })
  10. await ctx.plugin(PlanModeInvariant)
  11. return ctx
  12. }
  13. function event(active: unknown): SessionEvent {
  14. return { type: 'plan/mode', seq: 0, time: 0, data: { active } } as SessionEvent
  15. }
  16. function emitTurnStart(ctx: Context, session: Session): void {
  17. ctx.emit('session/event', session, {
  18. type: 'turn/start', seq: 0, time: 0,
  19. data: { turn: 1 },
  20. })
  21. }
  22. describe('plan-mode stream invariants', () => {
  23. it('accepts either boolean state', async () => {
  24. const ctx = await setup()
  25. const session = Session.create(SessionId('plan-state'))
  26. emitTurnStart(ctx, session)
  27. expect(() => { ctx.emit('session/event', session, event(true)) }).not.toThrow()
  28. expect(() => { ctx.emit('session/event', session, event(false)) }).not.toThrow()
  29. ctx.emit('session/event', session, {
  30. type: 'turn/end', seq: 3, time: 3, data: { turn: 1, reason: { kind: 'completed' } },
  31. })
  32. })
  33. it.each([42, 'plan', undefined])('rejects invalid durable plan state %j', async (active) => {
  34. const ctx = await setup()
  35. const session = Session.create(SessionId(`invalid-${String(active)}`))
  36. emitTurnStart(ctx, session)
  37. expect(() => { ctx.emit('session/event', session, event(active)) })
  38. .toThrow(/expected a boolean/)
  39. })
  40. it('accepts standalone plan state between turns (the idle immediate commit)', async () => {
  41. const ctx = await setup()
  42. expect(() => ctx.sessions.create().append('plan/mode', { active: true }))
  43. .not.toThrow()
  44. })
  45. it('ignores unrelated dispatches and session events', async () => {
  46. const ctx = await setup()
  47. const session = Session.create(SessionId('unrelated'))
  48. expect(() => {
  49. ctx.emit('tools/change')
  50. ctx.emit('session/event', session, {
  51. type: 'turn/start', seq: 0, time: 0, data: { turn: 1 },
  52. })
  53. }).not.toThrow()
  54. })
  55. it('rejects invalid existing state on late registration', async () => {
  56. const ctx = new Context()
  57. await ctx.plugin(SessionStore)
  58. const session = ctx.sessions.create()
  59. session.append('turn/start', { turn: 1 })
  60. session.append('plan/mode', { active: 'plan' as unknown as boolean })
  61. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  62. await ctx.plugin(InvariantService, { enabled: true })
  63. await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/expected a boolean/)
  64. })
  65. it('replays enclosed existing plan state through its closing boundary', async () => {
  66. const ctx = new Context()
  67. await ctx.plugin(SessionStore)
  68. const session = ctx.sessions.create()
  69. session.append('turn/start', { turn: 1 })
  70. session.append('plan/mode', { active: true })
  71. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  72. await ctx.plugin(InvariantService, { enabled: true })
  73. await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
  74. })
  75. it('accepts standalone existing plan state on late registration', async () => {
  76. const ctx = new Context()
  77. await ctx.plugin(SessionStore)
  78. ctx.sessions.create().append('plan/mode', { active: true })
  79. await ctx.plugin(InvariantService, { enabled: true })
  80. await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
  81. })
  82. })