package require tcltest #Piped characterization of the make.tcl project-version bake gate (added 2026-08-06). #The project version is a bake input: punkkit-stamp.toml project_version and the #versioned-scheme output names are read from punkproject.toml at bake time, so the #kit-assembling subcommands refuse to proceed with a non-interactive stdin when a #bump looks pending - src/ commits since punkproject.toml last changed, none staged - #unless -expect-projectversion asserts the intended version, and always refuse a #mismatched assertion. -confirm 0 deliberately does not waive the gate. #Runs make.tcl under the built punk executable's 'script' subcommand with output #captured through a pipe and stdin half-closed - a non-interactive stdin by #construction, exactly the population the gate exists to stop. # - the 'check' and 'projectversion' pins run against this WORKING TREE # - the gate VERDICT pins run against throwaway fixture projects under tcltest # temporaryDirectory, each carrying a copy of the working-tree make.tcl and of # src/bootsupport so punkboot::utils resolves. The git fixture is built at # version 0.1.0 with one src/ commit after the last punkproject.toml commit - # the pending-bump state; the no-git fixture exercises the documented NOTE # degradation. Downstream-of-gate outcomes in fixtures - no runtimes to bake, # no VCS for the bake workdir - are deliberately not pinned: the pins assert # only the gate's own contract. #Target executable resolved from env(PUNK_SHELL_TEST_EXE), else #/bin/punk902z.exe then /bin/punkshell902. Skipped #(constraint punkexeavailable) if none found; fixture tests additionally need git #on PATH (constraint gitavailable). 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 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]}] testConstraint gitavailable [expr {[llength [auto_execok git]] > 0}] 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 with output captured through a #pipe (stdin half-closed for immediate EOF - the gate must see a non-interactive #stdin). scriptpath defaults to the working tree's src/make.tcl; the fixture tests #pass their fixture's copy. Returns dict: timedout 0|1, exitcode , #output . proc maketcl_run {cmdargs {scriptpath ""}} { variable runstate variable maketcl_run_timeout_ms variable punkexe variable maketcl if {$scriptpath eq ""} { set scriptpath $maketcl } array unset runstate set runstate(output) "" set runstate(done) "" set chan [open |[list $punkexe script $scriptpath {*}$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 fixture_git {base args} { exec git -C $base -c user.name=fixture -c user.email=fixture@test -c commit.gpgsign=false -c init.defaultBranch=main {*}$args 2>@1 } #Minimal punk-project-shaped tree carrying a copy of the working-tree make.tcl and #of src/bootsupport. withgit builds the pending-bump state: version 0.1.0 #committed, then one src/ commit after it. proc fixture_new {dirtail withgit} { variable projectroot set base [file normalize [makeDirectory $dirtail]] file mkdir [file join $base src modules] [file join $base src lib] [file join $base lib] [file join $base modules] file copy [file join $projectroot src make.tcl] [file join $base src make.tcl] file copy [file join $projectroot src bootsupport] [file join $base src bootsupport] set fd [open [file join $base punkproject.toml] w] puts $fd "\[project\]" puts $fd "name = \"fixtureproj\"" puts $fd "version = \"0.1.0\"" close $fd set fd [open [file join $base CHANGELOG.md] w] puts $fd "# Changelog" puts $fd "" puts $fd "## \[0.1.0\] - 2026-08-06" puts $fd "- initial" close $fd if {$withgit} { fixture_git $base init -q -b main fixture_git $base add -A fixture_git $base commit -q -m "initial 0.1.0" set fd [open [file join $base src change.tcl] w] puts $fd "puts change" close $fd fixture_git $base add -A fixture_git $base commit -q -m "src change" } return $base } variable fixgit "" if {[testConstraint punkexeavailable] && [testConstraint gitavailable]} { set fixgit [fixture_new maketclpvgate_git 1] } variable fixnogit "" if {[testConstraint punkexeavailable]} { set fixnogit [fixture_new maketclpvgate_nogit 0] } proc has {out needle} { expr {[string first $needle $out] >= 0} } #added 2026-08-06 (agent, project-version gate) test maketcl_check_pvgate_status {make.tcl check reports the project-version bake gate with one of the two documented statuses, ESC-free}\ -constraints {punkexeavailable} -body { set r [maketcl_run {check}] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] lappend result esc [regexp -all {\x1b} $out] lappend result line [regexp -line {^project-version bake gate: (ACTIVE|UNAVAILABLE)} $out] set result }\ -result {timedout 0 exitcode 0 esc 0 line 1} #added 2026-08-06 (agent, project-version gate) test maketcl_check_pvgate_active_contract {with a current bootsupport snapshot the gate is ACTIVE, states the bake-input relationship, the no-waiver rule and a current state}\ -constraints {punkexeavailable} -body { set r [maketcl_run {check}] set out [dict get $r output] set result [list] lappend result active [regexp -line {^project-version bake gate: ACTIVE\s*$} $out] lappend result bakeinput [has $out "BAKE INPUT"] lappend result nowaiver [has $out "-confirm 0 does not waive the gate"] lappend result state [regexp -line {^ current state: } $out] set result }\ -result {active 1 bakeinput 1 nowaiver 1 state 1} #added 2026-08-06 (agent, project-version gate) test maketcl_projectversion_advisory_read_only {make.tcl projectversion stays a read-only exit-0 advisory, ESC-free}\ -constraints {punkexeavailable} -body { set r [maketcl_run {projectversion}] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] lappend result esc [regexp -all {\x1b} $out] lappend result header [has $out "project version check"] lappend result ppline [has $out "punkproject.toml version :"] set result }\ -result {timedout 0 exitcode 0 esc 0 header 1 ppline 1} #added 2026-08-06 (agent, project-version gate) test maketcl_pvgate_pending_flagless_aborts {pending-bump fixture: a flagless non-interactive bake aborts fast, naming the consequence and the assertion escape hatch}\ -constraints {punkexeavailable gitavailable} -body { variable fixgit set r [maketcl_run {bake -confirm 0} [file join $fixgit src make.tcl]] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] lappend result pending [has $out "BAKE-WARNING: project-version bump may be PENDING"] lappend result consequence [has $out "BAKE INPUT"] lappend result hint [has $out "-expect-projectversion 0.1.0"] lappend result aborted [has $out "-aborted- (project-version bump appears pending"] set result }\ -result {timedout 0 exitcode 1 pending 1 consequence 1 hint 1 aborted 1} #added 2026-08-06 (agent, project-version gate) test maketcl_pvgate_assert_match_proceeds {pending-bump fixture: a matching -expect-projectversion satisfies the gate and the bake proceeds past it}\ -constraints {punkexeavailable gitavailable} -body { variable fixgit set r [maketcl_run {bake -confirm 0 -expect-projectversion 0.1.0} [file join $fixgit src make.tcl]] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] lappend result gateok [has $out "project-version gate: OK (asserted -expect-projectversion 0.1.0"] lappend result note [has $out "proceeding on the assertion"] lappend result gateabort [has $out "-aborted- (project-version"] set result }\ -result {timedout 0 gateok 1 note 1 gateabort 0} #added 2026-08-06 (agent, project-version gate) test maketcl_pvgate_assert_mismatch_aborts {a mismatched -expect-projectversion aborts before any kit work}\ -constraints {punkexeavailable gitavailable} -body { variable fixgit set r [maketcl_run {bake -confirm 0 -expect-projectversion 9.9.9} [file join $fixgit src make.tcl]] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] exitcode [dict get $r exitcode] lappend result mismatch [has $out "-expect-projectversion MISMATCH: asserted 9.9.9 but punkproject.toml says 0.1.0"] lappend result aborted [has $out "-aborted- (project-version assertion mismatch"] set result }\ -result {timedout 0 exitcode 1 mismatch 1 aborted 1} #added 2026-08-06 (agent, project-version gate) test maketcl_pvgate_bump_staged_proceeds {a staged-but-uncommitted bump passes the gate - bake reads the working-tree value}\ -constraints {punkexeavailable gitavailable} -body { variable fixgit set fd [open [file join $fixgit punkproject.toml] w] puts $fd "\[project\]" puts $fd "name = \"fixtureproj\"" puts $fd "version = \"0.2.0\"" close $fd set r [maketcl_run {bake -confirm 0} [file join $fixgit src make.tcl]] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] lappend result staged [has $out "bumped 0.1.0 -> 0.2.0, commit pending"] lappend result gateabort [has $out "-aborted- (project-version"] set result }\ -result {timedout 0 staged 1 gateabort 0} #added 2026-08-06 (agent, project-version gate) test maketcl_pvgate_nogit_note_degradation {no-VCS fixture: the gate degrades to the documented NOTE and does not block}\ -constraints {punkexeavailable} -body { variable fixnogit set r [maketcl_run {bake -confirm 0} [file join $fixnogit src make.tcl]] set out [dict get $r output] set result [list] lappend result timedout [dict get $r timedout] lappend result note [has $out "NOTE: project-version gate skipped"] lappend result gateabort [has $out "-aborted- (project-version"] set result }\ -result {timedout 0 note 1 gateabort 0} #fixture dirs are makeDirectory-registered; pre-delete defensively with catch #(git object files are read-only on windows) so cleanupTests has nothing left #to trip on variable fixgit variable fixnogit if {$fixgit ne ""} {catch {file delete -force $fixgit}} if {$fixnogit ne ""} {catch {file delete -force $fixnogit}} cleanupTests } namespace delete ::testspace