diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml index 97dc84403b..df444bc96d 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml +++ b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md index ccac625171..8659c8a72d 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md +++ b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.md @@ -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. diff --git a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md index fe8a3271b2..19ced1f685 100644 --- a/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md +++ b/.agents/notes/implemented/bug-fix/2026-07-31-fail-loud-releases-the-terminal.zh.md @@ -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` 路径保留其原有断言,确认正常退出时同样会出现该重置序列。 diff --git a/packages/ui/app-boot/src/index.ts b/packages/ui/app-boot/src/index.ts index da824851cc..727c551187 100644 --- a/packages/ui/app-boot/src/index.ts +++ b/packages/ui/app-boot/src/index.ts @@ -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 }