rig 3.20.0
Cut redundant tests and fix two that tested nothing (#174)
An audit of the suite: which tests repeat another's case, and which don't test what they claim.
Two broken tests, fixed
pr.test.mjs— "the Direction … is lifted verbatim" wrote into a context doc and read the same file back.rig prnever ran on a written Direction. It now opens a PR on a second work and asserts the body. Checked by breakingprBody: the test fails.installation-update.test.mjs— the "no git on PATH" tests built PATH from'C:\Windows\System32'in a plain JS string, which reads asC:WindowsSystem32. The environment is now one helper,onPath, with the path escaped and Windows-only. The duplicate doctor run is folded into the node-only doctor test next to it.
Cut: ~20 tests that repeat another test's input and assertions
| Removed | Still covered by |
|---|---|
package — real npm install -g (51s locally) |
install — install.sh / install.ps1 do the same link + rig help |
release — verdict / notes CLI spawns (3) |
release-e2e runs the same commands |
release-e2e — "naming no bump" |
release unit test of the same case |
stage-cut — re-declare refused |
stages-e2e "and a stage is not declared twice" |
next — measured zero; draft entry alone |
same input, superset assertions, in the same file |
dash — window bounds merged |
--since test in the same file |
jira — createIssue markdown |
the two full-argv createIssue tests (rationale comment moved) |
doctor — unclosed work "whichever root" |
"a work folder that is missing is said once"; the multi-root case is in dataroots |
version — two dataMajor({}) tests; "stamp is applyMigrations" |
merged into one; migration 2's test stamps 1.4.0 → 2.0.0 |
attach — mirror-ref test |
folded into the doctor-behind test as a precondition |
smoke — dash with no payload |
the acceptance test runs the same dash (filename assertion moved) |
helpers — statusLine |
phase (plus one assertion for the no-repo-facts call path) |
installation-freshness — non-number interval (20s poll) |
roots unit test of the same normalisation |
Also:
skills.test.mjsno longer pins the exact list of skill folders, which broke on every new skill. It requires therigentry point, and checks that every skill rig names inbin/and in the entry point is a folder it ships.- Removes unused imports and fixture exports, and the
statusLinere-export frombin/rig.mjs.
No removed title is cited in DESIGN.md's decision log.
Result
18 fewer tests than main, and no change in run time. Measured back to back on one machine: main 53.2s / 56.0s, this branch 53.7s / 61.5s. CI: 58s on the last main push, 52s here. The difference between two runs of the same code is larger than the difference between the two.
node --test runs files in parallel, so wall time is the slowest file, not the sum. The 51s npm install -g test ran beside install.test.mjs and the scenarios, which take about as long. Cutting it saves CPU time without ending the run sooner. An earlier "144s → 114s" in this description was measured on a loaded machine and was wrong.
The case for this PR is correctness and upkeep, not speed.
Two tests that claimed more than they asserted
demo.test.mjs— "a merged PR is reported with the stretches it spent" only checked that the section heading rendered. It now asserts each stretch, including the two with no review to measure from. Checked by swapping one stretch's endpoints inbin/demo.mjs: the test fails.stages-e2e.test.mjs— stacked-stage test claimed "the chain orders them, not the array", but its stages are declared in chain order, so it could not tell. The claim is dropped; the out-of-order test after it is the one that proves it.
Looked at and kept
scenarios.test.mjs: the scenarios look like repeats ofdataroots.test.mjs, but they aren't.datarootssaves and restores the machine file between steps or fabricates the legacy state. The scenarios reach those states the way a machine does: an unnamed firstinit, then a rename, then a split. That sequence is the reason the file exists.stages-e2eagainststage-cut: one file cuts its branches by hand with git, the other has rig cut them with--cut. They cover two routes to the same discovery, and later tests build on each.
Not in this PR
- Fixture speed:
stacked()inworktrees-fixture.mjsbuilds a fresh remote per test; copying one prepared tree, as #164 did forcheckouts-*, would speed those files up.
Restore a work's worktrees on a second machine in one command (#176)
Restore a work's worktrees on a second machine in one command
Direction
One command, rig restore [<id>], rebuilds a work's folder from its record and changes nothing
in the record. It is not an attach (issue comment 2, point 3): attach decides a base, stamps
attachedAt and pushes a repo entry; a restore has all of that already and writes none of it.
work.json is byte-identical afterwards and there is no data-root commit. It is in MUTATING
only for prepareDataRoot's fast-forward, so a second machine restores from the newest records.
Per recorded repo whose worktree is missing (present ones are left alone, so it is idempotent):
- Pick the top of the repo's stack. Fetch the mirror, read
chain()over the declared stages,
order withstageOrder, and take the highest branch this repo carries on the remote or in the
mirror; failing any stage, the work branch. This is what puts infra-tim on its stage branch
rather than an unpushed, empty work branch. - Never recreate a branch. A new
trees().checkOutchecks out a branch the remote has, or the
copy the mirror kept, exactly ascut()does — and answers "absent" wherecut()would cut a
new branch from the remote HEAD. An absent repo is skipped and named with its PR state
(prForBranch): "no branch on the remote — PR #n CLOSED" or "never pushed, and no PR". - Name the branches built on top that rig does not know. One
gh pr list --base <top>per
restored repo, followed up the chain: the open PRs stacked on it that are not declared stages
are named, in order, withrig stage <branch>as the way to record them. Reported, not checked
out — which branch of an unrecorded stack is "the" work is the user's call. - Repeat attach's per-repo preparation, extracted out of
attachReposo both share it:
user.emailfromidentityFor,copySecrets, and the catalogue'ssetupprinted (--setup
runs it). Catalogue drafting stays attach-only — a restored repo is already catalogued. - Regenerate the work folder —
AGENTS.md,CLAUDE.md,.rig/id,.rig/data— with
regenerate, which never touches the record.
Around it:
-
rig nextoffersrig restore <id>first when a missing repo could still come back: its
PR is neitherMERGEDnorCLOSED. A repo whose branch went with a closed PR is not offered
forever; restore has already said why it cannot come back. -
rig doctornames the remedy on "work folder missing" and "worktree is gone". -
loadWork, asked for an id the root in hand does not hold, names the root that does
(--data <name>), sorig restore <id>on a machine with several roots says where to look. -
rig-handoffplaces the handoff withrig status --work <id>when the folder is missing,
and its continue prompt putsrig restore <id>before thecd. -
rig attach <repo>on a recorded repo whose worktree is gone restores that one repo through
the same routine instead of saying "nothing to do" (#99).attachedAtis left alone: it records
when the repo joined the work.
Not doing: recording anything, or picking between the branches of a forked stack.
Agreed with Hugo 2026-09-25: name rig restore; unrecorded stack reported by default, --tip to
check it out; #99 folded in.
Context doc: https://github.com/hugoforte/rig-data/blob/main/work/rig-restore/context.md
Handoff by URL (#171)
rig status prints a handoff line when the work has a handoff.md. It gives the file's URL on the data root's remote, or this machine's path, marked as such, when there is no remote. The rig-handoff continue prompt is built from that line and rig restore <id>, with no cd. Nothing in it belongs to the machine that wrote it.
Fixes #112
Fixes #99
Fixes #171
Full changelog: v3.19.0...v3.20.0