| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- import { hasProcess, isToolActivityMessage, parseSegments, splitMessage, stepCountLabel, toolActivitySummary } from './message-segments';
- /**
- * 过程行拆分(用于「工具调用折叠」)。
- *
- * 设计原则:**宁可漏判,不可误判** —— 把用户要看的结果折进去,
- * 比多留一行过程文本糟糕得多。
- */
- describe('message-segments', () => {
- describe('parseSegments 判定', () => {
- it('圆点 + 工具名 → tool 步', () => {
- const [s] = parseSegments('· bash');
- expect(s).toEqual({ kind: 'tool', label: 'bash' } as never);
- });
- it('圆点 + 工具名 + 参数 → detail 分离', () => {
- const [s] = parseSegments('· bash: git status --short');
- expect(s).toEqual({ kind: 'tool', label: 'bash', detail: 'git status --short' });
- });
- it('圆点后接中文短语 → 不算工具(交回正文)', () => {
- const [s] = parseSegments('· 已完成');
- expect(s.kind).toBe('text');
- });
- it('「正在…」短句 → status 步', () => {
- const [s] = parseSegments('正在读取配置文件…');
- expect(s.kind).toBe('status');
- });
- it('✅ 完成行 → status 步', () => {
- const [s] = parseSegments('✅ 3 个文件已处理');
- expect(s.kind).toBe('status');
- });
- it('🔴 长句即使以「正在」开头也不折叠(避免吃掉正文)', () => {
- const long = '正在说明一下:这次的问题是上报器把自己的心跳写进了 currentTask 字段,' +
- '导致用户看到永远正在执行,需要改成从 state.db 判断。';
- const [s] = parseSegments(long);
- expect(s.kind).toBe('text');
- });
- it('普通正文 → text', () => {
- const [s] = parseSegments('我看了下环境,没问题。');
- expect(s.kind).toBe('text');
- });
- it('空行 → blank', () => {
- const [s] = parseSegments(' ');
- expect(s.kind).toBe('blank');
- });
- });
- describe('toBlocks / splitMessage 分组', () => {
- it('连续过程行合成一个 block', () => {
- const blocks = splitMessage('先看环境。\n· bash\n· read_file\n· edit_file\n改好了。');
- const kinds = blocks.map((b) => b.kind);
- expect(kinds).toEqual(['text', 'process', 'text']);
- const proc = blocks[1] as { steps: unknown[] };
- expect(proc.steps.length).toBe(3);
- });
- it('过程行之间的空行不打断分组', () => {
- const blocks = splitMessage('· bash\n\n· read_file\n\n结论如下。');
- expect(blocks[0].kind).toBe('process');
- expect((blocks[0] as { steps: unknown[] }).steps.length).toBe(2);
- });
- it('孤立单步不折叠(内联成一行,少点一次)', () => {
- const blocks = splitMessage('正文一\n· bash\n正文二');
- expect(blocks.every((b) => b.kind !== 'process')).toBe(true);
- expect(blocks.some((b) => b.kind === 'text' && b.text.includes('bash'))).toBe(true);
- });
- it('没有过程行的消息 → hasProcess 为 false(走整段 markdown)', () => {
- expect(hasProcess({ text: '这是一段普通回复,含 **加粗**。\n\n第二段。' })).toBe(false);
- });
- it('含过程行 → hasProcess 为 true', () => {
- expect(hasProcess({ text: '· bash\n· read_file\n好的。' })).toBe(true);
- });
- it('步骤数文案', () => {
- expect(stepCountLabel(3)).toBe('运行了 3 步');
- });
- });
- describe("容器终端活动消息(实测格式)", () => {
- it("💻 terminal + 代码块 → 是终端活动", () => {
- expect(isToolActivityMessage("💻 terminal\\n```\\ncd /opt/data && chmod +x x.sh\\n```")).toBeTrue();
- });
- it("✍️ Writing 路径 + 命令 → 是终端活动", () => {
- expect(isToolActivityMessage("✍️ Writing /opt/data/work/i.html 💻 terminal\\n```\\nnode -e x\\n```")).toBeTrue();
- });
- it("带 emoji 的单行状态(攒经验)→ 是终端活动", () => {
- expect(isToolActivityMessage("💾 攒经验中,已入账:🔧 「ops」强化成功")).toBeTrue();
- });
- it("🔴 正常回复带一个 emoji → 不算终端活动(不能误折)", () => {
- expect(isToolActivityMessage("修复验证通过 ✅ 字节数一致,本地与云端 MD5 相同。")).toBeFalse();
- });
- it("🔴 长段落即使含命令片段 → 不算终端活动", () => {
- const long = "我执行了 curl 命令做验证,返回的是 200,说明整条链路是通的,这一步之后就可以把结果整理成报告了,目前没有未完成的事项需要处理。";
- expect(isToolActivityMessage(long)).toBeFalse();
- });
- it("空文本 → false", () => {
- expect(isToolActivityMessage("")).toBeFalse();
- expect(isToolActivityMessage(undefined)).toBeFalse();
- });
- it("摘要取第一行动作名", () => {
- expect(toolActivitySummary("💻 terminal\\n```\\nls\\n```")).toContain("terminal");
- });
- });
- /*
- * 2026-11-10 实测原文(lj-test 容器在临时房间里的真实输出)。
- *
- * 容器的进度消息是 `m.text`,且**同一个气泡里堆了多行工具行**,
- * 部分还带 `(×2)`、`(×3)` 这种折叠计数后缀。旧实现在「每一行都是工具行」
- * 时才整条折叠,只要混进一行中文叙述就整条进正文 —— 用户看到满屏 `🐍 Running code…`。
- */
- describe('容器进度消息(2026-11-10 实测原文)', () => {
- it('多行 🐍 Running code 堆叠 → 是终端活动', () => {
- const txt = [
- '* 🐍 Running code from hermes_tools import terminal # H...',
- '🐍 Running code from hermes_tools import terminal r=t...',
- ].join('\n');
- expect(isToolActivityMessage(txt)).toBeTrue();
- });
- it('带折叠计数 (×2) (×3) 仍识别为终端活动', () => {
- const txt = [
- '* 🐍 Running code from hermes_tools import terminal r=t...',
- '🐍 Running code from hermes_tools import search_files...',
- '🐍 Running code from hermes_tools import read_file, s...',
- '🐍 Running code from hermes_tools import search_files...',
- ].join('\n');
- expect(isToolActivityMessage(txt)).toBeTrue();
- });
- it('🔴 工具行 + 一句中文叙述混排 → 仍是终端活动(整条进过程区,不刷屏正文)', () => {
- const txt = [
- '🐍 Running code from hermes_tools import terminal r=t...',
- '现在我来检查 drain 脚本、配置里与日志和消息类型有关的部分。',
- '🐍 Running code from hermes_tools import terminal, re...',
- ].join('\n');
- expect(isToolActivityMessage(txt)).toBeTrue();
- });
- it('🔴 纯粹的中文分析仍是正文(不能因为常和工具混排就被误折)', () => {
- const txt = '我发现 ArtifactSyncService 这条链路其实是通用的,只不过之前从来没有容器把 artifacts 写进来过,所以右栏一直是空的。';
- expect(isToolActivityMessage(txt)).toBeFalse();
- });
- });
- });
- describe('emoji 工具行(2026-11-06 补齐)', () => {
- it('🔴 💻 terminal 被识别为过程行', () => {
- const seg = parseSegments('💻 terminal\n```\ncd /opt/data\n```');
- expect(seg[0].kind).toBe('tool');
- });
- it('🌐 browser_exec 带说明也算过程', () => {
- const seg = parseSegments('🌐 browser_exec: "打开本地表格页面并验证渲染"');
- expect(seg[0].kind).toBe('tool');
- });
- it('✍️ Writing 路径算过程', () => {
- expect(parseSegments('✍️ Writing /opt/data/work/index.html')[0].kind).toBe('tool');
- });
- it('🔴 ✅ 完成行判为 status(既有规则,不被 emoji 工具行抢占)', () => {
- // 既有设计:✅ 开头是「完成状态」过程行,会被折叠 —— 与 tool 区分开
- expect(parseSegments('✅ 全部测试通过')[0].kind).toBe('status');
- });
- it('❌ 失败说明按既有规则判为 status(不是 tool)', () => {
- const seg = parseSegments('❌ 依赖安装失败')[0];
- expect(seg.kind === 'status' || seg.kind === 'text').toBe(true);
- });
- it('正文段落不会被误判', () => {
- expect(parseSegments('我先看一下环境再决定怎么做。')[0].kind).toBe('text');
- });
- it('🔴 混合消息:正文 + 工具行 + 正文 → 中间被折叠', () => {
- const blocks = splitMessage(
- ['好,我先做一个页面。', '💻 terminal', '```', 'cd /opt/data', '```', '静态自检通过。'].join('\n'),
- );
- const kinds = blocks.map((b) => b.kind);
- expect(kinds).toContain('process');
- expect(kinds[0]).toBe('text');
- expect(kinds[kinds.length - 1]).toBe('text');
- });
- });
|