relay-intent.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. import { Agent } from './api/models';
  2. import { formatRelayQuestion, formatRelayReply, parseRelayIntent } from './relay-intent';
  3. /**
  4. * 容器中转 —— 识别「让 A 去问 B」的指令。
  5. *
  6. * 实测依据(2026-10-29):
  7. * · 容器有能力主动发 Matrix 消息(lj 实发成功并回读验证)
  8. * · 但 lj-test 发送失败(PUT 返回空体),且两端互相看不见对方
  9. * · 所以让容器互发不可靠 —— 由 App 搬运更稳
  10. */
  11. const agent = (id: string, name: string): Agent => ({
  12. id,
  13. name,
  14. status: 'idle' as Agent['status'],
  15. matrixReady: true,
  16. unread: 0,
  17. });
  18. const A = agent('a', 'lj');
  19. const B = agent('b', 'lj-test');
  20. const ALL = [A, B];
  21. describe('parseRelayIntent · 动词式', () => {
  22. it('「问 X:问题」', () => {
  23. const r = parseRelayIntent('问 lj-test:数据库用的什么版本', ALL)!;
  24. expect(r.target.id).toBe(B.id);
  25. expect(r.question).toBe('数据库用的什么版本');
  26. });
  27. it('「让 X 做某事」', () => {
  28. const r = parseRelayIntent('让 lj-test 检查一下接口', ALL)!;
  29. expect(r.target.id).toBe(B.id);
  30. expect(r.question).toBe('检查一下接口');
  31. });
  32. it('「问问 X」/「问一下 X」', () => {
  33. expect(parseRelayIntent('问问 lj-test:进度如何', ALL)!.question).toBe('进度如何');
  34. expect(parseRelayIntent('问一下 lj-test,进度如何', ALL)!.question).toBe('进度如何');
  35. });
  36. it('全角冒号与逗号也能分隔', () => {
  37. expect(parseRelayIntent('问 lj-test:进度如何', ALL)!.question).toBe('进度如何');
  38. expect(parseRelayIntent('问 lj-test,进度如何', ALL)!.question).toBe('进度如何');
  39. });
  40. });
  41. describe('parseRelayIntent · @ 式', () => {
  42. it('「@X 问题」', () => {
  43. const r = parseRelayIntent('@lj-test 这个问题你怎么看', ALL)!;
  44. expect(r.target.id).toBe(B.id);
  45. expect(r.question).toBe('这个问题你怎么看');
  46. });
  47. });
  48. describe('parseRelayIntent · 🔴 前缀陷阱', () => {
  49. /*
  50. * `lj` 是 `lj-test` 的前缀。若按短名优先匹配,
  51. * 「问 lj-test:xxx」会被误判成问 lj —— 实测容器间也有同样问题。
  52. */
  53. it('「问 lj-test」不会被误判成问 lj', () => {
  54. const r = parseRelayIntent('问 lj-test:接口怎么样了', ALL)!;
  55. expect(r.target.id).toBe(B.id);
  56. expect(r.target.name).toBe('lj-test');
  57. });
  58. it('「问 lj」正确命中短名那个', () => {
  59. const r = parseRelayIntent('问 lj:今天状态如何', ALL)!;
  60. expect(r.target.id).toBe(A.id);
  61. });
  62. it('短名出现在问题正文里不会误触发', () => {
  63. // 「问 lj-test」命中 lj-test;问题里的 lj 只是正文
  64. const r = parseRelayIntent('问 lj-test:lj 那边的接口好了吗', ALL)!;
  65. expect(r.target.id).toBe(B.id);
  66. expect(r.question).toBe('lj 那边的接口好了吗');
  67. });
  68. });
  69. describe('parseRelayIntent · 不该命中的情况', () => {
  70. it('普通对话不触发', () => {
  71. expect(parseRelayIntent('帮我做个登录页', ALL)).toBeNull();
  72. expect(parseRelayIntent('这个接口怎么写', ALL)).toBeNull();
  73. });
  74. // 只有名字没有问题时,转过去也没意义
  75. it('只有名字没问题时不触发', () => {
  76. expect(parseRelayIntent('问 lj-test', ALL)).toBeNull();
  77. expect(parseRelayIntent('问 lj-test:', ALL)).toBeNull();
  78. expect(parseRelayIntent('@lj-test', ALL)).toBeNull();
  79. });
  80. it('空文本 / 无候选容器时不触发', () => {
  81. expect(parseRelayIntent('', ALL)).toBeNull();
  82. expect(parseRelayIntent('问 lj-test:xxx', [])).toBeNull();
  83. });
  84. it('名字以正则元字符结尾也能正确匹配', () => {
  85. const dot = agent('c', 'bot.v2');
  86. const r = parseRelayIntent('问 bot.v2:你好', [dot])!;
  87. expect(r.target.id).toBe('c');
  88. expect(r.question).toBe('你好');
  89. });
  90. it('多行问题完整保留', () => {
  91. const r = parseRelayIntent('问 lj-test:第一行\n第二行', ALL)!;
  92. expect(r.question).toBe('第一行\n第二行');
  93. });
  94. });
  95. describe('formatRelayQuestion', () => {
  96. it('标明是代问,并说明答复会转回', () => {
  97. const s = formatRelayQuestion(A, '用户9836', '数据库什么版本');
  98. expect(s).toContain('lj');
  99. expect(s).toContain('用户9836');
  100. expect(s).toContain('数据库什么版本');
  101. expect(s).toContain('转回');
  102. });
  103. });
  104. describe('formatRelayReply', () => {
  105. it('标明来源与原问题', () => {
  106. const s = formatRelayReply(B, '数据库什么版本', 'MySQL 8.0');
  107. expect(s).toContain('lj-test');
  108. expect(s).toContain('数据库什么版本');
  109. expect(s).toContain('MySQL 8.0');
  110. });
  111. it('超长问题截断', () => {
  112. const s = formatRelayReply(B, '一'.repeat(200), '答');
  113. expect(s).toContain('…');
  114. });
  115. it('对方没给内容时如实说明', () => {
  116. expect(formatRelayReply(B, 'q', ' ')).toContain('没有给出内容');
  117. });
  118. });