docs(app-boot): correct the pre-fix capture claim and pin the exit seam contract

The PTY capture does continue past the terminal-takeover bytes with the
fatal diagnostic; only the reset never follows. State that precisely in
both notes.

Document on FailLoudProcess.exit that callers treat it as the end of the
run, matching how the release path already relies on it.
This commit is contained in:
Turtle
2026-07-31 20:29:06 +08:00
parent b35b06396d
commit b4f1675360
4 changed files with 9 additions and 4 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md
2026-07-31-fail-loud-releases-the-terminal.md: ccac625171ef5523a4ed27843b543c838bf43ce8
2026-07-31-fail-loud-releases-the-terminal.zh.md: fe8a3271b26a94b9d986b8d17744893e5f448593
2026-07-31-fail-loud-releases-the-terminal.md: 8659c8a72dbb25cceaccbb0fb99b8b0251e1d506
2026-07-31-fail-loud-releases-the-terminal.zh.md: 19ced1f685c8719a652ebabd27ac199519b09369

View File

@@ -54,6 +54,6 @@ The guarantee belongs to whichever bin owns the terminal: a surface that grabs t
`packages/ui/app-boot/tests/app-boot.spec.ts` covers the release contract: the hook is awaited before the exit commits, a rejecting hook still exits 1, a never-settling hook exits after `FAIL_LOUD_RELEASE_TIMEOUT_MS`, and a burst of rejections reports only the first while the release still completes.
Those fake-process tests cannot observe the two failure modes that matter most — process exit code with a real event loop, and terminal state after exit — so the regression lives in `apps/cli/tests/tui-keyless-smoke.e2e.ts`. It boots the shipped tree in a real PTY over `fixtures/tui-invalid-provider.cordis.yml` (a list-shaped `providers`, the mistake users actually make), expects exit 1, and asserts the captured bytes contain both the diagnostic and `ESC[?2004l`. Against the pre-fix source the captured stream ends at `ESC[?2004h ESC[>7u ESC[?u ESC[c` with no reset, and the case fails on that assertion.
Those fake-process tests cannot observe the two failure modes that matter most — process exit code with a real event loop, and terminal state after exit — so the regression lives in `apps/cli/tests/tui-keyless-smoke.e2e.ts`. It boots the shipped tree in a real PTY over `fixtures/tui-invalid-provider.cordis.yml` (a list-shaped `providers`, the mistake users actually make), expects exit 1, and asserts the captured bytes contain both the diagnostic and `ESC[?2004l`. Against the pre-fix source the capture still shows the terminal being taken (`ESC[?2004h ESC[>7u ESC[?u ESC[c`) and the diagnostic printed, but no reset ever follows, and the case fails on the `ESC[?2004l` assertion alone.
Testing policy requires a PTY case whenever terminal teardown changes, and this is it. The `/exit` path keeps its existing assertion that the same reset appears on a clean exit.

View File

@@ -54,6 +54,6 @@ Loader 并发挂载各个条目,因此条目失败的顺序并不等于启动
`packages/ui/app-boot/tests/app-boot.spec.ts` 覆盖 release 契约:退出提交前会等待该回调;回调 rejection 时仍退出 1永不结算的回调会在 `FAIL_LOUD_RELEASE_TIMEOUT_MS` 后退出;以及一连串 rejection 只报告第一个,同时 release 仍能跑完。
这些基于假进程的测试无法观测到最关键的两种失败形态——真实事件循环下的进程退出码,以及退出之后的终端状态——因此回归用例放在 `apps/cli/tests/tui-keyless-smoke.e2e.ts`。它在真实 PTY 中以 `fixtures/tui-invalid-provider.cordis.yml``providers` 为列表形状,正是用户真实会犯的错误)启动出厂配置树,期望退出码为 1并断言捕获到的字节流同时包含诊断信息与 `ESC[?2004l`。在修复前的源码上,捕获流止于 `ESC[?2004h ESC[>7u ESC[?u ESC[c`没有任何重置,该用例正是在这条断言上失败。
这些基于假进程的测试无法观测到最关键的两种失败形态——真实事件循环下的进程退出码,以及退出之后的终端状态——因此回归用例放在 `apps/cli/tests/tui-keyless-smoke.e2e.ts`。它在真实 PTY 中以 `fixtures/tui-invalid-provider.cordis.yml``providers` 为列表形状,正是用户真实会犯的错误)启动出厂配置树,期望退出码为 1并断言捕获到的字节流同时包含诊断信息与 `ESC[?2004l`。在修复前的源码上,捕获内容仍能看到终端被接管(`ESC[?2004h ESC[>7u ESC[?u ESC[c`)以及诊断信息被打印,但其后始终没有任何重置序列,该用例仅在 `ESC[?2004l` 这条断言上失败。
测试规范要求:只要改动终端拆卸,就必须有 PTY 用例——这就是它。`/exit` 路径保留其原有断言,确认正常退出时同样会出现该重置序列。

View File

@@ -295,6 +295,11 @@ export interface FailLoudProcess {
on(event: 'unhandledRejection', handler: (err: unknown) => void): unknown
off(event: 'unhandledRejection', handler: (err: unknown) => void): unknown
stderr: { write(chunk: string): unknown }
/**
* Terminate the process. Callers treat this as the end of the run, as
* `process.exit` is; a fake that returns lets the caller continue, which only
* a test observes.
*/
exit(code: number): void
}