Browse Source

shell tests: E3 piped-session proof of n/ namespace continuity and subshell carry-over (shellnavns.test)

New shell/testsuites/punkexe/shellnavns.test - one hang-proof punk_run piped
session against the built executable in src launch mode (dev modules, so
current punk::repl/nav source is exercised without a rebuild):

PROVEN end-to-end:
- n/ sets ::punk::nav::ns::ns_current in the running shell
- the current namespace continues into `subshell punk` and back out, and the
  0.5.2 stray-namespace seeding fix holds live (no phantom ns_current
  namespace in the subshell context)
- navigation inside the subshell takes effect; the session exits cleanly
  (exitcode 0, no timeout, exit unwinds subshell -> parent -> terminate)

FINDINGS from the E3 probes (pinned in the test with markers):
- the FIRST subshell evaluates in the SAME code interp as the parent shell -
  the "first subshell asymmetry" already TODO-documented in repl::init.
  Continuity therefore manifests via shared state at shell level (SHARED
  marker: a variable set in the subshell is visible to the parent after
  exit, and the parent adopts the subshell''s final namespace). The
  repl::start seeding template - the carry-over mechanism for genuinely
  separate code interps - is proven at module level in punk/repl
  nscurrent.test. A G-002-style isolated subshell would flip these pins.
- KNOWN GAP: piped-session submissions evaluate at :: even after n/ sets
  ns_current - the inscope application proven interactively and at module
  level does not take effect for subsequent piped submissions in this spawn
  shape. Root cause undetermined (piped-path evaluation vs kit/codethread
  interplay); flagged for investigation alongside G-038 session-context
  work.

Verified: shell/** suite 42 total / 40 passed / 0 failed (2 gated skips)
under Tcl 9.0.3. AGENTS updated (shell contracts bullet; punk/repl module
index E3 reference).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 day ago
parent
commit
2b46e2d86a
  1. 2
      src/tests/modules/AGENTS.md
  2. 1
      src/tests/shell/AGENTS.md
  3. 196
      src/tests/shell/testsuites/punkexe/shellnavns.test

2
src/tests/modules/AGENTS.md

@ -42,7 +42,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
- `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify), grepstr characterization (`grepstr.test`: return modes incl summarydict (linemap pinned as always-present - the -help says -n-only, reconciliation deferred to the planned hygiene pass), exact highlight SGR wrapping, -n line numbering, invert + empty-highlight strip, -C context/breaks, capture groups, and the tab deficiency: warns once per call on stderr, single-pass tab line survives - the multi-pass mangling is pinned at consumer level in punk/ns corp.test), and untabify characterization (`untabify.test`: -stops int/list/terminal, -with spaces/unicode/custom-pair, multiline, errors, plus the EXPERIMENTAL -plastic elastic-tabstop mode deliberately pinned-as-interim and retained for possible repl editbuf use). Console queries (get_tabstops/get_size + punk::console::tabwidth) are mocked per the overtype renderline.test pattern - they emit live terminal queries that block/error headless. ANSI codes in these tests are literal escape strings so results are colour-state independent
- `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, synopsis display characterization (`synopsis.test`: basic italic argname/`<type>` styling, longopt `--x=` alias forms, literal/literalprefix/stringstartswith/stringendswith type-alternates rendering unitalicised, option alternate parenthesization, multi-element clause display incl `?type?` members and argname tail-word hints, `-typesynopsis` value-element lists and option passthrough incl documenter ANSI, and the small-restricted-choice-set literal rule: 1-3 restricted choices render as unitalicised `|`-joined literals in leader/option/value positions with choicegroups counted, >3 or `-choicerestricted 0` falling back to italics, `-typesynopsis` taking precedence), usage-marking characterization (`usagemarking.test`: -parsedargs/-badarg/-parsestatus/-scheme marking primitives plus goodchoice highlighting of selected/default-in-effect choice words, asserted by SGR-parameter subset against the live colour arrays; the G-049 nocolour/colour-leak GAP pins flipped 2026-07-10 to scheme-statelessness assertions), the G-049 parse-status structure (`parsestatus.test`: punk::args::parse_status overall/per-argument statuses, badarg for type/allocation failures, -caller attribution, errorcode -argspecs stripping), -parsekey characterization (`parsekey.test`: result/received/solos/multis keying, shared-key required satisfaction and defaults, mash-path and prefix-abbreviation keying, plus GAP pins for last-defined-member default precedence, cross-member -multiple value loss, parsekey/optname collision conflation, and values/leaders parsekey breakage - desired-behaviour pins disabled behind punkargsKnownBug in `testsuites/dev/parsekey-knownbugs.test`), and tclcore doc/interpreter behavioural parity (`tclcoreparity.test`, G-054, gated on have_tclcoredocs: 'string is' class choices equal the live-harvested set, per-class docids exist, error-vs-ok agreement across the probe matrix, version-note labels conditional on class presence - expectations derived from the running interpreter, green on 8.6/8.7/9.0; under 8.6 run the file directly via a plain tclkit + tcltest driver since runtests' harness needs newer infrastructure)
- `punk/nav/ns/` — punk::nav::ns tests (`testsuites/nav/navns.test`): the n/ n// n/// navigation state machine (ns/ transitions absolute/relative/glob-no-nav, failed-nav state preservation, quad-colon normalization, v-form content selection, ensemble annotation) and the ::punk::nav::ns::ns_current variable contract the repl/codethread/subshell seeding all consume; display content is covered in punk/ns nslist.test
- `punk/repl/` — punk::repl tests (`testsuites/repl/`): opunk console backend integration (`consolebackends.test`) and repl current-namespace retention (`nscurrent.test`: real codethread via repl::init driven by synchronous runscript sends - inscope evaluation of ns_current, retention across submissions, n/-navigation retained, auto-create-with-notice for missing namespaces, the 2026-07-14 stray-namespace seeding fix pinned behaviourally plus a source-text guard on repl::start's inline template; end-to-end piped subshell carry-over (E3) remains a planned shell-level test)
- `punk/repl/` — punk::repl tests (`testsuites/repl/`): opunk console backend integration (`consolebackends.test`) and repl current-namespace retention (`nscurrent.test`: real codethread via repl::init driven by synchronous runscript sends - inscope evaluation of ns_current, retention across submissions, n/-navigation retained, auto-create-with-notice for missing namespaces, the 2026-07-14 stray-namespace seeding fix pinned behaviourally plus a source-text guard on repl::start's inline template; the end-to-end piped subshell session is covered at shell level by shell/testsuites/punkexe/shellnavns.test - which found the first-subshell shared-code-interp asymmetry and the piped-inscope gap recorded there)
- `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity), n/ display machinery characterization (`nslist.test`: tier A get_ns_dicts classification buckets as the machine contract for display reworks - incl package tail/prefix derivation, alias edge cases, usageinfo scan-dependence; tier B per-element layout-agnostic marking - underline/underdouble/underdotted namespace package styles, command type tag colours, exported/imported markers, the punkargs doc icon; tier C REWORK-flagged pins of the current hardcoded 2-col/4-col layout and nspath subtables, to flip deliberately with the planned punk-tables/width-responsive rework; plus the KNOWN QUIRK pin that bare nslist without -types errors with a malformed message), corp proc-retrieval and syntax/untabify interplay (`corp.test`: name edge cases, -ranges/-n line handling, basic-highlight ansistrip equivalence, -untabify spaces/unicode tab-free output, the KNOWN-DEFICIENCY pin for default -untabify none on tabbed bodies - grepstr warns per pass and brace overlays mangle tabbed lines, deterministic under mocked console tabstops - and a ::tcl::CopyDirectory -untabify spaces smoke test; precursor coverage for the planned punk::ns hygiene pass), cmdtrace characterization (`cmdtrace.test`: -pause 0 non-interactive runs, linedict line-mark keys for flat and 2-word-form nested switches as correct-mark guards, and GAP pins for the upstream nested-switch mismark - core.tcl-lang.org tktview 5d5b1052280c976ea3d4, arm bodies whose split-list index lands on a literal switch-command word report container-relative lines; mark tests gated on have_tclcoredocs because cmdtrace's arm-offset correction parses against the ::switch argdoc; plus the fixed-canary asserting punk::lib::check::has_tclbug_nestedswitch_tracelines still reports the bug - a live behavioural probe, so a fixed Tcl release fails the canary first and triggers the documented flip workflow), and cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); remaining GAP pins for pseudo-command cmdtype + space-form docid prefixes (G-051, real `string is` pins behind the have_tclcoredocs constraint), TclOO undocumented-method fallback (G-052), and synopsis marking absence (G-050))
- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), punk::mix::commandset::loadedlib tests (`testsuites/loadedlib/libsearch.test`: 'dev lib.search' match semantics via -return list — wrap-glob default, =exact prefix, case rules, explicit globs, version aggregation — plus the loadedlib 0.2.0 contract: deep discovery by default (deep .tm modules found without -refresh, registration persists), -refresh = genuine re-scan (epoch incr + rediscovery picks up .tm files added to already-scanned dirs), and highlight working without the shell-global a+ alias; shared provisioned child interp sourcing the source-tree libunknown directly — see the file's ORDERING NOTE), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix or via the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl` - staged to the WSL distro's native filesystem, G-059)
- `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} <prefix>` ifneeded mappings; no-op without a baseline), and the repl command-completeness engine (`commandcomplete.test`: punk::lib::system::incomplete pending-opener stacks - the info-complete quoting quirk progression (`set x "{*}{"` standalone vs in-proc-body), single openers, tabs, escapes, incomplete<->info-complete parity property; pre-repl-refactor characterization, see goals/G-044 detail preserve-list)

1
src/tests/shell/AGENTS.md

@ -17,6 +17,7 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- `testsuites/punkexe/` targets a built punk executable: resolved from `env(PUNK_SHELL_TEST_EXE)`, else `<projectroot>/bin/punk902z.exe`, else `<projectroot>/bin/punkshell902`; tests auto-skip via the `punkexeavailable` constraint when none is found.
- Tests that spawn the built executable must be hang-proof: use the event-loop `punk_run` pattern (pipe stdin, half-close for EOF, timeout then force-kill) rather than plain `exec`, because known failure modes leave the child waiting on a reopened console.
- `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling.
- `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, 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`.
- `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.

196
src/tests/shell/testsuites/punkexe/shellnavns.test

@ -0,0 +1,196 @@
package require tcltest
#added 2026-07-14 (agent) - E3 shell-level proof of namespace navigation state and
#the subshell namespace carry-over (companions: punk/nav/ns navns.test for the ns/
#state machine, punk/repl nscurrent.test for codethread inscope/retention/seeding).
#
#One piped session against the built executable in 'src' launch mode (dev modules -
#required so the punk::repl 0.5.2 stray-namespace seeding fix and current nav code
#are what actually runs; a plain kit launch would use kit-stamped modules).
#
#PROVEN here:
# - n/ sets ::punk::nav::ns::ns_current in the shell's code interp
# - the current namespace CONTINUES into a launched 'subshell punk' (the
# deliberate carry-over contract). Mechanism finding (2026-07-14 probes): the
# FIRST subshell evaluates in the SAME code interp as the parent shell (the
# 'first subshell asymmetry' TODO documented in punk::repl repl::init) - so
# continuity here manifests via shared state, pinned by the SHARED marker
# below. The repl::start seeding template (the carry-over mechanism for a
# genuinely separate code interp) is proven at module level against a real
# codethread in punk/repl nscurrent.test.
# - no stray ::punk::nav::ns::ns_current NAMESPACE exists after subshell launch
# (punk::repl 0.5.2 seeding-template fix)
# - navigation inside the subshell works, and with the shared context the parent
# sees the subshell's final namespace after exit (state continuity BOTH ways -
# a G-002-style non-nested/isolated subshell would change this pin)
# - subshell exit unwinds to the parent which keeps consuming piped input, and the
# session terminates cleanly (exitcode 0, no timeout)
#
#KNOWN GAP pinned (flip deliberately when addressed):
# - piped-session submissions evaluate at :: even after n/ has set ns_current -
# the inscope application observed interactively (and proven at module level
# against a real codethread in punk/repl nscurrent.test) does not take effect
# for subsequent piped submissions in this spawn shape. Root cause undetermined
# (candidates: piped-path evaluation difference, kit/codethread interplay in the
# spawned process) - investigate alongside G-038 session-context work.
namespace eval ::testspace {
namespace import ::tcltest::*
variable testdir [file dirname [file normalize [info script]]]
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 60000
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
}
}
#as per shellexit.test punk_run: pipe stdin, half-close for EOF, timeout+kill
proc punk_run {exe cmdargs stdin_data} {
variable runstate
variable punk_run_timeout_ms
array unset runstate
set runstate(output) ""
set runstate(done) ""
set env_had [info exists ::env(PUNK_PIPE_EOF)]
if {$env_had} {
set env_save $::env(PUNK_PIPE_EOF)
}
set ::env(PUNK_PIPE_EOF) exit
set chan [open |[list $exe {*}$cmdargs 2>@1] r+]
if {$env_had} {
set ::env(PUNK_PIPE_EOF) $env_save
} else {
unset ::env(PUNK_PIPE_EOF)
}
set pids [pid $chan]
chan configure $chan -blocking 0 -translation binary
catch {
puts -nonewline $chan $stdin_data
flush $chan
chan close $chan write
}
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)]
}
#extract MARKER=value occurrences from (ansi-laden) combined output
proc markvals {output marker} {
set vals [list]
foreach {_ v} [regexp -all -inline [string cat $marker {=([^\r\n\x1b]*)}] $output] {
lappend vals $v
}
return $vals
}
test shellnavns_session_nav_carryover_and_gaps {piped session: n/ state, subshell namespace carry-over, seeding-fix, exit unwinding - with the piped-inscope and parent-reset gaps pinned}\
-constraints punkexeavailable\
-setup {
set result ""
}\
-body {
variable punkexe
set session ""
append session {namespace eval ::carrye3 {namespace eval sub {}}} \n
append session {n/ ::carrye3} \n
append session {puts PARENTNS=$::punk::nav::ns::ns_current} \n
append session {puts PARENTCUR=[namespace current]} \n
append session {subshell punk} \n
append session {puts SUBNS=$::punk::nav::ns::ns_current} \n
append session {puts PHANTOM=[namespace exists ::punk::nav::ns::ns_current]} \n
append session {n/ ::carrye3::sub} \n
append session {puts SUBNAV=$::punk::nav::ns::ns_current} \n
append session {set ::submarker sub-was-here} \n
append session {exit} \n
append session {puts PARENTAFTER=$::punk::nav::ns::ns_current} \n
append session {puts SHARED=[info exists ::submarker]} \n
append session {exit} \n
set rd [punk_run $punkexe [list src] $session]
set output [dict get $rd output]
lappend result [dict get $rd timedout] [dict get $rd exitcode]
#n/ set the parent's ns_current
lappend result [markvals $output PARENTNS]
#KNOWN GAP: subsequent piped submissions still evaluate at :: (see header)
lappend result [markvals $output PARENTCUR]
#the subshell carry-over: new codethread seeded from parent's ns_current
lappend result [markvals $output SUBNS]
#punk::repl 0.5.2 fix: no stray namespace named like the variable
lappend result [markvals $output PHANTOM]
#navigation inside the subshell takes effect
lappend result [markvals $output SUBNAV]
#shared context: parent sees the subshell's final namespace after exit
#(and the subshell's variable - the first-subshell asymmetry)
lappend result [markvals $output PARENTAFTER]
lappend result [markvals $output SHARED]
}\
-result [list\
0 0\
::carrye3\
::\
::carrye3\
0\
::carrye3::sub\
::carrye3::sub\
1
]
}
tcltest::cleanupTests ;#needed to produce test summary.
Loading…
Cancel
Save