qwen-code/integration-tests/renderer-matrix.test.ts
ChiGao cd1ac72874
feat(opentui): bundle assets, CI matrix and parity tooling (Batch 7) (#10770)
* feat(opentui): add the build, CI and parity tooling for the renderer migration

Batch 7 of the ink→OpenTUI migration (#8662): everything the renderer
needs to be built, verified and shipped — ink stays the default.

Bundled builds silently lose code-block syntax highlighting because
@opentui/core resolves its tree-sitter assets package-relatively, which
misses inside the esbuild bundle. Relocate the runtime assets (parser
worker, grammar wasms/scm, web-tree-sitter, native render library) next
to the bundle, and gate OTUI_ASSET_ROOT on the tree being complete for
the running platform — @opentui/core throws on any missing key, so an
incomplete relocation must fall back instead of half-configuring.
Standalone archives gain a Bun runtime (the renderer needs bun:ffi) and
per-target @opentui platform packages via the existing release
packaging, with --runtime=node restoring classic packaging.

The renderer matrix drives interactive E2E under node+ink and bun+opentui,
but Batch 6's renderer gate silently falls back to ink on an unsupported
runtime, so a green opentui leg could be a false green. QWEN_TUI_RENDERER_STRICT
turns that fallback into a loud startup failure for the matrix legs only;
user-facing default behavior is unchanged.

Parity tooling: a tui-parity workflow with component snapshots and a
no-flicker gate (zero full-screen clears, balanced DEC 2026) that runs
offline without model credentials, an ink→opentui codemod with a
self-test, the PTY/tmux comparison harnesses, and the cli scripts/ tree
is now covered by the package typecheck.

* fix(ci): build all packages in the tui-parity jobs

With prepare skipped, npm ci leaves the CLI's workspace dependencies
unbuilt, and a workspace-scoped build of the CLI alone resolves their
types from a dist that does not exist yet. The parity jobs must run the
repository-root build like the other E2E legs.

* fix(build): enforce renderer strict mode and drop musl from standalone packages

Address yiliang114's review on the Batch 7 build/CI work:

- propagate QWEN_TUI_RENDERER_STRICT into RendererSelection so the
  dispatcher also fails loudly when the OpenTUI entry returns false or
  throws, closing the false-green path in the E2E renderer matrix
- stage the OpenTUI platform packages only for the bun runtime and drop
  the -musl variants: the bundled Bun binaries are glibc-linked and
  cannot start on musl hosts, so the musl render packages claimed
  support the archives cannot deliver
- wipe dist/opentui-assets before re-copying so a stale tree from an
  earlier bundle cannot satisfy the key-existence runtime gate
- route the script-PTY capture branch through spawnScriptPty so the
  argv form follows the platform, and give it the destroy() that
  finish() expects from the node-pty-like interface
- harden pty-e2e.sh (set -euo pipefail, mktemp+trap, full TCL escaping
  of the prompt) and tmux-compare.sh (missing-binary check, mktemp+trap)
- pin the CI Bun setups to 1.3.14 = DEFAULT_BUN_VERSION so an upstream
  Bun release cannot flip the gating legs on its own

* fix(build): keep classic Node packaging as the standalone default

The temporary OpenTUI preview flavor (--runtime=bun) stays available but
no longer replaces the default release archives, keeping Batch 7 additive
for existing standalone users.

* fix(scripts): compare prettier-normalized output in the codemod self-test

Address F1 and F3 from the deep verification report:

- The self-test compared the codemod's raw output against the
  prettier-formatted fixtures/after.tsx, so 2 of 17 tests failed as
  committed. Normalize both sides through the repo prettier config
  before comparing (tests 1 and 6); the harness awaits because
  prettier 3's format() is async-only.
- Refresh the tui-parity.yml baseline entry from 2190 to the measured
  2508 bytes so the ratchet tracks real growth.

* fix(build): gate standalone opentui assets by runtime flavor

Node archives carry no opentui-assets tree at all: the renderer needs
bun:ffi, and cross-built archives would only hold another platform's
native library that the all-or-nothing asset gate could never activate.
Bun archives now prune every @opentui/core-* directory that does not
belong to the build target, dropping the build host's library from the
archive. The npm-package allowlist comment is corrected to match the
observed fallback behavior.

* test(cli): cover the OpenTUI strict-mode dispatch and e2e renderer matrix

main() now has coverage for the four dispatch outcomes: strict mode
throws both when the OpenTUI entry declines to start and when it boots
with an error, and non-strict mode falls through to startInteractiveUI.
resolveE2eCliCommand gains coverage for the opentui leg — bun on PATH
resolves to bun, and a missing bun fails with the actionable error.
2026-09-02 08:36:20 +00:00

80 lines
2.2 KiB
TypeScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, describe, expect, it, vi } from 'vitest';
import {
E2E_RENDERER_ENV_VAR,
e2eRendererEnv,
pickE2eRenderer,
resolveE2eCliCommand,
} from './renderer-matrix.js';
const mockSpawnSync = vi.hoisted(() => vi.fn());
vi.mock('node:child_process', async (importOriginal) => {
const actual = await importOriginal<typeof import('node:child_process')>();
return {
...actual,
spawnSync: mockSpawnSync,
};
});
describe('renderer-matrix', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('defaults to ink so node-only runners keep their behavior', () => {
expect(pickE2eRenderer({})).toBe('ink');
expect(pickE2eRenderer({ [E2E_RENDERER_ENV_VAR]: '' })).toBe('ink');
expect(pickE2eRenderer({ [E2E_RENDERER_ENV_VAR]: 'garbage' })).toBe('ink');
});
it('opts into opentui via QWEN_E2E_RENDERER', () => {
expect(pickE2eRenderer({ [E2E_RENDERER_ENV_VAR]: 'opentui' })).toBe(
'opentui',
);
// Whitespace and case are tolerated — humans type these by hand.
expect(pickE2eRenderer({ [E2E_RENDERER_ENV_VAR]: ' OpenTUI ' })).toBe(
'opentui',
);
});
it('pins the renderer through the product env var', () => {
expect(e2eRendererEnv('ink')).toEqual({ QWEN_TUI_RENDERER: 'ink' });
expect(e2eRendererEnv('opentui')).toEqual({
QWEN_TUI_RENDERER: 'opentui',
QWEN_TUI_RENDERER_STRICT: '1',
});
});
it('uses node for ink', () => {
expect(resolveE2eCliCommand('ink')).toBe('node');
expect(mockSpawnSync).not.toHaveBeenCalled();
});
it('uses bun for opentui when bun is on PATH', () => {
mockSpawnSync.mockReturnValueOnce({ error: null, status: 0 });
expect(resolveE2eCliCommand('opentui')).toBe('bun');
expect(mockSpawnSync).toHaveBeenCalledWith(
'bun',
['--version'],
expect.anything(),
);
});
it('fails with an actionable error for opentui without bun', () => {
mockSpawnSync.mockReturnValueOnce({
error: new Error('spawn bun ENOENT'),
status: null,
});
expect(() => resolveE2eCliCommand('opentui')).toThrow(
/QWEN_E2E_RENDERER=opentui requires bun/,
);
});
});