Files
vue-tui/packages/runtime-tests/vitest.pty.config.ts
T
Yunfei He 216a7021a0 fix(runtime): restore terminal on signal exit via signal-exit (Ink parity, G18, HIGH) (#47)
* fix(runtime): restore terminal on signal exit via signal-exit (Ink parity, G18)

Previously nothing routed a process signal to teardown(): SIGINT-as-signal,
SIGTERM or SIGHUP killed the process with the cursor hidden, the alternate
screen active and raw mode on, leaving the terminal corrupted.

Mirror Ink (ink.tsx:426): register signal-exit's onExit(teardown,
{alwaysLast:false}) at interactive mount, storing the unsubscribe fn, and
call it first thing in teardown() (ink.tsx:765) so the handler is removed on
unmount()/exit() and can't leak or double-run. teardown() stays idempotent
(teardownStarted guard) so a signal-triggered teardown plus a later unmount
won't double-run, and we don't prevent the process from exiting. Only the
live interactive, non-debug mount registers — render-to-string /
non-interactive paths never touch process signal handlers; registration is
guarded against double-registration.

Uses signal-exit v4 (named onExit export; ships ESM + types, so no
@types/signal-exit needed). PTY test sends SIGINT/SIGTERM/SIGHUP to a mounted
alt-screen app and asserts the captured output ends with show-cursor
(\x1b[?25h) + leave-alt-screen (\x1b[?1049l).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Review follow-ups (3 fixes): register signal-exit whenever interactive
(drop the !debug gate so debug-but-interactive apps, which still enter the
alt-screen/hide the cursor, restore on signal — Ink ink.tsx:426); add
!teardownStarted to the registration so a spent app instance does not
re-register on a same-instance remount (the next unmount() returns early at
the teardownStarted guard before it could unsubscribe — a leak); and make
the PTY test prove the SIGNAL drove teardown (fixture never self-unmounts, so
restore bytes can only come from the signal path) with a debug-mode signal
test, an exit-anchored waitForOutput drain, and a bounded retry for the
async-flush race under saturated runners.

Review follow-ups (2 fixes): synchronous restore flush on signal — the
signal-exit teardown path now writes the restore escapes (show-cursor,
leave-alt-screen, disable-kitty) via fs.writeSync to the stdout fd so they
reach the terminal before signal-exit re-raises the signal (a buffered async
stream.write could be lost on abrupt exit); the normal unmount path keeps async
writes. Removed the config-wide retry:3 from vitest.pty.config.ts (it masked
the whole PTY suite) and scoped a retry:2 to the signal-teardown describe only,
for the residual parent-side node-pty onData read-race under a saturated runner.

* chore(parity): ledger — G18 pr-open

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 06:04:51 +08:00

38 lines
2.2 KiB
TypeScript

import { defineConfig } from "vite-plus";
import vueJsx from "@vitejs/plugin-vue-jsx";
export default defineConfig({
plugins: [vueJsx()],
test: {
include: ["integration/pty/**/*.test.{ts,tsx}"],
// node-pty requires child_process.fork(), not worker_threads, so the pool
// MUST be "forks". Each test spawns its own isolated PTY subprocess (no
// shared ports/files/global state), so test files parallelize safely across
// forked workers — ~3x faster than serial (the PTY suite is the CI's
// wall-clock bottleneck). testTimeout stays generous (15s vs ~2s slowest
// test) to absorb CPU contention on smaller CI runners.
pool: "forks",
// Files parallelize across forked workers (~3x faster than serial — the PTY
// suite is the CI bottleneck). Tests within a file run SERIALLY: many of
// these assert timing-sensitive render/commit counts driven by the ~32ms
// commit throttle, and in-file concurrency starves them of wall-clock on a
// 4-core CI runner (it passes on higher-core dev machines, which is the
// trap). File-level parallelism is the proven, stable win.
fileParallelism: true,
testTimeout: 15000,
// NO config-wide retry: it would mask flakiness/real regressions across the
// WHOLE PTY suite. The signal-teardown tests previously needed retry because
// the restore bytes were written with an async stream.write() that could lose
// the race against signal-exit's immediate re-raise under a saturated runner.
// That is now fixed at the source (render.ts/kitty-keyboard.ts Finding A):
// the signal path writes the restore escapes synchronously (fs.writeSync), so
// they reach the fd before the process dies and the tests pass deterministically
// without any retry. If a genuine parent-side onData read-race ever resurfaces,
// scope a retry to that suite/test only (e.g. `test(name, { retry: 2 }, fn)`).
// CI:"false" so the runner's CI=true doesn't flip interactive detection off
// for any in-process render tests under this config (the PTY child helpers
// set it per-spawn, but vitest-level tests need it too).
env: { FORCE_COLOR: "3", CI: "false" },
},
});