Browse Source

commandstack 0.7.0: remove_rename convenience forms (G-160 follow-on landed) (0.49.8)

- pop_rename renamer ?command? - pop the renamer's topmost stack entry and
  return the removed record (searched across live stacks when no command
  given; entries on multiple commands are an ambiguity error)
- remove_renamer renamer - remove ALL of a renamer's entries across every
  live stack (the unload-my-package form); returns removed records keyed
  by command
- restore_original command - unwind the whole stack to the original
  implementation regardless of renamer; registers stack-evidenced renamers
  into known_renamers so it is not gated by state loss (module re-source)
- all three route through remove_rename's re-linking/token-map machinery;
  Rename_stack-parked stacks are invisible to the renamer-wide forms
- suite 40/40 on tclsh90 (9.0.3) + punk86 (8.6); packagepreference
  consumer suites 6/6 on both; minted modules/commandstack-0.7.0.tm
  (bootsupport/vfscommon promotion left to the next cycle, as with 0.6.0)

Assisted-by: harness=opencode; primary-model=opencode/kimi-k3; api-location=unknown
master
Julian Noble 6 days ago
parent
commit
31c0cd0477
  1. 17
      CHANGELOG.md
  2. 2
      goals/archive/G-160-commandstack-hygiene.md
  3. 2
      punkproject.toml
  4. 226
      src/modules/commandstack-999999.0a1.0.tm
  5. 14
      src/modules/commandstack-buildversion.txt
  6. 2
      src/tests/modules/AGENTS.md
  7. 209
      src/tests/modules/commandstack/testsuites/commandstack/commandstack.test

17
CHANGELOG.md

@ -5,6 +5,23 @@ 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.8] - 2026-08-04
- commandstack 0.7.0 (G-160 follow-on, landed at user direction): the
remove_rename convenience forms parked as code todos since 2021 -
pop_rename (pop a renamer's topmost stack entry, per command or
searched across live stacks), remove_renamer (remove ALL of a
renamer's entries across every live stack - the unload-my-package
form), and restore_original (unwind a command's whole stack to the
original implementation regardless of renamer; deliberately not gated
by known_renamers so it survives state loss such as a module
re-source). All three route through remove_rename's re-linking/token
map machinery; Rename_stack-parked stacks are invisible to the
renamer-wide forms. commandstack suite 40/40 on tclsh90 (9.0.3) and
punk86 (8.6); packagepreference consumer suites 6/6 on both. Minted
as modules/commandstack-0.7.0.tm; bootsupport/vfscommon promotion
left to the next promotion cycle (as with 0.6.0).
## [0.49.7] - 2026-08-04
- punk::tcltestrun 0.4.1 (G-161 follow-on, landed at user direction):

2
goals/archive/G-160-commandstack-hygiene.md

@ -180,5 +180,5 @@ Verification evidence:
## Follow-ons
Follow-on: remove_rename convenience forms parked as code todos (pop topmost-for-renamer, remove all entries for a renamer, restore-to-original regardless of stack) => open
Follow-on: remove_rename convenience forms parked as code todos (pop topmost-for-renamer, remove all entries for a renamer, restore-to-original regardless of stack) => landed 2026-08-04 (commandstack 0.7.0 - pop_rename/remove_renamer/restore_original)
Follow-on: a module re-source resets known_renamers and debug while all_stacks/renamer_command_tokens/token_implementations survive their info-exists guards - guard known_renamers likewise or document the reload contract => open

2
punkproject.toml

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

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

@ -11,6 +11,16 @@
# - 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-04 (G-160 follow-on)
# - remove_rename convenience forms (the long-parked code todos):
# pop_rename (pop a renamer's topmost entry - per command, or searched
# across live stacks when unambiguous), remove_renamer (remove ALL of a
# renamer's entries across every live stack - the unload-my-package form),
# and restore_original (unwind a command's whole stack to the original
# implementation regardless of renamer; not gated by known_renamers so it
# works after state loss such as a module re-source). All three route
# through remove_rename's re-linking/token-map machinery; stacks parked
# by Rename_stack are invisible to the renamer-wide forms.
#2026-08-03 (hygiene pass - G-160)
# - fix tokenid counter: was incremented on an apply-local variable so it was stuck at 1 -
# tokenids are now unique and monotonic per (renamer,command), so same-renamer re-renames
@ -201,6 +211,21 @@ namespace eval commandstack {
unique per (renamer, command), so any entry - not just the topmost -
is removable by its exact token.
Convenience removal forms
commandstack::pop_rename <renamer> ?command?
Pop the renamer's topmost entry and return the removed record -
on the given command, or searched across all live stacks when the
renamer's entries live on only one command (multi-command entries
are an ambiguity error).
commandstack::remove_renamer <renamer>
Remove every entry the renamer recorded, across all live stacks -
the unload-my-package form. Returns the removed records keyed by
command. Stacks parked by Rename_stack are left untouched.
commandstack::restore_original <command>
Unwind the command's whole stack regardless of renamer, restoring
the original implementation (a repair/reset operation - not gated
by known_renamers). Returns the number of records removed.
Inspection
commandstack::get_stack ?command? - rename records (or all stacks;
raw stacks-dict key tried first,
@ -653,11 +678,6 @@ namespace eval commandstack {
uplevel 2 [list $COMMANDSTACKNEXT {*}$args]
}
#todo - concept of 'pop' for renamer. Remove topmost entry specific to the renamer
#todo - removal by token to allow renamer to have multiple entries for one command but to remove one that is not the topmost
#todo - removal of all entries pertaining to a particular renamer
#todo - allow restore to bottom-most implementation (original) - regardless of what renamers have cooperated in the stack?
#remove by token, or by commandname if called from same context as original rename_command
#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.
@ -764,6 +784,202 @@ namespace eval commandstack {
return [list]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::pop_rename
@cmd -name "commandstack::pop_rename" -&
-summary -&
"Pop a renamer's topmost rename-stack entry." -&
-help -&
{Removes the topmost (most recently stacked) rename entry
belonging to renamer and returns the removed record - the
LIFO undo for a package that renames as it loads and
unwinds as it unloads. With command given, the pop is
restricted to that command's stack (equivalent to
'remove_rename [list $command $renamer]'). Without command,
every live stack is searched: when the renamer's entries
all live on one command the pop happens there; entries
spread across multiple commands are ambiguous and raise an
error naming those commands (supply command, or use
commandstack::remove_renamer to remove all of them).
Stacks parked by Rename_stack are maintenance state and are
not searched. The renamer must be known to commandstack or
an error is raised. Returns the empty string when the
renamer has no matching entry.}
@values -min 1 -max 2
renamer -type string -help -&
"Renamer string recorded at rename time (must be known
to commandstack)."
command -type string -optional 1 -help -&
"Restrict the pop to this command's stack (resolved in
the caller's namespace context)."
}]
}
proc pop_rename {renamer {command ""}} {
variable all_stacks
variable known_renamers
variable debug
if {$renamer ni $known_renamers} {
error "(commandstack::pop_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time."
}
if {$command ne ""} {
set command [uplevel 1 [list namespace which $command]]
if {$command eq "" || ![dict exists $all_stacks $command]} {
return ""
}
set commands [list $command]
} else {
#find the live stacks holding entries for this renamer. Stacks parked
#by Rename_stack are skipped: their records keep the original token
#command, which never equals the parked key.
set commands [list]
dict for {key stack} $all_stacks {
if {![llength $stack]} {
continue
}
if {[lindex [dict get [lindex $stack 0] token] 0] ne $key} {
continue
}
if {[lsearch -index 3 $stack $renamer] > -1} {
lappend commands $key
}
}
if {[llength $commands] > 1} {
error "(commandstack::pop_rename) ERROR: renamer '$renamer' has entries on multiple commands ([join $commands {, }]) - supply the command argument, or use commandstack::remove_renamer to remove all of its entries"
}
if {![llength $commands]} {
return ""
}
}
set command [lindex $commands 0]
set stack [dict get $all_stacks $command]
set topmost [lindex [lsearch -all -index 3 $stack $renamer] end]
if {$topmost eq ""} {
return ""
}
set record [lindex $stack $topmost]
remove_rename [list $command $renamer]
if {$debug} {
puts stderr "(commandstack::pop_rename) popped [dict get $record token]"
}
return $record
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::remove_renamer
@cmd -name "commandstack::remove_renamer" -&
-summary -&
"Remove every rename-stack entry belonging to a renamer." -&
-help -&
{The unload-my-package convenience: removes ALL entries
recorded for renamer across every live command stack. Each
command's entries are popped topmost-first through the same
re-linking machinery as remove_rename, so other renamers'
overrides keep delegating correctly. Stacks parked by
Rename_stack are maintenance state and are left untouched.
The renamer must be known to commandstack or an error is
raised; the renamer is NOT removed from known_renamers.
Returns a dict keyed by command name whose values are the
removed records (topmost-first); an empty dict when the
renamer had no entries.}
@values -min 1 -max 1
renamer -type string -help -&
"Renamer string recorded at rename time (must be known
to commandstack)."
}]
}
proc remove_renamer {renamer} {
variable all_stacks
variable known_renamers
variable debug
if {$renamer ni $known_renamers} {
error "(commandstack::remove_renamer) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time."
}
set removed [dict create]
#remove_rename mutates all_stacks only under the command key it is
#given, so iterating over this snapshot of the stacks dict is safe.
#Stacks parked by Rename_stack are skipped: their records keep the
#original token command, which never equals the parked key.
dict for {command stack} $all_stacks {
if {![llength $stack]} {
continue
}
if {[lindex [dict get [lindex $stack 0] token] 0] ne $command} {
continue
}
while {[set topmost [lindex [lsearch -all -index 3 [dict get $all_stacks $command] $renamer] end]] ne ""} {
set record [lindex [dict get $all_stacks $command] $topmost]
remove_rename [list $command $renamer]
dict lappend removed $command $record
if {$debug} {
puts stderr "(commandstack::remove_renamer) removed [dict get $record token]"
}
}
}
return $removed
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::restore_original
@cmd -name "commandstack::restore_original" -&
-summary -&
"Unwind a command's whole rename stack, restoring the original implementation." -&
-help -&
{Removes EVERY record on the command's live rename stack -
regardless of which renamers contributed them - and
restores the bottom-of-stack (original) implementation as
the live command. Records are removed topmost-first through
the same machinery as remove_rename.
This is the repair/reset operation: it is deliberately NOT
gated by known_renamers (unlike remove_rename) - renamers
evidenced by the stack records are registered into
known_renamers first, mirroring rename_command, so a
restore still works after state loss such as a module
re-source that reset known_renamers while stacks survived.
Returns the number of records removed: 0 when the command
has no live stack (never renamed, or only the empty residue
entry - prune that with Delete_stack).}
@values -min 1 -max 1
command -type string -help -&
"Command name (resolved with 'namespace which' in the
caller's context)."
}]
}
proc restore_original {command} {
variable all_stacks
variable known_renamers
variable debug
set command [uplevel 1 [list namespace which $command]]
if {$command eq "" || ![dict exists $all_stacks $command]} {
return 0
}
set stack [dict get $all_stacks $command]
if {![llength $stack]} {
return 0
}
#a repair operation must not be gated by known_renamers (which a module
#re-source may have reset while stacks survived) - register the renamers
#the stack evidences, mirroring rename_command
foreach record $stack {
set record_renamer [dict get $record renamer]
if {$record_renamer ni $known_renamers} {
lappend known_renamers $record_renamer
}
}
set removed_count 0
while {[llength $stack]} {
remove_rename [dict get [lindex $stack end] token]
set stack [dict get $all_stacks $command]
incr removed_count
}
if {$debug} {
puts stderr "(commandstack::restore_original) restored '$command' to its original implementation ($removed_count override(s) unwound)"
}
return $removed_count
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::show_stack

14
src/modules/commandstack-buildversion.txt

@ -1,6 +1,18 @@
0.6.0
0.7.0
#First line must be a tm version number
#all other lines are ignored.
#0.7.0 - G-160 follow-on: remove_rename convenience forms (the parked code todos):
# - pop_rename renamer ?command? - pop the renamer's topmost stack entry and
# return the removed record (searched across live stacks when no command is
# given; entries on multiple commands are an ambiguity error)
# - remove_renamer renamer - remove ALL of a renamer's entries across every
# live stack (the unload-my-package form); returns removed records keyed by
# command
# - restore_original command - unwind the whole stack to the original
# implementation regardless of renamer; registers stack-evidenced renamers
# into known_renamers so it is not gated by state loss (module re-source)
# - all three route through remove_rename's re-linking/token-map machinery;
# stacks parked by Rename_stack are invisible to the renamer-wide forms
#0.6.0 - G-160 hygiene pass:
# - tokenid unique+monotonic per (renamer,command) - the counter now increments the real
# namespace variable (was an apply-local copy stuck at 1); same-renamer re-renames

2
src/tests/modules/AGENTS.md

@ -40,7 +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) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0; usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), 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, the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). Green on tclsh90 (9.0.3) and punk86 (8.6)
- `commandstack/` — commandstack (cooperative command renaming) tests (`testsuites/commandstack/commandstack.test`, 2026-08-03 - characterisation suite + the G-160 hygiene-pass contract at commandstack 0.6.0 + the 0.7.0 convenience removal forms (G-160 follow-on, 2026-08-04); usage-driven from punk::packagepreference/packagetrace/packagesuppress/punk-auto_execok/punk::nav::fs-cd): record shape as a contract (token first/renamer second dict key order for the lsearch -index 1/-index 3 convention, trailing `did_rename` 0|1, `{implementation {} did_rename 0}` no-rename signal), COMMANDSTACKNEXT/COMMANDSTACKNEXT_ORIGINAL delegation + the `commandstack::next` helper (caller-context parity with the manual uplevel convention pinned), unique+monotonic per-(renamer,command) tokenids (same-renamer re-renames chain and are removable by exact token, third rename succeeds), 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, the 0.7.0 convenience removal forms (pop_rename - command form pops topmost-for-renamer and returns the removed record, bare form searches live stacks with a multi-command ambiguity error; remove_renamer - all of a renamer's entries across live stacks with correct re-linking, Rename_stack-parked stacks skipped, removed records returned keyed by command; restore_original - whole-stack unwind to the original returning the record count, deliberately registering stack-evidenced renamers so it survives a known_renamers reset while the renamer-explicit forms keep the gate; all silent with debug off), the token->implementation map get_next_command resolves through (map/stack consistency pinned across rename/remove; parked stacks keep dispatching), channel discipline (silent full cycle with debug off; warnings only under debug), debug argument validation, -renamer misplacement errors, get_stack raw-key-first retrieval of Rename_stack-parked records + Rename_stack 1/0 returns, Delete_stack live-record guard (errors; empty/missing return 1), get_IMPLEMENTOR classification incl builtin-where-cmdtype-exists (dynamic expectation - undetermined on 8.6), lib::split_body round-trip, lib::splitx, show_stack fallback render, the help overview, and lazy punk::args registration of the PUNKARGS docs. Behavioural tests run in fresh child interps per test (module sourced by path relative to the test file; a ::puts shim captures module output for silence/warning assertions and keeps runner output clean); descriptions are single-line per the tcltestrun banner-parsing style guidance in src/tests/AGENTS.md (a hard contract until G-161 made the parser multi-line tolerant). 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)

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

@ -19,7 +19,13 @@
# proc signatures.
# 4. remove_rename forms: 3-element token, {command renamer} pair, bare
# command name resolved against the calling namespace; unknown renamer
# errors.
# errors. Convenience removal forms (0.7.0, the G-160 follow-on):
# pop_rename (pop topmost-for-renamer, per command or searched across
# live stacks with a multi-command ambiguity error), remove_renamer
# (ALL entries for a renamer across live stacks), restore_original
# (unwind the whole stack to the original - deliberately NOT gated by
# known_renamers so it survives state loss); stacks parked by
# Rename_stack are invisible to the renamer-wide forms.
# 5. The G-160 hygiene-pass contract (the 2026-08-03 characterisation
# suite's _GAP_ defect pins flipped to fixed-behaviour pins at
# commandstack 0.6.0): tokenids unique and monotonic per
@ -762,8 +768,8 @@ namespace eval ::testspace {
}
} -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.
#help returns a substantial dependency-free overview naming the core API, the
#COMMANDSTACKNEXT delegation contract, and the 0.7.0 convenience removal forms.
test commandstack_help_overview {help returns a substantial overview naming the core API and delegation variables}\
-constraints commandstacksrc -body {
cs_probe {
@ -772,9 +778,11 @@ namespace eval ::testspace {
[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] \
[string match "*commandstack::next*" $h]
[string match "*commandstack::next*" $h] \
[string match "*pop_rename*" $h] [string match "*remove_renamer*" $h] \
[string match "*restore_original*" $h]
}
} -result {1 1 1 1 1 1 1 1}
} -result {1 1 1 1 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
@ -787,7 +795,8 @@ namespace eval ::testspace {
::commandstack::help ::commandstack::debug ::commandstack::get_stack
::commandstack::get_next_command ::commandstack::basecall
::commandstack::rename_command ::commandstack::next
::commandstack::remove_rename
::commandstack::remove_rename ::commandstack::pop_rename
::commandstack::remove_renamer ::commandstack::restore_original
::commandstack::show_stack ::commandstack::Delete_stack
::commandstack::Rename_stack ::commandstack::util::get_IMPLEMENTOR
::commandstack::lib::splitx ::commandstack::lib::split_body
@ -799,5 +808,193 @@ namespace eval ::testspace {
set problems
} -result {}
#added 2026-08-04 (agent) - G-160 follow-on: remove_rename convenience forms
#(pop_rename / remove_renamer / restore_original at commandstack 0.7.0)
#pop_rename with a command argument pops the renamer's TOPMOST entry on that
#command and returns the removed record; the renamer's earlier entry becomes
#live again and delegates to the original, and the popped record's parked
#implementation name is consumed by the restore.
test commandstack_pop_rename_with_command {pop_rename command form pops the renamer's topmost entry and returns the removed record}\
-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 popped [commandstack::pop_rename ::csA ::tgt]
set after [::tgt x]
list [dict get $popped token] [dict get $popped did_rename] \
[llength [commandstack::get_stack ::tgt]] $after \
[dict get $popped implementation] \
[info commands [dict get $popped implementation]]
}
} -result {{::tgt ::csA 2} 1 1 {A {base x}} ::commandstack::renamed_commands::_ns_tgt-_ns_csA-_ns_csA-2 {}}
#pop_rename without a command searches the live stacks: the pop lands on the
#single command holding the renamer's entries; a renamer with entries on
#MULTIPLE commands is an ambiguity error naming the commands; a known renamer
#with no entries returns the empty string; an unknown renamer errors.
test commandstack_pop_rename_search_and_ambiguity {pop_rename without command searches live stacks, errors on multi-command ambiguity, empty for no entries}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt1 {args} {return [list b1 $args]}
proc ::tgt2 {args} {return [list b2 $args]}
proc ::tgt3 {args} {return [list b3 $args]}
commandstack::rename_command -renamer ::csA ::tgt1 {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
commandstack::rename_command -renamer ::csB ::tgt2 {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
commandstack::rename_command -renamer ::csB ::tgt3 {args} {
uplevel 1 [list $COMMANDSTACKNEXT {*}$args]
}
set popped [commandstack::pop_rename ::csA]
set c1 [catch {commandstack::pop_rename ::csB} msg1]
commandstack::remove_rename {::tgt3 ::csB}
set popped2 [commandstack::pop_rename ::csB]
set empty [commandstack::pop_rename ::csA]
set c2 [catch {commandstack::pop_rename ::neverheard} msg2]
list [dict get $popped token] [llength [commandstack::get_stack ::tgt1]] \
$c1 [string match "*multiple commands*::tgt2*::tgt3*" $msg1] \
[dict get $popped2 token] $empty \
$c2 [string match "*not in list of known_renamers*" $msg2]
}
} -result {{::tgt1 ::csA 1} 0 1 1 {::tgt2 ::csB 1} {} 1 1}
#remove_renamer removes ALL of a renamer's entries across every live stack -
#multiple entries on one command interleaved with another renamer's, plus
#entries on a second command - re-linking delegation correctly (the surviving
#re-override ends up delegating to the original), and returns the removed
#records keyed by command, topmost-first. Other renamers' overrides stay live.
test commandstack_remove_renamer_all_entries {remove_renamer removes every entry of a renamer across live stacks with correct re-linking}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt1 {args} {return [list b1 $args]}
proc ::tgt2 {args} {return [list b2 $args]}
commandstack::rename_command -renamer ::csA ::tgt1 {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csB ::tgt1 {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csA ::tgt1 {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csA ::tgt2 {args} {
return [list Ax [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
set before [::tgt1 q]
set removed [commandstack::remove_renamer ::csA]
set removed_tokens [lmap rec [dict get $removed ::tgt1] {dict get $rec token}]
list $before [::tgt1 q] [::tgt2 q] \
[dict keys $removed] $removed_tokens \
[dict get [lindex [dict get $removed ::tgt2] 0] token] \
[llength [commandstack::get_stack ::tgt1]] [llength [commandstack::get_stack ::tgt2]]
}
} -result {{A2 {B {A {b1 q}}}} {B {b1 q}} {b2 q} {::tgt1 ::tgt2} {{::tgt1 ::csA 2} {::tgt1 ::csA 1}} {::tgt2 ::csA 1} 1 0}
#remove_renamer leaves Rename_stack-parked records untouched (parked stacks
#are maintenance state - their records keep the original token command, which
#never equals the parked key); the live override keeps dispatching throughout;
#an unknown renamer errors.
test commandstack_remove_renamer_parked_and_unknown {remove_renamer skips parked stacks and errors for unknown renamers}\
-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_stack ::tgt ::tgt_parked
set removed [commandstack::remove_renamer ::csA]
set parked_still [llength [commandstack::get_stack ::tgt_parked]]
set c1 [catch {commandstack::remove_renamer ::neverheard} msg1]
commandstack::Rename_stack ::tgt_parked ::tgt
list $removed $parked_still [::tgt x] \
$c1 [string match "*not in list of known_renamers*" $msg1]
}
} -result {{} 1 {A {base x}} 1 1}
#restore_original unwinds a command's WHOLE stack - multiple renamers,
#regardless - topmost-first, restoring the original implementation as the
#live command; returns the record count; 0 for the empty residue after the
#restore and for a never-renamed command; parked implementations and token
#map entries are all consumed.
test commandstack_restore_original_unwinds_stack {restore_original unwinds the whole multi-renamer stack to the original and returns the count}\
-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::rename_command -renamer ::csA ::tgt {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
set before [::tgt x]
set n [commandstack::restore_original ::tgt]
set after [::tgt x]
set residuen [commandstack::restore_original ::tgt]
set nostack [commandstack::restore_original ::neverrenamed]
list $before $n $after \
[llength [commandstack::get_stack ::tgt]] $residuen $nostack \
[info commands ::commandstack::renamed_commands::*] \
[dict size $::commandstack::token_implementations]
}
} -result {{A2 {B {A {base x}}}} 3 {base x} 0 0 0 {} 0}
#restore_original is deliberately NOT gated by known_renamers: after state
#loss (a module re-source resetting known_renamers while stacks survive -
#the open follow-on-2 scenario) it registers the stack-evidenced renamers
#and still restores. The renamer-explicit convenience forms keep the gate
#and error in that state (matching remove_rename).
test commandstack_restore_original_survives_known_renamers_reset {restore_original registers stack-evidenced renamers after a known_renamers reset; explicit forms keep the gate}\
-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]]]
}
#simulate a module re-source: known_renamers resets, stacks survive
set ::commandstack::known_renamers {packagetrace packagesuppress}
set c1 [catch {commandstack::remove_renamer ::csA} msg1]
set c2 [catch {commandstack::pop_rename ::csA ::tgt} msg2]
set n [commandstack::restore_original ::tgt]
list $c1 [string match "*not in list of known_renamers*" $msg1] \
$c2 [string match "*not in list of known_renamers*" $msg2] \
$n [::tgt x] [expr {"::csA" in $::commandstack::known_renamers}]
}
} -result {1 1 1 1 1 {base x} 1}
#channel discipline holds for the convenience forms: with debug off a full
#pop_rename / remove_renamer / restore_original workout emits nothing on
#stdout or stderr (the ::puts shim would have captured any emission).
test commandstack_convenience_forms_silent_debug_off {convenience removal forms emit nothing with debug off}\
-constraints commandstacksrc -body {
cs_probe {
proc ::tgt1 {args} {return [list b1 $args]}
proc ::tgt2 {args} {return [list b2 $args]}
commandstack::rename_command -renamer ::csA ::tgt1 {args} {
return [list A [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csB ::tgt2 {args} {
return [list B [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::rename_command -renamer ::csA ::tgt2 {args} {
return [list A2 [uplevel 1 [list $COMMANDSTACKNEXT {*}$args]]]
}
commandstack::pop_rename ::csA ::tgt1
commandstack::remove_renamer ::csB
commandstack::restore_original ::tgt2
list [::tgt1 q] [::tgt2 q] [llength $::PUTS_LOG]
}
} -result {{b1 q} {b2 q} 0}
cleanupTests
}

Loading…
Cancel
Save