package require tcltest #G-119: behaviour tests for the punk-runtime freshness verdict - the stated #relationship between the ACTIVE runtime and the SERVER DEFAULT #(current/ahead/behind/incomparable) - against a FIXTURE punkbin server: a #child-process http file server (testsupport/httpfixture.tcl) serves a crafted #punkbin layout (defaults.txt, /sha1sums.txt, artifacts + metadata #tomls) and the PUNKBIN_URL override points both payloads at it. # #Both payloads are exercised and their verdict lines compared (parity clause): # - powershell payload: the committed bin/punk-runtime.cmd staged into a # tempdir and driven via cmd.exe (the wrap-pinned windows launch path) # - bash payload: src/scriptapps/bin/punk-runtime.bash staged beside it and # driven directly via a probed msys/git bash (uname MINGW*/MSYS*): on # windows the polyglot routes unix shells to the powershell payload (win32 # nextshell), so the bash payload is executed directly - the same direct # drive the G-059 WSL suite uses on real linux # #All execution happens in a tcltest tempdir staging area against a fixture #platform folder (testplat-x86_64) - the real bin/runtime store and the real #punkbin server are never touched. # #Covered (G-119 acceptance): # - active materialized from r2, server default r1 -> 'list -remote' prints an # explicit AHEAD verdict naming both revisions # - active r1, default r2 -> BEHIND verdict plus fetch+use update guidance # - active == default -> the existing '(= server default)' annotation, and no # freshness lines # - pre-family active (no toml, no -r name): stated no-revision-basis note; # hash-identity naming (with revision verdict) when its sha1 VALUE-matches a # server artifact # - a no-name fetch prints a one-line active-vs-default note when they diverge # - ps1/bash parity: identical 'freshness:' lines from both payloads # - (bash-side extra) different-series incomparable verdict; fetch-mode BEHIND # note with inline activation hint # #NOTE for agents: bin/punk-runtime.cmd is GENERATED - fix the payloads under #src/scriptapps/bin/ and re-wrap (see bin/AGENTS.md); the roundtrip pin lives in #src/tests/modules/punk/mix/testsuites/scriptwrap/runtimecmd_roundtrip.test. namespace eval ::testspace { namespace import ::tcltest::* variable common { set result "" } variable testdir [file dirname [file normalize [info script]]] #/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 .. ..]] variable target [file join $projectroot bin punk-runtime.cmd] variable bashsource [file join $projectroot src scriptapps bin punk-runtime.bash] testConstraint iswindows [expr {$::tcl_platform(platform) eq "windows"}] testConstraint haveruntimecmd [file exists $target] testConstraint havebashsource [file exists $bashsource] testConstraint havecertutil [expr {[auto_execok certutil] ne ""}] #msys/git bash probe: a WINDOWS bash whose uname reports MINGW*/MSYS* and #which carries curl + sha1sum (the payload's fetch/list dependencies). #Git-for-Windows locations are probed before PATH bash - PATH may resolve #the WSL System32 bash (uname Linux), which cannot reach a 127.0.0.1 #fixture server under WSL2 networking and is rejected. proc find_msys_bash {} { set candidates {} foreach c [list \ {C:/Program Files/Git/bin/bash.exe} \ {C:/Program Files/Git/usr/bin/bash.exe} \ {C:/msys64/usr/bin/bash.exe}] { if {[file exists $c]} { lappend candidates $c } } set pathbash [lindex [auto_execok bash] 0] if {$pathbash ne ""} { lappend candidates $pathbash } foreach c $candidates { if {![catch {exec -- $c -c "uname -s" << "" 2>@1} un]} { set un [string trim $un] if {[string match "MINGW*" $un] || [string match "MSYS*" $un]} { if {![catch {exec -- $c -c "command -v curl >/dev/null && command -v sha1sum >/dev/null" << "" 2>@1}]} { return $c } } } } return "" } variable msysbash "" if {[testConstraint iswindows] && [testConstraint havebashsource]} { set msysbash [find_msys_bash] } testConstraint msysbash [expr {$msysbash ne ""}] proc writefile_raw {path content} { set fd [open $path w] fconfigure $fd -translation binary puts -nonewline $fd $content close $fd } #sha1 via certutil (windows-native, no package dependency in the testinterp); #tolerates both contiguous and space-grouped hash output forms proc sha1_of_file {path} { set out [exec -- {*}[auto_execok certutil] -hashfile [file nativename $path] SHA1 << "" 2>@1] foreach ln [split $out \n] { set compact [string map {" " ""} [string trim $ln]] if {[regexp {^[0-9a-fA-F]{40}$} $compact]} { return [string tolower $compact] } } error "no sha1 parsed from certutil output: $out" } #--- fixture punkbin layout (server side) -------------------------------------- variable plat testplat-x86_64 variable artifacts [dict create \ fakert-alpha-r1.exe "FAKERT alpha revision one payload 1111\n" \ fakert-alpha-r2.exe "FAKERT alpha revision two payload 2222\n" \ fakert-beta-r1.exe "FAKERT beta revision one payload 3333\n" \ prefam.exe "PREFAM legacy runtime payload 4444\n"] proc artifact_toml {name rev} { variable plat set l {} lappend l "# punkbin artifact record (test fixture)" lappend l "schema = \"1\"" lappend l "name = \"$name\"" lappend l "revision = $rev" lappend l "target = \"$plat\"" lappend l "variant = \"punk\"" lappend l "tcl_patchlevel = \"9.0.5\"" return "[join $l \n]\n" } variable serverroot "" variable stagedir "" variable serverchan "" variable baseurl "" variable saved_env [dict create] variable fixtureok 0 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 "" } } #server-side curated default (a punkbin release decision in real life) proc set_default {name} { variable serverroot variable plat writefile_raw [file join $serverroot defaults.txt] "# test fixture defaults\n$plat $name\n" } #--- local staging (the payloads' script dir + runtime store) ------------------ proc reset_stage {} { variable stagedir variable plat file delete -force [file join $stagedir runtime] file mkdir [file join $stagedir runtime $plat] } proc stage_file {name content} { variable stagedir variable plat writefile_raw [file join $stagedir runtime $plat $name] $content } proc set_active {name} { variable stagedir variable plat writefile_raw [file join $stagedir runtime $plat active.toml] "active = \"$name\"\n" } #what 'use ' produces: working-name byte copy of the artifact with the #artifact's metadata toml beside it under the working root name, selected active proc stage_materialized {working artifact rev} { variable artifacts stage_file $working [dict get $artifacts $artifact] set root [file rootname $working] stage_file $root.toml [artifact_toml $artifact $rev] set_active $working } #--- payload drivers ------------------------------------------------------------ #powershell payload via the wrap-pinned windows path: cmd.exe /c proc run_cmdroute {args} { variable stagedir set comspec [expr {[info exists ::env(ComSpec)] ? $::env(ComSpec) : "cmd.exe"}] set cmdfile [file nativename [file join $stagedir punk-runtime.cmd]] set output "" set code 0 if {[catch {exec -- $comspec /c $cmdfile {*}$args << "" 2>@1} output opts]} { set ec [dict get $opts -errorcode] if {[lindex $ec 0] eq "CHILDSTATUS"} { set code [lindex $ec 2] } else { set code -1 } } return [dict create exit $code output $output] } #bash payload driven directly under msys bash from the staging dir proc run_bashroute {args} { variable stagedir variable msysbash set sdir [string map {\\ /} $stagedir] set script "cd '$sdir' && ./punk-runtime.bash" foreach a $args { append script " $a" } set output "" set code 0 if {[catch {exec -- $msysbash -c $script << "" 2>@1} output opts]} { set ec [dict get $opts -errorcode] if {[lindex $ec 0] eq "CHILDSTATUS"} { set code [lindex $ec 2] } else { set code -1 } } return [dict create exit $code output $output] } #the verdict lines under test (parity comparisons exclude the payload-specific #guidance lines, which carry each payload's own program-name convention) proc freshlines {output} { set res {} foreach ln [split $output \n] { set ln [string trimright $ln \r] if {[string match "freshness:*" $ln]} { lappend res $ln } } return $res } #--- one-time fixture build + server launch ------------------------------------- if {[testConstraint iswindows] && [testConstraint haveruntimecmd] && [testConstraint havebashsource] && [testConstraint havecertutil] && [testConstraint msysbash]} { variable serverroot [makeDirectory rtfresh_server] variable stagedir [makeDirectory rtfresh_stage] file mkdir [file join $serverroot $plat] file copy -force $target [file join $stagedir punk-runtime.cmd] file copy -force $bashsource [file join $stagedir punk-runtime.bash] #server artifacts + metadata tomls + sha1sums (toml rows included - the #value-match fallback must skip support files when identifying by content) set sums {} dict for {aname acontent} $artifacts { set af [file join $serverroot $plat $aname] writefile_raw $af $acontent lappend sums "[sha1_of_file $af] *$aname" } foreach {aname rev} {fakert-alpha-r1.exe 1 fakert-alpha-r2.exe 2 fakert-beta-r1.exe 1} { set tname "[file rootname $aname].toml" set tf [file join $serverroot $plat $tname] writefile_raw $tf [artifact_toml $aname $rev] lappend sums "[sha1_of_file $tf] *$tname" } writefile_raw [file join $serverroot $plat sha1sums.txt] "[join $sums \n]\n" set_default fakert-alpha-r1.exe variable fixtureok [server_start] if {$fixtureok} { #point both payloads at the fixture server; shield the run from #ambient punk-runtime env state (restored at end of file) foreach v {PUNKBIN_URL PUNK_RUNTIME_PLATFORM PUNK_ACTIVE_RUNTIME} { if {[info exists ::env($v)]} { dict set saved_env $v $::env($v) unset ::env($v) } } set ::env(PUNKBIN_URL) $baseurl } } testConstraint fixtureserver $fixtureok variable C {iswindows haveruntimecmd havebashsource havecertutil msysbash fixtureserver} #added 2026-07-25 (agent, G-119) - fixture-server freshness verdict coverage, #both payloads with parity assertions test runtimecmd_freshness_list_ahead {active materialized from r2 with server default r1: list -remote prints an explicit AHEAD verdict naming both revisions - identically from both payloads}\ -constraints $C\ -setup { set result "" reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r2.exe 2 set_default fakert-alpha-r1.exe }\ -body { set expect "freshness: active fakert-alpha.exe (r2) is AHEAD of the server default fakert-alpha-r1.exe (r1)" set rp [run_cmdroute list -remote -platform testplat-x86_64] lappend result [dict get $rp exit] lappend result [expr {[freshlines [dict get $rp output]] eq [list $expect]}] set rb [run_bashroute list -remote -platform testplat-x86_64] lappend result [dict get $rb exit] lappend result [expr {[freshlines [dict get $rb output]] eq [list $expect]}] }\ -cleanup { }\ -result [list 0 1 0 1] test runtimecmd_freshness_list_behind_guidance {active r1 with server default r2: BEHIND verdict plus fetch+use update guidance - verdict lines identical across payloads}\ -constraints $C\ -setup { set result "" reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r1.exe 1 set_default fakert-alpha-r2.exe }\ -body { set expect "freshness: active fakert-alpha.exe (r1) is BEHIND the server default fakert-alpha-r2.exe (r2)" set gpat "*update: *fetch -platform testplat-x86_64*then: *use fakert-alpha-r2.exe -platform testplat-x86_64*" set rp [run_cmdroute list -remote -platform testplat-x86_64] lappend result [dict get $rp exit] lappend result [expr {[freshlines [dict get $rp output]] eq [list $expect]}] lappend result [string match $gpat [dict get $rp output]] set rb [run_bashroute list -remote -platform testplat-x86_64] lappend result [dict get $rb exit] lappend result [expr {[freshlines [dict get $rb output]] eq [list $expect]}] lappend result [string match $gpat [dict get $rb output]] }\ -cleanup { }\ -result [list 0 1 1 0 1 1] test runtimecmd_freshness_list_current_annotation {active materialized from the default's artifact: the (= server default) annotation appears and no freshness lines are printed - both payloads}\ -constraints $C\ -setup { set result "" reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r1.exe 1 set_default fakert-alpha-r1.exe }\ -body { foreach r [list [run_cmdroute list -remote -platform testplat-x86_64] \ [run_bashroute list -remote -platform testplat-x86_64]] { lappend result [dict get $r exit] lappend result [string match "*active (local): fakert-alpha.exe (= server default)*" [dict get $r output]] lappend result [llength [freshlines [dict get $r output]]] } set result }\ -cleanup { }\ -result [list 0 1 0 0 1 0] test runtimecmd_freshness_prefam_no_basis {a pre-family active (no toml, no -r name, sha1 matching nothing on the server) yields a stated no-revision-basis note rather than silence - both payloads}\ -constraints $C\ -setup { set result "" reset_stage stage_file oldkit.exe "LOCAL ONLY legacy kit 9999\n" set_active oldkit.exe set_default fakert-alpha-r1.exe }\ -body { set expect "freshness: active oldkit.exe has no revision basis (no -r name, no metadata toml revision, sha1 matches no server artifact) - cannot compare with the server default fakert-alpha-r1.exe (r1)" foreach r [list [run_cmdroute list -remote -platform testplat-x86_64] \ [run_bashroute list -remote -platform testplat-x86_64]] { lappend result [dict get $r exit] lappend result [expr {[freshlines [dict get $r output]] eq [list $expect]}] } set result }\ -cleanup { }\ -result [list 0 1 0 1] test runtimecmd_freshness_prefam_hash_identity {a pre-family active whose sha1 VALUE-matches a server artifact is identified by content and gains that artifact's revision verdict - both payloads}\ -constraints $C\ -setup { set result "" variable artifacts reset_stage stage_file oldkit.exe [dict get $artifacts fakert-alpha-r2.exe] set_active oldkit.exe set_default fakert-alpha-r1.exe }\ -body { set expect "freshness: active oldkit.exe is server artifact fakert-alpha-r2.exe by content (r2) - AHEAD of the server default fakert-alpha-r1.exe (r1)" foreach r [list [run_cmdroute list -remote -platform testplat-x86_64] \ [run_bashroute list -remote -platform testplat-x86_64]] { lappend result [dict get $r exit] lappend result [expr {[freshlines [dict get $r output]] eq [list $expect]}] } set result }\ -cleanup { }\ -result [list 0 1 0 1] test runtimecmd_freshness_fetch_noname_note {a no-name fetch resolves the server default and prints a one-line active-vs-default note when the two diverge - both payloads (second run sees the artifact already present)}\ -constraints $C\ -setup { set result "" reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r2.exe 2 set_default fakert-alpha-r1.exe }\ -body { variable stagedir variable plat set expect "freshness: active fakert-alpha.exe (r2) is AHEAD of the server default fakert-alpha-r1.exe (r1)" set rp [run_cmdroute fetch -platform testplat-x86_64] lappend result [dict get $rp exit] lappend result [expr {[freshlines [dict get $rp output]] eq [list $expect]}] #the fetch actually delivered the default artifact + its metadata toml lappend result [file exists [file join $stagedir runtime $plat fakert-alpha-r1.exe]] lappend result [file exists [file join $stagedir runtime $plat fakert-alpha-r1.toml]] #the pre-existing active selection was not stolen by the fetch set fd [open [file join $stagedir runtime $plat active.toml] r] set adata [read $fd] close $fd lappend result [string match {*active = "fakert-alpha.exe"*} $adata] set rb [run_bashroute fetch -platform testplat-x86_64] lappend result [dict get $rb exit] lappend result [expr {[freshlines [dict get $rb output]] eq [list $expect]}] }\ -cleanup { }\ -result [list 0 1 1 1 1 0 1] test runtimecmd_freshness_bash_series_mismatch_and_fetch_behind {bash payload extras: different-series actives get an incomparable verdict, and a diverging no-name fetch prints the BEHIND note with an inline activation hint}\ -constraints $C\ -setup { set result "" }\ -body { #different runtime series: revisions exist on both sides but are not comparable reset_stage stage_materialized fakert-beta.exe fakert-beta-r1.exe 1 set_default fakert-alpha-r2.exe set r [run_bashroute list -remote -platform testplat-x86_64] lappend result [dict get $r exit] lappend result [expr {[freshlines [dict get $r output]] eq [list "freshness: active fakert-beta.exe (r1) and the server default fakert-alpha-r2.exe (r2) are different runtime series - no revision comparison"]}] #fetch-mode BEHIND: one line, inline activation hint (the artifact was just fetched) reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r1.exe 1 set_default fakert-alpha-r2.exe set r [run_bashroute fetch -platform testplat-x86_64] lappend result [dict get $r exit] lappend result [expr {[freshlines [dict get $r output]] eq [list "freshness: active fakert-alpha.exe (r1) is BEHIND the server default fakert-alpha-r2.exe (r2) - activate it with: use fakert-alpha-r2.exe"]}] }\ -cleanup { }\ -result [list 0 1 0 1] #added 2026-07-25 (agent, G-119) - follow-on refinement (user-directed shape): #identity middle column (from=/sha1=) so the salient starred row changes with #every 'use' switch and the Remote column carries a real server relationship test runtimecmd_freshness_identity_columns {rows the server listing does not name carry identity: from= on a materialized working copy (starred) and sha1= on a byte-copy stray, both with a real Remote status - the dead-end '(not listed on server)' no longer appears for identified rows - both payloads}\ -constraints $C\ -setup { set result "" variable artifacts reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r2.exe 2 stage_file oldkit.exe [dict get $artifacts fakert-alpha-r2.exe] set_default fakert-alpha-r1.exe }\ -body { foreach r [list [run_cmdroute list -remote -platform testplat-x86_64] \ [run_bashroute list -remote -platform testplat-x86_64]] { set out [dict get $r output] lappend result [dict get $r exit] lappend result [regexp {\* fakert-alpha\.exe\s+from=fakert-alpha-r2\.exe\s+Same version} $out] lappend result [regexp {oldkit\.exe\s+sha1=fakert-alpha-r2\.exe\s+Same version} $out] lappend result [string match "*(not listed on server)*" $out] } set result }\ -cleanup { }\ -result [list 0 1 1 0 0 1 1 0] test runtimecmd_freshness_identity_default_annotation {the (server default) row annotation follows artifact identity: a working copy materialized from the default artifact carries it on the starred row - both payloads}\ -constraints $C\ -setup { set result "" reset_stage stage_materialized fakert-alpha.exe fakert-alpha-r1.exe 1 set_default fakert-alpha-r1.exe }\ -body { foreach r [list [run_cmdroute list -remote -platform testplat-x86_64] \ [run_bashroute list -remote -platform testplat-x86_64]] { lappend result [dict get $r exit] lappend result [regexp {\* fakert-alpha\.exe\s+from=fakert-alpha-r1\.exe\s+Same version\s+\(server default\)} [dict get $r output]] } set result }\ -cleanup { }\ -result [list 0 1 0 1] test runtimecmd_freshness_identity_anomaly_statuses {identified working copies surface anomalies: content differing from the immutable source artifact reports CONTENT DIFFERS, and a source artifact absent from the server listing is stated - both payloads}\ -constraints $C\ -setup { set result "" variable artifacts reset_stage #working copy claiming r1 provenance but carrying r2 bytes (modified local #copy / mismatched sidecar - artifacts are immutable, so never an "update") stage_file fakert-alpha.exe [dict get $artifacts fakert-alpha-r2.exe] stage_file fakert-alpha.toml [artifact_toml fakert-alpha-r1.exe 1] #working copy whose recorded source artifact is not on the server stage_file fakert-beta.exe [dict get $artifacts fakert-beta-r1.exe] stage_file fakert-beta.toml [artifact_toml fakert-beta-r9.exe 9] set_active fakert-alpha.exe set_default fakert-alpha-r1.exe }\ -body { foreach r [list [run_cmdroute list -remote -platform testplat-x86_64] \ [run_bashroute list -remote -platform testplat-x86_64]] { set out [dict get $r output] lappend result [dict get $r exit] lappend result [regexp {fakert-alpha\.exe\s+from=fakert-alpha-r1\.exe\s+CONTENT DIFFERS} $out] lappend result [regexp {fakert-beta\.exe\s+from=fakert-beta-r9\.exe\s+\(artifact not listed on server\)} $out] } set result }\ -cleanup { }\ -result [list 0 1 1 0 1 1] #whole-file teardown: stop the fixture server, restore shielded env state if {$fixtureok} { server_stop unset -nocomplain ::env(PUNKBIN_URL) dict for {v val} $saved_env { set ::env($v) $val } } } tcltest::cleanupTests ;#needed to produce test summary line.