Compare commits

...

4 Commits

Author SHA1 Message Date
Julian Noble 0d87850873 CHANGELOG: repair control-character mangling in the 0.8.1 entry 3 weeks ago
Julian Noble f8793b308a runtime.cmd (ps1): cached sha1sums fallback backported to list -remote (punkshell 0.8.1) 3 weeks ago
Julian Noble 3a5a68ec9b G-059 achieved: WSL capability probing + native-filesystem staging for unix-side tests from windows 3 weeks ago
Julian Noble e6be532938 runtime.cmd: list -remote for the unix payload + PUNKBIN_URL override (punkshell 0.8.0) 3 weeks ago
  1. 10
      CHANGELOG.md
  2. 2
      GOALS.md
  3. 10
      bin/AGENTS.md
  4. 82
      bin/runtime.cmd
  5. 30
      goals/G-059-wsl-test-driving.md
  6. 2
      punkproject.toml
  7. 55
      src/scriptapps/bin/runtime.bash
  8. 27
      src/scriptapps/bin/runtime.ps1
  9. 1
      src/tests/AGENTS.md
  10. 2
      src/tests/modules/AGENTS.md
  11. 28
      src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test
  12. 1
      src/tests/shell/AGENTS.md
  13. 228
      src/tests/shell/testsuites/binscripts/runtimebash_wsl.test
  14. 133
      src/tests/testsupport/wslprobe.tcl

10
CHANGELOG.md

@ -5,6 +5,16 @@ 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` Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy. "Project Versioning" section for the bump policy.
## [0.8.1] - 2026-07-11
- `runtime.cmd` powershell payload: cached `sha1sums.txt` fallback backported from the bash payload - `list -remote` against an unreachable server now warns and compares using the previously fetched copy instead of dying on an unhandled download error, and the `fetch` path's pre-existing silent fall-through to a cached copy now announces itself. Also fixed a latent undefined-variable bug creating the runtime folder in the `list -remote` branch.
## [0.8.0] - 2026-07-11
- `runtime.cmd list -remote` parity for the unix (bash/zsh) payload: local-vs-server comparison table (Same version / UPDATE AVAILABLE / not-listed, plus remote-only entries), falling back to a cached `sha1sums.txt` with a warning when the server is unreachable. Both payloads gain a `PUNKBIN_URL` env override of the artifact server base url (mirrors/testing).
- The polyglot's 512-byte label guard caught its first real regression during this change: the payload growth pushed the template's `:exit_multishell` label across a boundary; fixed with a documented byte-alignment spacer comment in `runtime.bash` (workflow recorded in bin/AGENTS.md).
- G-059 achieved: WSL capability probing + native-filesystem staging for driving unix-side tests from Windows (`src/tests/testsupport/wslprobe.tcl`, `wsllinux` constraint). The MULTISHELL sh payload and the runtime.cmd unix payload (`runtime.bash` — never previously executed on real unix) now run green under WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback and offline `list -remote` — all staged to the distro's native filesystem, with a guard test asserting the Windows checkout's git state is untouched.
## [0.7.0] - 2026-07-11 ## [0.7.0] - 2026-07-11
- `bin/runtime.cmd` gains a `use` subcommand and an active-runtime selection: which runtime `run` launches is now the per-machine `bin/runtime/<platform>/active.toml` (`active = "<name>"`, shown with a `*` in `list`), resolved as `PUNK_ACTIVE_RUNTIME` env override → `active.toml` → sole installed candidate — with multiple runtimes and no selection, `run` errors listing candidates instead of silently launching the last name alphabetically. The first `fetch` sets the active runtime only when none is recorded; later fetches never steal it. - `bin/runtime.cmd` gains a `use` subcommand and an active-runtime selection: which runtime `run` launches is now the per-machine `bin/runtime/<platform>/active.toml` (`active = "<name>"`, shown with a `*` in `list`), resolved as `PUNK_ACTIVE_RUNTIME` env override → `active.toml` → sole installed candidate — with multiple runtimes and no selection, `run` errors listing candidates instead of silently launching the last name alphabetically. The first `fetch` sets the active runtime only when none is recorded; later fetches never steal it.

2
GOALS.md

@ -398,7 +398,7 @@ Detail: goals/G-058-static-runtime-packages.md
Goal: punkshell running on a runtime with statically-linked/builtin packages (tclsfe-x64: Thread/twapi/sqlite3/tdbc; punkbin runtimes' builtin Thread/tcllibc/vfs/vlerq; the expected shape of future zig-built static runtimes, G-005) keeps those packages resolvable in every interp and thread punkshell fabricates: boot captures the static baseline (info loaded entries with empty filename, plus their provided versions) before replacing package search paths, code interps and punkshell-created threads are seeded with ifneeded scripts mapping each static package to 'load {} <pkg>', and punk::packagepreference resolves static-vs-bundled by a documented version-aware policy instead of blindly loading a bundled dll over an already-provided static package. Goal: punkshell running on a runtime with statically-linked/builtin packages (tclsfe-x64: Thread/twapi/sqlite3/tdbc; punkbin runtimes' builtin Thread/tcllibc/vfs/vlerq; the expected shape of future zig-built static runtimes, G-005) keeps those packages resolvable in every interp and thread punkshell fabricates: boot captures the static baseline (info loaded entries with empty filename, plus their provided versions) before replacing package search paths, code interps and punkshell-created threads are seeded with ifneeded scripts mapping each static package to 'load {} <pkg>', and punk::packagepreference resolves static-vs-bundled by a documented version-aware policy instead of blindly loading a bundled dll over an already-provided static package.
Acceptance: a punk91-style kit (tclsfe-x64 + punk9win.vfs, no thread dll in the vfs) boots to a working repl with no "can't find package Thread" - punk::console loads in the code interp, and package require Thread succeeds there and in a punkshell-created worker thread, resolving to the static version; in the same kit, twapi resolves per the documented policy (no repeat of the observed static-Twapi-masked-by-older-vfs-twapi-5.0.2 double load; a genuinely newer bundled copy remains reachable by that policy); dll-based kits (punk902z) boot and pass their existing shell test baseline unchanged, as does a plain tclsh dev launch; the seeding mechanism is generic - driven by the captured baseline, no runtime-specific package naming - and the boot-time static baseline is introspectable at the repl; the seeding/preference logic is covered by un-gated unit tests against simulated baselines (runnable under plain tclsh), while kit-boot integration tests are gated behind a capability-probed tcltest constraint (a built kit whose baseline shows static entries including Thread) that skips cleanly when no such kit is present; the runtimes used for verification (tclsfe-x64.exe at minimum) are added to the punkbin artifact repository under win32-x86_64 with sha1sums.txt updated, so the constraint is satisfiable on other machines via the existing runtime-retrieval path; the punk91 code-interp vfs/vfs::zip load failure is re-diagnosed after the fix and either resolved or recorded as a distinct issue/candidate goal. Acceptance: a punk91-style kit (tclsfe-x64 + punk9win.vfs, no thread dll in the vfs) boots to a working repl with no "can't find package Thread" - punk::console loads in the code interp, and package require Thread succeeds there and in a punkshell-created worker thread, resolving to the static version; in the same kit, twapi resolves per the documented policy (no repeat of the observed static-Twapi-masked-by-older-vfs-twapi-5.0.2 double load; a genuinely newer bundled copy remains reachable by that policy); dll-based kits (punk902z) boot and pass their existing shell test baseline unchanged, as does a plain tclsh dev launch; the seeding mechanism is generic - driven by the captured baseline, no runtime-specific package naming - and the boot-time static baseline is introspectable at the repl; the seeding/preference logic is covered by un-gated unit tests against simulated baselines (runnable under plain tclsh), while kit-boot integration tests are gated behind a capability-probed tcltest constraint (a built kit whose baseline shows static entries including Thread) that skips cleanly when no such kit is present; the runtimes used for verification (tclsfe-x64.exe at minimum) are added to the punkbin artifact repository under win32-x86_64 with sha1sums.txt updated, so the constraint is satisfiable on other machines via the existing runtime-retrieval path; the punk91 code-interp vfs/vfs::zip load failure is re-diagnosed after the fix and either resolved or recorded as a distinct issue/candidate goal.
### G-059 [proposed] WSL detection and suitability probing for driving unix-side tests from Windows ### G-059 [achieved 2026-07-11] WSL detection and suitability probing for driving unix-side tests from Windows
Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes) Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes)
Detail: goals/G-059-wsl-test-driving.md Detail: goals/G-059-wsl-test-driving.md
Goal: test runs on a Windows dev machine can exercise selected unix-side behaviour through WSL when it is present AND suitable - a capability-probed tcltest constraint (not mere wsl.exe existence) verifies a launchable distro, required tools (bash, coreutils/sha1 tooling), and working one-way staging into the guest's NATIVE filesystem - with all guest-side execution happening on that native filesystem: per-test artifacts are staged into a WSL-native tempdir and results collected back, the Windows checkout is never operated on via the shared /mnt path (DrvFs is slow and cross-boundary stat differences make git re-hash its index and fossil see phantom changes), and any future full-suite mode uses a separate native clone rather than the shared working tree. Goal: test runs on a Windows dev machine can exercise selected unix-side behaviour through WSL when it is present AND suitable - a capability-probed tcltest constraint (not mere wsl.exe existence) verifies a launchable distro, required tools (bash, coreutils/sha1 tooling), and working one-way staging into the guest's NATIVE filesystem - with all guest-side execution happening on that native filesystem: per-test artifacts are staged into a WSL-native tempdir and results collected back, the Windows checkout is never operated on via the shared /mnt path (DrvFs is slow and cross-boundary stat differences make git re-hash its index and fossil see phantom changes), and any future full-suite mode uses a separate native clone rather than the shared working tree.

10
bin/AGENTS.md

@ -22,7 +22,11 @@ When asked to "fix bin/<name>.cmd":
(output defaults to `<projectroot>/bin`; requires Thread + the punk::mix modules, e.g (output defaults to `<projectroot>/bin`; requires Thread + the punk::mix modules, e.g
a punk shell or a tclsh with project module paths). a punk shell or a tclsh with project module paths).
3. The wrapper runs `checkfile` (the 512-byte label/boundary validator) automatically - 3. The wrapper runs `checkfile` (the 512-byte label/boundary validator) automatically -
heed ERROR output; "possibly bogus target" warnings are normal for polyglots. heed ERROR output; "possibly bogus target" warnings are normal for polyglots. Payload
growth can push a TEMPLATE label beyond the payloads (e.g `:exit_multishell`) across a
512-byte boundary: fix by resizing the byte-alignment spacer comment at the end of the
affected payload (see the `(512B spacer)` comment in `runtime.bash`) and re-wrapping
until checkfile is ERROR-free.
4. Commit source AND regenerated output together: the scriptwrap test suite pins that a 4. Commit source AND regenerated output together: the scriptwrap test suite pins that a
re-wrap of the runtime scriptset reproduces `bin/runtime.cmd` byte for byte re-wrap of the runtime scriptset reproduces `bin/runtime.cmd` byte for byte
(`src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test` (`src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test`
@ -46,6 +50,10 @@ The first argument to a built punk shell may be a dash-delimited package mode co
`bin/runtime/<platform>/` (retrieved from the punkbin artifact repo with sha1 `bin/runtime/<platform>/` (retrieved from the punkbin artifact repo with sha1
verification against its `sha1sums.txt` - both the powershell payload on windows and the verification against its `sha1sums.txt` - both the powershell payload on windows and the
bash payload on unix verify; a fetch whose checksum fails leaves only a `.tmp`). bash payload on unix verify; a fetch whose checksum fails leaves only a `.tmp`).
`list -remote` compares local runtimes against the server's sha1sums (Same version /
UPDATE AVAILABLE / not-listed, plus remote-only entries; the bash payload falls back to
a cached sha1sums.txt with a warning when the server is unreachable). The artifact
server base url is overridable via `PUNKBIN_URL` (mirrors/testing) in both payloads.
Which runtime `run` launches is the per-machine "active" selection in Which runtime `run` launches is the per-machine "active" selection in
`bin/runtime/<platform>/active.toml` (constrained single-key toml `active = "<name>"`, `bin/runtime/<platform>/active.toml` (constrained single-key toml `active = "<name>"`,
written by `runtime.cmd use <name>`, marked in `list`, VCS-ignored via the existing written by `runtime.cmd use <name>`, marked in `list`, VCS-ignored via the existing

82
bin/runtime.cmd

@ -1290,7 +1290,8 @@ scriptpath=$(realpath $scriptpath)
basename=$(basename "$scriptpath") #e.g fetchruntime.bash basename=$(basename "$scriptpath") #e.g fetchruntime.bash
scriptroot="${basename%.*}" #e.g "fetchruntime" scriptroot="${basename%.*}" #e.g "fetchruntime"
url_kitbase="https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master" #artifact server base url - overridable for mirrors/testing
url_kitbase="${PUNKBIN_URL:-https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master}"
runtime_available=0 runtime_available=0
#$OSTYPE varies in capitalization across for example zsh and bash #$OSTYPE varies in capitalization across for example zsh and bash
#uname probably a more consistent bet #uname probably a more consistent bet
@ -1489,6 +1490,53 @@ case "$1" in
fi fi
;; ;;
"list") "list")
if [[ "$2" == "-remote" ]]; then
#compare local runtimes with those listed on the artifact server
#(functional parity with the powershell payload's 'list -remote')
sha1url="${url_kitbase}/${archtail}/sha1sums.txt"
sha1local="${archdir}/sha1sums.txt"
mkdir -p "$archdir"
if curl -fsSL --output "$sha1local" "$sha1url"; then
echo "Fetched $sha1url"
elif [[ -f "$sha1local" ]]; then
echo "WARNING: could not fetch $sha1url - using cached copy at $sha1local"
else
echo "Unable to fetch $sha1url and no cached copy available"
exit 1
fi
probe=$(sha1_of "$sha1local")
if [[ -z "$probe" ]]; then
echo "No sha1 tool found (tried sha1sum, shasum, sha1, openssl) - cannot compare local runtimes."
exit 1
fi
echo "-----------------------------------------------------------------------"
echo "Runtimes for $archtail"
echo "Local $archdir"
echo "Remote ${url_kitbase}/${archtail}"
echo "-----------------------------------------------------------------------"
echo "Local Remote"
echo "-----------------------------------------------------------------------"
for f in $(list_candidates); do
local_sha1=$(lcase "$(sha1_of "$archdir/$f")")
stored_sha1=$(grep -E "^[0-9a-fA-F]{40} \*${f}\$" "$sha1local" | head -n 1 | cut -d' ' -f1)
if [[ -z "$stored_sha1" ]]; then
rhs="(not listed on server)"
elif [[ "$local_sha1" == "$(lcase "$stored_sha1")" ]]; then
rhs="Same version"
else
rhs="UPDATE AVAILABLE"
fi
printf '%-35s %s\n' "$f" "$rhs"
done
#remote-only entries
while IFS= read -r line; do
rname=$(printf '%s' "$line" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p')
if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then
printf '%-35s %s\n' "-" "$rname"
fi
done < "$sha1local"
echo "-----------------------------------------------------------------------"
else
if [[ -d "$archdir" ]]; then if [[ -d "$archdir" ]]; then
candidates=$(list_candidates) candidates=$(list_candidates)
active=$(get_active) active=$(get_active)
@ -1504,10 +1552,12 @@ case "$1" in
if [[ -n "$active" && ! -f "$archdir/$active" ]]; then if [[ -n "$active" && ! -f "$archdir/$active" ]]; then
echo "WARNING: active runtime '$active' (from $active_file) is not present - use '$0 use <name>' to reselect" echo "WARNING: active runtime '$active' (from $active_file) is not present - use '$0 use <name>' to reselect"
fi fi
echo "Use: '$0 list -remote' to compare local runtimes with those available on the artifact server"
else else
echo "No runtimes available in $archdir" echo "No runtimes available in $archdir"
echo " Use '$0 fetch' to install." echo " Use '$0 fetch' to install."
fi fi
fi
;; ;;
"use") "use")
if [[ -z "$2" ]]; then if [[ -z "$2" ]]; then
@ -1587,6 +1637,9 @@ case "$1" in
exit 1 exit 1
;; ;;
esac esac
#(512B spacer) byte-alignment padding so template labels beyond the payloads clear cmd's
#512-byte label-scan boundaries - resize this comment if scriptwrap checkfile reports a
#boundary-spanning label after a payload edit (see bin/AGENTS.md polyglot workflow)
#</shell-payload> #</shell-payload>
@ -2059,7 +2112,11 @@ function psmain {
#expected script location is the bin folder of a punk project #expected script location is the bin folder of a punk project
$rtfolder = Join-Path -Path $outbase -ChildPath "runtime" $rtfolder = Join-Path -Path $outbase -ChildPath "runtime"
#Binary artifact server url. (git is not ideal for this - but will do for now - todo - use artifact system within gitea?) #Binary artifact server url. (git is not ideal for this - but will do for now - todo - use artifact system within gitea?)
#overridable for mirrors/testing - keep in sync with the bash payload
$artifacturl = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master" $artifacturl = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master"
if ($env:PUNKBIN_URL) {
$artifacturl = $env:PUNKBIN_URL
}
switch ($action) { switch ($action) {
'fetch' { 'fetch' {
$arch = "win32-x86_64" $arch = "win32-x86_64"
@ -2090,6 +2147,10 @@ function psmain {
if ($_.Exception.Response) { if ($_.Exception.Response) {
Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)" Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)"
} }
if (Test-Path -Path $sha1local -PathType Leaf) {
#this path always fell through to the cached copy - now say so
Write-Host "WARNING: proceeding with cached copy at $sha1local"
}
} }
if (Test-Path -Path $sha1local -PathType Leaf) { if (Test-Path -Path $sha1local -PathType Leaf) {
$sha1Content = Get-Content -Path $sha1local $sha1Content = Get-Content -Path $sha1local
@ -2284,12 +2345,27 @@ function psmain {
$sha1url = "$archurl/sha1sums.txt" $sha1url = "$archurl/sha1sums.txt"
if ( $PSBoundParameters.ContainsKey('remote') ) { if ( $PSBoundParameters.ContainsKey('remote') ) {
if (-not (test-path -Path $archfolder -Type Container)) { if (-not (test-path -Path $archfolder -Type Container)) {
new-item -Path $container -ItemType Directory -force #create with intermediary folders if not already present new-item -Path $archfolder -ItemType Directory -force #create with intermediary folders if not already present
} }
write-host "Checking for available remote runtimes for" write-host "Checking for available remote runtimes for $arch"
Write-Host "Fetching $sha1url" Write-Host "Fetching $sha1url"
#cached fallback (parity with the bash payload): an unreachable server
#falls back to a previously fetched sha1sums.txt with a warning
try {
Invoke-WebRequest -Uri $sha1url -OutFile $sha1local -ErrorAction Stop Invoke-WebRequest -Uri $sha1url -OutFile $sha1local -ErrorAction Stop
Write-Host "sha1 saved at $sha1local" Write-Host "sha1 saved at $sha1local"
} catch {
if (Test-Path -Path $sha1local -PathType Leaf) {
Write-Host "WARNING: could not fetch ${sha1url}: $($_.Exception.Message)"
Write-Host "WARNING: using cached copy at $sha1local"
} else {
Write-Host "Unable to fetch $sha1url and no cached copy available: $($_.Exception.Message)"
if ($_.Exception.Response) {
Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)"
}
return
}
}
$sha1Content = Get-Content -Path $sha1local $sha1Content = Get-Content -Path $sha1local
$remotedict = @{} $remotedict = @{}
foreach ($line in $sha1Content) { foreach ($line in $sha1Content) {

30
goals/G-059-wsl-test-driving.md

@ -1,6 +1,6 @@
# G-059 WSL detection and suitability probing for driving unix-side tests from Windows # G-059 WSL detection and suitability probing for driving unix-side tests from Windows
Status: proposed Status: achieved 2026-07-11
Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes) Scope: src/tests/ (capability probe helpers + constraint-gated cases in existing suites, e.g the unix sh-payload execution test in modules/punk/mix/testsuites/scriptwrap/multishell.test and a runtime.bash behaviour test), src/tests/AGENTS.md (enablement notes)
Acceptance: see GOALS.md index entry (canonical). Acceptance: see GOALS.md index entry (canonical).
@ -62,3 +62,31 @@ a staged file. Any probe failure -> skip cleanly (never fail the suite).
cheapest provider of that guest-driving contract. cheapest provider of that guest-driving contract.
- WSL itself is a Windows feature (no licensing entanglement); distro choice is the - WSL itself is a Windows feature (no licensing entanglement); distro choice is the
user's - the probe should not assume a specific distro, only capabilities. user's - the probe should not assume a specific distro, only capabilities.
## Outcome (achieved 2026-07-11)
- `src/tests/testsupport/wslprobe.tcl` (`::punktest::wsl`): memoized capability probe
(wsl.exe present -> default distro launches and unames as Linux -> tools present:
bash/mktemp/wslpath + a sha1 tool -> native tempdir file round trip; any failure
yields available=0 with a reason and consumers skip) plus staging helpers
(staging_create/staging_copy_in/run_in/staging_cleanup - cleanup refuses paths it
didn't create). Probe uses `wsl -e <cmd>` invocations only: management commands
(`wsl --status`, `-l`) emit UTF-16 - discovered during implementation.
- Consumers: `scriptwrap_multishell_exec_wsl_sh` in multishell.test (the polyglot's sh
payload produces its marker under the distro's bash from native staging - the direct
isunix twin still skips on windows) and the new
`src/tests/shell/testsuites/binscripts/runtimebash_wsl.test` (8 tests): runtime.bash's
first-ever execution on real unix - multi-candidate run errors with candidates,
use/list/run round trip with argument passing, PUNK_ACTIVE_RUNTIME override,
unknown/unselectable use errors, stale-active guidance, single-candidate fallback,
offline `list -remote` (crafted cached sha1sums.txt + PUNKBIN_URL forced-failure), and
the checkout-untouched guard (git status --porcelain identical before/after the
WSL-gated run - the native-staging design made verifiable).
- All green on Tcl 9.0.3 and 8.7 against Ubuntu-24.04/WSL2; suites skip cleanly when the
probe fails (constraint gating; helper absence also degrades to skip).
- Enablement/limitations recorded in src/tests/AGENTS.md (incl the known limitation: a
present-but-hanging WSL can stall the probe - erroring installs are handled).
- Bonus finding: the WSL list -remote test work triggered the polyglot's first real
512-byte label regression (payload growth pushed the template's :exit_multishell
across a boundary); fixed via a documented spacer comment in runtime.bash - the G-05x
guardrail suite caught it exactly as designed.

2
punkproject.toml

@ -1,3 +1,3 @@
[project] [project]
name = "punkshell" name = "punkshell"
version = "0.7.0" version = "0.8.1"

55
src/scriptapps/bin/runtime.bash

@ -10,7 +10,8 @@ scriptpath=$(realpath $scriptpath)
basename=$(basename "$scriptpath") #e.g fetchruntime.bash basename=$(basename "$scriptpath") #e.g fetchruntime.bash
scriptroot="${basename%.*}" #e.g "fetchruntime" scriptroot="${basename%.*}" #e.g "fetchruntime"
url_kitbase="https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master" #artifact server base url - overridable for mirrors/testing
url_kitbase="${PUNKBIN_URL:-https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master}"
runtime_available=0 runtime_available=0
#$OSTYPE varies in capitalization across for example zsh and bash #$OSTYPE varies in capitalization across for example zsh and bash
#uname probably a more consistent bet #uname probably a more consistent bet
@ -209,6 +210,53 @@ case "$1" in
fi fi
;; ;;
"list") "list")
if [[ "$2" == "-remote" ]]; then
#compare local runtimes with those listed on the artifact server
#(functional parity with the powershell payload's 'list -remote')
sha1url="${url_kitbase}/${archtail}/sha1sums.txt"
sha1local="${archdir}/sha1sums.txt"
mkdir -p "$archdir"
if curl -fsSL --output "$sha1local" "$sha1url"; then
echo "Fetched $sha1url"
elif [[ -f "$sha1local" ]]; then
echo "WARNING: could not fetch $sha1url - using cached copy at $sha1local"
else
echo "Unable to fetch $sha1url and no cached copy available"
exit 1
fi
probe=$(sha1_of "$sha1local")
if [[ -z "$probe" ]]; then
echo "No sha1 tool found (tried sha1sum, shasum, sha1, openssl) - cannot compare local runtimes."
exit 1
fi
echo "-----------------------------------------------------------------------"
echo "Runtimes for $archtail"
echo "Local $archdir"
echo "Remote ${url_kitbase}/${archtail}"
echo "-----------------------------------------------------------------------"
echo "Local Remote"
echo "-----------------------------------------------------------------------"
for f in $(list_candidates); do
local_sha1=$(lcase "$(sha1_of "$archdir/$f")")
stored_sha1=$(grep -E "^[0-9a-fA-F]{40} \*${f}\$" "$sha1local" | head -n 1 | cut -d' ' -f1)
if [[ -z "$stored_sha1" ]]; then
rhs="(not listed on server)"
elif [[ "$local_sha1" == "$(lcase "$stored_sha1")" ]]; then
rhs="Same version"
else
rhs="UPDATE AVAILABLE"
fi
printf '%-35s %s\n' "$f" "$rhs"
done
#remote-only entries
while IFS= read -r line; do
rname=$(printf '%s' "$line" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p')
if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then
printf '%-35s %s\n' "-" "$rname"
fi
done < "$sha1local"
echo "-----------------------------------------------------------------------"
else
if [[ -d "$archdir" ]]; then if [[ -d "$archdir" ]]; then
candidates=$(list_candidates) candidates=$(list_candidates)
active=$(get_active) active=$(get_active)
@ -224,10 +272,12 @@ case "$1" in
if [[ -n "$active" && ! -f "$archdir/$active" ]]; then if [[ -n "$active" && ! -f "$archdir/$active" ]]; then
echo "WARNING: active runtime '$active' (from $active_file) is not present - use '$0 use <name>' to reselect" echo "WARNING: active runtime '$active' (from $active_file) is not present - use '$0 use <name>' to reselect"
fi fi
echo "Use: '$0 list -remote' to compare local runtimes with those available on the artifact server"
else else
echo "No runtimes available in $archdir" echo "No runtimes available in $archdir"
echo " Use '$0 fetch' to install." echo " Use '$0 fetch' to install."
fi fi
fi
;; ;;
"use") "use")
if [[ -z "$2" ]]; then if [[ -z "$2" ]]; then
@ -307,3 +357,6 @@ case "$1" in
exit 1 exit 1
;; ;;
esac esac
#(512B spacer) byte-alignment padding so template labels beyond the payloads clear cmd's
#512-byte label-scan boundaries - resize this comment if scriptwrap checkfile reports a
#boundary-spanning label after a payload edit (see bin/AGENTS.md polyglot workflow)

27
src/scriptapps/bin/runtime.ps1

@ -176,7 +176,11 @@ function psmain {
#expected script location is the bin folder of a punk project #expected script location is the bin folder of a punk project
$rtfolder = Join-Path -Path $outbase -ChildPath "runtime" $rtfolder = Join-Path -Path $outbase -ChildPath "runtime"
#Binary artifact server url. (git is not ideal for this - but will do for now - todo - use artifact system within gitea?) #Binary artifact server url. (git is not ideal for this - but will do for now - todo - use artifact system within gitea?)
#overridable for mirrors/testing - keep in sync with the bash payload
$artifacturl = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master" $artifacturl = "https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master"
if ($env:PUNKBIN_URL) {
$artifacturl = $env:PUNKBIN_URL
}
switch ($action) { switch ($action) {
'fetch' { 'fetch' {
$arch = "win32-x86_64" $arch = "win32-x86_64"
@ -207,6 +211,10 @@ function psmain {
if ($_.Exception.Response) { if ($_.Exception.Response) {
Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)" Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)"
} }
if (Test-Path -Path $sha1local -PathType Leaf) {
#this path always fell through to the cached copy - now say so
Write-Host "WARNING: proceeding with cached copy at $sha1local"
}
} }
if (Test-Path -Path $sha1local -PathType Leaf) { if (Test-Path -Path $sha1local -PathType Leaf) {
$sha1Content = Get-Content -Path $sha1local $sha1Content = Get-Content -Path $sha1local
@ -401,12 +409,27 @@ function psmain {
$sha1url = "$archurl/sha1sums.txt" $sha1url = "$archurl/sha1sums.txt"
if ( $PSBoundParameters.ContainsKey('remote') ) { if ( $PSBoundParameters.ContainsKey('remote') ) {
if (-not (test-path -Path $archfolder -Type Container)) { if (-not (test-path -Path $archfolder -Type Container)) {
new-item -Path $container -ItemType Directory -force #create with intermediary folders if not already present new-item -Path $archfolder -ItemType Directory -force #create with intermediary folders if not already present
} }
write-host "Checking for available remote runtimes for" write-host "Checking for available remote runtimes for $arch"
Write-Host "Fetching $sha1url" Write-Host "Fetching $sha1url"
#cached fallback (parity with the bash payload): an unreachable server
#falls back to a previously fetched sha1sums.txt with a warning
try {
Invoke-WebRequest -Uri $sha1url -OutFile $sha1local -ErrorAction Stop Invoke-WebRequest -Uri $sha1url -OutFile $sha1local -ErrorAction Stop
Write-Host "sha1 saved at $sha1local" Write-Host "sha1 saved at $sha1local"
} catch {
if (Test-Path -Path $sha1local -PathType Leaf) {
Write-Host "WARNING: could not fetch ${sha1url}: $($_.Exception.Message)"
Write-Host "WARNING: using cached copy at $sha1local"
} else {
Write-Host "Unable to fetch $sha1url and no cached copy available: $($_.Exception.Message)"
if ($_.Exception.Response) {
Write-Host "HTTP Status code: $($_.Exception.Response.StatusCode)"
}
return
}
}
$sha1Content = Get-Content -Path $sha1local $sha1Content = Get-Content -Path $sha1local
$remotedict = @{} $remotedict = @{}
foreach ($line in $sha1Content) { foreach ($line in $sha1Content) {

1
src/tests/AGENTS.md

@ -27,6 +27,7 @@ Top-level test harness and source-tree tests for ShellSpy/Punk. Tests here exerc
- `-report json` emits a machine-readable final summary, but package-load warnings may still precede it on stdout/stderr. - `-report json` emits a machine-readable final summary, but package-load warnings may still precede it on stdout/stderr.
- ERROR-status failures now surface `errorInfo` (full Tcl error message/stack trace) in markdown, compact, and json reports. Compact mode truncates to a single line. - ERROR-status failures now surface `errorInfo` (full Tcl error message/stack trace) in markdown, compact, and json reports. Compact mode truncates to a single line.
- FAILED-status failures (result mismatch, not error) now surface `result_was` (actual) and `result_expected` (expected) in markdown, compact, and json reports. Compact mode truncates each to a single line. - FAILED-status failures (result mismatch, not error) now surface `result_was` (actual) and `result_expected` (expected) in markdown, compact, and json reports. Compact mode truncates each to a single line.
- `testsupport/` holds helper `.tcl` files sourced by `.test` files (not discovered as suites). `testsupport/wslprobe.tcl` (G-059) provides `::punktest::wsl`: a memoized capability probe yielding the `wsllinux` constraint (default distro launches, answers uname/tool probes, and round-trips a file through a native tempdir - NOT mere wsl.exe existence) plus native-filesystem staging helpers (`staging_create`/`staging_copy_in`/`run_in`/`staging_cleanup`). WSL-gated tests must execute from a staging dir on the distro's native filesystem with the shared `/mnt` path used only for one-way copy-in/out - never operate on the Windows checkout from inside WSL (DrvFs is slow and cross-boundary stat differences make git re-hash its index and fossil see phantom changes). Probe invocations use `wsl -e <cmd>` only (`wsl --status`/`-l` emit UTF-16). Known limitation: a broken-but-present WSL that hangs (rather than errors) on `wsl -e` can stall the probe.
## Work Guidance ## Work Guidance

2
src/tests/modules/AGENTS.md

@ -41,7 +41,7 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/`
- `punk/ansi/` — punk::ansi tests (`testsuites/ansi/`): ansistrip/ansimerge, plus characterization of the ANSI-at-position mechanisms (`ansistring.test`: INDEX/INDEXCODE/INDEXCHAR/RANGE/INSERT grapheme indexing with SGR-prefix merging, INDEXCOLUMNS/COLUMNINDEX double-wide column mapping, trim/VIEW), code splitting invariants (`ta.test`: detect/detectcode distinction, split_codes/split_codes_single/split_at_codes shapes and round-trip) and single-code/effective-state semantics (`codetype.test`: is_sgr_reset/has_sgr_leadingreset, has_any/all_effective, sgr_merge, sequence_type classify). ANSI codes in these tests are literal escape strings so results are colour-state independent - `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). 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, 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), and 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) - `punk/args/` — punk::args tests (`testsuites/args/`): parsing, choices/choicegroups, forms, rendering/indentation characterization, 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), and 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)
- `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity) and cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); remaining GAP pins for pseudo-command cmdtype + space-form docid prefixes (G-051, real `string is` pins behind the have_tclcoredocs constraint), TclOO undocumented-method fallback (G-052), and synopsis marking absence (G-050)) - `punk/ns/` — punk::ns tests (`testsuites/ns/`): cmdwhich/cmdinfo/cmd_traverse doc-lookup flow (`cmdflow.test`, G-040 parity) and cmdhelp usage-rendering integration (`cmdhelp.test`: scheme selection, goodarg/badarg marking incl type/allocation failures, goodchoice highlighting of supplied/default choice words, alias path, cmdinfo result shape, queried-command failure attribution, and `-return dict` parse-status returns (G-049 - its GAP pins flipped 2026-07-10); remaining GAP pins for pseudo-command cmdtype + space-form docid prefixes (G-051, real `string is` pins behind the have_tclcoredocs constraint), TclOO undocumented-method fallback (G-052), and synopsis marking absence (G-050))
- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix) - `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources), punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change), and the MULTISHELL polyglot build machinery (`testsuites/scriptwrap/multishell.test`: scriptset wrap via the punk.multishell.cmd template - structure/LF-only/determinism, checkfile 512-byte label validation of fresh wraps AND the committed bin/runtime.cmd, the runtime scriptset round-trip byte-identity pin, and platform-gated execution smoke: cmd.exe→powershell payload on windows, sh payload on unix or via the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl` - staged to the WSL distro's native filesystem, G-059)
- `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, and G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} <prefix>` ifneeded mappings; no-op without a baseline) - `punk/lib/` — punk::lib tests (`testsuites/lib/`): range/index/parse/compat/interp_sync utilities, and G-058 static-baseline seeding (`staticseed.test`: interp_sync_package_paths/snapshot_package_paths propagate a simulated ::punkboot static baseline and seed `load {} <prefix>` ifneeded mappings; no-op without a baseline)
- `punk/packagepreference/` — punk::packagepreference tests (`testsuites/packagepreference/`): G-058 static-vs-bundled policy (`staticpolicy.test`: require of a baseline package triggers the index scan before resolution so a newer bundled copy wins, static beats older bundled, exact requires of bundled versions stay reachable, missing static mappings get seeded) - `punk/packagepreference/` — punk::packagepreference tests (`testsuites/packagepreference/`): G-058 static-vs-bundled policy (`staticpolicy.test`: require of a baseline package triggers the index scan before resolution so a newer bundled copy wins, static beats older bundled, exact requires of bundled versions stay reachable, missing static mappings get seeded)
- `punk/libunknown/` — .tm same-version shadowing pin-tests (`testsuites/shadowing/`): tcl::tm::add prepend rule, head-of-tm-list wins exact-version ties, version beats order, punk::libunknown parity — shipped behaviour depends on these (runtests tm ordering, punk_main package-mode precedence, G-033); mixed .tm/pkgIndex.tcl characterization is goal G-035 - `punk/libunknown/` — .tm same-version shadowing pin-tests (`testsuites/shadowing/`): tcl::tm::add prepend rule, head-of-tm-list wins exact-version ties, version beats order, punk::libunknown parity — shipped behaviour depends on these (runtests tm ordering, punk_main package-mode precedence, G-033); mixed .tm/pkgIndex.tcl characterization is goal G-035

28
src/tests/modules/punk/mix/testsuites/scriptwrap/multishell.test

@ -232,5 +232,33 @@ namespace eval ::testspace {
-cleanup { -cleanup {
}\ }\
-result [list 1] -result [list 1]
#--- G-059: drive the sh payload under WSL from windows ----------------------------------
#Staged into the distro's NATIVE filesystem (never executed from the shared /mnt path)
#per the G-059 design - see src/tests/testsupport/wslprobe.tcl.
if {[file exists [file join $testdir .. .. .. .. .. testsupport wslprobe.tcl]]} {
source [file join $testdir .. .. .. .. .. testsupport wslprobe.tcl]
testConstraint wsllinux [expr {$::tcl_platform(platform) eq "windows" && [dict get [punktest::wsl::probe] available]}]
} else {
testConstraint wsllinux 0
}
test scriptwrap_multishell_exec_wsl_sh {a suitable WSL distro runs the polyglot's shell payload from a native-filesystem staging dir}\
-constraints {wrapok wsllinux}\
-setup {
set result ""
variable outfile
set stagingdir [punktest::wsl::staging_create]
punktest::wsl::staging_copy_in $stagingdir $outfile
}\
-body {
set r [punktest::wsl::run_in $stagingdir "bash ./[file tail $outfile] </dev/null"]
lappend result [dict get $r ok]
lappend result [string match "*MARKER_SHELL_OK*" [dict get $r output]]
}\
-cleanup {
punktest::wsl::staging_cleanup $stagingdir
}\
-result [list 1 1]
} }
tcltest::cleanupTests ;#needed to produce test summary line. tcltest::cleanupTests ;#needed to produce test summary line.

1
src/tests/shell/AGENTS.md

@ -19,6 +19,7 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac
- `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling. - `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling.
- `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism, and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk. - `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:<name>` scriptlib-resolution mechanism, and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk.
- punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate` (requires interactive `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`. - punkexe tests run against the built binary: after changing punk::repl or app-punkshell source, rebuild via `make.tcl packages`, `make.tcl vfscommonupdate` (requires interactive `y`), then `make.tcl project` before trusting results; `make.tcl project` alone does not refresh `_vfscommon.vfs`.
- `testsuites/binscripts/runtimebash_wsl.test` (G-059) exercises the runtime.cmd unix payload (`src/scriptapps/bin/runtime.bash`) through WSL: active/use/run resolution, env override, stale-active guidance, single-candidate fallback, and `list -remote` local-vs-server comparison (offline via a crafted cached sha1sums.txt + `PUNKBIN_URL` pointed at an unreachable address). Gated by the `wsllinux` capability constraint from `src/tests/testsupport/wslprobe.tcl`; all execution happens in a WSL-native staging dir, and a final test asserts the Windows checkout's git status is unchanged by the run.
- `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `<projectroot>/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes. - `testsuites/punkexe/staticruntime.test` (G-058) targets a kit built on a runtime with statically-linked packages: resolved from `env(PUNK_STATICKIT_TEST_EXE)`, else `<projectroot>/bin/punk91.exe`, gated by the capability-probed `statickitavailable` constraint (a probe run must show a captured `::punkboot::static_packages` baseline including Thread - not mere file existence). It asserts repl boot without the codethread Thread failure, Thread resolution in the code interp / fabricated interps / snapshot-seeded threads, and twapi resolution per the static-vs-bundled policy. The verification runtime (tclsfe-x64.exe) is pinned in the punkbin artifact repo so the constraint is satisfiable off the original dev machine. Kit script-exec paths must be passed with forward slashes.
- GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build. - GOAL tests (constraint `punkgoals`, enabled by env `PUNK_TEST_GOALS=1`) cover the piped-stdin-then-interactive-shell drop-in (`shellpipe_eof_then_interactive_shell`, `shellpipe_script_and_eof_then_interactive_shell`); they are excluded from normal runs because they require an openable console in the test environment and briefly attach a live shell to it - they pass in console-attached environments against a current build.
- The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset. - The eof-policy discriminator is `env(PUNK_PIPE_EOF)` (`exit`|`interactive`|unset=console heuristic) implemented in app-punkshell; termination-guard tests spawn children with `exit`, goal tests spawn with it unset.

228
src/tests/shell/testsuites/binscripts/runtimebash_wsl.test

@ -0,0 +1,228 @@
package require tcltest
#G-059: behaviour tests for the runtime.cmd unix payload (src/scriptapps/bin/runtime.bash)
#driven through WSL from a windows dev machine. The payload had never been executed on
#real unix before this suite - only syntax-checked (bash -n / zsh -n).
#
#Staging design (goals/G-059-wsl-test-driving.md): runtime.bash plus a fixture runtime
#folder (fake executable "runtimes" that just echo markers) are staged ONE-WAY into a
#tempdir on the distro's NATIVE filesystem and all execution happens there. The windows
#checkout is never operated on from inside WSL, so git/fossil state cannot be disturbed
#(asserted by the final test via a before/after git status comparison).
#
#Covered (offline - no network/fetch cases; those are future env-gated work):
# - run with multiple runtimes and no active selection errors listing candidates
# - use <name> records active.toml; list marks the active entry; run launches it
# - PUNK_ACTIVE_RUNTIME env override wins over active.toml
# - use with an unknown/unselectable name errors
# - stale active.toml (file removed) errors with reselect guidance
# - single-candidate fallback when no active is recorded
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
variable testdir [file dirname [file normalize [info script]]]
#<projectroot>/src/tests/shell/testsuites/binscripts -> src/tests is 3 levels up
variable testsroot [file normalize [file join $testdir .. .. ..]]
variable projectroot [file normalize [file join $testsroot .. ..]]
if {[file exists [file join $testsroot testsupport wslprobe.tcl]]} {
source [file join $testsroot testsupport wslprobe.tcl]
testConstraint wsllinux [expr {$::tcl_platform(platform) eq "windows" && [dict get [punktest::wsl::probe] available]}]
} else {
testConstraint wsllinux 0
}
variable runtimebash [file join $projectroot src scriptapps bin runtime.bash]
testConstraint havesource [file exists $runtimebash]
#vcs-state guard (G-059 acceptance): snapshot before any WSL activity in this file
variable git_status_before ""
catch {
variable git_status_before [exec {*}[auto_execok git] -C $projectroot status --porcelain]
}
#one staging area for the whole file: runtime.bash + runtime/linux-x86_64 fixture
#with two fake "runtimes" (echo scripts). Rebuilt fresh via reset_fixture per test.
variable stagingdir ""
if {[testConstraint wsllinux] && [testConstraint havesource]} {
set stagingdir [punktest::wsl::staging_create]
punktest::wsl::staging_copy_in $stagingdir $runtimebash
punktest::wsl::run_in $stagingdir {
mkdir -p runtime/linux-x86_64 &&
printf '#!/bin/sh\necho FAKERT_ALPHA_RAN "$@"\n' > runtime/linux-x86_64/fakert_alpha &&
printf '#!/bin/sh\necho FAKERT_BETA_RAN "$@"\n' > runtime/linux-x86_64/fakert_beta &&
chmod +x runtime/linux-x86_64/fakert_alpha runtime/linux-x86_64/fakert_beta
}
}
#run runtime.bash inside the staging dir with args; returns dict ok/output
proc rtbash {args} {
variable stagingdir
return [punktest::wsl::run_in $stagingdir "bash ./runtime.bash $args"]
}
proc reset_fixture {} {
variable stagingdir
punktest::wsl::run_in $stagingdir {rm -f runtime/linux-x86_64/active.toml runtime/linux-x86_64/fakert_gamma}
}
test runtimebash_run_multi_no_active_errors {run with multiple runtimes and no active selection errors and lists candidates}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
set r [rtbash run]
lappend result [dict get $r ok]
lappend result [string match "*Multiple runtimes installed and no active runtime selected*" [dict get $r output]]
lappend result [string match "*fakert_alpha*" [dict get $r output]]
lappend result [string match "*fakert_beta*" [dict get $r output]]
}\
-cleanup {
}\
-result [list 0 1 1 1]
test runtimebash_use_records_and_run_launches {use records active.toml, list marks it, run launches the selected runtime with arguments}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
set r [rtbash use fakert_beta]
lappend result [dict get $r ok]
set r [punktest::wsl::run_in $::testspace::stagingdir {cat runtime/linux-x86_64/active.toml}]
lappend result [string trim [dict get $r output]]
set r [rtbash list]
lappend result [string match {*\* fakert_beta (active)*} [dict get $r output]]
set r [rtbash run hello world]
lappend result [dict get $r ok]
lappend result [string match "*FAKERT_BETA_RAN hello world*" [dict get $r output]]
}\
-cleanup {
}\
-result [list 1 {active = "fakert_beta"} 1 1 1]
test runtimebash_env_override_wins {PUNK_ACTIVE_RUNTIME env override wins over active.toml}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
rtbash use fakert_beta
variable stagingdir
set r [punktest::wsl::run_in $stagingdir "PUNK_ACTIVE_RUNTIME=fakert_alpha bash ./runtime.bash run"]
lappend result [dict get $r ok]
lappend result [string match "*FAKERT_ALPHA_RAN*" [dict get $r output]]
}\
-cleanup {
}\
-result [list 1 1]
test runtimebash_use_unknown_errors {use with an unknown name errors and lists candidates; non-runtime extensions are unselectable}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
set r [rtbash use no_such_runtime]
lappend result [dict get $r ok]
lappend result [string match "*No runtime named 'no_such_runtime'*" [dict get $r output]]
set r [rtbash use sha1sums.txt]
lappend result [dict get $r ok]
lappend result [string match "*not a selectable runtime*" [dict get $r output]]
}\
-cleanup {
}\
-result [list 0 1 0 1]
test runtimebash_stale_active_errors {a recorded active runtime that is no longer present errors with reselect guidance}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
variable stagingdir
punktest::wsl::run_in $stagingdir {printf 'active = "gone_runtime"\n' > runtime/linux-x86_64/active.toml}
set r [rtbash run]
lappend result [dict get $r ok]
lappend result [string match "*active runtime 'gone_runtime'*is not present*" [dict get $r output]]
lappend result [string match "*use <name>*" [dict get $r output]]
}\
-cleanup {
}\
-result [list 0 1 1]
test runtimebash_single_candidate_fallback {with exactly one installed runtime and no active recorded, run uses it}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
variable stagingdir
punktest::wsl::run_in $stagingdir {mkdir -p solo/runtime/linux-x86_64 && cp runtime.bash solo/ && cp runtime/linux-x86_64/fakert_alpha solo/runtime/linux-x86_64/}
set r [punktest::wsl::run_in $stagingdir "cd solo && bash ./runtime.bash run"]
lappend result [dict get $r ok]
lappend result [string match "*FAKERT_ALPHA_RAN*" [dict get $r output]]
}\
-cleanup {
}\
-result [list 1 1]
test runtimebash_list_remote_cached_compare {list -remote compares local runtimes against sha1sums.txt (offline: forced fetch failure falls back to the cached copy)}\
-constraints {wsllinux havesource}\
-setup {
set result ""
reset_fixture
}\
-body {
variable stagingdir
#craft a cached sha1sums.txt: correct hash for alpha, wrong hash for beta,
#plus a remote-only entry; PUNKBIN_URL points nowhere so curl fails fast
punktest::wsl::run_in $stagingdir {
a=$(sha1sum runtime/linux-x86_64/fakert_alpha | cut -d' ' -f1) &&
printf '%s *fakert_alpha\n0000000000000000000000000000000000000000 *fakert_beta\n1111111111111111111111111111111111111111 *fakert_remote_only\n' "$a" > runtime/linux-x86_64/sha1sums.txt
}
set r [punktest::wsl::run_in $stagingdir "PUNKBIN_URL=http://127.0.0.1:9/nowhere bash ./runtime.bash list -remote"]
lappend result [dict get $r ok]
set out [dict get $r output]
lappend result [string match "*using cached copy*" $out]
lappend result [regexp {fakert_alpha\s+Same version} $out]
lappend result [regexp {fakert_beta\s+UPDATE AVAILABLE} $out]
lappend result [regexp -- {-\s+fakert_remote_only} $out]
}\
-cleanup {
punktest::wsl::run_in $::testspace::stagingdir {rm -f runtime/linux-x86_64/sha1sums.txt}
}\
-result [list 1 1 1 1 1]
test runtimebash_wsl_leaves_checkout_untouched {WSL-gated runs do not disturb the windows checkout's git state (G-059 native-staging design)}\
-constraints {wsllinux havesource}\
-setup {
set result ""
}\
-body {
variable git_status_before
variable projectroot
set git_status_after ""
catch {
set git_status_after [exec {*}[auto_execok git] -C $projectroot status --porcelain]
}
lappend result [expr {$git_status_after eq $git_status_before}]
}\
-cleanup {
}\
-result [list 1]
#whole-file staging cleanup
if {[testConstraint wsllinux] && [testConstraint havesource] && $stagingdir ne ""} {
punktest::wsl::staging_cleanup $stagingdir
}
}
tcltest::cleanupTests ;#needed to produce test summary line.

133
src/tests/testsupport/wslprobe.tcl

@ -0,0 +1,133 @@
#wslprobe.tcl - G-059 test support: WSL capability probing + native-filesystem staging
#
#Sourced by .test files (runtests.tcl only discovers *.test - helper .tcl files are not
#run as suites). Provides ::punktest::wsl with:
# probe - memoized capability probe; dict: available 0|1, reason, uname
# winpath_to_wsl - translate a windows path for use inside the distro (wslpath -u)
# staging_create - mktemp -d on the distro's NATIVE filesystem
# staging_copy_in - one-way copy of windows files into a staging dir
# run_in - run a bash command with cwd set to a staging dir; returns
# dict: ok 0|1 output <combined stdout+stderr>
# staging_cleanup - rm -rf a staging dir (guarded: only paths staging_create made)
#
#DESIGN (see goals/G-059-wsl-test-driving.md): all guest-side execution happens on the
#distro's native filesystem. The shared /mnt path is used only for one-way copy-in of
#artifacts - the windows checkout is never operated on from inside WSL (DrvFs is slow
#and cross-boundary stat differences make git re-hash its index and fossil see phantom
#changes; two VCS clients must never share a working tree).
#
#ENCODING: wsl.exe management commands (--status, -l) emit UTF-16 - the probe only ever
#uses 'wsl -e <cmd>' invocations, whose output is the command's own (UTF-8).
#
#CAVEAT: a broken-but-present WSL install that HANGS (rather than erroring) on first
#'wsl -e' can stall the probe - known limitation, documented in src/tests/AGENTS.md.
namespace eval ::punktest::wsl {
variable probe_result ""
variable created_stagingdirs [list]
proc wslexe {} {
return [auto_execok wsl]
}
#memoized capability probe - capability-based, not wsl.exe-existence-based:
#the distro must launch, answer uname, have the required tools, and round-trip a
#file through a native tempdir. Any failure -> available 0 with a reason (skip, not fail).
proc probe {} {
variable probe_result
if {$probe_result ne ""} {
return $probe_result
}
set result [dict create available 0 reason "" uname ""]
set wsl [wslexe]
if {![llength $wsl]} {
dict set result reason "wsl.exe not found"
set probe_result $result
return $result
}
#distro launches and is linux
if {[catch {exec {*}$wsl -e uname -sm} unamesm]} {
dict set result reason "wsl -e uname failed (no usable default distro?): [lindex [split $unamesm \n] 0]"
set probe_result $result
return $result
}
set unamesm [string trim $unamesm]
dict set result uname $unamesm
if {![string match "Linux*" $unamesm]} {
dict set result reason "default distro uname '$unamesm' is not Linux"
set probe_result $result
return $result
}
#required tools
if {[catch {exec {*}$wsl -e bash -c {command -v bash mktemp wslpath >/dev/null && (command -v sha1sum >/dev/null || command -v shasum >/dev/null) && echo TOOLS_OK}} toolsout]
|| ![string match "*TOOLS_OK*" $toolsout]} {
dict set result reason "required tools missing in distro (bash/mktemp/wslpath + sha1 tool)"
set probe_result $result
return $result
}
#native staging round trip
if {[catch {
set tmpd [string trim [exec {*}$wsl -e mktemp -d]]
if {![string match "/*" $tmpd]} {
error "unexpected mktemp result '$tmpd'"
}
set echoed [string trim [exec {*}$wsl -e bash -c "echo punkprobe > $tmpd/probe.txt && cat $tmpd/probe.txt && rm -rf $tmpd"]]
if {$echoed ne "punkprobe"} {
error "staging round trip mismatch '$echoed'"
}
} errM]} {
dict set result reason "native tempdir staging round trip failed: [lindex [split $errM \n] 0]"
set probe_result $result
return $result
}
dict set result available 1
dict set result reason "ok"
set probe_result $result
return $result
}
proc winpath_to_wsl {winpath} {
set wsl [wslexe]
return [string trim [exec {*}$wsl -e wslpath -u [file normalize $winpath]]]
}
proc staging_create {} {
variable created_stagingdirs
set wsl [wslexe]
set tmpd [string trim [exec {*}$wsl -e mktemp -d -t punktest.XXXXXX]]
if {![string match "/tmp/*" $tmpd]} {
error "wsl staging_create: unexpected tempdir '$tmpd'"
}
lappend created_stagingdirs $tmpd
return $tmpd
}
#one-way copy-in: read each windows file over the shared path ONCE, into the native dir
proc staging_copy_in {stagingdir args} {
set wsl [wslexe]
foreach winfile $args {
set src [winpath_to_wsl $winfile]
exec {*}$wsl -e cp $src $stagingdir/
}
}
#run a bash command line with cwd at the staging dir. Returns dict: ok, output.
proc run_in {stagingdir cmdline} {
set wsl [wslexe]
set script "cd $stagingdir && $cmdline"
if {[catch {exec {*}$wsl -e bash -c $script 2>@1} output]} {
return [dict create ok 0 output $output]
}
return [dict create ok 1 output $output]
}
proc staging_cleanup {stagingdir} {
variable created_stagingdirs
if {$stagingdir ni $created_stagingdirs} {
error "wsl staging_cleanup: refusing to remove '$stagingdir' (not created by staging_create)"
}
set wsl [wslexe]
catch {exec {*}$wsl -e rm -rf $stagingdir}
set created_stagingdirs [lsearch -all -inline -not -exact $created_stagingdirs $stagingdir]
}
}
Loading…
Cancel
Save