diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c7ee2..f5ae264 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,7 +59,11 @@ which records not just what changed but what each drill run proved. one verb that already worked would be a regression shipped as a refactor. A row marked `confirm` with no words is now a hard internal error rather than a blank question. `--force` and the no-TTY refusal come - free — `confirm()` already had both. Coverage went from two + free — `confirm()` already had both. The one automated caller had to + consent explicitly: `drill/multiuser.sh` drives restore unattended on real + Incus and now passes `--force`, which is the rehearsal proving the gate + rather than working around it — the CI run of this very PR failed there + first, which is the shape a gate is supposed to have. Coverage went from two argument-validation checks that never reached dispatch to the destructive path itself, driven against a fake incus: refusing leaves the call log empty, `--force` produces exactly one `incus snapshot restore`. Not diff --git a/README.md b/README.md index 3e09390..71e535b 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,10 @@ box new --name feature --from work/authed # clone the authed state into a new `--from` copies the whole box (agent login, git creds, clones and all) while preserving isolation. You can also `box new --name x --from work` to clone -a box's live state, or roll a box back with `box restore work authed`. +a box's live state, or roll a box back with `box restore work authed` — which +asks first, since a rollback discards everything since the snapshot (`--force` +skips the prompt, and scripts must pass it: with no terminal to ask on, box +refuses rather than assuming yes). Forgotten what you called a checkpoint? `box info work` prints the box's snapshot labels and the `--from` line to clone one. @@ -388,7 +391,8 @@ box shell # enter as the template's user box exec -- # run a command in the box box tmux [session] # attach/create a tmux session — survives disconnects box snapshot [label] # checkpoint (label defaults to manual-) -box restore # roll back to a snapshot +box restore [--force] + # roll back to a snapshot — destructive, asks first box export [] [--instance-only] # one portable file (snapshots incl.) — survives rm & host box import [--name ] diff --git a/drill/multiuser.sh b/drill/multiuser.sh index 3f2dd7c..fc2a7b0 100644 --- a/drill/multiuser.sh +++ b/drill/multiuser.sh @@ -206,7 +206,13 @@ as_u "$U1" box info mine 2>/dev/null | grep -qF "$(boxnet_pfx)" \ && ok "(g) box info shows a boxnet ($(boxnet_pfx)x) address — placed on the hardened network" \ || no "(g) mine has no boxnet address in box info" as_u "$U1" box snapshot mine s1 >/dev/null 2>&1 && ok "(b) box snapshot mine s1" || no "(b) snapshot refused" -as_u "$U1" box restore mine s1 >/dev/null 2>&1 && ok "(b) box restore mine s1 (the incus 6 'snapshot restore' spelling)" || no "(b) restore failed" +# --force, and it is the assertion as much as the fix: restore is destructive +# and now asks (#105), so an unattended drill MUST consent explicitly. Without +# the flag this line fails — correctly — because confirm() refuses rather than +# assuming yes when there is no terminal to ask on. Adding --force here is not +# working around the gate; it is the drill proving the gate is real, and that +# the documented non-interactive path through it works on real Incus. +as_u "$U1" box restore mine s1 --force >/dev/null 2>&1 && ok "(b) box restore mine s1 --force (the incus 6 'snapshot restore' spelling, through the #105 gate)" || no "(b) restore failed" as_u "$U1" box new --name c1 --from mine/s1 >/dev/null 2>&1 && ok "(b) box new --from mine/s1 — the clone workflow" || no "(b) clone failed" # c1 stays alive through phase g: it is the distinctly-NAMED sibling the # enumeration probe needs (both users' primaries are 'mine' by design of d). diff --git a/test/cli.sh b/test/cli.sh index 1f644e4..5e21255 100644 --- a/test/cli.sh +++ b/test/cli.sh @@ -877,6 +877,11 @@ check "table: every 'confirm' row supplies a prompt (#105)" 0 "" \ # shellcheck disable=SC2016 # the $-strings are literals in the target file check "dispatch: the confirm prompt comes from the row, not a constant (#105)" 0 "" \ grep -qF 'confirm "$(fill "$cnf" "$inst")"' "$ROOT/bin/box" +# The rehearsal drives restore unattended on real Incus, so it must consent +# EXPLICITLY — the gate is only real if the one automated caller had to change. +# Pinned here because the rehearsal itself needs a daemon and this suite has none. +check "rehearsal: the unattended restore passes --force (#105)" 0 "" \ + grep -qF 'box restore mine s1 --force' "$ROOT/drill/multiuser.sh" rm -rf "$CSHIM" "$CWORK" # ---------------------------------------------------------------------------