|
1 | 1 | import { describe, it, expect } from 'vitest'; |
2 | | -import { buildProgressCard, buildResultCard, buildStreamingCard, buildPipelineCard, buildStatusCard, buildTurnCard, buildToolProgressCard, buildTextContentCard, buildOverviewCard, buildSimpleResultCard } from '../message-builder.js'; |
| 2 | +import { buildProgressCard, buildResultCard, buildStreamingCard, buildPipelineCard, buildStatusCard, buildTurnCard, buildToolProgressCard, buildTextContentCard, buildCombinedProgressCard, buildOverviewCard, buildSimpleResultCard } from '../message-builder.js'; |
| 3 | +import type { CombinedCardResult } from '../message-builder.js'; |
3 | 4 | import type { TurnInfo, ToolCallInfo, ActivityStatus } from '../../claude/types.js'; |
4 | 5 |
|
5 | 6 | describe('buildProgressCard', () => { |
@@ -513,6 +514,130 @@ describe('buildToolProgressCard', () => { |
513 | 514 | const body = card.elements[0].text.content as string; |
514 | 515 | expect(body).toContain('_(无工具调用)_'); |
515 | 516 | }); |
| 517 | + |
| 518 | + it('should collapse tool calls when completed with many entries', () => { |
| 519 | + const tools: ToolCallInfo[] = Array.from({ length: 8 }, (_, i) => ({ |
| 520 | + name: 'Read', |
| 521 | + input: { file_path: `/src/file${i}.ts` }, |
| 522 | + })); |
| 523 | + const card = buildToolProgressCard(tools, 10, undefined, true) as any; |
| 524 | + // 前 3 行作为摘要直接显示 |
| 525 | + const summary = card.elements[0].text.content as string; |
| 526 | + expect(summary).toContain('file0.ts'); |
| 527 | + expect(summary).toContain('file2.ts'); |
| 528 | + expect(summary).not.toContain('file3.ts'); |
| 529 | + // 剩余部分在折叠面板中 |
| 530 | + const panel = card.elements[1]; |
| 531 | + expect(panel.tag).toBe('collapsible_panel'); |
| 532 | + expect(panel.expanded).toBe(false); |
| 533 | + const panelContent = panel.elements[0].text.content as string; |
| 534 | + expect(panelContent).toContain('file3.ts'); |
| 535 | + expect(panelContent).toContain('file7.ts'); |
| 536 | + }); |
| 537 | +}); |
| 538 | + |
| 539 | +describe('buildCombinedProgressCard', () => { |
| 540 | + it('should show text and tool calls panel when in progress', () => { |
| 541 | + const tools: ToolCallInfo[] = [ |
| 542 | + { name: 'Read', input: { file_path: '/src/index.ts' } }, |
| 543 | + { name: 'Bash', input: { command: 'npm test' } }, |
| 544 | + ]; |
| 545 | + const card = buildCombinedProgressCard('正在分析代码...', tools, 2) as any; |
| 546 | + expect(card.header).toBeUndefined(); |
| 547 | + // 文本区域直接展示 |
| 548 | + expect(card.elements[0].text.content).toContain('正在分析代码'); |
| 549 | + // hr 分隔 |
| 550 | + expect(card.elements[1].tag).toBe('hr'); |
| 551 | + // 工具调用在折叠面板中 |
| 552 | + const panel = card.elements[2]; |
| 553 | + expect(panel.tag).toBe('collapsible_panel'); |
| 554 | + expect(panel.expanded).toBe(false); |
| 555 | + // 面板标题包含最新 tool call |
| 556 | + expect(panel.header.title.content).toContain('npm test'); |
| 557 | + expect(panel.header.title.content).toContain('2'); |
| 558 | + // 面板内容包含所有 tool calls |
| 559 | + const panelContent = panel.elements[0].text.content as string; |
| 560 | + expect(panelContent).toContain('/src/index.ts'); |
| 561 | + expect(panelContent).toContain('npm test'); |
| 562 | + }); |
| 563 | + |
| 564 | + it('should show indigo header and collapsed tools when completed', () => { |
| 565 | + const tools: ToolCallInfo[] = Array.from({ length: 5 }, (_, i) => ({ |
| 566 | + name: 'Read', |
| 567 | + input: { file_path: `/src/file${i}.ts` }, |
| 568 | + })); |
| 569 | + const card = buildCombinedProgressCard('分析完成', tools, 5, true) as any; |
| 570 | + expect(card.header).toBeUndefined(); |
| 571 | + // 工具调用折叠面板 |
| 572 | + const panel = card.elements.find((e: any) => e.tag === 'collapsible_panel'); |
| 573 | + expect(panel).toBeDefined(); |
| 574 | + expect(panel.expanded).toBe(false); |
| 575 | + expect(panel.header.title.content).toContain('5 条'); |
| 576 | + // note 无 "执行中" |
| 577 | + const note = card.elements.find((e: any) => e.tag === 'note'); |
| 578 | + expect(note.elements[0].content).not.toContain('⏳'); |
| 579 | + expect(note.elements[0].content).toContain('5 轮'); |
| 580 | + }); |
| 581 | + |
| 582 | + it('should show only text when no tool calls', () => { |
| 583 | + const card = buildCombinedProgressCard('只有文字', [], 1) as any; |
| 584 | + expect(card.elements[0].text.content).toContain('只有文字'); |
| 585 | + // 无 collapsible_panel |
| 586 | + const panel = card.elements.find((e: any) => e.tag === 'collapsible_panel'); |
| 587 | + expect(panel).toBeUndefined(); |
| 588 | + }); |
| 589 | + |
| 590 | + it('should show only tool panel when no text', () => { |
| 591 | + const tools: ToolCallInfo[] = [ |
| 592 | + { name: 'Glob', input: { pattern: '**/*.ts' } }, |
| 593 | + ]; |
| 594 | + const card = buildCombinedProgressCard('', tools, 1) as any; |
| 595 | + // 第一个元素应该是 collapsible_panel(无文本区和 hr) |
| 596 | + expect(card.elements[0].tag).toBe('collapsible_panel'); |
| 597 | + }); |
| 598 | + |
| 599 | + it('should show placeholder when both empty', () => { |
| 600 | + const card = buildCombinedProgressCard('', [], 0) as any; |
| 601 | + expect(card.elements[0].text.content).toContain('正在处理'); |
| 602 | + }); |
| 603 | + |
| 604 | + it('should keep combined card payload under 30KB', () => { |
| 605 | + const longText = '内'.repeat(12000); |
| 606 | + const tools: ToolCallInfo[] = Array.from({ length: 16 }, (_, i) => ({ |
| 607 | + name: 'Bash', |
| 608 | + input: { command: `very-long-command-number-${i} --flag=value --another=arg` }, |
| 609 | + })); |
| 610 | + const card = buildCombinedProgressCard(longText, tools, 20, true); |
| 611 | + const serialized = JSON.stringify(card); |
| 612 | + expect(Buffer.byteLength(serialized, 'utf-8')).toBeLessThan(30720); |
| 613 | + }); |
| 614 | + |
| 615 | + it('should show green header on success result', () => { |
| 616 | + const tools: ToolCallInfo[] = [ |
| 617 | + { name: 'Bash', input: { command: 'npm test' } }, |
| 618 | + ]; |
| 619 | + const result: CombinedCardResult = { success: true, durationStr: '12s | 💰 $0.05' }; |
| 620 | + const card = buildCombinedProgressCard('All tests passed', tools, 3, true, undefined, result) as any; |
| 621 | + expect(card.header).toBeUndefined(); |
| 622 | + const note = card.elements.find((e: any) => e.tag === 'note'); |
| 623 | + expect(note.elements[0].content).toContain('✅ 执行完成'); |
| 624 | + expect(note.elements[0].content).toContain('12s'); |
| 625 | + }); |
| 626 | + |
| 627 | + it('should show red header and error on failure result', () => { |
| 628 | + const result: CombinedCardResult = { success: false, durationStr: '5s', error: 'Something broke' }; |
| 629 | + const card = buildCombinedProgressCard('', [], 1, true, undefined, result) as any; |
| 630 | + expect(card.header).toBeUndefined(); |
| 631 | + // 错误信息应直接展示 |
| 632 | + const errorEl = card.elements.find((e: any) => e.text?.content?.includes('Something broke')); |
| 633 | + expect(errorEl).toBeDefined(); |
| 634 | + }); |
| 635 | + |
| 636 | + it('should show orange header on timeout result', () => { |
| 637 | + const result: CombinedCardResult = { success: false, durationStr: '300s', timedOut: true }; |
| 638 | + const card = buildCombinedProgressCard('partial output', [], 10, true, undefined, result) as any; |
| 639 | + expect(card.header).toBeUndefined(); |
| 640 | + }); |
516 | 641 | }); |
517 | 642 |
|
518 | 643 | describe('buildTextContentCard', () => { |
|
0 commit comments