Browse Source

tclsh subcommand review: tclshcmd.test suite + contract doc corrections

- new punkexe suite (17 tests): dispatch contract (script-arg argv0/argv/
  info-script state, honest exit codes, piped no-arg eval, stock arg forms,
  lib: refusal) plus the piperepl launch-state contract on patched kits
  (istty/dorepl/evalinput published, piped script-arg state all-zero,
  unconsumed-piped-input safety default, evalinput opt-in ordering, script
  consuming stdin itself) and the unpatched degraded-mode notice
- hang rule baked in: no test or fixture may set ::tclsh(dorepl) (the
  console-reopen path blocks under a piped harness; G-106-recipe territory)
- patched-kit resolution: env PUNK_PIPEREPL_TEST_EXE, else the punkexe if it
  probes patched, else bin/punk9_beta.exe / punk9bi_beta.exe
- encoding coverage is byte-level (utf-8 vs iso8859-1 fixture; [format %c 233]
  keeps the .test file pure ascii, runner-encoding-proof)
- patches/README.md: corrected the tcl_interactive divergence note (console
  script-arg runs READ 1 where stock reads 0 - piped launches identical; the
  early link stays load-bearing for the piped prompt opt-in) and recorded the
  '? reopen stdin from console' stdout residue as known
- shell/AGENTS.md: tclshcmd bullet; stale runtime.bash names corrected to
  punk-runtime.bash/punk-runtime.cmd

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
e61411c19a
  1. 13
      src/buildsuites/suite_tcl90/patches/README.md
  2. 3
      src/tests/shell/AGENTS.md
  3. 386
      src/tests/shell/testsuites/punkexe/tclshcmd.test

13
src/buildsuites/suite_tcl90/patches/README.md

@ -44,9 +44,16 @@ stock-behaviour escape hatches are `TCLSH_PIPEREPL=0` and the unpatched plain
products. Era files (`tclMain_piperepl.c`, the `.patch`) are kept unmodified as the products. Era files (`tclMain_piperepl.c`, the `.patch`) are kept unmodified as the
recovered 2024 artifacts. Known divergence when the gate is OPEN: `tcl_interactive` recovered 2024 artifacts. Known divergence when the gate is OPEN: `tcl_interactive`
is linked to the internal tty flag BEFORE the startup script runs (stock links it is linked to the internal tty flag BEFORE the startup script runs (stock links it
only when entering the repl loop), so a script that WRITES ::tcl_interactive only when entering the repl loop). Two visible effects: a script that WRITES
influences prompting behaviour in the patched shell where stock would ignore the ::tcl_interactive influences prompting behaviour in the patched shell where stock
write until repl entry - scripts that merely read it see identical values. Note the would ignore the write until repl entry (load-bearing - it is how a piped script
that sets dorepl opts into prompts/echo in the reopened repl); and a script-arg
launch from a CONSOLE READS 1 where stock reads 0 (stock sets it once to
`!path && istty`; the early link overwrites that with the tty flag. Piped launches
read 0 either way, so only console script runs diverge for readers - verified
against punk9_beta 2026-07-23). Known residue: on the eof-with-dorepl path the
patch unconditionally prints "? reopen stdin from console" to STDOUT (debug-era
message); quieting/stderr/env-gating it needs a patch rev + suite rebuild. Note the
punkshell-level analogue for the punk repl is G-038 (piped-to-interactive session punkshell-level analogue for the punk repl is G-038 (piped-to-interactive session
continuity) - this patch is the plain-tclsh/kit-runtime counterpart. continuity) - this patch is the plain-tclsh/kit-runtime counterpart.

3
src/tests/shell/AGENTS.md

@ -20,7 +20,8 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038. - `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038.
- `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk. - `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk.
- punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate -confirm 0` (unattended runs must pass the flag - the REPLACE confirmation aborts fast on non-interactive stdin instead of reading a piped `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`. - punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate -confirm 0` (unattended runs must pass the flag - the REPLACE confirmation aborts fast on non-interactive stdin instead of reading a piped `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`.
- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the runtime.cmd unix payload (`src/scriptapps/bin/runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run. - `testsuites/punkexe/tclshcmd.test` covers the `tclsh` subcommand (punk_main.tcl dispatch): script-file sourcing (argv0/argv/info-script state, honest exit codes incl. explicit exit), piped no-arg stdin evaluation, stock argument forms (leading `-` arg = no script, all args stay in `::argv`; `-encoding name file` sources with the named encoding, incomplete `-encoding` forms fall through to argv; verified byte-level via a utf-8/iso8859-1 fixture), the `lib:` refusal (plain-tclsh semantics - pointer to the `script` subcommand, exit 1), and - on kits built on a TCLSH_PIPEREPL-patched runtime (G-096/G-103, see `src/buildsuites/suite_tcl90/patches/README.md`) - the piperepl launch-state contract: ::tclsh(istty) published, piped script-arg launch state all-zero, unconsumed piped input NOT evaluated unless the script sets `::tclsh(evalinput) 1`, and the script's right to consume stdin itself; plus the degraded-mode notice on unpatched runtimes. Patched-kit resolution: `env(PUNK_PIPEREPL_TEST_EXE)` if it probes as patched, else the punkexe if patched, else `bin/punk9_beta.exe`/`bin/punk9bi_beta.exe`. HANG RULE: no test or fixture may set `::tclsh(dorepl)` - that path reopens stdin from the console (CONIN$) and blocks under this piped harness; console-reopen coverage needs the G-106 hidden-console recipe and is deliberately absent.
- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the punk-runtime.cmd unix payload (`src/scriptapps/bin/punk-runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run.
- `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `<projectroot>/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes. - `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `<projectroot>/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes.
- GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build. - GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build.
- The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset. - The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset.

386
src/tests/shell/testsuites/punkexe/tclshcmd.test

@ -0,0 +1,386 @@
package require tcltest
#Tests for the built punk executable's 'tclsh' subcommand (punk_main.tcl dispatch).
#Covers two layers:
# - dispatch contract (any punk kit): script-file sourcing with argv0/argv/info-script
# state, piped-stdin evaluation when no script argument is given, honest exit codes
# (script error, explicit exit code, piped-input error), stock tclsh argument forms
# (leading '-' arg means no script - all args stay in ::argv; '-encoding name file'
# sources with the named encoding; incomplete -encoding forms fall through to argv),
# and the lib: refusal (scriptlib resolution is a punk facility - the subcommand keeps
# plain tclsh semantics and points at the 'script' subcommand, exit 1).
# - piperepl launch-state contract (kits built on a TCLSH_PIPEREPL-patched runtime,
# G-096/G-103 - see src/buildsuites/suite_tcl90/patches/README.md): ::tclsh(istty)
# published as the launch-time stdin-tty fact, script-arg piped launch state
# (istty 0, tcl_interactive 0, dorepl 0, evalinput 0), unconsumed piped input NOT
# evaluated by default (safety default), ::tclsh(evalinput) opt-in evaluating
# leftover input after the script, and the script's right to consume stdin itself.
#
#HANG SAFETY: no test or fixture ever sets ::tclsh(dorepl) - that is the console-reopen
#path (stdin reopened from CONIN$ at eof), which blocks on a real console and cannot be
#driven by this piped harness (needs the hidden-console/WriteConsoleInput recipe, G-106).
#All children here terminate at stdin EOF by construction; punk_run force-kills on
#timeout regardless.
#
#Dispatch tests target the standard punkexe: env(PUNK_SHELL_TEST_EXE), else
#<projectroot>/bin/punk902z.exe, else <projectroot>/bin/punkshell902 (constraint
#punkexeavailable). Piperepl tests target a patched kit: env(PUNK_PIPEREPL_TEST_EXE)
#if it probes as patched, else the punkexe if patched, else the first patched candidate
#of bin/punk9_beta.exe, bin/punk9bi_beta.exe (constraint pipereplkitavailable; probe =
#piped no-arg run checking [info exists ::tclsh(istty)], the G-103 family_check
#discriminator). Constraint pipereplmissing covers the degraded-mode notice on kits
#whose runtime lacks the patch.
#
#NOTE: exercises BUILT executables - after changing the tclsh dispatch in
#src/vfs/_config/punk_main.tcl, rebuild the kit under test ('make.tcl project' for the
#standard punkexe; family kits like punk9_beta come from src/buildsuites wrap runs) or
#these tests run against a stale binary.
namespace eval ::testspace {
namespace import ::tcltest::*
variable testdir [file dirname [file normalize [info script]]]
#<projectroot>/src/tests/shell/testsuites/punkexe -> 5 levels up to <projectroot>
variable projectroot [file normalize [file join $testdir .. .. .. .. ..]]
variable punkexe ""
if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} {
set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)]
} else {
foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] {
if {[file exists $candidate]} {
set punkexe $candidate
break
}
}
}
testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}]
variable punk_run_timeout_ms 15000
variable runstate
array set runstate {}
proc punk_run_read {chan} {
variable runstate
append runstate(output) [read $chan]
if {[chan eof $chan]} {
chan event $chan readable {}
set runstate(done) eof
}
}
#Run the target executable with args, feed stdin_data via pipe then half-close (EOF).
#Returns dict: timedout 0|1, exitcode <int|kill-info|"">, output <combined stdout+stderr>.
#On timeout the process (and any pipeline members) are forcibly killed so the suite cannot hang.
#No PUNK_PIPE_EOF here - the tclsh subcommand has no eof-policy env; children terminate at
#stdin EOF because nothing in this suite sets ::tclsh(dorepl) (see HANG SAFETY above).
proc punk_run {exe cmdargs stdin_data} {
variable runstate
variable punk_run_timeout_ms
array unset runstate
set runstate(output) ""
set runstate(done) ""
set chan [open |[list $exe {*}$cmdargs 2>@1] r+]
set pids [pid $chan]
chan configure $chan -blocking 0 -translation binary
catch {
puts -nonewline $chan $stdin_data
flush $chan
chan close $chan write ;#half-close - child sees EOF on stdin
}
set timerid [after $punk_run_timeout_ms [list set [namespace current]::runstate(done) timeout]]
chan event $chan readable [list [namespace current]::punk_run_read $chan]
while {$runstate(done) eq ""} {
vwait [namespace current]::runstate(done)
}
after cancel $timerid
chan event $chan readable {}
set timedout [expr {$runstate(done) eq "timeout"}]
set exitcode ""
if {$timedout} {
foreach p $pids {
if {$::tcl_platform(platform) eq "windows"} {
catch {exec {*}[auto_execok taskkill] /F /PID $p}
} else {
catch {exec kill -9 $p}
}
}
catch {close $chan}
} else {
catch {chan configure $chan -blocking 1}
if {[catch {close $chan} errmsg erropts]} {
set ecode [dict get $erropts -errorcode]
switch -- [lindex $ecode 0] {
CHILDSTATUS { set exitcode [lindex $ecode 2] }
default { set exitcode $ecode }
}
} else {
set exitcode 0
}
}
return [dict create timedout $timedout exitcode $exitcode output $runstate(output)]
}
#Classify a kit's tclsh subcommand via a piped no-arg run (terminates at EOF on every
#known dispatch version; never sets dorepl):
# machinery - piperepl-patched runtime, ::tclsh machinery published
# evalok - unpatched runtime, but piped no-arg stdin evaluation works
# noeval - piped stdin was not evaluated (pre-else-branch kit or launch failure)
variable piperepl_probe_script [string cat {puts [list PRPROBE [info exists ::tclsh(istty)]]} \n]
proc piperepl_probe {exe} {
variable piperepl_probe_script
set rd [punk_run $exe [list tclsh] $piperepl_probe_script]
if {[dict get $rd timedout]} {
return noeval
}
if {[string first "PRPROBE 1" [dict get $rd output]] >= 0} {
return machinery
}
if {[string first "PRPROBE 0" [dict get $rd output]] >= 0} {
return evalok
}
return noeval
}
variable punkexe_probe noeval
if {[testConstraint punkexeavailable]} {
set punkexe_probe [piperepl_probe $punkexe]
}
variable pipereplexe ""
if {[info exists ::env(PUNK_PIPEREPL_TEST_EXE)] && $::env(PUNK_PIPEREPL_TEST_EXE) ne ""} {
set cand [file normalize $::env(PUNK_PIPEREPL_TEST_EXE)]
if {[file exists $cand] && [piperepl_probe $cand] eq "machinery"} {
set pipereplexe $cand
}
} elseif {$punkexe_probe eq "machinery"} {
set pipereplexe $punkexe
} else {
foreach cand [list [file join $projectroot bin punk9_beta.exe] [file join $projectroot bin punk9bi_beta.exe]] {
if {[file exists $cand] && [piperepl_probe $cand] eq "machinery"} {
set pipereplexe $cand
break
}
}
}
testConstraint pipereplkitavailable [expr {$pipereplexe ne ""}]
testConstraint pipereplmissing [expr {[testConstraint punkexeavailable] && $punkexe_probe eq "evalok"}]
#fixture scripts (tcltest -tmpdir; forward-slash paths - kit script args need them)
variable fx_dispatch_argv [makeFile {puts [list TARGS argv0tail [file tail $::argv0] argv $::argv scripttail [file tail [info script]] tcli $::tcl_interactive]} dispatch_argv.tcl]
variable fx_dispatch_err [makeFile {puts TCLSHF-BEFORE-ERR
error tclshfile-boom} dispatch_err.tcl]
variable fx_dispatch_exit5 [makeFile {puts TCLSHF-EXIT5-MARK
exit 5} dispatch_exit5.tcl]
variable fx_pr_state [makeFile {puts [list PRSCRIPT istty $::tclsh(istty) tcli $::tcl_interactive dorepl $::tclsh(dorepl) evalinput $::tclsh(evalinput)]} pr_state.tcl]
variable fx_pr_noconsume [makeFile {puts PRNOCON-MARKER} pr_noconsume.tcl]
variable fx_pr_optin [makeFile {puts PROPTIN-MARKER
set ::tclsh(evalinput) 1} pr_optin.tcl]
variable fx_pr_consume [makeFile {set d [read stdin]
puts "PRGOT:[string trim $d]"} pr_consume.tcl]
#encoding fixture written with explicit utf-8 control (runner-encoding-proof): a single
#U+00E9 char as raw utf-8 bytes (0xC3 0xA9) - 1 char sourced via utf-8, 2 via iso8859-1.
#the char is built with [format %c 233] so this .test file stays pure ascii (immune to
#the runner's source encoding).
variable fx_enc_utf8 [file join [temporaryDirectory] enc_utf8.tcl]
apply {{path} {
set fd [open $path w]
chan configure $fd -encoding utf-8
puts $fd "puts ENCLEN=\[string length \"[format %c 233]\"\]"
close $fd
}} $fx_enc_utf8
variable common {
set result ""
}
# -- dispatch contract (any punk kit) ----------------------------------------------------------
#added 2026-07-23 (agent) - tclsh subcommand dispatch contract (script-arg sourcing, piped eval, exit codes)
test tclsh_piped_eval {piped no-arg 'tclsh' evaluates stdin and terminates, exitcode 0}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "puts TCLSHPIPE-M1\nputs TCLSHPIPE-M2\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHPIPE-M1" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "TCLSHPIPE-M2" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
test tclsh_piped_error_exitcode {piped no-arg 'tclsh' input error: error text on output, exitcode 1, no hang}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "error tclshpipe-boom\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "tclshpipe-boom" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1]
test tclsh_piped_not_interactive {piped no-arg 'tclsh' runs with tcl_interactive 0}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "puts \[list TSTATE \$::tcl_interactive\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TSTATE 0" [dict get $rd output]] >= 0}]
} -result [list 0 1]
test tclsh_script_file_argv {'tclsh <file> a b': argv0/info script = file, argv = remaining args, tcl_interactive 0 (piped launch)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_argv
set rd [punk_run $punkexe [list tclsh $fx_dispatch_argv alpha beta] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first {TARGS argv0tail dispatch_argv.tcl argv {alpha beta} scripttail dispatch_argv.tcl tcli 0} [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_script_error_exitcode {'tclsh <file>' script error: prior output present, error text on output, exitcode 1}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_err
set rd [punk_run $punkexe [list tclsh $fx_dispatch_err] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHF-BEFORE-ERR" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "tclshfile-boom" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 1]
test tclsh_script_exitcode {'tclsh <file>' explicit 'exit 5' propagates exitcode 5}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_exit5
set rd [punk_run $punkexe [list tclsh $fx_dispatch_exit5] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHF-EXIT5-MARK" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 5]
# -- stock argument forms: leading dash, -encoding, lib: refusal -------------------------------
#added 2026-07-23 (agent) - stock leading-dash/-encoding argument forms + lib: pointer refusal (tclsh subcommand)
test tclsh_dash_args_to_argv {leading '-' arg is not a script: all args stay in ::argv, stdin evaluated (stock parity)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh - hmm etc blah] "puts \[list DASHARGS \$::argv\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "DASHARGS {- hmm etc blah}" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_encoding_applied {'-encoding name file' form: the named encoding is honoured when sourcing}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_enc_utf8
set a [punk_run $punkexe [list tclsh -encoding utf-8 $fx_enc_utf8] ""]
set b [punk_run $punkexe [list tclsh -encoding iso8859-1 $fx_enc_utf8] ""]
lappend result [expr {[string first "ENCLEN=1" [dict get $a output]] >= 0}]
lappend result [expr {[string first "ENCLEN=2" [dict get $b output]] >= 0}]
lappend result [dict get $a exitcode]
lappend result [dict get $b exitcode]
} -result [list 1 1 0 0]
test tclsh_encoding_args_passed {'-encoding name file a b': script argv0/argv state as with the plain file form}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
variable fx_dispatch_argv
set rd [punk_run $punkexe [list tclsh -encoding utf-8 $fx_dispatch_argv alpha beta] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first {TARGS argv0tail dispatch_argv.tcl argv {alpha beta} scripttail dispatch_argv.tcl tcli 0} [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_encoding_incomplete_form {'-encoding name' with no script name following: all args stay in ::argv (stock parity)}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh -encoding utf-8] "puts \[list ENCARGV \$::argv\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "ENCARGV {-encoding utf-8}" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_lib_pointer {'tclsh lib:...' refuses with a pointer to the script subcommand, exit 1, nothing sourced}\
-constraints punkexeavailable -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh lib:_punktest/echo alpha] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "not supported by the tclsh subcommand" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "script lib:_punktest/echo" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "PUNKTEST_ECHO" [dict get $rd output]] < 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 1 1]
# -- piperepl launch-state contract (patched runtime kits) -------------------------------------
#added 2026-07-23 (agent) - piperepl ::tclsh launch-state contract via the tclsh subcommand (G-096/G-103 patched kits)
test tclsh_pr_noarg_machinery {piped no-arg run publishes ::tclsh machinery: istty 0, dorepl 0, evalinput 0}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
set rd [punk_run $pipereplexe [list tclsh] "puts \[list PRSTATE \[info exists ::tclsh(istty)\] \$::tclsh(istty) \$::tclsh(dorepl) \$::tclsh(evalinput)\]\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRSTATE 1 0 0 0" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_pr_scriptarg_state {piped 'tclsh <file>' launch state: istty 0, tcl_interactive 0, dorepl 0, evalinput 0}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_state
set rd [punk_run $pipereplexe [list tclsh $fx_pr_state] ""]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRSCRIPT istty 0 tcli 0 dorepl 0 evalinput 0" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
test tclsh_pr_unconsumed_not_evald {piped input a script did not consume is NOT evaluated by default (safety default)}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_noconsume
set rd [punk_run $pipereplexe [list tclsh $fx_pr_noconsume] "puts PRSHOULDNOTEVAL\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRNOCON-MARKER" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "PRSHOULDNOTEVAL" [dict get $rd output]] < 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
test tclsh_pr_evalinput_optin {script setting ::tclsh(evalinput) 1: leftover piped input evaluated after the script}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_optin
set rd [punk_run $pipereplexe [list tclsh $fx_pr_optin] "puts PREVALLED\n"]
set out [dict get $rd output]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PROPTIN-MARKER" $out] >= 0}]
lappend result [expr {[string first "PREVALLED" $out] >= 0}]
#leftover input runs AFTER the script completes
lappend result [expr {[string first "PROPTIN-MARKER" $out] < [string first "PREVALLED" $out]}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 1 0]
test tclsh_pr_script_consumes_stdin {the script may read piped stdin itself; consumed data is not evaluated}\
-constraints pipereplkitavailable -setup $common -body {
variable pipereplexe
variable fx_pr_consume
set rd [punk_run $pipereplexe [list tclsh $fx_pr_consume] "pr-hello-data\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "PRGOT:pr-hello-data" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 0]
# -- degraded mode (kit runtime without the piperepl patch) ------------------------------------
#added 2026-07-23 (agent) - degraded-mode contract: unpatched runtime still evaluates piped input, with a patch-missing notice
test tclsh_unpatched_notice_and_eval {unpatched runtime: piped no-arg eval still works, with a piperepl notice on stderr}\
-constraints pipereplmissing -setup $common -body {
variable punkexe
set rd [punk_run $punkexe [list tclsh] "puts TCLSHUNP-MARK\n"]
lappend result [dict get $rd timedout]
lappend result [expr {[string first "TCLSHUNP-MARK" [dict get $rd output]] >= 0}]
lappend result [expr {[string first "piperepl" [dict get $rd output]] >= 0}]
lappend result [dict get $rd exitcode]
} -result [list 0 1 1 0]
}
tcltest::cleanupTests ;#needed to produce test summary.
Loading…
Cancel
Save