From d50115517fa336f384a7b5fc33d5734a2591289f Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Thu, 30 Jul 2026 21:28:06 +1000 Subject: [PATCH] libfetch characterization test + isolation seams (punkshell 0.32.1) make.tcl libfetch gains env-only isolation seams - PUNK_LIBFETCH_CONFIG (alternate declarations file) and PUNK_LIBFETCH_PACKAGES (alternate tier root), the PUNK_RUNTIME_PLATFORM idiom - so fixture/mirror runs never read the real declarations or write the real bin/packages tier. Neither seam bypasses the -trust-server consent gate. New shell/testsuites/punkexe/maketcllibfetch.test (piped maketcl* harness + testsupport/httpfixture.tcl + a file:// mirror; the canonical origin is never contacted) pins the G-139 behaviours that previously had only manual session evidence: the server-trust gate refusing non-canonical origins with exit 3 before any network access or tier write, -serverurl-over-PUNKBIN_URL precedence, consented fetch/verify/materialize (sha1sums + zip + sidecar + installed-shape tree with embedded record), idempotent re-run vs -force, declared-revision-change re-materialization keyed to the tree record (with the sidecar-not-listed note lane), sha1-MISMATCH rejection without residue, missing-from-server-sha1sums failure, and the no-config no-op. 8/8 pass in both runner modes (singleproc + -jobs); full punkexe subtree green (93 runnable, 0 failed); the real bin/packages tier verified untouched. Also refreshes the stale src/runtime/libpackages.toml header (r1 IS published as of 2026-07-30; the G-139 reference now points at the archive) and documents the seams there and in the libfetch help text. Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 16 + punkproject.toml | 2 +- src/make.tcl | 25 +- src/runtime/libpackages.toml | 23 +- src/tests/shell/AGENTS.md | 1 + .../testsuites/punkexe/maketcllibfetch.test | 457 ++++++++++++++++++ 6 files changed, 512 insertions(+), 12 deletions(-) create mode 100644 src/tests/shell/testsuites/punkexe/maketcllibfetch.test diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c2347ce..aa8f3ab3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ 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.32.1] - 2026-07-30 + +- `make.tcl libfetch` gains env-only isolation seams: `PUNK_LIBFETCH_CONFIG` + (alternate declarations file) and `PUNK_LIBFETCH_PACKAGES` (alternate tier + root), so fixture/mirror runs never read the real declarations or write the + real `bin/packages` tier; neither seam bypasses the `-trust-server` consent + gate. New characterization suite + `src/tests/shell/testsuites/punkexe/maketcllibfetch.test` pins the G-139 + behaviours previously verified only by manual session evidence - the + server-trust gate (refusal with exit 3 before any network access), + `-serverurl`-over-`PUNKBIN_URL` precedence, fetch/verify/materialize, + idempotency vs `-force`, declared-revision-change re-materialization, + sha1-mismatch rejection and the no-config no-op - against + httpfixture-served and `file://` fixture origins (the canonical origin is + never contacted). + ## [0.32.0] - 2026-07-30 - Kit payloads switched to punkbin lib-tier artifacts (G-139): the 8.6-family diff --git a/punkproject.toml b/punkproject.toml index 31f2d81f..0075f4ca 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,4 +1,4 @@ [project] name = "punkshell" -version = "0.32.0" +version = "0.32.1" license = "BSD-2-Clause" diff --git a/src/make.tcl b/src/make.tcl index 27001e08..a55eda23 100644 --- a/src/make.tcl +++ b/src/make.tcl @@ -2685,7 +2685,12 @@ if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBO (never an interactive prompt - unattended runs stay unattended). A file:// origin is supported for local mirrors/fixtures. Transport for http(s): Tcl http+tls when loadable, else curl, - else PowerShell (windows)." + else PowerShell (windows). + Isolation seams (env-only): PUNK_LIBFETCH_CONFIG points at an + alternate declarations file and PUNK_LIBFETCH_PACKAGES at an + alternate tier root - fixture/test runs leave the real + declarations and bin/packages untouched. Neither bypasses the + -trust-server consent gate." bin " Install executables from src/bin to /bin, then check vfs @@ -5286,6 +5291,20 @@ if {$::punkboot::command eq "libfetch"} { } set libfetch_config $sourcefolder/runtime/libpackages.toml + set libfetch_packagesroot $projectroot/bin/packages + #Isolation seams (env-only, the PUNK_RUNTIME_PLATFORM idiom): an alternate + #declarations file and an alternate tier root, so fixture-driven runs (the + #shell/testsuites/punkexe/maketcllibfetch.test characterization suite, + #local-mirror experiments) never read the real declarations or write the + #real bin/packages tier. The server-trust gate below is deliberately NOT + #seam-bypassable - fixtures consent with -trust-server like any other + #non-canonical origin. + if {[info exists ::env(PUNK_LIBFETCH_CONFIG)] && $::env(PUNK_LIBFETCH_CONFIG) ne ""} { + set libfetch_config [file normalize $::env(PUNK_LIBFETCH_CONFIG)] + } + if {[info exists ::env(PUNK_LIBFETCH_PACKAGES)] && $::env(PUNK_LIBFETCH_PACKAGES) ne ""} { + set libfetch_packagesroot [file normalize $::env(PUNK_LIBFETCH_PACKAGES)] + } #Canonical origin: the punkbin raw-file base (the punk-runtime.cmd #convention; lib tier addressed as /lib//...). #Server-trust consent (G-139, the G-123 posture): the canonical origin @@ -5368,7 +5387,7 @@ if {$::punkboot::command eq "libfetch"} { puts stderr "$A(BAD)LIBFETCH: artifact.$aname name '$artifact' does not match -tcl-r.zip$A(RST)" exit 3 } - set tierdir $projectroot/bin/packages/$target + set tierdir $libfetch_packagesroot/$target file mkdir $tierdir #server truth per target: lib//sha1sums.txt (fetched once per #run per target; the local copy in the tier mirrors the punk-runtime @@ -5505,7 +5524,7 @@ if {$::punkboot::command eq "libfetch"} { puts stderr "$A(BAD)LIBFETCH: FAILED - one or more declared artifacts could not be fetched/verified/materialized (see above)$A(RST)" exit 3 } - puts stdout "LIBFETCH: done ([llength $libfetch_actions] action(s)) -> $projectroot/bin/packages" + puts stdout "LIBFETCH: done ([llength $libfetch_actions] action(s)) -> $libfetch_packagesroot" } flush stdout ::exit 0 diff --git a/src/runtime/libpackages.toml b/src/runtime/libpackages.toml index 5dc5c3aa..c5625742 100644 --- a/src/runtime/libpackages.toml +++ b/src/runtime/libpackages.toml @@ -20,15 +20,22 @@ # target - lib-tier subfolder: 'allplatforms' (platform-neutral, pure-tcl) # or a canonical platform-dir name (binary packages) # -# NOTE: the r1 artifacts below are emitted by the suite_tcl90/suite_tcl86 -# 'library-artifacts' steps (G-138, achieved 2026-07-30) but PUBLICATION to the -# canonical punkbin origin is a deliberate user step - until it happens, -# libfetch against the canonical origin reports them missing from the server -# sha1sums. A local mirror (file:// origin + -trust-server) or a pre-populated -# bin/packages tier serves in the interim. +# The r1 artifacts below are emitted by the suite_tcl90/suite_tcl86 +# 'library-artifacts' steps and published in the canonical punkbin lib tier +# (G-138 + G-139, both achieved 2026-07-30). Publication of any NEW revision +# remains a deliberate user step - a declared name not yet on the server +# reports as missing from its sha1sums; a local mirror (file:// origin + +# -trust-server) or a pre-populated bin/packages tier serves during +# development. # -# Downstream consumption (lib_tcl deployment, kit vfs declarations) is the -# remainder of G-139 - see goals/G-139-punkbin-library-consumption.md. +# Isolation seams (env-only): PUNK_LIBFETCH_CONFIG substitutes an alternate +# declarations file for this one, PUNK_LIBFETCH_PACKAGES an alternate tier +# root - used by the maketcllibfetch.test fixture characterization; neither +# bypasses the -trust-server consent gate. +# +# Downstream consumption (lib_tcl deployment via the PACKAGES phase, kit +# vfs declarations in vendorlib_vfs.toml) - see +# goals/archive/G-139-punkbin-library-consumption.md. [artifact.tcllib-tcl9] name = "tcllib2.0-tcl9-r1.zip" diff --git a/src/tests/shell/AGENTS.md b/src/tests/shell/AGENTS.md index cc69a291..c1168b03 100644 --- a/src/tests/shell/AGENTS.md +++ b/src/tests/shell/AGENTS.md @@ -18,6 +18,7 @@ Tests for shell-level behavior, command-line execution, and stdin/stdout interac - Tests that spawn the built executable must be hang-proof: use the event-loop `punk_run` pattern (pipe stdin, half-close for EOF, timeout then force-kill) rather than plain `exec`, because known failure modes leave the child waiting on a reopened console. - Three punkexe suites drive the WORKING TREE's `src/make.tcl` through the built executable's `script` subcommand with output captured through a pipe, pinning its reporting surfaces rather than running builds: `maketclcolour.test` (G-113 piped colour policy), `maketclbakelist.test` (G-121 kit matrix report + the selective-bake no-build guard) and `maketclplatform.test` (G-122 host/target platform split - the `check` derivation line, per-kit store tier/target, and cross-target artifact naming). Their per-kit row expectations are characterization of the current `src/runtime/mapvfs.config` matrix, so a deliberate config change legitimately updates them. `maketclplatform.test` additionally carries self-gating cygwin-host tests: they discover an msys2/cygwin-runtime tclsh (`env(PUNK_MSYS_TCLSH)`, else the usual msys2/cygwin roots) and PROBE it - a candidate only counts when it really reports `tcl_platform(platform)` `unix` with a cygwin-family os string - then assert that host derives the same win32 target, store and kit set as the native run. Such a tclsh is a POSIX Tcl: pass it a `/c/...`-spelled script path, never `C:/...`. - `testsuites/punkexe/maketclbootgate.test` (G-125) pins the make.tcl side of the boot-precondition gate through the same piped `script` harness: `check` reports the gate as `ACTIVE` or `UNAVAILABLE` (the documented degradation when the bootsupport `punkboot::utils` snapshot lacks `vfs_boot_library_report`), names both tcl-library conventions, and states the never-deployed guarantee - ESC-free per the G-113 piped policy. The gate's VERDICT is characterized against directory fixtures in `modules/punkboot/utils/testsuites/utils/bootlibrary.test`; the predicate lives in `punkboot::utils` precisely so it is testable without running a bake. +- `testsuites/punkexe/maketcllibfetch.test` (G-139 follow-up) characterizes `make.tcl libfetch` through the same piped `script` harness against FIXTURE origins only - a `testsupport/httpfixture.tcl` server plus a `file://` mirror of a crafted punkbin `lib/` layout; the real canonical origin is never contacted. Pins: the server-trust consent gate (any non-canonical origin - `PUNKBIN_URL` env or `-serverurl` - without `-trust-server` refuses with exit 3 BEFORE any network access or tier write, naming both the offending and canonical origins), `-serverurl`-beats-`PUNKBIN_URL` precedence, consented fetch/verify/materialize (per-target sha1sums + zip + sidecar into the tier, installed-shape tree with its embedded `punkbin-artifact.toml`), idempotent re-run vs `-force`, declared-revision-change re-materialization keyed to the tree's embedded record (including the sidecar-not-listed note lane), sha1-MISMATCH rejection without residue, artifact-missing-from-server-sha1sums failure, and the no-config no-op. Isolation rides the env seams `PUNK_LIBFETCH_CONFIG` + `PUNK_LIBFETCH_PACKAGES` (all state in tcltest tempdirs - the real `src/runtime/libpackages.toml` and `bin/packages` tier are never touched; the seams deliberately cannot bypass the consent gate). The asserted canonical-origin url is characterization of the constant in `src/make.tcl` - a deliberate canonical-origin change legitimately updates it. - `testsuites/punkexe/shellexit.test` guards piped-stdin termination behaviour of the built executable (no `invalid thread handle` on shutdown races, no console-reopen hang on eof/error paths, exitcode propagation); do not weaken these tests - a red result means a regression in punk::repl shutdown or app-punkshell eof handling. - `testsuites/punkexe/shellnavns.test` proves the namespace-navigation session contract end-to-end over a piped 'src'-mode session (dev modules - deliberate, so current punk::repl/nav source is exercised without a rebuild): n/ sets ns_current, the current namespace continues into `subshell punk` and back out (currently via the documented first-subshell shared-code-interp asymmetry - the SHARED marker pins it; the repl::start seeding template for genuinely separate interps is proven in modules/punk/repl nscurrent.test), the punk::repl 0.5.2 stray-namespace fix holds live, and the session exits cleanly. KNOWN GAP pinned there: piped submissions evaluate at :: despite ns_current (interactive and module-level inscope behaviour differ) - investigate alongside G-038. - `testsuites/punkexe/scriptexec.test` covers the `script` subcommand (app-punkscript, G-015): piped execution + honest exit codes, result echo, file-form argv, the `lib:` scriptlib-resolution mechanism (incl. punk::path 0.3.0 extensionless-tcl matching: first-line identification, called-spelling precedence over a `.tcl` sibling, sh-trampoline shebang acceptance, and the non-tcl skip note on not-found), and the tclsh-matching Tk main-loop behaviour. The `lib:` and Tk cases resolve committed fixtures under `scriptlib/_punktest/` (test-owned; a `lib:`-resolvable fixture cannot live in a tcltest temp dir since resolution only searches scriptlib locations relative to the exe). Tk cases are gated by the `punktk` constraint, auto-detected by probing whether the kit can load Tk. diff --git a/src/tests/shell/testsuites/punkexe/maketcllibfetch.test b/src/tests/shell/testsuites/punkexe/maketcllibfetch.test new file mode 100644 index 00000000..ea830b25 --- /dev/null +++ b/src/tests/shell/testsuites/punkexe/maketcllibfetch.test @@ -0,0 +1,457 @@ +package require tcltest +package require punk::zip +package require sha1 + +#Piped characterization of 'make.tcl libfetch' (G-139): the punkbin lib-tier +#fetch/verify/materialize entrypoint, driven ONLY against fixture origins - a +#child-process http server (testsupport/httpfixture.tcl) serving a crafted +#punkbin lib/ layout, plus a file:// mirror of the same layout and +#deliberately-unreachable urls. The real canonical origin is never contacted. +#Runs the WORKING TREE's src/make.tcl under the built punk executable's +#'script' subcommand with output captured through a pipe (the maketcl* family +#harness), and pins: +# - the server-trust consent gate: any NON-canonical origin (PUNKBIN_URL env +# or -serverurl flag) without -trust-server refuses with exit 3 BEFORE any +# network access or tier write (proven with an unreachable origin), naming +# the offending origin and the canonical one +# - -serverurl beats PUNKBIN_URL (documented precedence): a consented fetch +# succeeds against the fixture while the env var points at a dead address +# - consented fetch: per-target sha1sums + artifact zip + .toml sidecar +# fetched and sha1-verified into the tier, installed-shape tree +# materialized under //tcl/ with pkgIndex.tcl +# and the embedded punkbin-artifact.toml record, exit 0, ESC-free output +# (G-113 piped policy) +# - idempotent re-run: verified-present + tree-current, no re-fetch, no +# re-materialization; -force re-fetches and re-materializes +# - editing the declared name to a new revision re-materializes WITHOUT +# -force (tree currency is keyed to the tree's embedded record name), with +# immutable revisions side-by-side in the tier; a sidecar missing from the +# server sha1sums is noted and skipped without failing the artifact +# - sha1 MISMATCH against the server sha1sums: artifact rejected (no .part +# residue, no zip, no tree), run FAILED with exit 3 +# - an artifact missing from the server sha1sums fails the run (exit 3) +# - no config at the declarations path: 'nothing declared' no-op, exit 0 +# - a file:// origin works end-to-end via the native copy transport (and +# still needs -trust-server like any non-canonical origin) +#The canonical-origin url asserted in the gate output is characterization of +#the constant in src/make.tcl - a deliberate canonical-origin change +#legitimately updates it. +#Isolation: the env seams PUNK_LIBFETCH_CONFIG + PUNK_LIBFETCH_PACKAGES point +#every run at tcltest-tempdir fixture state - the real +#src/runtime/libpackages.toml and bin/packages tier are never touched. +# +#Target executable resolved from env(PUNK_SHELL_TEST_EXE), else /bin/punk902z.exe +#then /bin/punkshell902. Skipped (constraint punkexeavailable) if none found. + +namespace eval ::testspace { + namespace import ::tcltest::* + + variable testdir [file dirname [file normalize [info script]]] + #/src/tests/shell/testsuites/punkexe -> 5 levels up to + variable projectroot [file normalize [file join $testdir .. .. .. .. ..]] + variable testsroot [file normalize [file join $testdir .. .. ..]] + variable maketcl [file join $projectroot src make.tcl] + + variable punkexe "" + if {[info exists ::env(PUNK_SHELL_TEST_EXE)] && $::env(PUNK_SHELL_TEST_EXE) ne ""} { + set punkexe [file normalize $::env(PUNK_SHELL_TEST_EXE)] + } else { + foreach candidate [list [file join $projectroot bin punk902z.exe] [file join $projectroot bin punkshell902]] { + if {[file exists $candidate]} { + set punkexe $candidate + break + } + } + } + testConstraint punkexeavailable [expr {$punkexe ne "" && [file exists $punkexe]}] + + variable maketcl_run_timeout_ms 60000 + + variable runstate + array set runstate {} + + proc maketcl_run_read {chan} { + variable runstate + append runstate(output) [read $chan] + if {[chan eof $chan]} { + chan event $chan readable {} + set runstate(done) eof + } + } + + #Run script src/make.tcl with output captured through a + #pipe (stdin half-closed for immediate EOF - make.tcl must never wait on stdin for + #these subcommands; the child inherits ::env, which carries the seams and the + #per-case PUNKBIN_URL state). Returns dict: timedout 0|1, exitcode , + #output . + proc maketcl_run {cmdargs} { + variable runstate + variable maketcl_run_timeout_ms + variable punkexe + variable maketcl + array unset runstate + set runstate(output) "" + set runstate(done) "" + + set chan [open |[list $punkexe script $maketcl {*}$cmdargs 2>@1] r+] + chan configure $chan -blocking 0 -translation binary + catch {chan close $chan write} ;#no stdin for the child - immediate EOF + set timerid [after $maketcl_run_timeout_ms [list set [namespace current]::runstate(done) timeout]] + chan event $chan readable [list [namespace current]::maketcl_run_read $chan] + while {$runstate(done) eq ""} { + vwait [namespace current]::runstate(done) + } + after cancel $timerid + set timedout [expr {$runstate(done) eq "timeout"}] + set exitcode "" + if {$timedout} { + catch {exec {*}[auto_execok taskkill] /F /T /PID [lindex [pid $chan] 0]} + catch {chan close $chan} + } else { + chan configure $chan -blocking 1 + if {[catch {chan close $chan} errdata errdict]} { + set exitcode [lindex [dict get $errdict -errorcode] end] + } else { + set exitcode 0 + } + } + return [dict create timedout $timedout exitcode $exitcode output $runstate(output)] + } + + proc esc_count {text} { + return [regexp -all {\x1b} $text] + } + + proc writefile_raw {path content} { + set fd [open $path w] + fconfigure $fd -translation binary + puts -nonewline $fd $content + close $fd + } + proc readfile {path} { + set fd [open $path r] + set data [read $fd] + close $fd + return $data + } + + #--- fixture punkbin lib-tier layout (server side) ----------------------------- + variable plat testplat-x86_64 + variable serverroot "" + variable configdir "" + variable configfile "" + variable packagesroot "" + variable serverchan "" + variable baseurl "" + variable saved_env [dict create] + variable fixtureok 0 + + #a minimal installed-shape package folder: pkgIndex.tcl + one script + the + #embedded punkbin-artifact.toml record whose name line keys tree currency + proc fixture_record {artifactname} { + set l {} + lappend l "# punkbin artifact record (test fixture)" + lappend l "schema = \"2\"" + lappend l "name = \"$artifactname\"" + lappend l "class = \"library\"" + return "[join $l \n]\n" + } + proc build_fixture_zip {stageparent pkgfolder artifactname payload zipfile} { + set pdir [file join $stageparent $pkgfolder] + file mkdir $pdir + writefile_raw [file join $pdir pkgIndex.tcl] "package ifneeded fixturepkg 1.0 \[list source \[file join \$dir fixturepkg.tcl\]\]\n" + writefile_raw [file join $pdir fixturepkg.tcl] $payload + writefile_raw [file join $pdir punkbin-artifact.toml] [fixture_record $artifactname] + punk::zip::mkzip -directory $stageparent $zipfile + file delete -force $pdir + } + + proc write_config {entries} { + variable configfile + set lines [list "# maketcllibfetch.test fixture declarations"] + foreach e $entries { + lassign $e key name target + lappend lines "\[artifact.$key\]" "name = \"$name\"" "target = \"$target\"" "" + } + writefile_raw $configfile "[join $lines \n]\n" + } + + proc reset_packages {} { + variable packagesroot + file delete -force $packagesroot + file mkdir $packagesroot + } + + proc server_start {} { + variable serverroot + variable serverchan + variable baseurl + variable testsroot + set helper [file join $testsroot testsupport httpfixture.tcl] + if {![file exists $helper]} { return 0 } + if {[catch {open |[list [info nameofexecutable] $helper $serverroot] r+} chan]} { + return 0 + } + chan configure $chan -blocking 1 -buffering line + set line [gets $chan] + if {[regexp {^PORT (\d+)$} $line -> p]} { + set serverchan $chan + set baseurl "http://127.0.0.1:$p" + return 1 + } + catch {close $chan} + return 0 + } + proc server_stop {} { + variable serverchan + if {$serverchan ne ""} { + #closing the pipe EOFs the child's stdin - it exits on that + catch {close $serverchan} + set serverchan "" + } + } + + #--- one-time fixture build + server launch ------------------------------------ + if {[testConstraint punkexeavailable]} { + variable serverroot [file normalize [makeDirectory lf_server]] + variable configdir [file normalize [makeDirectory lf_config]] + variable packagesroot [file normalize [makeDirectory lf_packages]] + variable configfile [file join $configdir libpackages.toml] + set tplat [file join $serverroot lib $plat] + file mkdir $tplat + set zipstage [file normalize [makeDirectory lf_zipstage]] + #two immutable revisions of the same package folder (r2 = new bytes, new + #embedded record) + a corrupt-served artifact for the mismatch case + build_fixture_zip $zipstage fixturepkg fixturepkg-tcl9-r1.zip \ + "proc fixturepkg::rev {} {return r1}\n" [file join $tplat fixturepkg-tcl9-r1.zip] + build_fixture_zip $zipstage fixturepkg fixturepkg-tcl9-r2.zip \ + "proc fixturepkg::rev {} {return r2}\n" [file join $tplat fixturepkg-tcl9-r2.zip] + writefile_raw [file join $tplat fixturepkg-tcl9-r1.toml] [fixture_record fixturepkg-tcl9-r1.zip] + writefile_raw [file join $tplat badpkg-tcl9-r1.zip] "these served bytes do not match the sha1 the server sha1sums promises\n" + #server sha1sums: r1 zip + r1 sidecar listed with true hashes; r2 zip + #listed with a true hash but its sidecar deliberately NOT listed (the + #sidecar-skipped note lane); badpkg listed with a WRONG hash (mismatch + #lane). ghostpkg-tcl9-r1.zip is declared by a config but never listed. + set sums [list] + foreach fname [list fixturepkg-tcl9-r1.zip fixturepkg-tcl9-r1.toml fixturepkg-tcl9-r2.zip] { + lappend sums "[string tolower [sha1::sha1 -hex -file [file join $tplat $fname]]] *$fname" + } + lappend sums "[string repeat 0 40] *badpkg-tcl9-r1.zip" + writefile_raw [file join $tplat sha1sums.txt] "[join $sums \n]\n" + + variable fixtureok [server_start] + #shield the runs from ambient state, then point the seams at the fixture + #config + packages tier (restored at end of file) + foreach v {PUNKBIN_URL PUNK_LIBFETCH_CONFIG PUNK_LIBFETCH_PACKAGES} { + if {[info exists ::env($v)]} { + dict set saved_env $v $::env($v) + unset ::env($v) + } + } + set ::env(PUNK_LIBFETCH_CONFIG) $configfile + set ::env(PUNK_LIBFETCH_PACKAGES) $packagesroot + } + testConstraint fixtureserver $fixtureok + variable deadurl "http://127.0.0.1:1" + + #added 2026-07-30 (agent, G-139) - post-achievement follow-up: fixture-driven + #characterization of the libfetch consent gate, verification and idempotency + #behaviours (the goal's live verification was manual session evidence) + + test maketcl_libfetch_trustgate_env {a non-canonical PUNKBIN_URL without -trust-server refuses with exit 3 before any network access or tier write - proven against an unreachable origin} -constraints {punkexeavailable} -setup { + reset_packages + set ::env(PUNKBIN_URL) $::testspace::deadurl + } -body { + variable deadurl + variable packagesroot + set r [maketcl_run {libfetch}] + set out [dict get $r output] + set result [list] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result esc [esc_count $out] + lappend result origin_named [string match "*LIBFETCH: origin '$deadurl' is not the canonical punkbin origin*" $out] + lappend result canonical_named [string match "*LIBFETCH: canonical: https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master*" $out] + lappend result consent_stated [string match "*requires the explicit -trust-server flag*Nothing was fetched.*" $out] + lappend result tier_untouched [expr {[llength [glob -nocomplain -directory $packagesroot *]] == 0}] + set result + } -cleanup { + unset -nocomplain ::env(PUNKBIN_URL) + } -result {timedout 0 exitcode 3 esc 0 origin_named 1 canonical_named 1 consent_stated 1 tier_untouched 1} + + test maketcl_libfetch_trustgate_flag {a non-canonical -serverurl without -trust-server refuses naming that origin} -constraints {punkexeavailable fixtureserver} -setup { + reset_packages + unset -nocomplain ::env(PUNKBIN_URL) + } -body { + variable baseurl + variable packagesroot + set r [maketcl_run [list libfetch -serverurl $baseurl]] + set out [dict get $r output] + set result [list] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result origin_named [string match "*LIBFETCH: origin '$baseurl' is not the canonical punkbin origin*" $out] + lappend result tier_untouched [expr {[llength [glob -nocomplain -directory $packagesroot *]] == 0}] + set result + } -cleanup { + } -result {timedout 0 exitcode 3 origin_named 1 tier_untouched 1} + + test maketcl_libfetch_fetch_idempotent_force {consented http fetch materializes the declared artifact (with -serverurl beating a dead PUNKBIN_URL); a re-run is idempotent; -force re-fetches and re-materializes} -constraints {punkexeavailable fixtureserver} -setup { + reset_packages + write_config {{fixture fixturepkg-tcl9-r1.zip testplat-x86_64}} + #the dead env origin proves flag-over-env precedence: only -serverurl + #reaching the fixture lets this succeed + set ::env(PUNKBIN_URL) $::testspace::deadurl + } -body { + variable baseurl + variable packagesroot + variable plat + set tier [file join $packagesroot $plat] + set result [list] + + set r [maketcl_run [list libfetch -serverurl $baseurl -trust-server]] + set out [dict get $r output] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result esc [esc_count $out] + lappend result fetched [string match "*LIBFETCH: fetched lib/$plat/fixturepkg-tcl9-r1.zip*" $out] + lappend result materialized [string match "*LIBFETCH: materialized $plat/tcl9/fixturepkg*" $out] + lappend result done_line [string match "*LIBFETCH: done (2 action(s)) -> $packagesroot*" $out] + lappend result tier_zip [file isfile [file join $tier fixturepkg-tcl9-r1.zip]] + lappend result tier_sidecar [file isfile [file join $tier fixturepkg-tcl9-r1.toml]] + lappend result tier_sums [file isfile [file join $tier sha1sums.txt]] + lappend result tree_pkgindex [file isfile [file join $tier tcl9 fixturepkg pkgIndex.tcl]] + lappend result tree_record [regexp -line {^name = "fixturepkg-tcl9-r1\.zip"$} [readfile [file join $tier tcl9 fixturepkg punkbin-artifact.toml]]] + lappend result no_part [expr {[llength [glob -nocomplain -directory $tier *.part]] == 0}] + + set r2 [maketcl_run [list libfetch -serverurl $baseurl -trust-server]] + set out2 [dict get $r2 output] + lappend result rerun_exitcode [dict get $r2 exitcode] + lappend result rerun_verified [string match "*LIBFETCH: verified-present lib/$plat/fixturepkg-tcl9-r1.zip*" $out2] + lappend result rerun_treecurrent [string match "*LIBFETCH: tree-current $plat/tcl9/fixturepkg*" $out2] + lappend result rerun_no_refetch [expr {![string match "*LIBFETCH: fetched *" $out2] && ![string match "*LIBFETCH: materialized *" $out2]}] + + set r3 [maketcl_run [list libfetch -serverurl $baseurl -trust-server -force]] + set out3 [dict get $r3 output] + lappend result force_exitcode [dict get $r3 exitcode] + lappend result force_refetched [string match "*LIBFETCH: fetched lib/$plat/fixturepkg-tcl9-r1.zip*" $out3] + lappend result force_rematerialized [string match "*LIBFETCH: materialized $plat/tcl9/fixturepkg*" $out3] + set result + } -cleanup { + unset -nocomplain ::env(PUNKBIN_URL) + } -result {timedout 0 exitcode 0 esc 0 fetched 1 materialized 1 done_line 1 tier_zip 1 tier_sidecar 1 tier_sums 1 tree_pkgindex 1 tree_record 1 no_part 1 rerun_exitcode 0 rerun_verified 1 rerun_treecurrent 1 rerun_no_refetch 1 force_exitcode 0 force_refetched 1 force_rematerialized 1} + + test maketcl_libfetch_declared_revision_change {editing the declared name to a new revision re-materializes without -force (tree currency keys off the embedded record); revisions sit side-by-side in the tier; a sidecar absent from the server sha1sums is noted and skipped without failing} -constraints {punkexeavailable fixtureserver} -setup { + reset_packages + write_config {{fixture fixturepkg-tcl9-r1.zip testplat-x86_64}} + unset -nocomplain ::env(PUNKBIN_URL) + } -body { + variable baseurl + variable packagesroot + variable plat + set tier [file join $packagesroot $plat] + set result [list] + set r [maketcl_run [list libfetch -serverurl $baseurl -trust-server]] + lappend result prime_exitcode [dict get $r exitcode] + write_config {{fixture fixturepkg-tcl9-r2.zip testplat-x86_64}} + set r2 [maketcl_run [list libfetch -serverurl $baseurl -trust-server]] + set out2 [dict get $r2 output] + lappend result exitcode [dict get $r2 exitcode] + lappend result fetched_r2 [string match "*LIBFETCH: fetched lib/$plat/fixturepkg-tcl9-r2.zip*" $out2] + lappend result rematerialized [string match "*LIBFETCH: materialized $plat/tcl9/fixturepkg*" $out2] + lappend result sidecar_noted [string match "*LIBFETCH: note - fixturepkg-tcl9-r2.toml not listed in server sha1sums (artifact accepted; sidecar skipped)*" $out2] + lappend result tree_record_r2 [regexp -line {^name = "fixturepkg-tcl9-r2\.zip"$} [readfile [file join $tier tcl9 fixturepkg punkbin-artifact.toml]]] + lappend result r1_still_present [file isfile [file join $tier fixturepkg-tcl9-r1.zip]] + lappend result r2_present [file isfile [file join $tier fixturepkg-tcl9-r2.zip]] + lappend result no_r2_sidecar [expr {![file exists [file join $tier fixturepkg-tcl9-r2.toml]]}] + set result + } -cleanup { + } -result {prime_exitcode 0 exitcode 0 fetched_r2 1 rematerialized 1 sidecar_noted 1 tree_record_r2 1 r1_still_present 1 r2_present 1 no_r2_sidecar 1} + + test maketcl_libfetch_sha1_mismatch {an artifact whose served bytes do not match the server sha1sums is rejected: no zip, no .part residue, no tree, run FAILED exit 3} -constraints {punkexeavailable fixtureserver} -setup { + reset_packages + write_config {{bad badpkg-tcl9-r1.zip testplat-x86_64}} + unset -nocomplain ::env(PUNKBIN_URL) + } -body { + variable baseurl + variable packagesroot + variable plat + set tier [file join $packagesroot $plat] + set result [list] + set r [maketcl_run [list libfetch -serverurl $baseurl -trust-server]] + set out [dict get $r output] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result mismatch_named [string match "*sha1 MISMATCH for badpkg-tcl9-r1.zip*- deleted*" $out] + lappend result failed_stated [string match "*LIBFETCH: FAILED - one or more declared artifacts*" $out] + lappend result no_zip [expr {![file exists [file join $tier badpkg-tcl9-r1.zip]]}] + lappend result no_part [expr {[llength [glob -nocomplain -directory $tier *.part]] == 0}] + lappend result no_tree [expr {![file exists [file join $tier tcl9 badpkg]]}] + set result + } -cleanup { + } -result {timedout 0 exitcode 3 mismatch_named 1 failed_stated 1 no_zip 1 no_part 1 no_tree 1} + + test maketcl_libfetch_not_in_server_sums {a declared artifact absent from the server sha1sums fails the run} -constraints {punkexeavailable fixtureserver} -setup { + reset_packages + write_config {{ghost ghostpkg-tcl9-r1.zip testplat-x86_64}} + unset -nocomplain ::env(PUNKBIN_URL) + } -body { + variable baseurl + variable plat + set result [list] + set r [maketcl_run [list libfetch -serverurl $baseurl -trust-server]] + set out [dict get $r output] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result not_listed [string match "*'ghostpkg-tcl9-r1.zip' is not listed in the server's lib/$plat/sha1sums.txt*" $out] + lappend result failed_stated [string match "*LIBFETCH: FAILED - one or more declared artifacts*" $out] + set result + } -cleanup { + } -result {timedout 0 exitcode 3 not_listed 1 failed_stated 1} + + test maketcl_libfetch_no_config {a missing declarations file is a stated no-op with exit 0 (canonical origin, no network involved)} -constraints {punkexeavailable} -setup { + reset_packages + unset -nocomplain ::env(PUNKBIN_URL) + set ::env(PUNK_LIBFETCH_CONFIG) [file join $::testspace::configdir absent_declarations.toml] + } -body { + set result [list] + set r [maketcl_run {libfetch}] + set out [dict get $r output] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result noop_stated [string match "*LIBFETCH: no config at *absent_declarations.toml - nothing declared, nothing to fetch*" $out] + set result + } -cleanup { + set ::env(PUNK_LIBFETCH_CONFIG) $::testspace::configfile + } -result {timedout 0 exitcode 0 noop_stated 1} + + test maketcl_libfetch_file_origin {a consented file:// mirror origin fetches and materializes via the native copy transport (no server process involved)} -constraints {punkexeavailable} -setup { + reset_packages + write_config {{fixture fixturepkg-tcl9-r1.zip testplat-x86_64}} + unset -nocomplain ::env(PUNKBIN_URL) + } -body { + variable serverroot + variable packagesroot + variable plat + set tier [file join $packagesroot $plat] + set result [list] + set r [maketcl_run [list libfetch -serverurl "file://$serverroot" -trust-server]] + set out [dict get $r output] + lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] + lappend result fetched [string match "*LIBFETCH: fetched lib/$plat/fixturepkg-tcl9-r1.zip*" $out] + lappend result materialized [string match "*LIBFETCH: materialized $plat/tcl9/fixturepkg*" $out] + lappend result tree_pkgindex [file isfile [file join $tier tcl9 fixturepkg pkgIndex.tcl]] + lappend result tree_record [regexp -line {^name = "fixturepkg-tcl9-r1\.zip"$} [readfile [file join $tier tcl9 fixturepkg punkbin-artifact.toml]]] + set result + } -cleanup { + } -result {timedout 0 exitcode 0 fetched 1 materialized 1 tree_pkgindex 1 tree_record 1} + + #whole-file teardown: stop the fixture server, restore shielded env state + server_stop + if {[testConstraint punkexeavailable]} { + unset -nocomplain ::env(PUNKBIN_URL) + unset -nocomplain ::env(PUNK_LIBFETCH_CONFIG) + unset -nocomplain ::env(PUNK_LIBFETCH_PACKAGES) + variable saved_env + dict for {v val} $saved_env { + set ::env($v) $val + } + } + + cleanupTests +} +namespace delete ::testspace