invariant.spec.ts 4.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. // Title-source invariant: `messageSeqs` is empty iff `source.kind` is `user`.
  2. // — the durable relationship every appended session/title event must keep.
  3. import { describe, expect, it } from 'vitest'
  4. import { Context } from '@deepseek-ai/cordis'
  5. import * as SessionTitleInvariantCompanion from '@deepseek-ai/dsh-session-title/invariant'
  6. import InvariantRegistry, { InvariantError } from '@deepseek-ai/dsh-invariants'
  7. import SessionStore, { SessionId, SessionSeq } from '@deepseek-ai/dsh-session'
  8. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  9. async function setup(): Promise<Context> {
  10. const ctx = new Context()
  11. await ctx.plugin(SessionStore)
  12. await ctx.plugin(InvariantRegistry, { enabled: true })
  13. await ctx.plugin(SessionTitleInvariantCompanion)
  14. return ctx
  15. }
  16. describe('session-title source invariant', () => {
  17. it('accepts cited automatic titles and citation-free user renames', async () => {
  18. const ctx = await setup()
  19. const session = ctx.sessions.create(SessionId('title-invariant-valid'))
  20. const source = session.append('user/message', createUserMessage({
  21. content: [{ type: 'text', text: 'title me' }], source: { kind: 'user' },
  22. }), { surfaceOp: 'append' })
  23. expect(() => {
  24. session.append('session/title', { title: 'auto', messageSeqs: [source.seq], source: { kind: 'fallback' } })
  25. session.append('session/title', { title: 'named', messageSeqs: [], source: { kind: 'user' } })
  26. }).not.toThrow()
  27. })
  28. it('rejects a citation-free automatic title and a user rename that cites messages', async () => {
  29. const ctx = await setup()
  30. const session = ctx.sessions.create(SessionId('title-invariant-invalid'))
  31. const source = session.append('user/message', createUserMessage({
  32. content: [{ type: 'text', text: 'title me' }], source: { kind: 'user' },
  33. }), { surfaceOp: 'append' })
  34. expect(() => {
  35. session.append('session/title', { title: 'auto', messageSeqs: [], source: { kind: 'fallback' } })
  36. }).toThrow(expect.objectContaining<Partial<InvariantError>>({
  37. code: 'INVARIANT',
  38. packageName: '@deepseek-ai/dsh-session-title',
  39. }))
  40. expect(() => {
  41. session.append('session/title', { title: 'named', messageSeqs: [source.seq], source: { kind: 'user' } })
  42. }).toThrow(expect.objectContaining<Partial<InvariantError>>({
  43. code: 'INVARIANT',
  44. packageName: '@deepseek-ai/dsh-session-title',
  45. }))
  46. expect(session.seq).toBe(1)
  47. })
  48. it('requires automatic-title citations to name distinct earlier human messages', async () => {
  49. const ctx = await setup()
  50. const session = ctx.sessions.create(SessionId('title-invariant-sources'))
  51. const boundary = session.append('turn/start', { turn: 1 })
  52. expect(() => session.append('session/title', {
  53. title: 'wrong source', messageSeqs: [boundary.seq], source: { kind: 'fallback' },
  54. })).toThrow(/must name an earlier human user\/message/)
  55. expect(() => session.append('session/title', {
  56. title: 'future source', messageSeqs: [SessionSeq(session.seq)], source: { kind: 'fallback' },
  57. })).toThrow(/must name an earlier human user\/message/)
  58. expect(() => session.append('session/title', {
  59. title: 'malformed source', messageSeqs: [-1 as never], source: { kind: 'fallback' },
  60. })).toThrow(/invalid message seq/)
  61. const pluginMessage = session.append('user/message', createUserMessage({
  62. content: [{ type: 'text', text: 'plugin context' }],
  63. source: { kind: 'plugin', plugin: 'test' },
  64. }), { surfaceOp: 'append' })
  65. expect(() => session.append('session/title', {
  66. title: 'plugin source', messageSeqs: [pluginMessage.seq], source: { kind: 'fallback' },
  67. })).toThrow(/must name an earlier human user\/message/)
  68. const source = session.append('user/message', createUserMessage({
  69. content: [{ type: 'text', text: 'title me' }], source: { kind: 'user' },
  70. }), { surfaceOp: 'append' })
  71. expect(() => session.append('session/title', {
  72. title: 'duplicate source', messageSeqs: [source.seq, source.seq], source: { kind: 'fallback' },
  73. })).toThrow(/repeats message seq/)
  74. })
  75. it('validates title relations when the companion loads after a Session', async () => {
  76. const ctx = new Context()
  77. await ctx.plugin(SessionStore)
  78. const session = ctx.sessions.create(SessionId('title-invariant-existing'))
  79. const boundary = session.append('turn/start', { turn: 1 })
  80. session.append('session/title', {
  81. title: 'wrong source', messageSeqs: [boundary.seq], source: { kind: 'fallback' },
  82. })
  83. await ctx.plugin(InvariantRegistry, { enabled: true })
  84. await expect(ctx.plugin(SessionTitleInvariantCompanion).then(() => undefined))
  85. .rejects.toThrow(/must name an earlier human user\/message/)
  86. })
  87. })