Browse Source

commandstack 0.5.0: help overview + PUNKARGS docs; first characterisation suite

Module (src/modules/commandstack-999999.0a1.0.tm, 0.4.1 -> 0.5.0):
- commandstack::help now returns a real dependency-free API overview (was
  an empty string): rename/remove workflow, COMMANDSTACKNEXT delegation
  contract, inspection commands, in-tree users.
- PUNKARGS documentation blocks added for every API proc (help, debug,
  get_stack, get_next_command, basecall, rename_command, remove_rename,
  show_stack, Delete_stack, Rename_stack, util::get_IMPLEMENTOR,
  lib::splitx, lib::split_body), registered lazily via
  ::punk::args::register::NAMESPACES - no punk::args dependency added.

Tests (new src/tests/modules/commandstack/testsuites/commandstack/commandstack.test,
29 tests, green on tclsh90 9.0.3 + punk86 8.6):
- usage-driven characterisation of the behaviours in-tree consumers rely on
  (punk::packagepreference, packagetrace, packagesuppress, punk auto_execok,
  punk::nav::fs cd): record shape/key-order contract, implementation {} as
  the no-rename signal, COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL
  delegation, multi-renamer stacking with removal in any order, builtin
  renames, remove_rename argument forms, show_stack fallback render,
  get_IMPLEMENTOR/split_body/splitx, help, lazy punk::args registration.
- known defects pinned as _GAP_ tests: per-renamer tokenid stuck at 1
  (apply-local counter var) so same-renamer re-renames stack duplicate
  tokens whose dispatch bypasses intermediate layers, and a third rename
  collides on the parked-implementation name; get_IMPLEMENTOR builtin
  branch unreachable (unqualified info commands pattern inside
  commandstack::util); Delete_stack under a live rename self-delegates to
  recursion; stray bare puts stderr writes literal stderr to stdout.
- behavioural tests run in fresh child interps (module sourced by path,
  ::puts shim captures module warnings for assertion).

Docs:
- src/tests/AGENTS.md: new contract - test descriptions must be single
  content line (multi-line descriptions make tcltest failure banners
  multi-line; punk::tcltestrun parser then swallows all later events
  including the summary line - discovered authoring this suite).
- src/tests/modules/AGENTS.md: commandstack child index entry.
- punkproject.toml 0.49.2 + CHANGELOG entry.

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 7 days ago
parent
commit
145e785c42
  1. 13
      CHANGELOG.md
  2. 2
      punkproject.toml
  3. 362
      src/modules/commandstack-999999.0a1.0.tm
  4. 6
      src/modules/commandstack-buildversion.txt
  5. 1
      src/tests/AGENTS.md
  6. 1
      src/tests/modules/AGENTS.md
  7. 656
      src/tests/modules/commandstack/testsuites/commandstack/commandstack.test

13
CHANGELOG.md

@ -5,6 +5,19 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.49.2] - 2026-08-03
- commandstack 0.5.0: `commandstack::help` now returns a real API overview
(was an empty string), and every API proc carries a PUNKARGS documentation
block registered lazily with punk::args (no punk::args dependency added -
the module remains dependency-free). First test suite added:
`src/tests/modules/commandstack/` characterises the rename/remove/stack
behaviour the in-tree consumers (punk::packagepreference, punk::nav::fs,
punk auto_execok, packagetrace, packagesuppress) rely on, and pins known
defects as _GAP_ tests (tokenid stuck at 1 causing duplicate-token dispatch
bypass, unreachable get_IMPLEMENTOR 'builtin' branch, Delete_stack
live-rename recursion).
## [0.49.1] - 2026-08-03
- punk::libunknown 0.2.4: deep tm discovery (`register_all_tm`, used by

2
punkproject.toml

@ -1,6 +1,6 @@
[project]
name = "punkshell"
version = "0.49.1"
version = "0.49.2"
license = "BSD-2-Clause"
url = "https://www.gitea1.intx.com.au/jn/punkshell"
#packager: declared identity for published artifacts (declarative, not proof -

362
src/modules/commandstack-999999.0a1.0.tm

@ -11,6 +11,9 @@
# - oo dispatch features may be a better implementation - especially for allowing undoing command renames in the middle of a stack.
# - document that replacement command should use 'commandstack::get_next_command <cmd> <renamer>' for delegating to command as it was prior to rename
#changes:
#2026-08-03
# - implement commandstack::help overview text (was returning empty string)
# - add PUNKARGS documentation blocks for the API (lazy punk::args registration - no punk::args dependency added)
#2024
# - mungecommand to support namespaced commands
# - fix mistake - hardcoded _originalcommand_package -> _originalcommand_<mungedcommand>
@ -43,6 +46,25 @@ namespace eval commandstack::util {
#return unspecified if the command is a proc with a body but no magic comment ID
#return unknown if the command doesn't have a proc body to analyze
#otherwise return the package name identified in the magic comment
namespace eval ::commandstack::argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::util::get_IMPLEMENTOR
@cmd -name "commandstack::util::get_IMPLEMENTOR" -&
-summary -&
"Identify which package implemented a command's current proc body." -&
-help -&
{Searches the proc body of command for the magic comment
marker IMPLEMENTOR_<name>! (which rename_command adds
automatically, keyed by renamer) and returns the <name>
portion. Returns 'unspecified' for a proc body without the
marker, 'builtin' for a native command when
tcl::info::cmdtype is available (Tcl 8.7+/9), otherwise
'undetermined'.}
@values -min 1 -max 1
command -type string -help -&
"Command name - must already be fully qualified."
}]
}
proc get_IMPLEMENTOR {command} {
#assert - command has already been resolved to a namespace ie fully qualified
if {[llength [info procs $command]]} {
@ -81,12 +103,90 @@ namespace eval commandstack::temp {} ;#where we create proc initially before ren
namespace eval commandstack {
namespace export {[a-z]*}
proc help {} {
return {
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::help
@cmd -name "commandstack::help" -&
-summary -&
"Plain-text overview of the commandstack module." -&
-help -&
"Returns a dependency-free plain text overview of the
cooperative command renaming system: the core
rename_command/remove_rename workflow, the COMMANDSTACKNEXT
delegation variables injected into installed proc bodies,
and the inspection commands."
@values -min 0 -max 0
}]
}
proc help {} {
return {commandstack - cooperative command renaming (stacked command overrides)
Purpose
Allows multiple packages to override the same command (e.g the ::package
builtin) and to load/unload their overrides in any order. Each override is
recorded on a per-command stack so that removing one re-links the
surrounding entries instead of clobbering them.
Core workflow
set record [commandstack::rename_command -renamer <mypkg> <command> <procargs> <procbody>]
Renames <command> aside and installs <procbody> in its place.
The previous implementation is preserved at the command name given by
[dict get $record implementation] (empty string means no rename was
performed). Two variables are pre-set at the top of the installed
proc body:
COMMANDSTACKNEXT - the implementation to delegate to,
re-resolved on every call via
commandstack::get_next_command (so it
stays correct when the stack changes)
COMMANDSTACKNEXT_ORIGINAL - the implementation as at rename time
(static - informational/debug)
A delegating body normally contains:
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
commandstack::remove_rename <token_or_command>
Undo a rename. Accepts the token from the rename record
([dict get $record token] = {<command> <renamer> <tokenid>}),
a 2-element {<command> <renamer>}, or just <command> when called from
the same namespace context that performed the rename.
Inspection
commandstack::get_stack ?command? - rename records (or all stacks)
commandstack::show_stack ?glob? - printable stack display
commandstack::basecall command ?arg ...? - call bottom-of-stack (original)
commandstack::get_next_command command renamer tokenid
- implementation a record points to
commandstack::debug ?on_off? - query/set debug messages
Notes
- The renamer string defaults to the calling namespace.
- Cooperating packages are identified by a magic comment in installed proc
bodies: IMPLEMENTOR_<renamer>! (added automatically by rename_command).
- Per-command detail is registered lazily with punk::args - e.g
`i commandstack::rename_command` in punkshell, or
`punk::args::usage ::commandstack::rename_command` when punk::args is loaded.
In-tree users: punk::packagepreference, punk::nav::fs, punk (auto_execok),
packagetrace, packagesuppress.
}
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::debug
@cmd -name "commandstack::debug" -&
-summary -&
"Query or set commandstack debug messaging." -&
-help -&
"With no argument, returns the current debug state (0|1).
With a boolean argument, sets the state and returns it.
When enabled, rename_command reports rename progress on
stderr."
@values -min 0 -max 1
on_off -type boolean -optional 1 -help -&
"New debug state. Omit to query the current state."
}]
}
proc debug {{on_off {}}} {
variable debug
if {$on_off eq ""} {
@ -99,6 +199,30 @@ namespace eval commandstack {
}
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::get_stack
@cmd -name "commandstack::get_stack" -&
-summary -&
"Return the rename-record stack for a command, or all stacks." -&
-help -&
{With no argument, returns the entire stacks dict keyed by
fully qualified command name - each value a list of rename
records (bottom of stack first).
With a command argument (resolved with 'namespace which' in
the caller's context), returns that command's list of rename
records - empty if the command has never been renamed.
Each record is a dict with keys in this order:
token renamer next_implementor next_getter implementation
The key order is a contract: cooperating code may locate
records with 'lsearch -index 1' (token value) or
'lsearch -index 3' (renamer value).}
@values -min 0 -max 1
command -type string -optional 1 -help -&
"Command name (resolved in the caller's namespace context).
Omit to return the dict of all stacks."
}]
}
proc get_stack {{command ""}} {
variable all_stacks
if {$command eq ""} {
@ -115,6 +239,30 @@ namespace eval commandstack {
#get the implementation to which the renamer (renamer is usually calling namespace) originally renamed it, or the implementation it now points to.
#review - performance impact. Possible to use oo for faster dispatch whilst allowing stack re-orgs?
#e.g if renaming builtin 'package' - this command is generally called 'a lot'
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::get_next_command
@cmd -name "commandstack::get_next_command" -&
-summary -&
"Resolve the implementation a rename record delegates to." -&
-help -&
{Returns the implementation command to which the stack entry
identified by the token elements (command renamer tokenid)
currently points. Installed override bodies call this on
every invocation (via the pre-set COMMANDSTACKNEXT variable),
so removals from the stack re-route delegation automatically.
If the command has no stack at all, command is returned
unchanged. An error is raised when a stack exists but no
record matches the token.}
@values -min 3 -max 3
command -type string -help -&
"Fully qualified command name (first token element)."
renamer -type string -help -&
"Renamer string recorded at rename time (second token element)."
tokenid -type int -help -&
"Token id recorded at rename time (third token element)."
}]
}
proc get_next_command {command renamer tokenid} {
variable all_stacks
if {[dict exists $all_stacks $command]} {
@ -131,6 +279,25 @@ namespace eval commandstack {
return $command
}
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::basecall
@cmd -name "commandstack::basecall" -&
-summary -&
"Call the original (bottom-of-stack) implementation of a command." -&
-help -&
{Tailcalls the implementation recorded at the bottom of the
command's rename stack (the original command as it was first
renamed aside), bypassing all stacked overrides. A command
with no rename stack is called directly. The command name is
resolved with 'namespace which' in the caller's context.}
@values -min 1 -max -1
command -type string -help -&
"Command name (resolved in the caller's namespace context)."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed through to the implementation."
}]
}
proc basecall {command args} {
variable all_stacks
set command [uplevel 1 [list namespace which $command]]
@ -150,6 +317,57 @@ namespace eval commandstack {
#review.
#<renamer> defaults to calling namespace - but can be arbitrary string
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::rename_command
@cmd -name "commandstack::rename_command" -&
-summary -&
"Cooperatively rename a command, stacking the override." -&
-help -&
{Renames command aside (to a name under
::commandstack::renamed_commands) and installs a proc with
procargs/procbody in its place, recording the operation on
the command's rename stack so overrides from multiple
cooperating packages can be added and removed in any order.
A header is prepended to procbody which sets two variables:
COMMANDSTACKNEXT - the implementation to delegate
to (re-resolved every call via
commandstack::get_next_command)
COMMANDSTACKNEXT_ORIGINAL - the implementation as at rename
time (static/debug)
A delegating procbody normally contains:
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
Returns the new stack record - a dict with keys:
token renamer next_implementor next_getter implementation
An implementation value of empty string means no rename was
performed (command not found, or this renamer already
installed an identical procbody). Keep the token
({command renamer tokenid}) or the {command renamer} pair for
a later remove_rename.
The proc is first built at a temp location so a procargs or
procbody compile error raises before the stack or the live
command are touched.}
@opts
-renamer -type string -optional 1 -help -&
"Identity string recorded for this rename - defaults to
the calling namespace. Cooperating packages use their
package/namespace name. Note: this flag is recognised
only as the FIRST argument (manual parse)."
@values -min 3 -max 3
command -type string -help -&
"Command to rename (resolved with 'namespace which' in
the caller's context - builtins and procs both work)."
procargs -type list -help -&
"Argument list for the replacement proc (commonly {args},
but any signature matching the target's call pattern)."
procbody -type string -help -&
"Body for the replacement proc. Delegate onward via the
pre-set COMMANDSTACKNEXT variable."
}]
}
proc rename_command {args} {
#todo: consider -forcebase 1 or similar to allow this rename to point to bottom of stack (original command) bypassing existing renames
# - need to consider that upon removing, that any remaining rename that was higher on the stack should not also be diverted to the base - but rather to the next lower in the stack
@ -322,6 +540,37 @@ namespace eval commandstack {
#If only a commandname is supplied, and there were multiple renames from the same context (same -renamer) only the topmost is removed.
#A call to remove_rename with no token or renamer, and from a namespace context which didn't perform a rename will not remove anything.
#similarly a nonexistant token or renamer will not remove anything and will just return the current stack
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::remove_rename
@cmd -name "commandstack::remove_rename" -&
-summary -&
"Undo a rename previously made with rename_command." -&
-help -&
{Removes one entry from a command's rename stack, restoring
or re-linking implementations as needed. Entries other than
the topmost can be removed - the entry above is re-pointed at
what the removed entry delegated to (the load/unload-in-any-
order design goal).
token_or_command is one of:
3 elements - the exact token from the rename record:
{command renamer tokenid}
2 elements - {command renamer} - removes that renamer's
topmost entry for the command
1 element - command name only - renamer defaults to the
calling namespace
The renamer must be known to commandstack (recorded by a
rename_command call) or an error is raised. A token or
renamer with no matching stack entry removes nothing.
Returns the command's stack after the removal (empty list if
the command has no stack).}
@values -min 1 -max 1
token_or_command -type list -help -&
"Token {command renamer tokenid}, pair {command renamer},
or bare command name (see -help above)."
}]
}
proc remove_rename {token_or_command} {
if {[llength $token_or_command] == 3} {
#is token
@ -384,6 +633,26 @@ namespace eval commandstack {
return [list]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::show_stack
@cmd -name "commandstack::show_stack" -&
-summary -&
"Return a printable display of rename stacks." -&
-help -&
{Returns a formatted text display of the rename stacks whose
command names match commandname_glob. An argument without
glob characters is resolved with 'namespace which' in the
caller's context first. When the punk and punk::lib packages
are already loaded the display is rendered with
punk::lib::pdict - otherwise a plain aligned-text fallback is
used. Returns an empty string when nothing matches.}
@values -min 0 -max 1
commandname_glob -type string -default * -optional 1 -help -&
"Glob pattern (or exact command name) selecting which
command stacks to display."
}]
}
proc show_stack {{commandname_glob *}} {
variable all_stacks
if {![regexp {[?*]} $commandname_glob]} {
@ -430,6 +699,27 @@ namespace eval commandstack {
#review
#document when this is to be called. Wiping stacks without undoing renames seems odd.
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::Delete_stack
@cmd -name "commandstack::Delete_stack" -&
-summary -&
"Discard a command's rename-stack records (maintenance - unexported)." -&
-help -&
{Removes the command's entry from the stacks dict WITHOUT
undoing any renames - the renamed commands themselves are
left in place. Always returns 1, whether or not a stack
existed. CAUTION: if overrides installed by rename_command
are still live, deleting their stack breaks the
COMMANDSTACKNEXT lookup - get_next_command then resolves to
the (overridden) command itself and the next call recurses
until the interp recursion limit. Not exported - intended for
maintenance/experimentation only (under review).}
@values -min 1 -max 1
command -type string -help -&
"Fully qualified command name key (no resolution is performed)."
}]
}
proc Delete_stack {command} {
variable all_stacks
if {[dict exists $all_stacks $command]} {
@ -442,6 +732,29 @@ namespace eval commandstack {
#can be used to temporarily put a stack aside - should manually rename back when done.
#review - document how/when to use. example? intention?
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::Rename_stack
@cmd -name "commandstack::Rename_stack" -&
-summary -&
"Re-key a command's rename-stack records (maintenance - unexported)." -&
-help -&
{Moves the stack records stored under oldname to newname in
the stacks dict. No commands are renamed - this only changes
the dict key, e.g to temporarily put a stack aside (rename
back manually when done). An error is raised if newname
already has a stack. Note that get_stack <name> resolves its
argument with 'namespace which', so records parked under a
name that is not an existing command are only visible via the
no-argument get_stack dict. Not exported - intended for
maintenance/experimentation only (under review).}
@values -min 2 -max 2
oldname -type string -help -&
"Existing stacks-dict key (no resolution is performed)."
newname -type string -help -&
"New stacks-dict key."
}]
}
proc Rename_stack {oldname newname} {
variable all_stacks
if {[dict exists $all_stacks $oldname]} {
@ -465,6 +778,26 @@ namespace eval commandstack {
namespace eval commandstack::lib {
namespace eval ::commandstack::argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::lib::splitx
@cmd -name "commandstack::lib::splitx" -&
-summary -&
"Split a string on a regexp separator." -&
-help -&
{Local copy of tcllib textutil::split::splitx (to avoid the
dependency). Splits str on each match of regexp. A
parenthesised subexpression in regexp includes the separator
match in the result list. An empty regexp splits into
characters. A regexp matching the empty string raises an
'infinite loop' error.}
@values -min 1 -max 2
str -type string -help -&
"String to split."
regexp -type string -optional 1 -default {[\t \r\n]+} -help -&
"Separator regular expression."
}]
}
proc splitx {str {regexp {[\t \r\n]+}}} {
#snarfed from tcllib textutil::splitx to avoid the dependency
# Bugfix 476988
@ -494,6 +827,23 @@ namespace eval commandstack::lib {
lappend list [string range $str $start end]
return $list
}
namespace eval ::commandstack::argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::lib::split_body
@cmd -name "commandstack::lib::split_body" -&
-summary -&
"Split an installed override body into commandstack header and original code." -&
-help -&
{Splits a proc body at the #<commandstack_separator># marker
line that rename_command embeds between its generated header
(the COMMANDSTACKNEXT setup) and the renamer-supplied
procbody. Returns a 2-element list {header code}. A body
without the marker returns {"" procbody}.}
@values -min 1 -max 1
procbody -type string -help -&
"Proc body text (e.g from 'info body <command>')."
}]
}
proc split_body {procbody} {
set marker "#<commandstack_separator>#"
set header ""
@ -518,6 +868,14 @@ namespace eval commandstack::lib {
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
#Register namespaces punk::args should scan for PUNKARGS documentation.
#The PUNKARGS metadata here is inert documentation - this module deliberately
#does not depend on (or call) punk::args.
lappend ::punk::args::register::NAMESPACES ::commandstack ::commandstack::argdoc
}
package provide commandstack [namespace eval commandstack {
set version 999999.0a1.0
}]

6
src/modules/commandstack-buildversion.txt

@ -1,3 +1,7 @@
0.4.1
0.5.0
#First line must be a tm version number
#all other lines are ignored.
#0.5.0 - commandstack::help now returns a real overview (was empty string)
# - PUNKARGS documentation blocks added for all API procs (lazy punk::args
# registration via ::punk::args::register::NAMESPACES - no punk::args
# dependency added; module remains dependency-free)

1
src/tests/AGENTS.md

@ -30,6 +30,7 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc
- Test files must `package require` any extra packages explicitly.
- Tcltest files must finish with `tcltest::cleanupTests`; missing cleanup produces a `missing-cleanupTests` runner warning and only untrusted observed testcase events.
- 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`.
- Test DESCRIPTIONS must be a single content line (long lines are fine; the corpus style is `test name {one long description}\` with the options on following lines). tcltest prints a failing test's opening banner as one `puts` of `==== <name> <description> FAILED`, and tcltest only trims the description's ENDS — a description with embedded newlines makes that banner multi-line, punk::tcltestrun's per-line output parser then never matches the opening banner, misreads the closing `==== <name> FAILED` line as an opener, and stays in its failure-capture state to end of stream: every later event INCLUDING the summary line is swallowed, so the file reports `warn`/`missing-cleanupTests` with observed passes stopping at the failure and no failure detail at all (discovered 2026-08-03 authoring the commandstack suite). Put longer prose in comments above the test.
- 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.

1
src/tests/modules/AGENTS.md

@ -40,6 +40,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
- `opunk/console/` — ::opunk::Console backend subclass tests (`testsuites/console/backends.test`, G-001): virtual dispatch of subclass overrides through base-class calls and punk::console::console_spec_resolve (both unchanged), TestConsole determinism + probe-free at_eof, SshConsole capability/eof + the flagship size-via-ANSI-query-over-socket case (a scripted remote terminal answers CSI 6n), TkConsole widget size/eof (gated behind env PUNK_TEST_TK=1 - Tk in the shared testinterp has side effects; also verifiable standalone under a tk-capable kit e.g `punk91 src <script>`)
- `punkboot/utils/` — punkboot::utils tests (`testsuites/utils/`): the make.tcl helper module. `utils.test` (punkproject.toml/CHANGELOG version parsing), `vcsdirty.test` (dirty fossil/git provenance warnings behind the producing-commands gate - git-fixture based), and `bootlibrary.test` (G-125 boot-precondition predicate `vfs_boot_library_report`: both tcl-library conventions - `tcl_library/` for zipfs-attached kits and `lib/tcl<major>.<minor>/` for starkit-style kits - the companion-file requirement that stops the `lib/BWidget1.10.1/init.tcl` every punkshell kit carries from answering for a tcl library, near-miss reporting, missing/empty trees, and a sweep asserting every assembled `src/_bake/*.vfs` tree still passes so the gate cannot fail kits that boot today). All three are pure fixture tests - no mint or bake is run; the make.tcl side of the gate is pinned separately in `shell/testsuites/punkexe/maketclbootgate.test`
- `modpod/` — modpod (vendored zip-based .tm wrapper) tests (`testsuites/modpod/roundtrip.test`, G-111 - the module's first suite): make_zip_modpod wrap emits stub+zip (\x1A separator, PK local header), is_valid_tm_version accept/reject, and child-process load round-trips of per-run generated fixture pods - require from a real-disk module path (stub self-mounts via zipfs, or the vfs::zip fallback on 8.6), the -offsettype file form, the unwrapped #modpod-<pkg>-<ver> redirect form (extracted folder beside the .tm is sourced directly - exact path asserted, no mount signature), a binary payload (dll discovered by a child probe among packages NOT already loaded in a bare child, repo lib_tcl<N> trees offered as auto_path - no committed binaries) loading from the mounted pod in a fresh child, and the tm-residing-on-a-zipfs-path zip-in-zip form (childzipfs-gated: skips on 8.6, the recorded G-034-class limitation). Child spawn probes the kit `script` subcommand form first, then plain script-file dispatch (native tclsh)
- `commandstack/` — commandstack (cooperative command renaming) characterisation tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - the module's first suite, usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (dict key order for the lsearch -index 1/-index 3 convention, token, `implementation {}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation, multi-renamer stacking with removal in any order (bottom-removal re-linking), builtin renames (next_implementor `original`), remove_rename's three argument forms + unknown-renamer errors, show_stack fallback render, Rename_stack parking quirks, util::get_IMPLEMENTOR, lib::split_body round-trip, lib::splitx, the help overview, and lazy punk::args registration of the PUNKARGS docs. Known defects pinned as _GAP_ tests: tokenid stuck at 1 (apply-local counter) making same-renamer re-renames stack duplicate tokens whose dispatch bypasses intermediate layers and a third rename collide on the parked name; the get_IMPLEMENTOR `builtin` branch unreachable (unqualified `info commands tcl::info::cmdtype` pattern inside commandstack::util); Delete_stack under a live rename causing self-delegation recursion. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module stderr warnings for assertion and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing contract in src/tests/AGENTS.md. Green on tclsh90 (9.0.3) and punk86 (8.6)
- `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack)
- `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)

656
src/tests/modules/commandstack/testsuites/commandstack/commandstack.test

@ -0,0 +1,656 @@
# -*- tcl -*-
# Characterisation tests for the commandstack module (cooperative command
# renaming / stacked command overrides).
#
# Coverage is usage-driven - the pinned behaviours are the ones in-tree
# consumers rely on (punk::packagepreference and packagetrace/packagesuppress
# rename ::package; punk::nav::fs renames the ::cd builtin and uses basecall;
# punk renames ::auto_execok with a single-arg signature):
# 1. rename_command record shape: a dict whose KEY ORDER is a documented
# contract (token renamer next_implementor next_getter implementation -
# cooperating code uses lsearch -index 1 / -index 3), token
# {command renamer tokenid}, and 'implementation' empty string as the
# no-rename-performed signal consumers test for.
# 2. Delegation through the injected COMMANDSTACKNEXT (re-resolved each call
# via get_next_command) and static COMMANDSTACKNEXT_ORIGINAL variables.
# 3. Stacked overrides from different renamers and removal in ANY order
# (the design goal: entry above a removed entry is re-pointed), including
# renames of true builtins (next_implementor 'original') and non-{args}
# proc signatures.
# 4. remove_rename forms: 3-element token, {command renamer} pair, bare
# command name resolved against the calling namespace; unknown renamer
# errors.
# 5. Known defects pinned as _GAP_ tests: the per-renamer token counter
# increments an apply-local variable so tokenid never advances past 1 -
# a same-renamer re-rename creates a DUPLICATE token, dispatch then
# resolves to the first (oldest) record and bypasses intermediate layers,
# and a third same-renamer rename dies on a renamed_commands name
# collision; Delete_stack under a live rename leaves the override
# delegating to itself (infinite recursion at next call); the
# get_IMPLEMENTOR 'builtin' classification branch is unreachable (its
# tcl::info::cmdtype guard uses an unqualified info commands pattern from
# inside commandstack::util) so builtins report 'undetermined' everywhere.
# 6. util::get_IMPLEMENTOR magic-comment classification, lib::split_body
# header/code round-trip, lib::splitx, debug accessor, show_stack
# fallback rendering, Rename_stack parking quirks, help overview text,
# and lazy punk::args registration of the PUNKARGS documentation.
#
# Tests run against the SOURCE-TREE module. Behavioural tests use a fresh
# child interp per test (sourcing the module by path) so that: module state
# (all_stacks/known_renamers) starts clean every time, renames of builtins
# cannot damage the shared testinterp, show_stack deterministically takes its
# no-punk::lib fallback branch, and the module's stderr warnings are captured
# (a ::puts shim in the child) rather than polluting runner output.
#
# NOTE (runner parsing contract, discovered 2026-08-03): test DESCRIPTIONS in
# this suite are deliberately single-line. tcltest prints a failing test's
# banner as one puts of "==== <name> <description> FAILED"; a description with
# embedded newlines makes that banner multi-line and punk::tcltestrun's
# per-line output parser then misses the opening banner, misreads the closing
# "==== <name> FAILED" line as an opener, and swallows every subsequent event
# INCLUDING the summary line (file reports warn/missing-cleanupTests with no
# failure detail). Longer prose lives in comments above each test instead.
#
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/commandstack/*** commandstack.test
package require tcltest
package require commandstack
namespace eval ::testspace {
namespace import ::tcltest::*
#The SOURCE-TREE commandstack module, located relative to this test file
#(highest version by vcompare should multiple commandstack-*.tm coexist).
variable commandstack_src ""
variable commandstack_ver ""
variable script_dir [file dirname [file normalize [info script]]]
set srcmodules [file normalize [file join $script_dir .. .. .. .. .. modules]]
foreach candidate [glob -nocomplain [file join $srcmodules commandstack-*.tm]] {
set thisver [file rootname [lindex [split [file tail $candidate] -] 1]]
if {$commandstack_src eq "" || [package vcompare $thisver $commandstack_ver] == 1} {
set commandstack_src $candidate
set commandstack_ver $thisver
}
}
testConstraint commandstacksrc [expr {$commandstack_src ne ""}]
if {$commandstack_src eq ""} {
puts stderr "commandstack.test: cannot locate src/modules/commandstack-*.tm relative to [info script] (probe tests will be skipped)"
}
testConstraint havepunkargs [expr {![catch {package require punk::args}]}]
if {[testConstraint havepunkargs]} {
#guarded punk::lib require AFTER punk::args - 8.6 render paths use
#forward-compat commands from punk::lib (recorded project trap)
catch {package require punk::lib}
}
# -------------------------------------------------------------------------
# Probe: fresh child interp with the source-tree commandstack sourced.
# A ::puts shim captures 1-arg and stdout/stderr 2-arg writes into
# ::PUTS_LOG (list of {channel text}) - assertable, and keeps module
# warnings out of runner output. Other/real channel writes pass through.
# -------------------------------------------------------------------------
proc cs_probe {script} {
variable commandstack_src
set i [interp create]
try {
interp eval $i {
set ::PUTS_LOG [list]
rename ::puts ::puts_real
proc ::puts {args} {
set a $args
if {[lindex $a 0] eq "-nonewline"} {set a [lrange $a 1 end]}
if {[llength $a] == 1} {
#note - a bare 'puts stderr' (no message) lands here as {stdout stderr}
lappend ::PUTS_LOG [list stdout [lindex $a 0]]
} elseif {[llength $a] == 2 && [lindex $a 0] in {stdout stderr}} {
lappend ::PUTS_LOG [list [lindex $a 0] [lindex $a 1]]
} else {
::puts_real {*}$args
}
return
}
proc ::putslog_matches {pattern} {
set n 0
foreach entry $::PUTS_LOG {
if {[string match $pattern [lindex $entry 1]]} {incr n}
}
return $n
}
}
interp eval $i [list source $commandstack_src]
interp eval $i $script
} finally {
interp delete $i
}
}
#added 2026-08-03 (agent) - characterisation suite for commandstack (usage-driven:
#punk::packagepreference / packagetrace / packagesuppress / punk auto_execok / punk::nav::fs cd)
test commandstack_package_version {package require commandstack resolves the source-tree magic version via prefer-latest}\
-body {
package require commandstack
package provide commandstack
} -result {999999.0a1.0}
#rename_command on a plain proc: full record pinned - the dict KEY ORDER is a
#documented contract (lsearch -index 1 / -index 3), token {command renamer 1},
#next_implementor 'unspecified' (proc body without magic comment), and the
#munged renamed_commands implementation name (underscore form for unspecified).
test commandstack_rename_basic_record {rename_command plain-proc record: key order, token, unspecified implementor, munged implementation name}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
}
} -result {token {::tgt ::csA 1} renamer ::csA next_implementor unspecified next_getter {::commandstack::get_next_command ::tgt ::csA 1} implementation ::commandstack::renamed_commands::_ns_tgt_unspecified-_ns_csA-1}
#installed body sees COMMANDSTACKNEXT (dynamic, re-resolved per call) and
#COMMANDSTACKNEXT_ORIGINAL (static) - both equal the record implementation
#after a single rename, and delegation reaches the original.
test commandstack_rename_delegation_variables {installed body sees COMMANDSTACKNEXT + COMMANDSTACKNEXT_ORIGINAL and delegates to the original}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
set rec [commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list $COMMANDSTACKNEXT $COMMANDSTACKNEXT_ORIGINAL [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
set impl [dict get $rec implementation]
lassign [::tgt x y] next orig delegated
list [expr {$next eq $impl}] [expr {$orig eq $impl}] $delegated
}
} -result {1 1 {base {x y}}}
#rename_command on a true builtin (::lrepeat): the not-a-proc path records
#next_implementor 'original' with the dash-form implementation name; delegation
#works and remove_rename restores the native command, leaving
#::commandstack::renamed_commands empty.
test commandstack_rename_builtin_original_and_restore {builtin rename: next_implementor original, delegation, remove restores native command}\
-constraints commandstacksrc -body {
cs_probe {
set rec [commandstack::rename_command -renamer ::csB ::lrepeat {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}]
set through [lrepeat 2 ab]
commandstack::remove_rename {::lrepeat ::csB}
list [dict get $rec next_implementor] [dict get $rec implementation] \
$through [lrepeat 2 cd] [llength [commandstack::get_stack ::lrepeat]] \
[info procs ::lrepeat] [info commands ::commandstack::renamed_commands::*]
}
} -result {original ::commandstack::renamed_commands::_ns_lrepeat-original-_ns_csB-1 {ab ab} {cd cd} 0 {} {}}
#get_stack: no-arg returns the all-stacks dict keyed by fq name; command arg
#returns that command's record list; unstacked and nonexistent commands return
#an empty list.
test commandstack_get_stack_forms {get_stack: all-stacks dict, per-command list, empty for unstacked and nonexistent commands}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set all [commandstack::get_stack]
list [dict size $all] [dict keys $all] [llength [commandstack::get_stack ::tgt]] \
[commandstack::get_stack ::lrepeat] [commandstack::get_stack ::no_such_cmd]
}
} -result {1 ::tgt 1 {} {}}
#get_next_command: a valid token resolves to the record implementation; a
#command with no stack passes through unchanged; a stacked command with a
#non-matching token raises the pinned error.
test commandstack_get_next_command_resolution {get_next_command: token resolves implementation, no-stack passthrough, bad token errors}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
set rec [commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}]
set good [commandstack::get_next_command ::tgt ::csA 1]
set passthrough [commandstack::get_next_command ::lrepeat ::whoever 1]
set code [catch {commandstack::get_next_command ::tgt ::csA 99} msg]
list [expr {$good eq [dict get $rec implementation]}] $passthrough \
$code [string match "*unable to determine next command*" $msg]
}
} -result {1 ::lrepeat 1 1}
#overrides from two different renamers stack bottom-first; the second record
#identifies the first renamer via the injected IMPLEMENTOR magic comment and
#parks its proc at the dash-form name; calls run top-down through the chain.
test commandstack_two_renamers_stack_and_chain {two renamers stack bottom-first and calls chain top-down through both to the original}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
set r2 [commandstack::rename_command -renamer ::csB ::tgt {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
set stack [commandstack::get_stack ::tgt]
list [::tgt x] [llength $stack] \
[dict get [lindex $stack 0] renamer] [dict get [lindex $stack 1] renamer] \
[dict get $r2 next_implementor] [dict get $r2 implementation]
}
} -result {{B {A {base x}}} 2 ::csA ::csB ::csA ::commandstack::renamed_commands::_ns_tgt-_ns_csA-_ns_csB-1}
#basecall reaches the bottom-of-stack original implementation regardless of
#stacked overrides; on a never-renamed command it just calls the command.
test commandstack_basecall_bypasses_overrides {basecall reaches the original under stacked overrides and calls unstacked commands directly}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
proc ::plainproc {args} {return [list plain $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csB ::tgt {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
list [commandstack::basecall ::tgt x] [commandstack::basecall ::plainproc y]
}
} -result {{base x} {plain y}}
#the load/unload-any-order design goal: removing the BOTTOM entry re-points the
#entry above at what the removed entry delegated to (implementation and
#next_implementor copied down), deletes the removed renamer's parked proc, and
#dispatch skips the removed layer; removing the remaining entry then restores
#the original completely.
test commandstack_remove_bottom_entry_relinks {removing the bottom entry re-links the entry above; final remove restores the original}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csB ::tgt {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::remove_rename {::tgt ::csA}
set stack [commandstack::get_stack ::tgt]
set rec [lindex $stack 0]
set midcall [::tgt x]
set aparked [info commands ::commandstack::renamed_commands::_ns_tgt-_ns_csA-_ns_csB-1]
commandstack::remove_rename {::tgt ::csB}
list $midcall [llength $stack] $aparked \
[dict get $rec implementation] [dict get $rec next_implementor] \
[::tgt x] [llength [commandstack::get_stack ::tgt]] \
[info commands ::commandstack::renamed_commands::*]
}
} -result {{B {base x}} 1 {} ::commandstack::renamed_commands::_ns_tgt_unspecified-_ns_csA-1 unspecified {base x} 0 {}}
#removing the TOP entry (by exact token) reinstates the previous override as the
#live command; removing the last entry restores the original.
test commandstack_remove_top_entry_restores_previous {removing the top entry by token reinstates the previous override, then the original}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
set r1 [commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
set r2 [commandstack::rename_command -renamer ::csB ::tgt {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
commandstack::remove_rename [dict get $r2 token]
set midcall [::tgt x]
set midlen [llength [commandstack::get_stack ::tgt]]
commandstack::remove_rename [dict get $r1 token]
list $midcall $midlen [::tgt x] [llength [commandstack::get_stack ::tgt]]
}
} -result {{A {base x}} 1 {base x} 0}
#rename_command without -renamer records the calling namespace as renamer and
#resolves a relative command name in the caller's context; remove_rename with a
#bare command name works from that same namespace context.
test commandstack_default_renamer_and_bare_remove {default renamer is the calling namespace; bare-name remove works from that context}\
-constraints commandstacksrc -body {
cs_probe {
namespace eval ::csC {}
proc ::csC::victim {args} {return [list vbase $args]}
set rec [namespace eval ::csC {
commandstack::rename_command victim {args} {
return [list wrapped [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
}]
set call1 [::csC::victim k]
set after [namespace eval ::csC {commandstack::remove_rename victim}]
list [dict get $rec token] $call1 [::csC::victim k] [llength $after]
}
} -result {{::csC::victim ::csC 1} {wrapped {vbase k}} {vbase k} 0}
#remove_rename errors for a renamer commandstack has never seen - including the
#bare-command form called from a namespace context (here ::) that performed no
#rename.
test commandstack_remove_unknown_renamer_errors {remove_rename errors for unknown renamers, including bare-name form from a non-renamer context}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set c1 [catch {commandstack::remove_rename {::tgt ::neverheardof}} msg1]
set c2 [catch {commandstack::remove_rename ::tgt} msg2] ;#renamer defaults to caller ns ::
list $c1 [string match "*not in list of known_renamers*" $msg1] \
$c2 [string match "*not in list of known_renamers*" $msg2]
}
} -result {1 1 1 1}
#known_renamers ships preseeded with ::packagetrace and ::packageSuppress (the
#original cooperating packages - note the vendored packages currently register
#as plain-word 'packagetrace'/'packagesuppress', so these defaults do not match
#them); new renamers are appended by rename_command.
test commandstack_known_renamers_defaults {known_renamers preseeded with ::packagetrace ::packageSuppress; rename_command appends new renamers}\
-constraints commandstacksrc -body {
cs_probe {
set initial $::commandstack::known_renamers
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
list $initial $::commandstack::known_renamers
}
} -result {{::packagetrace ::packageSuppress} {::packagetrace ::packageSuppress ::csA}}
#rename_command on a nonexistent command performs no rename: returns a record of
#just 'implementation {}' (the signal consumers test for), warns on stderr, and
#adds nothing to the stacks.
test commandstack_rename_missing_command {rename of a nonexistent command returns implementation {} and warns, stacks untouched}\
-constraints commandstacksrc -body {
cs_probe {
set rec [commandstack::rename_command -renamer ::csX ::definitely_not_here {args} {}]
list $rec [dict size [commandstack::get_stack]] \
[putslog_matches "*not found in calling context*"]
}
} -result {{implementation {}} 0 1}
test commandstack_rename_command_usage_error {rename_command arg-count validation message is pinned}\
-constraints commandstacksrc -body {
cs_probe {
catch {commandstack::rename_command ::whatever} msg
set msg
}
} -result {commandstack::rename_command usage: rename_command ?-renamer <string>? command procargs procbody}
#a same-renamer re-rename with an identical procbody is refused: record is
#'implementation {}', stack unchanged, warning + stack display captured.
test commandstack_samerenamer_same_body_aborted {same-renamer re-rename with identical procbody is refused with implementation {}}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
set body {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csA ::tgt {args} $body
set rec2 [commandstack::rename_command -renamer ::csA ::tgt {args} $body]
list $rec2 [llength [commandstack::get_stack ::tgt]] [::tgt x] \
[putslog_matches "*same procbody - Aborting rename*"]
}
} -result {{implementation {}} 1 {A {base x}} 1}
#KNOWN DEFECT (pinned): the per-renamer token counter increments an apply-local
#renamer_command_tokens variable, so tokenid never advances past 1. A
#same-renamer re-rename with NEW code proceeds (warning captured) and stacks a
#second record - but with a DUPLICATE token {::tgt ::csA 1}. get_next_command
#finds the FIRST matching record, so the new override delegates straight to the
#ORIGINAL, silently bypassing the renamer's still-stacked first override.
test commandstack_GAP_samerenamer_new_body_duplicate_token_bypass {same-renamer new-body re-rename stacks a duplicate token and dispatch bypasses the first override}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
set r2 [commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
set stack [commandstack::get_stack ::tgt]
list [llength $stack] [dict get $r2 token] [dict get [lindex $stack 0] token] \
[dict get $r2 implementation] [::tgt x] \
[putslog_matches "*appears to be with new code - proceeding*"]
}
} -result {2 {::tgt ::csA 1} {::tgt ::csA 1} ::commandstack::renamed_commands::_ns_tgt-_ns_csA-_ns_csA-1 {A2 {base x}} 1}
#KNOWN DEFECT (pinned): because tokenid is stuck at 1, a THIRD same-renamer
#rename computes the same renamed_commands target name as the second and the
#underlying [rename] errors ('command already exists'). The failure is at least
#clean: it occurs before the live command is touched - the stack keeps its 2
#records and the command remains callable.
test commandstack_GAP_third_samerenamer_rename_name_collision {third same-renamer rename collides on the parked-implementation name and errors cleanly}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
set c [catch {
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A3 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
} msg]
list $c [string match "*already exists*" $msg] \
[llength [commandstack::get_stack ::tgt]] [::tgt x]
}
} -result {1 1 2 {A2 {base x}}}
#KNOWN DEFECT (pinned): a renamer re-renaming after ANOTHER renamer took the top
#proceeds with the 'not immediate predecessor' warning (and a stray bare
#'puts stderr' that writes the literal word 'stderr' to stdout - asserted via
#the shim's {stdout stderr} log entry), stacking a third record - but its
#duplicate token again resolves to the renamer's FIRST record, so dispatch
#bypasses both intermediate overrides.
test commandstack_GAP_rerename_after_other_renamer_bypasses_chain {re-rename after another renamer warns, stacks a third record, and dispatch bypasses both intermediate overrides}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csB ::tgt {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
set r3 [commandstack::rename_command -renamer ::csA ::tgt {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}]
list [llength [commandstack::get_stack ::tgt]] \
[dict get $r3 implementation] [::tgt x] \
[putslog_matches "*is not immediate predecessor - proceeding anyway*"] \
[expr {[list stdout stderr] in $::PUTS_LOG}]
}
} -result {3 ::commandstack::renamed_commands::_ns_tgt-_ns_csB-_ns_csA-1 {A2 {base x}} 1 1}
#rename_command accepts arbitrary proc signatures (the punk auto_execok and
#packagetrace tcl_findLibrary usage) - defaults still apply through the shim.
test commandstack_non_args_signature {rename_command works with non-args signatures including defaulted arguments}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt3 {first {second dflt}} {return [list orig $first $second]}
commandstack::rename_command -renamer ::csS ::tgt3 {first {second dflt}} {
return [list shim [uplevel 1 [list $COMMANDSTACKNEXT $first $second]]]
}
list [::tgt3 a] [::tgt3 a b]
}
} -result {{shim {orig a dflt}} {shim {orig a b}}}
#show_stack (punk::lib absent - plain fallback branch): exact command name is
#resolved in the caller's context, output carries the record fields, a glob
#matching the same single key renders identically, no match returns empty.
test commandstack_show_stack_fallback_render {show_stack fallback render carries record fields; glob and exact forms agree; no match is empty}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set out [commandstack::show_stack ::tgt]
set outglob [commandstack::show_stack ::tg*]
list [string match "::tgt = *" $out] [string match "*token*" $out] \
[string match "*renamer*" $out] [string match "*implementation*" $out] \
[expr {$outglob eq $out}] [commandstack::show_stack ::zzz_nomatch*]
}
} -result {1 1 1 1 1 {}}
#debug: defaults 0, set/query round-trip, non-boolean argument errors; with
#debug on, rename_command reports the first rename on stderr.
test commandstack_debug_accessor {debug accessor round-trip, non-boolean errors, debug-on rename reports progress}\
-constraints commandstacksrc -body {
cs_probe {
set a [commandstack::debug]
set b [commandstack::debug 1]
set c [commandstack::debug]
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set logged [putslog_matches "*1st detected rename of command '::tgt'*"]
set d [commandstack::debug 0]
set e [catch {commandstack::debug notabool}]
list $a $b $c $logged $d $e
}
} -result {0 1 1 1 0 1}
#Delete_stack always returns 1 (stack present or not) and does NOT undo renames.
#KNOWN FOOT-GUN (pinned): deleting the stack under a live override breaks
#COMMANDSTACKNEXT resolution - get_next_command falls back to the command name
#itself, so the next call recurses to the interp limit.
test commandstack_GAP_delete_stack_live_rename_recursion {Delete_stack under a live rename leaves the override calling itself - recursion limit error}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set d1 [commandstack::Delete_stack ::tgt]
set d2 [commandstack::Delete_stack ::never_stacked]
set code [catch {::tgt x} msg]
list $d1 $d2 $code [string match "*too many nested evaluations*" $msg]
}
} -result {1 1 1 1}
#Rename_stack re-keys stack records without touching commands; the parked key is
#invisible to get_stack <name> (namespace which resolution fails for a
#non-command name) but present in the no-arg dict; renaming onto an existing
#stack key errors.
test commandstack_rename_stack_parks_records {Rename_stack parks records under a new key - visible only via the no-arg dict; existing key errors}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
commandstack::Rename_stack ::tgt ::tgt_parked
set full [commandstack::get_stack]
set err [catch {
commandstack::Rename_stack ::tgt_parked ::tgt_parked
} msg]
list [commandstack::get_stack ::tgt_parked] [dict keys $full] \
$err [string match "*already exists in stack*" $msg]
}
} -result {{} ::tgt_parked 1 1}
#get_IMPLEMENTOR classification: magic-comment proc -> package name; plain proc
#-> unspecified; a commandstack-renamed command reports its renamer via the
#injected IMPLEMENTOR comment. KNOWN DEFECT (pinned): builtins report
#'undetermined' on EVERY Tcl version - the guard probes 'tcl::info::cmdtype'
#with an unqualified pattern from inside commandstack::util, and info commands
#pattern matching does not fall back to the global namespace, so the 'builtin'
#classification branch is unreachable even on 8.7+/9 where cmdtype exists (the
#guard_sees element pins the empty pattern-match result that causes it).
test commandstack_GAP_get_implementor_builtin_branch_unreachable {get_IMPLEMENTOR classifies magic-comment/plain/renamed procs but builtins report undetermined - cmdtype guard unreachable}\
-constraints commandstacksrc -body {
cs_probe {
proc ::withmagic {args} {
#IMPLEMENTOR_mypkg! (magic comment convention)
return ok
}
proc ::plainone {args} {return ok}
proc ::tgt {args} {return [list base $args]}
commandstack::rename_command -renamer ::csA ::tgt {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set guard_sees [namespace eval ::commandstack::util {info commands tcl::info::cmdtype}]
list [commandstack::util::get_IMPLEMENTOR ::withmagic] \
[commandstack::util::get_IMPLEMENTOR ::plainone] \
[commandstack::util::get_IMPLEMENTOR ::lrepeat] \
[commandstack::util::get_IMPLEMENTOR ::tgt] $guard_sees
}
} -result {mypkg unspecified undetermined ::csA {}}
#split_body splits an installed override body at the separator marker: header
#carries the IMPLEMENTOR comment and COMMANDSTACKNEXT setup (marker line itself
#dropped from both parts), code round-trips the renamer-supplied procbody; a
#body without the marker returns {{} body}.
test commandstack_lib_split_body_roundtrip {split_body separates commandstack header from procbody and round-trips the original code}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt {args} {return [list base $args]}
set body {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csA ::tgt {args} $body
lassign [commandstack::lib::split_body [info body ::tgt]] header code
list [string match "*IMPLEMENTOR_::csA!*" $header] \
[string match "*COMMANDSTACKNEXT*" $header] \
[string match "*<commandstack_separator>*" $header] \
[string match "*<commandstack_separator>*" $code] \
[expr {[string trim $code] eq [string trim $body]}] \
[commandstack::lib::split_body {a body without marker}]
}
} -result {1 1 0 0 1 {{} {a body without marker}}}
#splitx (vendored textutil::splitx): whitespace default, empty string, empty
#regexp splits chars, custom class, parenthesised submatch keeps separators,
#empty-matching regexp raises the infinite-loop error.
test commandstack_lib_splitx {splitx splits on regexp with submatch-keeps-separator and infinite-loop guard semantics}\
-constraints commandstacksrc -body {
cs_probe {
list [commandstack::lib::splitx "a b c"] \
[commandstack::lib::splitx ""] \
[commandstack::lib::splitx "abc" ""] \
[commandstack::lib::splitx "a+b-c" {[+-]}] \
[commandstack::lib::splitx "a+b" {(\+)}] \
[catch {commandstack::lib::splitx "abc" {x*}}]
}
} -result {{a b c} {} {a b c} {a b c} {a + b} 1}
#help returns a substantial dependency-free overview naming the core API and the
#COMMANDSTACKNEXT delegation contract.
test commandstack_help_overview {help returns a substantial overview naming the core API and delegation variables}\
-constraints commandstacksrc -body {
cs_probe {
set h [commandstack::help]
list [expr {[string length $h] > 500}] \
[string match "*rename_command*" $h] [string match "*remove_rename*" $h] \
[string match "*COMMANDSTACKNEXT*" $h] [string match "*basecall*" $h] \
[string match "*get_next_command*" $h] [string match "*get_stack*" $h]
}
} -result {1 1 1 1 1 1 1}
#the PUNKARGS documentation registers lazily with punk::args (no punk::args
#dependency in the module itself) - every documented id resolves and renders a
#synopsis.
test commandstack_punkargs_docs_resolve {every commandstack PUNKARGS id resolves through punk::args lazy registration}\
-constraints {commandstacksrc havepunkargs} -body {
package require commandstack
set problems [list]
foreach id {
::commandstack::help ::commandstack::debug ::commandstack::get_stack
::commandstack::get_next_command ::commandstack::basecall
::commandstack::rename_command ::commandstack::remove_rename
::commandstack::show_stack ::commandstack::Delete_stack
::commandstack::Rename_stack ::commandstack::util::get_IMPLEMENTOR
::commandstack::lib::splitx ::commandstack::lib::split_body
} {
if {[catch {punk::args::synopsis $id} err]} {
lappend problems [list $id $err]
}
}
set problems
} -result {}
cleanupTests
}
Loading…
Cancel
Save