fleetd #635 follow-up: refuse empty --set values, gitignore backups, preserve file mode
CI / shell-tests (pull_request) Failing after 8s
CI / contract (pull_request) Successful in 1m43s
CI / build (pull_request) Failing after 1m52s

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.
This commit is contained in:
Dai Ha
2026-10-01 18:07:12 +02:00
parent 0db6d31dc2
commit 4eb720029c
4 changed files with 265 additions and 18 deletions
+137 -3
View File
@@ -51,6 +51,7 @@ broker:
profiles:
sonnet:
weight: 3
maxLoad: 5
YAML
: > "$dir/fleetd.out"
printf '%s' "$dir"
@@ -143,7 +144,7 @@ test_silence_is_its_own_answer() {
assert_contains '--restore' "$RUN_OUTPUT" "silence prints the --restore command"
local backup restore_cmd
backup="$(ls -t "$dir"/fleetd.yaml.bak.* | head -1)"
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:]]*//')"
@@ -186,6 +187,7 @@ broker:
profiles:
sonnet:
weight: 3
maxLoad: 5
YAML
}
@@ -203,7 +205,14 @@ test_marker_skips_lines_before_it() {
assert_equals 0 "$RUN_RC" "a stale refusal before the marker must not be read as this edit's verdict"
}
# ------------------------------------------------------------------- acceptance criterion 7
# ------------------------------------------------------------------- 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)"
@@ -216,6 +225,121 @@ test_redaction_holds() {
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.
@@ -234,6 +358,8 @@ test_dry_run_never_installs_and_redacts() {
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".
@@ -273,8 +399,16 @@ 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: the redaction holds =="
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 =="