qwen-code/integration-tests/interactive/cron-interactive.test.ts
pomelo e92fcbab46
fix(cli): switch TUI prefix ✦→◆ to fix glyph overflow on some terminals (#5974)
* fix(cli): replace ✦ (U+2726) with ◆ (U+25C6) and add ∵/∴ thinking icons

- Replace ✦ with ◆ across all TUI components to fix East Asian
  Ambiguous width misalignment (string-width reports 1 but terminals
  render 2 columns).
- Use ∵ (because) during thinking streaming, ∴ (therefore) when
  thinking is complete — matches the mathematical reasoning pair.

Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>

* fix(cli): reduce STATUS_INDICATOR_WIDTH from 3 to 2 after ◆ replacement

◆ (U+25C6) is a consistent width-1 character across all terminals,
so the tool status indicator no longer needs the extra column that
was reserved for the ambiguous-width ✦ (U+2726).

Co-Authored-By: Qwen Code <noreply@alibaba-inc.com>

* fix(cli): catch missed ✦→◆ references in tests, docs, and scenarios

* fix(cli): shorten tmux spinner frames from 3 to 2 chars to match STATUS_INDICATOR_WIDTH=2

TMUX_SPINNER_FRAMES changed from ['.  ', '.. ', '...'] to ['· ', '··']
to prevent 1-column overflow in tmux when STATUS_INDICATOR_WIDTH was
reduced from 3 to 2 after the ◆ replacement.

* revert(cli): keep narrow '.' tmux spinner frames instead of ambiguous '·'

'.' (U+002E) is Narrow (always 1 col), giving a guaranteed fixed-width
tmux spinner. '·' (U+00B7) is East Asian Ambiguous, so on ambiguous-width=2
terminals the frames become 3/4 cols and the spinner jitters — the opposite
of the "fixed-width frames" the surrounding comment promises.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen Code <noreply@alibaba-inc.com>
Co-authored-by: pomelo.lcw <pomelo.lcw@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-06-30 22:44:34 +00:00

127 lines
3.6 KiB
TypeScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* In-session cron/loop interactive E2E tests.
*
* These drive the full interactive TUI via InteractiveSession (node-pty +
* @xterm/headless) and read the rendered terminal screen. No browser needed.
*
* Ported from the standalone script at
* terminal-capture/test-cron-interactive-e2e.ts.
*/
import { describe, it, expect, afterEach } from 'vitest';
import { InteractiveSession } from './interactive-session.js';
const SANDBOX_MODE = process.env['QWEN_SANDBOX']?.toLowerCase().trim();
const IS_SANDBOX = Boolean(
SANDBOX_MODE && SANDBOX_MODE !== 'false' && SANDBOX_MODE !== '0',
);
function makeEnv(): NodeJS.ProcessEnv {
const env = { ...process.env };
delete env['NO_COLOR'];
return {
...env,
FORCE_COLOR: '1',
QWEN_CODE_LANG: 'en',
TERM: 'xterm-256color',
NODE_NO_WARNINGS: '1',
};
}
// These tests are flaky in the Docker sandbox environment, skip for now.
(IS_SANDBOX ? describe.skip : describe)('cron interactive', () => {
let session: InteractiveSession | null = null;
afterEach(async () => {
if (session) {
await session.close();
session = null;
}
});
it('loop fires inline in conversation', { timeout: 180_000 }, async () => {
session = await InteractiveSession.start({
env: makeEnv(),
args: ['--approval-mode', 'yolo'],
});
await session.send(
'Call cron_create with expression "*/1 * * * *" and prompt "PONG7742" and recurring true. Confirm briefly.',
);
await session.waitForScreen(
(scr) => scr.includes('Cron: PONG7742'),
'cron notification "Cron: PONG7742"',
90_000,
);
await session.idle(5000);
const finalScreen = await session.screen();
const afterPrompt = finalScreen.slice(
finalScreen.lastIndexOf('Cron: PONG7742'),
);
expect(afterPrompt).toContain('◆');
});
it('user input takes priority over cron', { timeout: 180_000 }, async () => {
session = await InteractiveSession.start({
env: makeEnv(),
args: ['--approval-mode', 'yolo'],
});
await session.send(
'Call cron_create with expression "*/1 * * * *" and prompt "CRONTICK99" and recurring true. Confirm briefly.',
);
await session.waitForScreen(
(scr) => scr.includes('Cron: CRONTICK99'),
'first cron fire "Cron: CRONTICK99"',
90_000,
);
await session.idle(5000);
await session.send('Reply with exactly USERPRIORITY77 nothing else');
await session.waitForScreen(
(scr) => scr.includes('USERPRIORITY77'),
'model response containing USERPRIORITY77',
);
const screen = await session.screen();
expect(screen).toContain('Type your message');
});
it(
'error during cron turn does not kill the loop',
{ timeout: 180_000 },
async () => {
session = await InteractiveSession.start({
env: makeEnv(),
args: ['--approval-mode', 'yolo'],
});
await session.send(
'Call cron_create with expression "*/1 * * * *" and prompt "Read the file /tmp/nonexistent_e2e_99.txt and report its contents. If it does not exist say FILEERR88." and recurring true. Confirm briefly.',
);
await session.waitForScreen(
(scr) => scr.includes('FILEERR88'),
'model reporting FILEERR88 from cron prompt',
90_000,
);
await session.idle(5000);
await session.send('Reply with exactly ALIVE99 nothing else');
await session.waitForScreen(
(scr) => scr.includes('ALIVE99'),
'model response ALIVE99',
);
},
);
});