identity.spec.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. import { describe, expect, it } from 'vitest'
  2. import { redactSessionSnapshotIds } from '../src/identity.ts'
  3. const parentId = '11111111-1111-4111-8111-111111111111'
  4. const childId = '22222222-2222-4222-8222-222222222222'
  5. const messageId = '33333333-3333-4333-8333-333333333333'
  6. const approvalId = '44444444-4444-4444-8444-444444444444'
  7. const runId = '55555555-5555-4555-8555-555555555555'
  8. const otherId = '66666666-6666-4666-8666-666666666666'
  9. const proseUuid = '77777777-7777-4777-8777-777777777777'
  10. describe('session snapshot identity redaction', () => {
  11. it('preserves feedback versions and target relationships without redacting unrelated prose', () => {
  12. const source = [
  13. { type: 'session', id: parentId },
  14. { type: 'assistant/message', data: { message: { id: messageId, role: 'assistant', content: [], source: {} } } },
  15. { type: 'feedback/message-put', data: { sessionId: parentId, item: { messageId, version: approvalId, note: proseUuid } } },
  16. { type: 'feedback/message-put', data: { sessionId: parentId, item: { messageId, version: runId } } },
  17. { type: 'feedback/message-delete', data: { sessionId: parentId, messageId } },
  18. { type: 'example', data: { version: proseUuid } },
  19. ].map(record => JSON.stringify(record)).join('\n')
  20. const [output] = redactSessionSnapshotIds([source])
  21. expect(output).toContain('"version":"{{id:1}}"')
  22. expect(output).toContain('"version":"{{id:2}}"')
  23. expect(output?.match(/"messageId":"{{message:1}}"/g)).toHaveLength(3)
  24. expect(output).toContain(proseUuid)
  25. expect(redactSessionSnapshotIds([output!])).toEqual([output])
  26. })
  27. it('preserves typed relationships across parent and child logs', () => {
  28. const parent = [
  29. JSON.stringify({ type: 'session', id: parentId, createdAt: 1, cwd: '/tmp/work' }),
  30. JSON.stringify({
  31. type: 'agent/inbox/spliced',
  32. data: {
  33. inserted: [{
  34. role: 'user',
  35. content: [{ type: 'text', text: `keep unrelated ${proseUuid}; session ${childId}` }],
  36. source: { kind: 'user' },
  37. id: messageId,
  38. }],
  39. },
  40. }),
  41. JSON.stringify({ type: 'approval/asked', data: { id: approvalId } }),
  42. JSON.stringify({ type: 'tool-workflow/run-start', data: { runId } }),
  43. JSON.stringify({ type: 'example', data: { requestId: otherId, echoed: otherId } }),
  44. '',
  45. ].join('\n')
  46. const child = [
  47. JSON.stringify({ type: 'session', id: childId, parentSession: parentId, createdAt: 2, cwd: '/tmp/work' }),
  48. JSON.stringify({
  49. type: 'user/message',
  50. data: {
  51. role: 'user', content: [], source: { kind: 'user' }, id: messageId,
  52. },
  53. }),
  54. '',
  55. ].join('\n')
  56. const redacted = redactSessionSnapshotIds([parent, child])
  57. expect(redacted[0]).toContain('"id":"{{session:1}}"')
  58. expect(redacted[1]).toContain('"id":"{{session:2}}"')
  59. expect(redacted[1]).toContain('"parentSession":"{{session:1}}"')
  60. expect(redacted.join('\n').match(/\{\{message:1\}\}/g)).toHaveLength(2)
  61. expect(redacted[0]).toContain('"id":"{{approval:1}}"')
  62. expect(redacted[0]).toContain('"runId":"{{workflow:1}}"')
  63. expect(redacted[0]).toContain('"requestId":"{{id:1}}"')
  64. expect(redacted[0]).toContain('"echoed":"{{id:1}}"')
  65. expect(redacted[0]).toContain(proseUuid)
  66. expect(redacted[0]).toContain('session {{session:2}}')
  67. expect(redactSessionSnapshotIds(redacted)).toEqual(redacted)
  68. })
  69. it('classifies semantic text plus command, RPC, and retry identity fields', () => {
  70. const semanticMessage = '88888888-8888-4888-8888-888888888888'
  71. const anonymousUser = '99999999-9999-4999-8999-999999999999'
  72. const retryId = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'
  73. const source = [
  74. JSON.stringify({ type: 'not-a-session', data: { value: 'plain' } }),
  75. JSON.stringify({
  76. type: 'example',
  77. data: {
  78. commandId: 'command-7',
  79. rpcId: 'rpc-9',
  80. retryId,
  81. requestId: 'stable-readable-id',
  82. text: `Retain this as message ${semanticMessage}. Anonymous user: ${anonymousUser}`,
  83. },
  84. }),
  85. ].join('\n')
  86. const [redacted] = redactSessionSnapshotIds([source])
  87. expect(redacted).toContain('"commandId":"{{command:1}}"')
  88. expect(redacted).toContain('"rpcId":"{{rpc:1}}"')
  89. expect(redacted).toContain('"retryId":"{{retry:1}}"')
  90. expect(redacted).toContain('as message {{message:1}}')
  91. expect(redacted).toContain('Anonymous user: {{id:1}}')
  92. expect(redacted).toContain('"requestId":"stable-readable-id"')
  93. expect(redacted?.endsWith('\n')).toBe(false)
  94. })
  95. it('keeps a canonical token first seen through a generic id key', () => {
  96. const canonical = '{{message:7}}'
  97. const nextMessage = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb'
  98. const source = [
  99. JSON.stringify({ type: 'example', data: { requestId: canonical } }),
  100. JSON.stringify({
  101. type: 'user/message',
  102. data: { role: 'user', content: [], source: { kind: 'user' }, id: canonical },
  103. }),
  104. JSON.stringify({
  105. type: 'user/message',
  106. data: { role: 'user', content: [], source: { kind: 'user' }, id: nextMessage },
  107. }),
  108. '',
  109. ].join('\n')
  110. const [redacted] = redactSessionSnapshotIds([source])
  111. expect(redacted?.match(/\{\{message:7\}\}/g)).toHaveLength(2)
  112. expect(redacted).toContain('"id":"{{message:8}}"')
  113. expect(redacted).not.toContain('{{id:')
  114. })
  115. })