From 330ae00de3926d2e11ec01bd4e479bdce036eb16 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Mon, 6 Jul 2026 17:00:27 +1000 Subject: [PATCH] G-022 fossil rename goal + move/rename characterization tests; fix shellrun Goal G-022: scriptable safe fossil move/rename in dev repo (all checkouts repointed, no phantom central config-db entries, no dangling old repo db, project-name renamable with project-code preserved), then rename this project fossil repo shellspy -> punkshell through that mechanism via a G-015 piped script call. New tests src/tests/modules/punk/mix/testsuites/repo/fossilmove.test pin current behaviour in a FOSSIL_HOME-isolated sandbox with an isolation guard: the file-copy + fossil test-move-repository sequence repoints the checkout but leaves the old db file on disk, a phantom repo: entry in the central config-db, and a stale ckout: back-reference; file moves never touch project-name. GAP-marked tests flip when G-022 lands. shellrun 0.1.4: add missing package require punk::args - the 0.1.3 migration left run/runout/runerr/runx calling punk::args::parse without requiring it, which broke shellrun in bare interps and made every src/tests/runtests.tcl run fail at test-file invocation. Full suite now back to baseline (523 tests: 508 pass, 14 skip, exec-14.3 known failure). Project version 0.2.4 (changelog entry for the shellrun fix). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 4 + GOALS.md | 6 + goals/G-022-fossil-rename-punkshell.md | 75 ++++++ punkproject.toml | 2 +- src/modules/shellrun-999999.0a1.0.tm | 1 + src/modules/shellrun-buildversion.txt | 3 +- src/tests/modules/AGENTS.md | 2 +- .../punk/mix/testsuites/repo/fossilmove.test | 251 ++++++++++++++++++ 8 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 goals/G-022-fossil-rename-punkshell.md create mode 100644 src/tests/modules/punk/mix/testsuites/repo/fossilmove.test diff --git a/CHANGELOG.md b/CHANGELOG.md index fe34220d..f439f4a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ 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.2.4] - 2026-07-06 + +- shellrun 0.1.4: added missing `package require punk::args` (the 0.1.3 migration left `runx`/`run`/`runout`/`runerr` calling `punk::args::parse` without requiring it, breaking shellrun in bare interps — including the `src/tests/runtests.tcl` testinterp, which made every source-tree test run fail). + ## [0.2.3] - 2026-07-06 - shellrun: removed `-tcl` from `sh_run`, `sh_runout`, `sh_runerr`, `sh_runx` (was silently swallowed with no useful effect because `sh -c` is always an external shell; now rejected with a usage message). diff --git a/GOALS.md b/GOALS.md index bb233838..aa45803d 100644 --- a/GOALS.md +++ b/GOALS.md @@ -174,3 +174,9 @@ Scope: src/modules/punk/ (G-020 module's agent-facing surface), AGENTS.md guidan Detail: goals/G-021-agent-visual-verification.md Goal: a tool-calling agent can, during a session, use piped script calls (G-015) to a punk executable to locate the applicable UI window, snapshot it to a PNG file and/or base64 output suitable for AI image analysis, and drive mouse/keyboard interactions - enabling tests whose verification is visual-only and/or input-driven. Acceptance: on Windows, single piped script calls (no interactive session) can: list/match windows for a pattern with machine-parseable output; save a located window's snapshot to a caller-specified path and optionally emit it base64 on stdout; run a scripted interaction sequence (focus, click at offset, type text, snapshot) end-to-end; failures exit nonzero with the error on stderr per G-015 semantics; the invocation patterns are documented for agents alongside the G-017 guidance; at least one real visual-or-input-driven verification (e.g. a Tk app smoke test) is exercised through this path. + +### G-022 [proposed] Scriptable safe fossil move/rename in `dev repo`; rename this project's fossil repo to punkshell +Scope: src/modules/punk/mix/commandset/repo-999999.0a1.0.tm, src/modules/punk/repo-999999.0a1.0.tm, src/tests/modules/punk/mix/testsuites/repo/ +Detail: goals/G-022-fossil-rename-punkshell.md +Goal: the `dev repo` commandset can move and rename fossil repositories non-interactively and safely - all checkouts repointed, no phantom central config-db entries, no dangling old repo db, fossil project-name renamable with project-code unchanged - and this project's fossil repo (currently project-name 'shellspy') is renamed to 'punkshell' through that mechanism via a G-015 piped script call, not by hand. +Acceptance: the commandset provides a flag-driven (no stdin prompts) move/rename operation which on a scratch repo with an open checkout: repoints every registered checkout, leaves the central config-db listing only the new path, removes or archives the old repo db file (per option), clears stale ckout: back-references, and applies a requested project-name change while preserving project-code; the GAP characterization tests in src/tests/modules/punk/mix/testsuites/repo/fossilmove.test are updated to assert the clean behaviour and pass; after G-015 is achieved, this repo's fossil db (shellspy.fossil / project-name shellspy) is renamed to punkshell via the new operation invoked through a piped `script` call, with `fossil info` in this checkout showing the new repository path and project-name and `fossil all ls` free of the old path. diff --git a/goals/G-022-fossil-rename-punkshell.md b/goals/G-022-fossil-rename-punkshell.md new file mode 100644 index 00000000..6afe040e --- /dev/null +++ b/goals/G-022-fossil-rename-punkshell.md @@ -0,0 +1,75 @@ +# G-022 Scriptable safe fossil move/rename in `dev repo`; rename this project's fossil repo to punkshell + +Status: proposed +Scope: src/modules/punk/mix/commandset/repo-999999.0a1.0.tm, src/modules/punk/repo-999999.0a1.0.tm, src/tests/modules/punk/mix/testsuites/repo/ +Acceptance: as in root GOALS.md index (canonical). + +## Context + +This project's fossil repository is recorded as project-name 'shellspy' +(C:/Users/sleek/.fossils/shellspy.fossil at the time of writing) but the +desired project name is punkshell ('shellspy' names only the experimental +launch subcommand). Rather than having an agent perform ad-hoc repo surgery, +the rename should go through the `dev repo` system - which makes the +prerequisite the reliability of agent-driven `script` calls (G-015). + +Current capability (examined 2026-07-06, characterized by +src/tests/modules/punk/mix/testsuites/repo/fossilmove.test): + +- `punk::mix::commandset::repo::fossil-move-repository` exists but is fully + interactive (punk::repo::askuser prompts throughout) - unusable from a + piped script call. +- Its move primitive is `file copy` + `fossil test-move-repository` per + checkout. Verified in a FOSSIL_HOME-isolated sandbox, this sequence: + - correctly repoints the checkout's repository link (the part that works); + - leaves the old .fossil file on disk (copy, never delete); + - leaves a phantom `repo:` entry for the old path in the central fossil + config-db (`fossil all ls` lists both old and new paths); + - leaves a stale `ckout:` back-reference in the old repo db. +- Nothing renames the fossil project-name: moving/renaming the db file leaves + `project-name` in the repo db config table untouched, and fossil has no + plain CLI setter for it (web UI Admin/Configuration, or a config-table + update e.g. via `fossil sql`). + +Phantom central-db entries matter beyond hygiene: `dev projects.work` +(G-016/G-017's agent-facing project discovery) reads the central config-db, so +phantoms surface directly in agent search results. + +## Approach + +- Extend the commandset with a non-interactive mode (explicit flags for + target path/name, project-name change, old-db disposition) alongside the + existing interactive flow; punk::args-documented per module conventions. +- Safe-move sequence: copy to target, repoint every registered checkout + (`fossil test-move-repository` per checkout, or direct checkout-db vvar + update), remove the old central-db `repo:` entry, clear/transfer stale + `ckout:` records, then remove or archive the old db file - ordered so a + failure mid-way never leaves checkouts pointing at a missing db. +- Project-name rename: update `project-name` in the repo db config table + (project-code must never change - it is the repo's identity); surface both + names in the operation's report. +- The fossilmove.test GAP tests (3.2, 3.3, 3.4, 4.1) pin today's behaviour; + flipping them to assert clean behaviour is part of this goal's verification. +- The final application to this repo runs through a G-015 piped `script` + call, as the proving use of agent-driven `dev repo` operations. + +## Alternatives considered + +- Ad-hoc agent-performed rename (fossil sql + file rename + manual central-db + cleanup by hand) - rejected: the point is that repo surgery goes through + audited `dev repo` tooling agents can reuse, not one-off incantations. +- Renaming only the .fossil file and leaving project-name as shellspy - + rejected: the project-name is the visible identity in `fossil info`, + timelines and project listings; it is the thing actually wrong. + +## Notes + +- Dependent on G-015 (reliable piped script calls) for the application step; + the capability work itself has no dependency and can proceed earlier. +- The working-tree directory name (C:/repo/jn/shellspy) is out of scope - + fossil does not require checkout dirs to match the project name. +- Other checkouts of shellspy.fossil (e.g. a shellspy_copy working tree, if + open) must be enumerated and repointed by the same run. +- Test isolation pattern for anything touching fossil state: set FOSSIL_HOME + to a temp dir and verify `fossil info` reports config-db under it before + mutating (see fossilmove.test fixture guard). diff --git a/punkproject.toml b/punkproject.toml index 9e827eb2..af54e295 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.2.3" +version = "0.2.4" diff --git a/src/modules/shellrun-999999.0a1.0.tm b/src/modules/shellrun-999999.0a1.0.tm index 406fa6c9..6bf54b0e 100644 --- a/src/modules/shellrun-999999.0a1.0.tm +++ b/src/modules/shellrun-999999.0a1.0.tm @@ -5,6 +5,7 @@ package require shellfilter package require punk::ansi +package require punk::args #NOTE: the run,runout,runerr,runx commands only produce an error if the command didn't run. # - If it did run, but there was a non-zero exitcode it is up to the application to check that. diff --git a/src/modules/shellrun-buildversion.txt b/src/modules/shellrun-buildversion.txt index 8adb77cc..1a2866ce 100644 --- a/src/modules/shellrun-buildversion.txt +++ b/src/modules/shellrun-buildversion.txt @@ -1,6 +1,7 @@ -0.1.3 +0.1.4 #First line must be a semantic version number #all other lines are ignored. +#0.1.4 - added missing 'package require punk::args' (0.1.3 call sites use punk::args::parse; absence broke shellrun in bare interps e.g. the runtests testinterp) #0.1.3 - migrated runx from get_run_opts to punk::args::parse (added PUNKARGS spec with @cmd + -help) #0.1.3 - migrated sh_run/sh_runout/sh_runerr/sh_runx from get_run_opts to punk::args::parse (added PUNKARGS specs) #0.1.3 - removed get_run_opts proc; all callers now use punk::args diff --git a/src/tests/modules/AGENTS.md b/src/tests/modules/AGENTS.md index ee96da2c..a21d27f7 100644 --- a/src/tests/modules/AGENTS.md +++ b/src/tests/modules/AGENTS.md @@ -38,5 +38,5 @@ Unit tests for editable source modules under `src/modules/`, `src/modules_tcl8/` ## Child DOX Index - `punkcheck/` — punkcheck module tests (install, summarize_install_resultdict, installtrack) -- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources) +- `punk/mix/` — punk::mix::cli tests (prune helpers, punkcheck virtual sources) and punk::mix::commandset::repo fossil move/rename characterization tests (`testsuites/repo/`, FOSSIL_HOME-isolated; GAP-marked tests pin behaviour G-022 will change) diff --git a/src/tests/modules/punk/mix/testsuites/repo/fossilmove.test b/src/tests/modules/punk/mix/testsuites/repo/fossilmove.test new file mode 100644 index 00000000..d7767ee3 --- /dev/null +++ b/src/tests/modules/punk/mix/testsuites/repo/fossilmove.test @@ -0,0 +1,251 @@ +# -*- tcl -*- +# Tests for the fossil repository move/rename area used by +# punk::mix::commandset::repo::fossil-move-repository: +# - punk::repo fossil discovery primitives against a scratch repo+checkout +# (fossil_get_configdb, find_fossil, fossil_get_repository_file) +# - the move primitive sequence the commandset uses (file copy + fossil +# test-move-repository) and what it leaves behind +# The 3.x/4.x tests are CHARACTERIZATION tests: they pin the current behaviour, +# including the gaps a future scriptable safe-move/rename must close (see root +# GOALS.md G-022): the old repo db file is left on disk, the central config-db +# keeps a phantom repo: entry for the old path, the old repo db keeps a stale +# ckout: back-reference, and nothing renames the fossil project-name. +# When that capability lands, the gap tests here are expected to be updated to +# assert the new (clean) behaviour. +# +# Isolation: all fossil activity runs with FOSSIL_HOME pointing into a temp +# folder, so the user's real central fossil config-db is never touched. Every +# mutating step is gated on verifying fossil actually reports its config-db +# under that temp folder. +# +# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punk/mix/** fossilmove.test + +package require tcltest +package require punk::lib +package require punk::repo + +namespace eval ::testspace { + namespace import ::tcltest::* + + # ------------------------------------------------------------------------- + # Constraints and isolation guard + # ------------------------------------------------------------------------- + set fossilcmd [auto_execok fossil] + tcltest::testConstraint fossil_available [expr {[llength $fossilcmd] > 0}] + + variable env_saved_exists [info exists ::env(FOSSIL_HOME)] + variable env_saved_value [expr {$env_saved_exists ? $::env(FOSSIL_HOME) : ""}] + + variable tempbase "" + variable fosshome "" + variable repodir "" + variable codir "" + + proc path_eq {a b} { + string equal -nocase [file normalize $a] [file normalize $b] + } + + proc run_fossil {workdir args} { + variable fossilcmd + set original_cwd [pwd] + try { + if {$workdir ne ""} { + cd $workdir + } + exec -ignorestderr {*}$fossilcmd {*}$args + } finally { + cd $original_cwd + } + } + + # central config-db repo entries as reported by fossil itself + proc central_repo_list {} { + variable codir + set lines [run_fossil $codir all ls] + set paths [list] + foreach ln [split [string map {\r\n \n} $lines] \n] { + set ln [string trim $ln] + if {$ln ne ""} { lappend paths $ln } + } + return $paths + } + + proc central_has_repo {repopath} { + foreach p [central_repo_list] { + if {[path_eq $p $repopath]} { return 1 } + } + return 0 + } + + # ckout: checkout-record paths registered in a repository db + proc repo_ckout_dirs {repodb} { + set lines [run_fossil "" sql -R $repodb \ + "select name from config where name like 'ckout:%'"] + set dirs [list] + foreach ln [split [string map {\r\n \n} $lines] \n] { + set ln [string trim $ln] + if {[string match ckout:* $ln]} { + lappend dirs [string trimright [string range $ln 6 end] /] + } + } + return $dirs + } + + proc repo_project_name {repodb} { + string trim [run_fossil "" sql -R $repodb \ + "select value from config where name='project-name'"] + } + + # ------------------------------------------------------------------------- + # Fixture: isolated FOSSIL_HOME, scratch repo db + one open checkout. + # Any failure (including an isolation check failure) disables the + # fossil_fixture constraint so no test proceeds against the wrong config-db. + # ------------------------------------------------------------------------- + variable fixture_error "" + if {[tcltest::testConstraint fossil_available]} { + try { + set tempbase [punk::lib::tempdir_newfolder -prefix pkfossilmovetest] + set fosshome [file join $tempbase fosshome] + set repodir [file join $tempbase repos] + set codir [file join $tempbase co] + file mkdir $fosshome $repodir $codir + set ::env(FOSSIL_HOME) $fosshome + + run_fossil "" init [file join $repodir repoA.fossil] -project-name testmoveproj + run_fossil $codir open [file join $repodir repoA.fossil] + + # isolation guard: fossil must report its config-db under our + # temp FOSSIL_HOME or nothing else may run + set infolines [run_fossil $codir info] + set configdb "" + foreach ln [split [string map {\r\n \n} $infolines] \n] { + if {[string match "config-db:*" [string trim $ln]]} { + set configdb [string trim [string range [string trim $ln] 10 end]] + } + } + if {$configdb eq "" || ![string match -nocase "[file normalize $fosshome]*" [file normalize $configdb]]} { + error "isolation check failed: fossil config-db '$configdb' is not under temp FOSSIL_HOME '$fosshome'" + } + } on error {result} { + set fixture_error $result + } + } + tcltest::testConstraint fossil_fixture [expr { + [tcltest::testConstraint fossil_available] && $fixture_error eq "" + }] + if {$fixture_error ne ""} { + puts stderr "fossilmove.test fixture setup failed (tests will be skipped): $fixture_error" + } + + # ------------------------------------------------------------------------- + # 1.x punk::repo discovery primitives against the scratch checkout + # ------------------------------------------------------------------------- + test fossilmove-1.1 {fossil_get_configdb resolves to the FOSSIL_HOME-isolated config-db} -constraints fossil_fixture -body { + variable fosshome + set configdb [punk::repo::fossil_get_configdb $::testspace::codir] + string match -nocase "[file normalize $fosshome]*" [file normalize $configdb] + } -result 1 + + test fossilmove-1.2 {find_fossil locates the scratch checkout root} -constraints fossil_fixture -body { + variable codir + path_eq [punk::repo::find_fossil $codir] $codir + } -result 1 + + test fossilmove-1.3 {fossil_get_repository_file returns the opened repo db path} -constraints fossil_fixture -body { + variable codir + variable repodir + path_eq [punk::repo::fossil_get_repository_file $codir] [file join $repodir repoA.fossil] + } -result 1 + + # ------------------------------------------------------------------------- + # 2.x central config-db registration baseline + # ------------------------------------------------------------------------- + test fossilmove-2.1 {open registers the repo db in the central config-db} -constraints fossil_fixture -body { + variable repodir + central_has_repo [file join $repodir repoA.fossil] + } -result 1 + + test fossilmove-2.2 {repo db records its checkout as a ckout: entry} -constraints fossil_fixture -body { + variable repodir + variable codir + set dirs [repo_ckout_dirs [file join $repodir repoA.fossil]] + expr {[llength $dirs] == 1 && [path_eq [lindex $dirs 0] $codir]} + } -result 1 + + # ------------------------------------------------------------------------- + # 3.x the move primitive sequence used by fossil-move-repository: + # file copy old->new, then fossil test-move-repository in the checkout. + # 3.1 pins the part that works; 3.2-3.4 pin the current gaps. + # ------------------------------------------------------------------------- + variable move_error "" + if {[tcltest::testConstraint fossil_fixture]} { + try { + file copy [file join $repodir repoA.fossil] [file join $repodir repoB.fossil] + run_fossil $codir test-move-repository [file join $repodir repoB.fossil] + } on error {result} { + set move_error $result + } + } + tcltest::testConstraint fossil_moved [expr { + [tcltest::testConstraint fossil_fixture] && $move_error eq "" + }] + if {$move_error ne ""} { + puts stderr "fossilmove.test move sequence failed (3.x/4.x will be skipped): $move_error" + } + + test fossilmove-3.1 {test-move-repository repoints the checkout to the new repo db} -constraints fossil_moved -body { + variable codir + variable repodir + path_eq [punk::repo::fossil_get_repository_file $codir] [file join $repodir repoB.fossil] + } -result 1 + + test fossilmove-3.2 {GAP: move-by-copy leaves the old repo db file on disk} -constraints fossil_moved -body { + # current commandset behaviour is file copy + repoint - the old db file + # remains; a safe scriptable move should remove/archive it once no + # checkout references it (G-022) + variable repodir + file exists [file join $repodir repoA.fossil] + } -result 1 + + test fossilmove-3.3 {GAP: central config-db keeps a phantom repo: entry for the old path} -constraints fossil_moved -body { + # both old and new paths are listed after the move - the old entry is + # the phantom a safe move must remove (G-022) + variable repodir + list [central_has_repo [file join $repodir repoA.fossil]] \ + [central_has_repo [file join $repodir repoB.fossil]] + } -result {1 1} + + test fossilmove-3.4 {GAP: old repo db retains a stale ckout: back-reference} -constraints fossil_moved -body { + # the checkout no longer uses repoA but repoA still claims it (G-022) + variable repodir + variable codir + set dirs [repo_ckout_dirs [file join $repodir repoA.fossil]] + expr {[llength $dirs] == 1 && [path_eq [lindex $dirs 0] $codir]} + } -result 1 + + # ------------------------------------------------------------------------- + # 4.x project-name: moving/renaming the db file does not rename the project + # ------------------------------------------------------------------------- + test fossilmove-4.1 {GAP: repo db file move/rename leaves fossil project-name unchanged} -constraints fossil_moved -body { + # no current commandset capability renames the project-name recorded in + # the repo db config table (G-022 requires it for shellspy->punkshell) + variable repodir + repo_project_name [file join $repodir repoB.fossil] + } -result testmoveproj + + # ------------------------------------------------------------------------- + # Cleanup: restore FOSSIL_HOME and remove the temp tree + # ------------------------------------------------------------------------- + variable env_saved_exists + if {$env_saved_exists} { + set ::env(FOSSIL_HOME) $env_saved_value + } else { + unset -nocomplain ::env(FOSSIL_HOME) + } + if {$tempbase ne "" && [file isdirectory $tempbase]} { + catch {file delete -force $tempbase} + } +} + +tcltest::cleanupTests +namespace delete ::testspace