4eb720029c
Five fixes against PR #636, all verified by the lead's own review and reproduced here: 1. --set .a.b= (a forgotten value) is now refused outright instead of silently nulling the field — a null numeric config value falls back to its default rather than erroring, which widens capacity silently instead of failing loudly. A deliberate clear gets its own spelling, --set .a.b=null, which writes a literal YAML null via yq, never through strenv(). (criteria 9, 10) 2. Backups move from beside fleetd.yaml to a dedicated fleetd/.config-backups/ directory, gitignored at the repo root (so it also covers scripts/test-config-edit.sh's own throwaway fixtures) and in fleetd/.gitignore, plus a fleetd.yaml.bak.* glob backstop for any stray backup written the old way. A backup of a file that must never be committed inherits that requirement. (criterion 11) 3. The live config's file mode now survives both an edit and a restore. mv from a mktemp candidate used to carry mktemp's 0600 onto the live path forever, and cp onto an existing file keeps the destination's mode, so a restore did not undo it either. (criterion 12) 4. A global CAND + single EXIT/INT/TERM trap prevents an uninstalled .config-edit.XXXXXX candidate from leaking if the script is interrupted mid-run. No acceptance criterion is gated on this — a reproducible leak could not be made to happen on demand — but it is cheap and obviously right. 5. Acceptance criterion 7's redaction check gained a positive control: it now asserts the output actually CONTAINS the redaction marker and the changed key, not only that it lacks the secret. The prior two assertions were negative-only and passed just as happily when the diff was never printed at all — confirmed by reproducing the lead's own mutation (deleting the redacted diff print on the edit path) and watching it survive the old test and get caught by the new one. (criterion 13) All 13 acceptance criteria plus 3 extras pass in scripts/test-config-edit.sh. Criteria 9, 10, 11, 12 and 13 were each proven non-vacuous: criteria 9/10 by mutating the test's own expected value and watching it fail by name, then reverting; criteria 11/12/13 by reverting or mutating the corresponding fix in config-edit.sh and watching the matching criterion fail by name, then restoring the fix and re-confirming a clean pass.
420 lines
18 KiB
Bash
Executable File
420 lines
18 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Self-contained checks for scripts/config-edit.sh — fleetd ticket #635.
|
|
#
|
|
# Drives the REAL config-edit.sh as a subprocess against a FIXTURE config and a FIXTURE log in a
|
|
# throwaway temp directory this file creates and removes. Never touches fleetd/fleetd.yaml or
|
|
# fleetd/fleetd.out, and never starts, stops, or contacts a daemon — there is no daemon here, so
|
|
# each test PLAYS the daemon: it starts config-edit.sh in the background (it is waiting on the
|
|
# log), appends the verdict line it wants, then collects the real exit code.
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
EDIT="$ROOT/scripts/config-edit.sh"
|
|
TMP="$(mktemp -d "$ROOT/.config-edit-test.XXXXXX")"
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$*" >&2
|
|
return 1
|
|
}
|
|
|
|
assert_equals() {
|
|
local expected="$1" actual="$2" description="$3"
|
|
[ "$expected" = "$actual" ] || fail "$description: expected $expected, got $actual"
|
|
}
|
|
|
|
assert_contains() {
|
|
local needle="$1" text="$2" description="$3"
|
|
printf '%s' "$text" | grep -qF -- "$needle" || fail "$description: missing [$needle]"
|
|
}
|
|
|
|
assert_not_contains() {
|
|
local needle="$1" text="$2" description="$3"
|
|
if printf '%s' "$text" | grep -qF -- "$needle"; then
|
|
fail "$description: must NOT contain [$needle], but it does"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# A fresh fixture pair per test: $1/fleetd.yaml (the config) and $1/fleetd.out (the log), plus a
|
|
# small wait-seconds budget so no test takes long. Returns the fixture dir via stdout.
|
|
new_fixture() {
|
|
local dir
|
|
dir="$(mktemp -d "$TMP/fixture.XXXXXX")"
|
|
cat > "$dir/fleetd.yaml" <<'YAML'
|
|
bind:
|
|
host: 127.0.0.1
|
|
port: 19999
|
|
broker:
|
|
uri: amqp://user:hunter2@host/vhost
|
|
profiles:
|
|
sonnet:
|
|
weight: 3
|
|
maxLoad: 5
|
|
YAML
|
|
: > "$dir/fleetd.out"
|
|
printf '%s' "$dir"
|
|
}
|
|
|
|
# Runs config-edit.sh in the background against $dir's fixtures, with the given extra args, and
|
|
# a short --wait-seconds. Sets RUN_PID. Caller appends to $dir/fleetd.out (or not, for the
|
|
# silence test) and then calls collect_run to block for the exit code.
|
|
start_run() {
|
|
local dir="$1" wait_s="$2"; shift 2
|
|
(
|
|
# config-edit.sh deliberately exits 3/4/5 on several of these tests. This subshell inherits
|
|
# the parent's `set -e`, and without disabling it here the FIRST nonzero exit would kill the
|
|
# subshell before the `echo $? > rc` line ever ran — the real code would never reach the file.
|
|
set +e
|
|
"$EDIT" --config "$dir/fleetd.yaml" --log "$dir/fleetd.out" --wait-seconds "$wait_s" "$@" \
|
|
> "$dir/stdout.log" 2>&1
|
|
echo $? > "$dir/rc"
|
|
) &
|
|
RUN_PID=$!
|
|
}
|
|
|
|
collect_run() {
|
|
local dir="$1"
|
|
# wait echoes back the backgrounded subshell's own exit status (here, deliberately 3/4/5 on
|
|
# several tests) — under `set -e` a bare nonzero `wait` would abort this whole test script, so
|
|
# it is neutralized with `|| true`; the real code is read from $dir/rc right after.
|
|
wait "$RUN_PID" || true
|
|
RUN_OUTPUT="$(cat "$dir/stdout.log")"
|
|
RUN_RC="$(cat "$dir/rc")"
|
|
}
|
|
|
|
# -------------------------------------------------------------- acceptance criterion 1: refusal
|
|
test_refusal_restores_byte_for_byte() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
cp "$dir/fleetd.yaml" "$dir/pre-edit.yaml"
|
|
|
|
start_run "$dir" 5 --set '.broker.uri=amqp://changed@host/x'
|
|
sleep 1
|
|
printf 'config reload refused — these keys cannot change under a running daemon: broker. Restart fleetd to apply them.\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 4 "$RUN_RC" "refusal exit code"
|
|
cmp -s "$dir/fleetd.yaml" "$dir/pre-edit.yaml" \
|
|
|| fail "refusal must restore the config byte for byte onto the pre-edit backup"
|
|
}
|
|
|
|
# -------------------------------------------------------------- acceptance criterion 2: clean
|
|
test_clean_reload_keeps_the_edit() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.weight=7'
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 0 "$RUN_RC" "clean reload exit code"
|
|
assert_equals "7" "$(yq eval '.profiles.sonnet.weight' "$dir/fleetd.yaml")" "clean reload live value"
|
|
}
|
|
|
|
# ----------------------------------------------------- acceptance criterion 3: deferred != clean
|
|
test_deferred_reload_is_told_apart_from_clean() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.weight=9'
|
|
sleep 1
|
|
printf 'config reloaded; these changes need a restart to take effect: profiles\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 3 "$RUN_RC" "deferred reload exit code"
|
|
[ "$RUN_RC" != 0 ] || fail "deferred reload must not report exit 0"
|
|
assert_contains "profiles" "$RUN_OUTPUT" "deferred reload names the key"
|
|
assert_contains "restart" "$RUN_OUTPUT" "deferred reload says a restart is needed"
|
|
}
|
|
|
|
# -------------------------------------------------------------- acceptance criterion 4: silence
|
|
test_silence_is_its_own_answer() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
|
|
start_run "$dir" 2 --set '.profiles.sonnet.weight=11'
|
|
# Feed the log nothing.
|
|
collect_run "$dir"
|
|
|
|
assert_equals 5 "$RUN_RC" "silence exit code"
|
|
assert_equals "11" "$(yq eval '.profiles.sonnet.weight' "$dir/fleetd.yaml")" "the edited value must still be on disk"
|
|
assert_contains '--restore' "$RUN_OUTPUT" "silence prints the --restore command"
|
|
|
|
local backup restore_cmd
|
|
backup="$(ls -t "$dir"/.config-backups/fleetd.yaml.bak.* | head -1)"
|
|
[ -n "$backup" ] || fail "silence must still have taken a backup"
|
|
|
|
restore_cmd="$(printf '%s\n' "$RUN_OUTPUT" | grep -F -- '--restore --config' | sed -E 's/^[[:space:]]*//')"
|
|
[ -n "$restore_cmd" ] || fail "could not find the printed --restore invocation in the output"
|
|
# Running this --restore invocation installs the backup, then itself waits for a confirming
|
|
# verdict that this fixture never feeds — so it legitimately exits 5 ("cannot tell") here, same
|
|
# as any edit with no daemon on the other end. Only a usage/internal error (1 or 2) is a real
|
|
# failure of the command itself; the actual assertion is the byte-for-byte cmp below.
|
|
local restore_rc=0
|
|
eval "$restore_cmd" > "$dir/restore.log" 2>&1 || restore_rc=$?
|
|
case "$restore_rc" in
|
|
0|3|4|5) : ;;
|
|
*) fail "the printed --restore command errored out (exit $restore_rc): $(cat "$dir/restore.log")" ;;
|
|
esac
|
|
|
|
cmp -s "$dir/fleetd.yaml" "$backup" \
|
|
|| fail "running the printed --restore command must put the file back to the original backup"
|
|
}
|
|
|
|
# --------------------------------------------------------- acceptance criterion 5: bad candidate
|
|
test_broken_candidate_never_reaches_live_path() {
|
|
local dir rc=0
|
|
dir="$(new_fixture)"
|
|
printf 'foo: [unclosed\n' > "$dir/broken.yaml"
|
|
|
|
"$EDIT" --from "$dir/broken.yaml" --config "$dir/fleetd.yaml" --log "$dir/fleetd.out" --wait-seconds 2 \
|
|
> "$dir/stdout.log" 2>&1 || rc=$?
|
|
|
|
[ "$rc" -ne 0 ] || fail "a broken --from candidate must exit non-zero"
|
|
cmp -s "$dir/fleetd.yaml" <(new_fixture_yaml) \
|
|
|| fail "the broken candidate must never reach the live fixture config"
|
|
}
|
|
new_fixture_yaml() {
|
|
cat <<'YAML'
|
|
bind:
|
|
host: 127.0.0.1
|
|
port: 19999
|
|
broker:
|
|
uri: amqp://user:hunter2@host/vhost
|
|
profiles:
|
|
sonnet:
|
|
weight: 3
|
|
maxLoad: 5
|
|
YAML
|
|
}
|
|
|
|
# -------------------------------------------------------------------- acceptance criterion 6
|
|
test_marker_skips_lines_before_it() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
printf 'config reload refused — something ancient\n' > "$dir/fleetd.out"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.weight=5'
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 0 "$RUN_RC" "a stale refusal before the marker must not be read as this edit's verdict"
|
|
}
|
|
|
|
# ------------------------------------------------------------------- acceptance criterion 7 (+13)
|
|
# fleetd #635 follow-up (ticket comment 17659) — the two assertions below this comment were the
|
|
# WHOLE test before the follow-up, and both are negative-only: they pass just as happily when the
|
|
# diff is never printed at all as when it is printed and correctly redacted. A mutant that deletes
|
|
# `diff -u "$backup" "$cand" | redact` from the edit path survives them, because an absent output
|
|
# contains neither "hunter2" nor "user:" either — see the mutation-and-revert proof in the reply.
|
|
# Criterion 13 is the fix: a LOUD positive control that only passes when a diff was demonstrably
|
|
# printed AND the redaction demonstrably ran on real content, not merely that nothing leaked.
|
|
test_redaction_holds() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.weight=4'
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 0 "$RUN_RC" "redaction-case reload exit code"
|
|
assert_not_contains "hunter2" "$RUN_OUTPUT" "full output must never contain the password"
|
|
assert_not_contains "user:" "$RUN_OUTPUT" "full output must never contain the userinfo"
|
|
# acceptance criterion 13 — positive control: the diff's default 3-line context around the
|
|
# changed "weight" key also covers the fixture's "uri:" line, so a genuinely-printed, genuinely-
|
|
# redacted diff must contain BOTH the redaction marker and the changed key's name. A test that
|
|
# only ever asserts absence cannot tell "redacted" from "never printed" apart; this can.
|
|
assert_contains "<redacted>" "$RUN_OUTPUT" "the redaction must be PROVEN to have run on real content, not merely absent"
|
|
assert_contains "weight" "$RUN_OUTPUT" "a diff must have been demonstrably printed at all"
|
|
}
|
|
|
|
# ------------------------------------------------------- acceptance criterion 9: forgotten value
|
|
# `--set .a.b=` is a plausible typo (the value simply forgotten), and it must be refused outright
|
|
# rather than silently nulling the field — a null numeric field falls back to its default, which
|
|
# widens capacity instead of failing loudly. No background verdict feeder here: a refused --set
|
|
# must never even reach the daemon, so this never starts a background run at all.
|
|
test_forgotten_value_refuses_and_installs_nothing() {
|
|
local dir rc=0
|
|
dir="$(new_fixture)"
|
|
cp "$dir/fleetd.yaml" "$dir/pre-edit.yaml"
|
|
|
|
"$EDIT" --set '.profiles.sonnet.maxLoad=' \
|
|
--config "$dir/fleetd.yaml" --log "$dir/fleetd.out" --wait-seconds 2 \
|
|
> "$dir/stdout.log" 2>&1 || rc=$?
|
|
RUN_OUTPUT="$(cat "$dir/stdout.log")"
|
|
|
|
[ "$rc" -ne 0 ] || fail "an empty --set value must exit non-zero, got 0"
|
|
cmp -s "$dir/fleetd.yaml" "$dir/pre-edit.yaml" \
|
|
|| fail "an empty --set value must install nothing — the live fixture changed"
|
|
assert_contains "EMPTY value" "$RUN_OUTPUT" "the refusal must name the empty value"
|
|
}
|
|
|
|
# ---------------------------------------------------------- acceptance criterion 10: explicit null
|
|
# `--set .a.b=null` is the deliberate-clear spelling, and it must write a REAL yaml null, never
|
|
# the string "''" — those are different values to the daemon's loader (fleetd ticket #635's
|
|
# follow-up comment measured `""` reading back as a null field anyway, which is exactly why the
|
|
# two forms must not collapse onto each other: `--set path=` refuses instead of silently reaching
|
|
# this same null outcome through the back door). Read the RAW line with grep, never only through
|
|
# `yq` — `yq eval` reports `null` for both an actual null and a missing/absent key, so it cannot
|
|
# tell "wrote null" apart from "wrote nothing"; only the literal line on disk can.
|
|
test_explicit_null_writes_bare_null_not_empty_string() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.maxLoad=null'
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 0 "$RUN_RC" "explicit null clear exit code"
|
|
local raw_line
|
|
raw_line="$(grep -E 'maxLoad' "$dir/fleetd.yaml")"
|
|
assert_contains "null" "$raw_line" "the installed line must spell a bare null"
|
|
assert_not_contains '""' "$raw_line" "the installed line must NOT be a quoted empty string"
|
|
}
|
|
|
|
# ------------------------------------------------------- acceptance criterion 11: backup never committable
|
|
# A backup of fleetd.yaml inherits fleetd.yaml's own "never commit this" requirement (fleetd #635
|
|
# follow-up, ticket comment 17655). Proves two things: the backup lands somewhere `git
|
|
# check-ignore` reports as ignored (equivalently, a path `git status --porcelain` never lists as
|
|
# untracked), AND that --restore still finds and uses it from that location.
|
|
test_backup_is_never_committable() {
|
|
local dir backup
|
|
dir="$(new_fixture)"
|
|
cp "$dir/fleetd.yaml" "$dir/pre-edit.yaml"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.weight=55'
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
assert_equals 0 "$RUN_RC" "setup edit exit code"
|
|
|
|
backup="$(ls -t "$dir"/.config-backups/fleetd.yaml.bak.* 2>/dev/null | head -1)"
|
|
[ -n "$backup" ] || fail "no backup found under .config-backups/ — did the location change?"
|
|
|
|
git -C "$ROOT" check-ignore -q -- "$backup" \
|
|
|| fail "the backup at $backup is NOT gitignored — it would survive a git add -A"
|
|
if git -C "$ROOT" status --porcelain -- "$backup" 2>/dev/null | grep -q '^??'; then
|
|
fail "git status still lists the backup as untracked: $backup"
|
|
fi
|
|
|
|
start_run "$dir" 5 --restore
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
assert_equals 0 "$RUN_RC" "--restore after the backup-location change exit code"
|
|
cmp -s "$dir/fleetd.yaml" "$dir/pre-edit.yaml" \
|
|
|| fail "--restore from the new backup location must still put the file back byte for byte"
|
|
}
|
|
|
|
# --------------------------------------------------------- acceptance criterion 12: file mode
|
|
# `mv` from a mktemp candidate carries mktemp's 0600 forever, and a plain `cp` onto an existing
|
|
# file keeps the DESTINATION's mode rather than the source's, so a restore does not undo the
|
|
# narrowing either (fleetd #635 follow-up, ticket comment 17657). Proves the mode survives an edit
|
|
# AND a subsequent restore, from two different starting points — 644 is the common case, 600
|
|
# proves the fix PRESERVES whatever mode was there rather than hardcoding 644.
|
|
test_file_mode_survives_edit_and_restore() {
|
|
local dir want got
|
|
for want in 644 600; do
|
|
dir="$(new_fixture)"
|
|
chmod "$want" "$dir/fleetd.yaml"
|
|
|
|
start_run "$dir" 5 --set ".profiles.sonnet.weight=${want}"
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
assert_equals 0 "$RUN_RC" "mode-preservation setup edit exit code ($want)"
|
|
got="$(stat -f '%Lp' "$dir/fleetd.yaml" 2>/dev/null || stat -c '%a' "$dir/fleetd.yaml")"
|
|
assert_equals "$want" "$got" "mode must survive a --set ($want)"
|
|
|
|
start_run "$dir" 5 --restore
|
|
sleep 1
|
|
printf 'config reloaded\n' >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
assert_equals 0 "$RUN_RC" "mode-preservation restore exit code ($want)"
|
|
got="$(stat -f '%Lp' "$dir/fleetd.yaml" 2>/dev/null || stat -c '%a' "$dir/fleetd.yaml")"
|
|
assert_equals "$want" "$got" "mode must survive a --restore ($want)"
|
|
done
|
|
}
|
|
|
|
# dry-run must never touch the live file and must still redact.
|
|
test_dry_run_never_installs_and_redacts() {
|
|
local dir before
|
|
dir="$(new_fixture)"
|
|
before="$(cat "$dir/fleetd.yaml")"
|
|
|
|
"$EDIT" --dry-run --set '.profiles.sonnet.weight=99' \
|
|
--config "$dir/fleetd.yaml" --log "$dir/fleetd.out" --wait-seconds 2 \
|
|
> "$dir/stdout.log" 2>&1
|
|
local rc=$?
|
|
RUN_OUTPUT="$(cat "$dir/stdout.log")"
|
|
|
|
assert_equals 0 "$rc" "dry-run exit code"
|
|
assert_equals "$before" "$(cat "$dir/fleetd.yaml")" "dry-run must never write the live config"
|
|
assert_not_contains "hunter2" "$RUN_OUTPUT" "dry-run diff must also be redacted"
|
|
assert_contains "99" "$RUN_OUTPUT" "dry-run diff must show the candidate value"
|
|
# Same positive-control reasoning as acceptance criterion 13, applied to the dry-run diff path.
|
|
assert_contains "<redacted>" "$RUN_OUTPUT" "the dry-run diff's redaction must be PROVEN to have run, not merely absent"
|
|
}
|
|
|
|
# --check is read-only and always exits 0, even against a dead "daemon".
|
|
test_check_is_read_only_and_exits_zero() {
|
|
local dir before rc=0
|
|
dir="$(new_fixture)"
|
|
before="$(cat "$dir/fleetd.yaml")"
|
|
|
|
"$EDIT" --check --config "$dir/fleetd.yaml" --log "$dir/fleetd.out" \
|
|
> "$dir/stdout.log" 2>&1 || rc=$?
|
|
|
|
assert_equals 0 "$rc" "--check exit code"
|
|
assert_equals "$before" "$(cat "$dir/fleetd.yaml")" "--check must never modify the config"
|
|
}
|
|
|
|
test_refusal_shape_from_parse_failure_wording_is_recognised() {
|
|
local dir
|
|
dir="$(new_fixture)"
|
|
|
|
start_run "$dir" 5 --set '.profiles.sonnet.weight=6'
|
|
sleep 1
|
|
printf 'config reload from %s refused, keeping the running config: boom\n' "$dir/fleetd.yaml" >> "$dir/fleetd.out"
|
|
collect_run "$dir"
|
|
|
|
assert_equals 4 "$RUN_RC" "the parse-failure refusal shape must also exit 4, not be read as silence"
|
|
}
|
|
|
|
echo "== acceptance criterion 1: refusal restores byte for byte =="
|
|
test_refusal_restores_byte_for_byte
|
|
echo "== acceptance criterion 2: clean reload keeps the edit =="
|
|
test_clean_reload_keeps_the_edit
|
|
echo "== acceptance criterion 3: deferred reload told apart from clean =="
|
|
test_deferred_reload_is_told_apart_from_clean
|
|
echo "== acceptance criterion 4: silence is its own answer =="
|
|
test_silence_is_its_own_answer
|
|
echo "== acceptance criterion 5: broken candidate never reaches the live path =="
|
|
test_broken_candidate_never_reaches_live_path
|
|
echo "== acceptance criterion 6: the marker works =="
|
|
test_marker_skips_lines_before_it
|
|
echo "== acceptance criterion 7 (+13: redaction is proven to have run) =="
|
|
test_redaction_holds
|
|
echo "== acceptance criterion 9: a forgotten value refuses and installs nothing =="
|
|
test_forgotten_value_refuses_and_installs_nothing
|
|
echo "== acceptance criterion 10: an explicit clear writes a bare null =="
|
|
test_explicit_null_writes_bare_null_not_empty_string
|
|
echo "== acceptance criterion 11: a backup is never committable =="
|
|
test_backup_is_never_committable
|
|
echo "== acceptance criterion 12: the file mode survives an edit and a restore =="
|
|
test_file_mode_survives_edit_and_restore
|
|
echo "== extra: dry-run never installs, and redacts =="
|
|
test_dry_run_never_installs_and_redacts
|
|
echo "== extra: --check is read-only and always exits 0 =="
|
|
test_check_is_read_only_and_exits_zero
|
|
echo "== extra: the parse-failure refusal shape is also recognised =="
|
|
test_refusal_shape_from_parse_failure_wording_is_recognised
|
|
|
|
printf 'PASS: config-edit acceptance criteria\n'
|