Browse Source
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.commaster
8 changed files with 341 additions and 3 deletions
@ -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). |
||||||
@ -1,3 +1,3 @@ |
|||||||
[project] |
[project] |
||||||
name = "punkshell" |
name = "punkshell" |
||||||
version = "0.2.3" |
version = "0.2.4" |
||||||
|
|||||||
@ -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…
Reference in new issue