workspaceRelay.test.ts 4.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
  1. // @vitest-environment node
  2. import { describe, expect, it } from 'vitest'
  3. import { botRelayIntent, relayFingerprint, relayMessageValue } from '../../src/server/workspaceRelay'
  4. import { mentionedAgents } from '../../src/server/workspaceMentions'
  5. import type { WorkspaceMessage } from '../../src/shared/workspace'
  6. const members = [{ id: 'reviewer', name: '审核' }, { id: 'writer', name: '文案' }, { id: 'lead', name: '竹儿' }]
  7. describe('assistant relay intent', () => {
  8. it.each([
  9. '收到 @审核 终审维持通过。\n\n交付状态(不变):入草稿箱 Media ID `abc123`,发布目录 `/work/final/`。\n等民哥群发指令。',
  10. '@竹儿 维持通过,**等民哥群发指令**。无新事实、无新派工,本轮不重复核验。',
  11. '感谢 @审核,等待用户指令。',
  12. '@审核 已通过。',
  13. '收到,等待用户指令。',
  14. 'Thanks @审核.\nWaiting for your input',
  15. ])('does not dispatch a closing receipt: %s', text => {
  16. expect(botRelayIntent(text, members)).toMatchObject({ targetIds: [], acknowledgementOnly: true })
  17. })
  18. it.each([
  19. '感谢 @审核,请重新检查第二段。',
  20. '收到 @审核,第二段还有一个问题。',
  21. '请@审核检查接口,@文案检查数据',
  22. '@审核 第一步',
  23. '@审核 继续',
  24. '@审核',
  25. '请复核:@审核',
  26. '请 @审核 等待数据库恢复后重试',
  27. '@审核 不要修改第一段,请修改第二段',
  28. ])('keeps new or ambiguous assignments: %s', text => {
  29. expect(botRelayIntent(text, members).targetIds).toContain('reviewer')
  30. })
  31. it('filters targets individually and preserves repeated mention overrides', () => {
  32. expect(botRelayIntent('感谢 @审核,@文案 请修改第二段', members).targetIds).toEqual(['writer'])
  33. expect(botRelayIntent('感谢 @审核。@文案 已收到。请 @审核 重新检查。', members).targetIds).toEqual(['reviewer'])
  34. })
  35. it('keeps user routing unchanged and ignores quoted, code, URL and email mentions', () => {
  36. expect(mentionedAgents('感谢 @审核', members)).toEqual(['reviewer'])
  37. expect(botRelayIntent('收到。\n> @审核\n```\n@文案\n```\n`@竹儿` <quoted_message>@审核</quoted_message>', members).targetIds).toEqual([])
  38. })
  39. it('leaves unfamiliar text and long repetitive text to the cycle guard', () => {
  40. expect(botRelayIntent('本轮还缺少数据', members).acknowledgementOnly).toBe(false)
  41. expect(botRelayIntent('收到'.repeat(16000) + ' @审核', members).targetIds).toEqual(['reviewer'])
  42. })
  43. })
  44. const message = (): WorkspaceMessage => ({
  45. id: 'message-1', conversationId: 'chat', seq: 1, role: 'assistant', agentId: 'reviewer', content: '检查 /work/final 版本 2',
  46. reasoning: 'thinking', status: 'complete', createdAt: 1,
  47. tools: [{ id: 'event-1', name: 'check', status: 'tool.complete', createdAt: 1, arguments: { id: 10 }, result: { timestamp: 123, count: 2 } }],
  48. attachments: [{ id: 'file-1', name: 'report.txt', mimeType: 'text/plain', size: 12, createdAt: 1, messageId: 'message-1' }],
  49. })
  50. const key = (m: WorkspaceMessage) => relayFingerprint(relayMessageValue(m))
  51. describe('relay progress fingerprints', () => {
  52. it('ignores delivery IDs, timestamps, reasoning, whitespace and object key order', () => {
  53. const original = message(), other = message()
  54. other.id = 'message-2'; other.seq = 200; other.createdAt = 10; other.reasoning = 'new thinking'
  55. other.content = ' 检查 /work/final\n版本 2 '
  56. other.tools[0] = { ...other.tools[0], id: 'event-2', createdAt: 20, durationMs: 44 }
  57. other.attachments[0] = { ...other.attachments[0]!, messageId: 'message-2', createdAt: 20 }
  58. expect(key(other)).toBe(key(original))
  59. expect(relayFingerprint({ a: 1, b: 2 })).toBe(relayFingerprint({ b: 2, a: 1 }))
  60. })
  61. it.each(['content', 'path', 'toolArgument', 'toolResult', 'artifact', 'codeWhitespace', 'argumentWhitespace'] as const)('preserves new business evidence: %s', field => {
  62. const original = message(), other = message()
  63. if (field === 'content') other.content = '检查 /work/final 版本 3'
  64. if (field === 'path') other.content = '检查 /work/next 版本 2'
  65. if (field === 'toolArgument') other.tools[0]!.arguments = { id: 11 }
  66. if (field === 'toolResult') other.tools[0]!.result = { timestamp: 124, count: 2 }
  67. if (field === 'artifact') other.attachments[0]!.id = 'file-2'
  68. if (field === 'codeWhitespace') { original.content = '`/work/a b`'; other.content = '`/work/a b`' }
  69. if (field === 'argumentWhitespace') { original.tools[0]!.arguments = { path: '/work/a b' }; other.tools[0]!.arguments = { path: '/work/a b' } }
  70. expect(key(other)).not.toBe(key(original))
  71. })
  72. })