message-segments.spec.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import { hasProcess, isToolActivityMessage, parseSegments, splitMessage, stepCountLabel, toolActivitySummary } from './message-segments';
  2. /**
  3. * 过程行拆分(用于「工具调用折叠」)。
  4. *
  5. * 设计原则:**宁可漏判,不可误判** —— 把用户要看的结果折进去,
  6. * 比多留一行过程文本糟糕得多。
  7. */
  8. describe('message-segments', () => {
  9. describe('parseSegments 判定', () => {
  10. it('圆点 + 工具名 → tool 步', () => {
  11. const [s] = parseSegments('· bash');
  12. expect(s).toEqual({ kind: 'tool', label: 'bash' } as never);
  13. });
  14. it('圆点 + 工具名 + 参数 → detail 分离', () => {
  15. const [s] = parseSegments('· bash: git status --short');
  16. expect(s).toEqual({ kind: 'tool', label: 'bash', detail: 'git status --short' });
  17. });
  18. it('圆点后接中文短语 → 不算工具(交回正文)', () => {
  19. const [s] = parseSegments('· 已完成');
  20. expect(s.kind).toBe('text');
  21. });
  22. it('「正在…」短句 → status 步', () => {
  23. const [s] = parseSegments('正在读取配置文件…');
  24. expect(s.kind).toBe('status');
  25. });
  26. it('✅ 完成行 → status 步', () => {
  27. const [s] = parseSegments('✅ 3 个文件已处理');
  28. expect(s.kind).toBe('status');
  29. });
  30. it('🔴 长句即使以「正在」开头也不折叠(避免吃掉正文)', () => {
  31. const long = '正在说明一下:这次的问题是上报器把自己的心跳写进了 currentTask 字段,' +
  32. '导致用户看到永远正在执行,需要改成从 state.db 判断。';
  33. const [s] = parseSegments(long);
  34. expect(s.kind).toBe('text');
  35. });
  36. it('普通正文 → text', () => {
  37. const [s] = parseSegments('我看了下环境,没问题。');
  38. expect(s.kind).toBe('text');
  39. });
  40. it('空行 → blank', () => {
  41. const [s] = parseSegments(' ');
  42. expect(s.kind).toBe('blank');
  43. });
  44. });
  45. describe('toBlocks / splitMessage 分组', () => {
  46. it('连续过程行合成一个 block', () => {
  47. const blocks = splitMessage('先看环境。\n· bash\n· read_file\n· edit_file\n改好了。');
  48. const kinds = blocks.map((b) => b.kind);
  49. expect(kinds).toEqual(['text', 'process', 'text']);
  50. const proc = blocks[1] as { steps: unknown[] };
  51. expect(proc.steps.length).toBe(3);
  52. });
  53. it('过程行之间的空行不打断分组', () => {
  54. const blocks = splitMessage('· bash\n\n· read_file\n\n结论如下。');
  55. expect(blocks[0].kind).toBe('process');
  56. expect((blocks[0] as { steps: unknown[] }).steps.length).toBe(2);
  57. });
  58. it('孤立单步不折叠(内联成一行,少点一次)', () => {
  59. const blocks = splitMessage('正文一\n· bash\n正文二');
  60. expect(blocks.every((b) => b.kind !== 'process')).toBe(true);
  61. expect(blocks.some((b) => b.kind === 'text' && b.text.includes('bash'))).toBe(true);
  62. });
  63. it('没有过程行的消息 → hasProcess 为 false(走整段 markdown)', () => {
  64. expect(hasProcess({ text: '这是一段普通回复,含 **加粗**。\n\n第二段。' })).toBe(false);
  65. });
  66. it('含过程行 → hasProcess 为 true', () => {
  67. expect(hasProcess({ text: '· bash\n· read_file\n好的。' })).toBe(true);
  68. });
  69. it('步骤数文案', () => {
  70. expect(stepCountLabel(3)).toBe('运行了 3 步');
  71. });
  72. });
  73. describe("容器终端活动消息(实测格式)", () => {
  74. it("💻 terminal + 代码块 → 是终端活动", () => {
  75. expect(isToolActivityMessage("💻 terminal\\n```\\ncd /opt/data && chmod +x x.sh\\n```")).toBeTrue();
  76. });
  77. it("✍️ Writing 路径 + 命令 → 是终端活动", () => {
  78. expect(isToolActivityMessage("✍️ Writing /opt/data/work/i.html 💻 terminal\\n```\\nnode -e x\\n```")).toBeTrue();
  79. });
  80. it("带 emoji 的单行状态(攒经验)→ 是终端活动", () => {
  81. expect(isToolActivityMessage("💾 攒经验中,已入账:🔧 「ops」强化成功")).toBeTrue();
  82. });
  83. it("🔴 正常回复带一个 emoji → 不算终端活动(不能误折)", () => {
  84. expect(isToolActivityMessage("修复验证通过 ✅ 字节数一致,本地与云端 MD5 相同。")).toBeFalse();
  85. });
  86. it("🔴 长段落即使含命令片段 → 不算终端活动", () => {
  87. const long = "我执行了 curl 命令做验证,返回的是 200,说明整条链路是通的,这一步之后就可以把结果整理成报告了,目前没有未完成的事项需要处理。";
  88. expect(isToolActivityMessage(long)).toBeFalse();
  89. });
  90. it("空文本 → false", () => {
  91. expect(isToolActivityMessage("")).toBeFalse();
  92. expect(isToolActivityMessage(undefined)).toBeFalse();
  93. });
  94. it("摘要取第一行动作名", () => {
  95. expect(toolActivitySummary("💻 terminal\\n```\\nls\\n```")).toContain("terminal");
  96. });
  97. });
  98. });