invariant.spec.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  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, trigger: { kind: 'message', source: { kind: 'user' } } },
  20. })
  21. }
  22. describe('plan-mode stream invariants', () => {
  23. it('accepts either boolean state', async () => {
  24. const ctx = await setup()
  25. const session = new Session(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,
  31. data: { turn: 1, reason: { kind: 'completed' } },
  32. })
  33. })
  34. it.each([42, 'plan', undefined])('rejects invalid durable plan state %j', async (active) => {
  35. const ctx = await setup()
  36. const session = new Session(SessionId(`invalid-${String(active)}`))
  37. emitTurnStart(ctx, session)
  38. expect(() => { ctx.emit('session/event', session, event(active)) })
  39. .toThrow(/expected a boolean/)
  40. })
  41. it('accepts standalone plan state between turns (the idle immediate commit)', async () => {
  42. const ctx = await setup()
  43. expect(() => ctx.sessions.create().append('plan/mode', { active: true }))
  44. .not.toThrow()
  45. })
  46. it('ignores unrelated dispatches and session events', async () => {
  47. const ctx = await setup()
  48. const session = new Session(SessionId('unrelated'))
  49. expect(() => {
  50. ctx.emit('tools/change')
  51. ctx.emit('session/event', session, {
  52. type: 'turn/start', seq: 0, time: 0, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } },
  53. })
  54. }).not.toThrow()
  55. })
  56. it('rejects invalid existing state on late registration', async () => {
  57. const ctx = new Context()
  58. await ctx.plugin(SessionStore)
  59. const session = ctx.sessions.create()
  60. session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
  61. session.append('plan/mode', { active: 'plan' as unknown as boolean })
  62. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  63. await ctx.plugin(InvariantService, { enabled: true })
  64. await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).rejects.toThrow(/expected a boolean/)
  65. })
  66. it('replays enclosed existing plan state through its closing boundary', async () => {
  67. const ctx = new Context()
  68. await ctx.plugin(SessionStore)
  69. const session = ctx.sessions.create()
  70. session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
  71. session.append('plan/mode', { active: true })
  72. session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
  73. await ctx.plugin(InvariantService, { enabled: true })
  74. await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
  75. })
  76. it('accepts standalone existing plan state on late registration', async () => {
  77. const ctx = new Context()
  78. await ctx.plugin(SessionStore)
  79. ctx.sessions.create().append('plan/mode', { active: true })
  80. await ctx.plugin(InvariantService, { enabled: true })
  81. await expect(ctx.plugin(PlanModeInvariant).then(() => undefined)).resolves.toBeUndefined()
  82. })
  83. })