Browse Source

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
master
Julian Noble 2 weeks ago
parent
commit
330ae00de3
  1. 4
      CHANGELOG.md
  2. 6
      GOALS.md
  3. 75
      goals/G-022-fossil-rename-punkshell.md
  4. 2
      punkproject.toml
  5. 1
      src/modules/shellrun-999999.0a1.0.tm
  6. 3
      src/modules/shellrun-buildversion.txt
  7. 2
      src/tests/modules/AGENTS.md
  8. 251
      src/tests/modules/punk/mix/testsuites/repo/fossilmove.test

4
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).

6
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.

75
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).

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.2.3"
version = "0.2.4"

1
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.

3
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

2
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)

251
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
Loading…
Cancel
Save