* fix(diagnostics): kill hung CLI child shell-side with timeout --kill-after
runCommand wraps every diagnostics command in GNU timeout --kill-after
(50s + 5s kill grace), following the provider secret-command pattern.
disconnectSource cannot kill the child, so a hung codexbar diagnose
previously survived the 60s QML timer and the dialog close; the shell
bound sits inside that timer, keeping it the outer bound. Systems
without GNU timeout run the raw command, so diagnostics keep working
without coreutils. --redact and the run-nonce prefix are unchanged.
Evidence: tests/test_diagnostic_command_timeout.py extracts the real
runCommand path with shrunken 0.6s/0.2s bounds. A fake codexbar that
ignores SIGTERM and sleeps is reaped on schedule (exit 124, no /proc
survivor) for both runDiagnostic and runProviderList, and both commands
still exit 0 with timeout absent from PATH. Without the runCommand
hook the same test fails (no timeout in the generated line; hung
children outlive the 10s communicate window).
* test(providers): pin --redact in providerCliCommandText diagnose line
The copied CLI text is pasted into terminals and bug reports, so the
diagnose line must keep --redact. loadProviderSettings already had
coverage; this extends test_providerCliCommandTextListsHelpfulCommands
to assert the diagnose line carries --format json --redact for keyed
and unknown providers.
Evidence: deleting ' --redact' from the providerCliCommandText line
fails ProviderCommandWiring/
test_providerCliCommandTextListsHelpfulCommands (verify() FALSE);
pristine tree passes the full test_secret_prompt_lifecycle suite
(4 tests OK).
* fix(diagnostics): report shell-bound reap as timeout, not exit code
handleDiagnosticData maps a silent 124/137 completion onto the existing
'Diagnostic command timed out. Try again.' message, so the shell-bound kill
reads the same as the 60s QML timer instead of 'codexbar exited with code
124'. stderr precedence is unchanged: any CLI error text still wins.
Status choice, verified against GNU timeout (coreutils 9.11) using the
branch's own TERM-ignoring fake shape: a child reaped by the bound's TERM
reports 124, while a child that ignores SIGTERM survives to the
--kill-after SIGKILL and reports 137 -- the escalation does not preserve
124. Only these two are mapped; any other 128+signal death stays a genuine
CLI failure, since that is the CLI's own crash rather than our bound.
Trade-off: the exit status alone cannot tell a shell-bound reap from a
genuine silent 124/137 on a system without GNU timeout, where the raw
command runs. Mapping both is the behavior that cannot mislead in practice:
124 is timeout's documented status and 137 is SIGKILL, neither of which a
CLI exits silent without cause, while any genuine failure carrying stderr
still reports its own message via the unchanged precedence branch.
Evidence: the new test_shell_bound_reap_surfaces_timeout_message fails on
the unfixed code with 'codexbar exited with code 124' != 'Diagnostic
command timed out. Try again.'; after the fix the full
test_diagnostic_command_timeout module passes (6 tests OK). qmllint clean
on configDiagnostics.qml; check-i18n-catalog passes (reused msgid, no new
string); check-python-lint skipped (pyflakes absent).
* fix(diagnostics): keep shell-bound reap silent with timeout --foreground
CI's sh announces a reaped signalled child with 'Killed' on stderr, so a
SIGTERM-ignoring diagnostics child escalated by timeout --kill-after
surfaced 'Killed' instead of the timeout message: stderr outranks the
124/137 mapping in handleDiagnosticData. The 2a7cf9a claim that the
precedence is harmless there was wrong on announcing shells.
Root cause, verified empirically: without --foreground, --kill-after
escalation SIGKILLs timeout's whole process group, timeout included (a
sibling sleep in the group died and timeout itself died by SIGKILL:
rc=-9 direct, 137-as-signal under a shell). Any shell reaping that
signalled child may announce it on the same stderr the page reads, so
the mapping never sees the empty stderr it requires.
Decision: stop the shell from emitting the notice (the preferred
direction) via timeout --foreground, instead of letting the 124/137
mapping win over stderr. In the foreground the TERM/KILL signals go to
the hung child only, timeout exits 124/137 as a normal status, and no
shell in the chain ever reaps a signalled child -- nothing spurious
reaches stderr on any shell, and no genuine CLI output is discarded.
The mapping-wins alternative was rejected because it drops genuine
partial stderr (a CLI that prints an error and then hangs would lose
its message). The capability probe now also requires --foreground and
falls back to the raw command otherwise, as before. Trade-off, per the
timeout manual: children the CLI itself spawns are no longer timed out
-- accepted because these are leaf JSON dumps, and as a bonus the
escalation can no longer SIGKILL the widget's own process-group
siblings.
Evidence: dash probes show the plain bound group-kills (sibling died,
timeout rc=-9) while the foreground bound exits 137 normally with empty
stderr and the sibling alive; TERM path stays 124 and an ordinary
'exit 3' keeps its stderr text. 'exit 137' is silent on dash and bash,
so announcers (which only fire on signal deaths) stay quiet. The
rewritten test_shell_bound_reap_surfaces_timeout_message runs the live
reap streams back through the real handleDiagnosticData and asserts the
timeout message instead of asserting raw stderr silence; a synthetic
('', 'Killed', 137) completion still maps to 'Killed', proving the test
would catch noise rather than assume silence. A new live fail-mode
test proves an ordinary CLI error passes the bound unchanged.
Checks: make check-python-test_diagnostic_command_timeout (7 tests OK,
also OK with dash as sh), check-changelog, check-i18n-catalog and
check-qml-hardening pass; check-python-lint skipped (pyflakes absent).
Full make check not run per task.4 files changedLine totals unavailable