redact.spec.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  2. /**
  3. * The `session-telemetry/record` waterfall contract: pass-through when no listener is
  4. * mounted, listener stacking and replacement, ops-record coverage, the
  5. * untouched canonical log, and the fail-closed containment of a throwing rule.
  6. */
  7. import { describe, expect, it } from 'vitest'
  8. import { Context } from '@deepseek-ai/cordis'
  9. import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
  10. import {
  11. SessionTelemetryCoordinator,
  12. type SessionTelemetrySink,
  13. type SessionTelemetryRecord,
  14. } from '../src/index.ts'
  15. const FIXTURE_SECRET = 'sk-fixture1234567890'
  16. class CollectingBackend implements SessionTelemetrySink {
  17. records: SessionTelemetryRecord[] = []
  18. emit(record: SessionTelemetryRecord): void {
  19. this.records.push(record)
  20. }
  21. async shutdown(): Promise<void> {}
  22. }
  23. async function setup() {
  24. const backend = new CollectingBackend()
  25. const ctx = new Context()
  26. await ctx.plugin(SessionStore)
  27. const fiber = await ctx.plugin({
  28. name: 'fake-telemetry',
  29. inject: ['sessions'],
  30. apply: (inner: Context) => void new SessionTelemetryCoordinator(inner, backend),
  31. })
  32. return { ctx, backend, fiber }
  33. }
  34. describe('session-telemetry/record waterfall', () => {
  35. it('passes records through unchanged when no listener is mounted', async () => {
  36. const { ctx, backend } = await setup()
  37. const session = ctx.sessions.create(SessionId('w'))
  38. session.append('user/message', createUserMessage({
  39. content: [{ type: 'text', text: `key ${FIXTURE_SECRET}` }], source: { kind: 'user' },
  40. }), { surfaceOp: 'append' })
  41. const body = backend.records[0]!.body as { content: { text: string }[] }
  42. expect(body.content[0]!.text).toBe(`key ${FIXTURE_SECRET}`)
  43. })
  44. it('applies a mounted rule to every outbound record, ops records included', async () => {
  45. const { ctx, backend, fiber } = await setup()
  46. ctx.on('session-telemetry/record', (_record, next) => {
  47. const record = next()
  48. return { ...record, body: { scrubbed: true } }
  49. })
  50. const session = ctx.sessions.create(SessionId('rule'))
  51. session.append('user/message', createUserMessage({
  52. content: [{ type: 'text', text: FIXTURE_SECRET }], source: { kind: 'user' },
  53. }), { surfaceOp: 'append' })
  54. expect(backend.records[0]!.body).toEqual({ scrubbed: true })
  55. // The dispose-time shutdown ops record passes through the same waterfall.
  56. await fiber.dispose()
  57. const ops = backend.records.filter(record => record.channel === 'ops')
  58. expect(ops).toHaveLength(1)
  59. expect(ops[0]!.body).toEqual({ scrubbed: true })
  60. })
  61. it('keeps the canonical log untouched by a mounted rule', async () => {
  62. const { ctx } = await setup()
  63. ctx.on('session-telemetry/record', (_record, next) => ({ ...next(), body: null }))
  64. const session = ctx.sessions.create(SessionId('log'))
  65. session.append('user/message', createUserMessage({
  66. content: [{ type: 'text', text: FIXTURE_SECRET }], source: { kind: 'user' },
  67. }), { surfaceOp: 'append' })
  68. const logged = session.snapshotEvents()[0]!.data as { content: { text: string }[] }
  69. expect(logged.content[0]!.text).toBe(FIXTURE_SECRET)
  70. })
  71. it('stacks listeners outermost-first around next()', async () => {
  72. const { ctx, backend } = await setup()
  73. const order: string[] = []
  74. ctx.on('session-telemetry/record', (_record, next) => {
  75. order.push('outer-before')
  76. const record = next()
  77. order.push('outer-after')
  78. return { ...record, attributes: { ...record.attributes, outer: 1 } }
  79. })
  80. ctx.on('session-telemetry/record', (_record, next) => {
  81. order.push('inner')
  82. const record = next()
  83. return { ...record, attributes: { ...record.attributes, inner: 1 } }
  84. })
  85. const session = ctx.sessions.create(SessionId('stack'))
  86. session.append('user/message', createUserMessage({
  87. content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
  88. }), { surfaceOp: 'append' })
  89. expect(order).toEqual(['outer-before', 'inner', 'outer-after'])
  90. expect(backend.records[0]!.attributes).toMatchObject({ outer: 1, inner: 1 })
  91. })
  92. it('a listener that skips next() replaces everything beneath it', async () => {
  93. const { ctx, backend } = await setup()
  94. const inner = { called: false }
  95. ctx.on('session-telemetry/record', () => ({ channel: 'ops', time: 0, severity: 'info', attributes: {}, body: 'replaced' } satisfies SessionTelemetryRecord))
  96. ctx.on('session-telemetry/record', (_record, next) => {
  97. inner.called = true
  98. return next()
  99. })
  100. const session = ctx.sessions.create(SessionId('veto'))
  101. session.append('user/message', createUserMessage({
  102. content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
  103. }), { surfaceOp: 'append' })
  104. expect(backend.records[0]!.body).toBe('replaced')
  105. expect(inner.called).toBe(false)
  106. })
  107. it('a throwing rule withholds the record fail-closed without disturbing the log', async () => {
  108. const { ctx, backend } = await setup()
  109. ctx.on('session-telemetry/record', () => {
  110. throw new Error('rule exploded')
  111. })
  112. const session = ctx.sessions.create(SessionId('closed'))
  113. session.append('user/message', createUserMessage({
  114. content: [{ type: 'text', text: 'hi' }], source: { kind: 'user' },
  115. }), { surfaceOp: 'append' })
  116. expect(backend.records).toHaveLength(0)
  117. expect(session.snapshotEvents()).toHaveLength(1)
  118. })
  119. })