qwen-code/packages/cli/src/ui/components/messages/CompactToolGroupDisplay.test.tsx
Shaojin Wen 69da115dcf
Some checks are pending
Qwen Code CI / Lint (push) Waiting to run
Qwen Code CI / Test (push) Blocked by required conditions
Qwen Code CI / Test-1 (push) Blocked by required conditions
Qwen Code CI / Test-2 (push) Blocked by required conditions
Qwen Code CI / Test-3 (push) Blocked by required conditions
Qwen Code CI / Test-4 (push) Blocked by required conditions
Qwen Code CI / Test-5 (push) Blocked by required conditions
Qwen Code CI / Test-6 (push) Blocked by required conditions
Qwen Code CI / Test-7 (push) Blocked by required conditions
Qwen Code CI / Test-8 (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
feat(cli): combine elapsed + timeout in shell time indicator (#3512)
* feat(cli): combine elapsed + timeout in shell time indicator

Render shell tools that have an explicit timeout as
`(elapsed · timeout N)` inline with the Running… status from t=0,
instead of splitting the information across the right-aligned elapsed
indicator and the ShellStatsBar row.

- formatters: add a `hideTrailingZeros` option so whole seconds render
  as `5s` rather than `5.0s` while fractional values like `5.5s` stay
  intact
- ToolElapsedTime: accept optional `timeoutMs`; when set, skip the 3s
  quiet threshold and render the combined `(elapsed · timeout N)` label
- ToolMessage: extract `timeoutMs` from AnsiOutputDisplay and feed it
  to ToolElapsedTime
- ShellStatsBar: drop its `timeoutMs` field (now inline); keeps
  `+N lines` and memory usage only
- Unify both modes on `formatDuration` so hour-range output is
  consistent (`1h 2m 6s` across timeout and no-timeout paths)

* feat(cli): thread shell timeoutMs through compact tool group display

The combined `(elapsed · timeout N)` format introduced in the previous
commit was only wired through the expanded ToolMessage path. Compact
tool groups kept rendering ToolElapsedTime without timeoutMs, so shell
tools displayed in compact mode silently dropped the timeout budget.

- CompactToolGroupDisplay: add getShellTimeoutMs() to pull timeoutMs
  off the active tool's AnsiOutputDisplay result (same shape used by
  ToolMessage) and feed it to ToolElapsedTime
- add CompactToolGroupDisplay.test.tsx covering the three paths:
  ansi display with timeoutMs, ansi display without timeoutMs, and
  non-ansi resultDisplay (string)
2026-04-23 08:52:37 +08:00

93 lines
2.6 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render } from 'ink-testing-library';
import { Text } from 'ink';
import { CompactToolGroupDisplay } from './CompactToolGroupDisplay.js';
import { ToolCallStatus } from '../../types.js';
import type { IndividualToolCallDisplay } from '../../types.js';
// ToolStatusIndicator pulls in GeminiRespondingSpinner which requires
// StreamingContext; stub it out so we can test the elapsed/timeout
// plumbing in isolation.
vi.mock('../shared/ToolStatusIndicator.js', () => ({
ToolStatusIndicator: () => <Text></Text>,
STATUS_INDICATOR_WIDTH: 2,
}));
const NOW = 1_700_000_000_000;
function shellTool(
overrides: Partial<IndividualToolCallDisplay> = {},
): IndividualToolCallDisplay {
return {
callId: 'c1',
name: 'Shell',
description: 'sleep 10',
status: ToolCallStatus.Executing,
executionStartTime: NOW,
resultDisplay: undefined,
confirmationDetails: undefined,
...overrides,
};
}
describe('<CompactToolGroupDisplay />', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(NOW);
});
afterEach(() => {
vi.useRealTimers();
});
it('surfaces shell timeoutMs inline via ToolElapsedTime', () => {
const tool = shellTool({
resultDisplay: {
ansiOutput: [],
totalLines: 0,
totalBytes: 0,
timeoutMs: 30_000,
},
});
const { lastFrame } = render(
<CompactToolGroupDisplay toolCalls={[tool]} contentWidth={80} />,
);
expect(lastFrame()).toContain('(0s · timeout 30s)');
});
it('falls back to quiet elapsed-only when no timeout is surfaced', () => {
const tool = shellTool({
resultDisplay: {
ansiOutput: [],
totalLines: 0,
totalBytes: 0,
},
});
const { lastFrame } = render(
<CompactToolGroupDisplay toolCalls={[tool]} contentWidth={80} />,
);
// Sub-3s without a timeout budget → indicator is quiet.
expect(lastFrame()).not.toContain('timeout');
expect(lastFrame()).not.toContain('0s');
});
it('ignores non-ansi resultDisplay shapes', () => {
const tool = shellTool({
resultDisplay: 'plain text output',
});
const { lastFrame, rerender } = render(
<CompactToolGroupDisplay toolCalls={[tool]} contentWidth={80} />,
);
vi.advanceTimersByTime(5_000);
rerender(<CompactToolGroupDisplay toolCalls={[tool]} contentWidth={80} />);
// No timeout in display → legacy 3s-threshold elapsed.
expect(lastFrame()).toContain('5s');
expect(lastFrame()).not.toContain('timeout');
});
});