docs(exit-cause): pin why isProvenProcessExit(0) must stay true

isProvenProcessExit asks whether the process ended; the cause resolvers ask
why. Their disagreement on 0 is the design, not a defect: login(1) wraps
every macOS local PTY once the TCC preflight passes, so routing
hostReportsChildExitStatus through the predicate would leave every pane a
user closed with `exit` mounted forever.

No behavior change; comment and regression tests only.
This commit is contained in:
Neil
2026-09-01 07:28:39 -07:00
parent 5aa02ead59
commit cbd2b483ab
2 changed files with 35 additions and 0 deletions
+18
View File
@@ -109,4 +109,22 @@ describe('isProvenProcessExit', () => {
// A host shutdown can deliver -1 for every PTY without proving process death.
expect(isProvenProcessExit(-1)).toBe(false)
})
it('answers whether the process ended, not why — so it may differ from the cause on zero', () => {
// Deliberate, not an oversight: an unknowable *reason* is not an unknown
// *fact of death*. Collapsing these would strand every cleanly-exited pane.
expect(resolveUnreportedExitCause(0)).toEqual({ kind: 'unknown', reason: 'cause_unreported' })
expect(isProvenProcessExit(0)).toBe(true)
})
it('keeps a shell that a user closed with `exit` tearing down on macOS', () => {
// login(1) wraps every macOS local PTY once the TCC preflight passes, so
// hostReportsChildExitStatus is false for essentially all of them. login
// forks the shell and waits, so its exit still proves the shell died —
// routing that evidence through here would leave the pane mounted forever.
expect(
resolveProcessExitCause({ exitCode: 0, signal: 0, hostReportsChildExitStatus: false }).kind
).toBe('unknown')
expect(isProvenProcessExit(0)).toBe(true)
})
})
+17
View File
@@ -108,6 +108,23 @@ export function isDeliberateTerminalExit(cause: TerminalExitCause): boolean {
* Negative codes are synthetic stop sentinels; they mean that the host lost
* contact before it could vouch for the child, so downstream cleanup must use
* the `unverifiable` path instead of treating the tab as exited.
*
* This asks *whether* the process ended; the cause resolvers above ask *why*.
* The two deliberately disagree about `0`, and that is not a defect:
*
* - `resolveUnreportedExitCause(0)` is `unknown` because a bare zero cannot
* distinguish a clean finish from a signal — an unknowable **reason**.
* - `isProvenProcessExit(0)` is `true` because a host only forwards a
* non-negative code after its provider observed the process end — a known
* **fact of death**, whatever the reason.
*
* Do not route `hostReportsChildExitStatus` or `signal` through here to
* "narrow" the zero. `login(1)` wraps every macOS local PTY once the TCC
* preflight passes, so `hostReportsChildExitStatus` is false for essentially
* all of them (macos-tcc-login-shell.ts) — yet login forks the shell and waits,
* so its own exit *is* evidence the shell died. Treating those as unproven
* would strand every macOS pane that a user closed with `exit`, which is the
* mirror-image regression of reporting a lost host as exited.
*/
export function isProvenProcessExit(exitCode: number): boolean {
return resolveProcessExitCause({ exitCode }).kind !== 'unknown'