runtests: ANSI red/green/yellow status indicators for human output
User request after console-context failures went unnoticed: compact
per-file status words, failure/warning list entries, the per-file
had-failures/passed/child-failed notices, the markdown
failures/warnings file lists, the tally Passed/Failed segments and a
coloured overall STATUS on the DONE line are now SGR-coloured (green
pass, bold red fail/error, bold yellow warning).
Gating: -colour auto|on|off (default auto) colours only when stdout is
a real windows console, detected via twapi GetConsoleMode on the
STD_OUTPUT handle - the isatty-equivalent; get_console_handle proved
unsuitable (it succeeds for piped children of console shells, verified
both ways). NO_COLOR honoured in auto. Machine output (RUNTESTS_RESULT,
json) is never coloured; piped output stays plain for agents (verified
zero escape bytes piped, codes present with -colour on). Raw literal
SGR is used rather than a+/a so the runner's indicators are independent
of punk::console's process-global colour state (the same 2026-07-19
console runs showed that state shifting under mixed tty-ness). The
previously UNCONDITIONAL raw-red errorInfo block in the file-error
branch now goes through the same gate, so piped error output is cleaner
than before.
Also sharpens the -jobs guidance wording: subtree runs (e.g 28-file
modules/punk/args/**: ~50s sequential vs ~11s at -jobs 16) benefit from
-jobs - only single-file/few-file runs do not.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
@ -30,6 +30,7 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc
- tcltest compares the `-body` RETURN VALUE against `-result`. The suite convention of accumulating into `$result` via `lappend` works because `lappend` returns the list — but a body whose last command is a loop (`foreach`, `while`) returns the empty string; end such bodies with an explicit `set result`.
- Agent-oriented runner output should use `-report compact -show-passes 0` for focused checks unless detailed Markdown pass lists are needed.
- `-report json` emits a machine-readable final summary, but package-load warnings may still precede it on stdout/stderr, and the punk ANSI output stack may emit an SGR reset immediately before the JSON on the same line (`scriptlib/developer/runtests_parity.tcl` tolerates both).
- Human-facing pass/fail/warning indicators in markdown/compact reports are ANSI-coloured only when stdout is a real windows console (`-colour auto` default: twapi `GetConsoleMode` on the STD_OUTPUT handle as the isatty-equivalent - `get_console_handle` is unsuitable, it succeeds for piped children of console shells; `NO_COLOR` honoured; `-colour on|off` overrides). Machine-facing output (the `RUNTESTS_RESULT` line, json reports) is never coloured, and piped/redirected output stays plain for agents. The runner uses raw literal SGR rather than `a+`/`a` so its indicators are independent of punk::console's process-global colour state.
- ERROR-status failures now surface `errorInfo` (full Tcl error message/stack trace) in markdown, compact, and json reports. Compact mode truncates to a single line.
- FAILED-status failures (result mismatch, not error) now surface `result_was` (actual) and `result_expected` (expected) in markdown, compact, and json reports. Compact mode truncates each to a single line.
- `testsupport/` holds helper `.tcl` files sourced or exec'd by `.test` files or the runner (not discovered as suites). `testsupport/child_test_runner.tcl` is the multi-process-mode bootstrap `runtests.tcl` execs per test file (see the multi-process bullets above). `testsupport/repl_console_driver.tcl` (G-001) is exec'd in a child tclsh by `modules/punk/repl/testsuites/repl/consolebackends.test`: an interactive repl cannot run inside the shared testinterp (the codethread's quit/exit callbacks thread::send to the thread's MAIN interp, bypassing a child testinterp), so repl-through-console verification always drives a child process. `testsupport/wslprobe.tcl` (G-059) provides `::punktest::wsl`: a memoized capability probe yielding the `wsllinux` constraint (default distro launches, answers uname/tool probes, and round-trips a file through a native tempdir - NOT mere wsl.exe existence) plus native-filesystem staging helpers (`staging_create`/`staging_copy_in`/`run_in`/`staging_cleanup`). WSL-gated tests must execute from a staging dir on the distro's native filesystem with the shared `/mnt` path used only for one-way copy-in/out - never operate on the Windows checkout from inside WSL (DrvFs is slow and cross-boundary stat differences make git re-hash its index and fossil see phantom changes). Probe invocations use `wsl -e <cmd>` only (`wsl --status`/`-l` emit UTF-16). Known limitation: a broken-but-present WSL that hangs (rather than errors) on `wsl -e` can stall the probe.
@ -47,7 +48,7 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc
- Add `-slowest <n>` when timing outliers are relevant.
- Multi-process runs use `-tcltestoptions {-singleproc 0}` with otherwise identical flags. Check result parity between modes by capturing `-report json` stdout from each and comparing with `tclsh scriptlib/developer/runtests_parity.tcl <a.json> <b.json>` (ignores timings; exit 0 on parity).
- To watch runs live (including runs launched by agents or other processes, which inherit the environment): set `PUNK_TEST_UDPTEE=41197` and point a UDP listener/viewer at that port. No runner flags needed; results are unaffected.
- For broad runs use `-jobs` - e.g `<tcl_interpreter> src/tests/runtests.tcl -jobs 16 -report compact -show-passes 0`. Reference-machine full-suite scaling (2026-07-19, post G-092 split, result parity verified at each level): sequential ~5m40s, `-jobs 8` ~79-86s, `-jobs 16` ~61s (~5.6x), plateau by `-jobs 24` (~60s) - beyond 16 there is nothing left to overlap: the wall decomposes into the slowest single child (~31s, the runtimecmd pair - single tests, not further splittable), the serial console tail (~16s piped; larger under a real console where those suites actually run), and runner overhead (~10s). `-jobs 16` is the reference-machine sweet spot; focused runs rarely need `-jobs`.
- For broad runs use `-jobs` - e.g `<tcl_interpreter> src/tests/runtests.tcl -jobs 16 -report compact -show-passes 0`. Reference-machine full-suite scaling (2026-07-19, post G-092 split, result parity verified at each level): sequential ~5m40s, `-jobs 8` ~79-86s, `-jobs 16` ~61s (~5.6x), plateau by `-jobs 24` (~60s) - beyond 16 there is nothing left to overlap: the wall decomposes into the slowest single child (~31s, the runtimecmd pair - single tests, not further splittable), the serial console tail (~16s piped; larger under a real console where those suites actually run), and runner overhead (~10s). `-jobs 16` is the reference-machine sweet spot. Use `-jobs` for ANY run spanning more than a handful of test files - subtree runs count (e.g `-include-paths modules/punk/args/**` is 28 files: ~50s sequential vs ~11s at `-jobs 16`); only single-file or few-file runs gain nothing from it.
- Add `-strict-exit 1` when a nonzero shell exit code is needed for failures or parser warnings.
- Capture enough stderr or failure context to identify the failing command or assertion.
- For ERROR-status failures, the markdown report's `errorInfo` block and compact `message=` field carry the full Tcl error message; use `-report markdown` for untruncated context.
set child_exit_failures [list[dict create name $testfile_relative status ERROR errorcode [list CHILDPROCESS exit $child_exitcode] errorinfo $stderr_tail]]
set line "-[dictget$file_summarystatus]`[dictget$file_summarypath]`total=[dictget$file_summarytotal]passed=[dictget$file_summarypassed]skipped=[dictget$file_summaryskipped]failed=[dictget$file_summaryfailed]"
set fstatus [dict get $file_summary status]
set line "-[runtests_clr$fstatus$fstatus]`[dictget$file_summarypath]`total=[dictget$file_summarytotal]passed=[dictget$file_summarypassed]skipped=[dictget$file_summaryskipped]failed=[dictget$file_summaryfailed]"
if{[dict get $file_summary status]eq"warning"}{
set warnings [dict get $file_summary warnings]
if{[llength$warnings]>0}{
@ -1319,7 +1368,7 @@ if {$report_compact} {
}
append line "expected=$first_line"
}
puts stdout $line
puts stdout [runtests_clr fail $line]
}
}
puts stdout ""
@ -1327,7 +1376,7 @@ if {$report_compact} {
puts stdout ""
foreach file_summary $file_summaries{
foreach warning [dict get $file_summary warnings]{