Files
fleetd/scripts/test-config-edit.sh
T
Dai Ha d7f94cafa2
CI / shell-tests (pull_request) Failing after 7s
CI / contract (pull_request) Successful in 1m42s
CI / build (pull_request) Failing after 1m50s
fleetd #635 follow-up: redact() masks block-scalar continuations + passphrase; --set failures stop echoing the value (defects 7 and 8, criteria 15a/15b/16)
Defect 7 (comment 17670): redact() only masked a line that itself started with a
secret-looking key, so a YAML block scalar's value leaked on the lines that
followed the key while the key line right above it printed a reassuring
"<redacted>". Fixed by tracking the masked key's own indentation and masking
every following line indented deeper than it, stopping once indentation returns
to the key's level or shallower; the diff's leading +/-/space marker is stripped
before indentation is measured, per the comment's own pitfall. "passphrase" is
now also in the key-name backstop.

Defect 8 (comment 17673): apply_set_pairs echoed the operator's full
"path=value" input, unredacted, in both of its yq-failure die messages — a
failing --set with a secret-looking value printed that value right back. Fixed
to print only the path; deliberately not routed through redact, which would
pass a non-"key: value"-shaped string straight through.

Adds acceptance criteria 15a (block-scalar continuation), 15b (passphrase key),
and 16 (failing --set never echoes its value) to scripts/test-config-edit.sh,
each with a positive control proving the relevant line really was in the
printed output before asserting the secret is absent. All three confirmed RED
against the pre-fix code and GREEN after, in isolation, before being folded
into the full suite (16 criteria + 3 extras, exit 0).

Also updates PR #636's description per comment 17671: the redact() sentence now
names the continuation-masking rule and says plainly that the key-name list is
a backstop, never a complete list.
2026-10-01 18:47:00 +02:00

586 lines
26 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
}
# ----------------------------------------- acceptance criterion 14: restore message names the real directory
# fleetd #635 follow-up (ticket comment 17664, defect 6) — the --restore "no backup found"
# message used to print the OLD beside-the-config glob even though newest_backup had already
# moved to searching the managed directory. Proves BOTH directions: the not-found message names
# the directory actually searched (not merely that it says SOMETHING), and that a real backup
# sitting in that directory still lets --restore succeed — otherwise the fix could regress into
# a message that is always printed regardless of whether a backup exists.
test_restore_message_names_the_searched_directory() {
local dir rc=0
# Direction 1: no backup anywhere — the message must name .config-backups/, not the bare
# beside-the-config glob the OLD code printed.
dir="$(new_fixture)"
"$EDIT" --restore --config "$dir/fleetd.yaml" --log "$dir/fleetd.out" --wait-seconds 2 \
> "$dir/stdout.log" 2>&1 || rc=$?
RUN_OUTPUT="$(cat "$dir/stdout.log")"
assert_equals 1 "$rc" "--restore with no backup anywhere exit code"
assert_contains ".config-backups/fleetd.yaml.bak.*" "$RUN_OUTPUT" \
"the not-found message must name the directory actually searched, not the old beside-the-config glob"
# Direction 2: a real backup IS present in .config-backups/ — --restore must still succeed, so
# the message fix cannot have turned into one that prints regardless of whether a backup exists.
dir="$(new_fixture)"
cp "$dir/fleetd.yaml" "$dir/pre-edit.yaml"
start_run "$dir" 5 --set '.profiles.sonnet.weight=77'
sleep 1
printf 'config reloaded\n' >> "$dir/fleetd.out"
collect_run "$dir"
assert_equals 0 "$RUN_RC" "setup edit exit code for criterion 14's second half"
start_run "$dir" 5 --restore
sleep 1
printf 'config reloaded\n' >> "$dir/fleetd.out"
collect_run "$dir"
assert_equals 0 "$RUN_RC" "--restore with a real backup present must still succeed"
cmp -s "$dir/fleetd.yaml" "$dir/pre-edit.yaml" \
|| fail "--restore with a real backup present must put the file back byte for byte"
}
# ------------------------- acceptance criterion 15a: block-scalar continuation lines are redacted
# fleetd #635 follow-up (ticket comment 17670, defect 7) — redact() used to look only AT the key
# line. A YAML block scalar (`|`) puts its value on the lines that FOLLOW the key, each indented
# deeper than it, so the real secret flowed through untouched while the key line right above it
# printed a reassuring "<redacted>" — worse than no redaction, because the marker stops a reader
# from looking further. The edited key here ("retries") sits directly next to the block scalar,
# well inside diff -u's default 3-line context window, so the printed hunk is GUARANTEED to
# include the secret's lines — placing the edit further away would let this pass today even
# without the fix, proving nothing (the ticket comment's own warning, from the lead's first
# reproduction attempt). The positive control runs FIRST: without it, "the secret never entered
# the diff at all" would pass identically to "it entered and was correctly redacted".
new_fixture_block_scalar() {
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
auth:
token: |
FAKELEAK-BLOCK-SCALAR
retries: 1
profiles:
sonnet:
weight: 3
maxLoad: 5
YAML
: > "$dir/fleetd.out"
printf '%s' "$dir"
}
test_block_scalar_continuation_is_redacted() {
local dir
dir="$(new_fixture_block_scalar)"
start_run "$dir" 5 --set '.auth.retries=2'
sleep 1
printf 'config reloaded\n' >> "$dir/fleetd.out"
collect_run "$dir"
assert_equals 0 "$RUN_RC" "block-scalar case reload exit code"
# Positive control FIRST: the key's own (masked) line must really be in the printed diff, or the
# negative assertion right after proves nothing — see the comment above this test.
assert_contains "token:" "$RUN_OUTPUT" "block-scalar case: the key's line must be in the printed diff"
assert_contains "<redacted>" "$RUN_OUTPUT" "block-scalar case: redaction must be proven to have run on real content"
assert_not_contains "FAKELEAK-BLOCK-SCALAR" "$RUN_OUTPUT" "block-scalar case: the block scalar's VALUE must never leak"
}
# ------------------------------------- acceptance criterion 15b: "passphrase" is also recognised
# "passphrase" was in none of TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|URI|_KEY (ticket comment
# 17670). This is a plain key:value line, not a block scalar — kept in its OWN fixture and OWN
# function, separate from criterion 15a, so that a failure in one case can never mask a failure in
# the other (a single combined test would abort under `set -e` at its first failing assertion,
# and the second case would then never even run).
new_fixture_passphrase() {
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
auth:
passphrase: FAKELEAK-PASSPHRASE
retries: 1
profiles:
sonnet:
weight: 3
maxLoad: 5
YAML
: > "$dir/fleetd.out"
printf '%s' "$dir"
}
test_passphrase_key_is_redacted() {
local dir
dir="$(new_fixture_passphrase)"
start_run "$dir" 5 --set '.auth.retries=2'
sleep 1
printf 'config reloaded\n' >> "$dir/fleetd.out"
collect_run "$dir"
assert_equals 0 "$RUN_RC" "passphrase case reload exit code"
assert_contains "passphrase:" "$RUN_OUTPUT" "passphrase case: the key's line must be in the printed diff"
assert_contains "<redacted>" "$RUN_OUTPUT" "passphrase case: redaction must be proven to have run on real content"
assert_not_contains "FAKELEAK-PASSPHRASE" "$RUN_OUTPUT" "passphrase case: the passphrase VALUE must never leak"
}
# ----------------------------------- acceptance criterion 16: a failing --set must not echo value
# fleetd #635 follow-up (ticket comment 17673, defect 8) — apply_set_pairs used to echo the FULL
# "$kv" (path=value, exactly as typed) in its yq-failure messages, so a broken --set with a
# secret-looking value printed that value right back out. The path alone is what the positive
# control proves is still there — it is what the operator needs to fix their command — and the
# negative assertion proves the value itself never appears. Kept to exactly this one failure
# shape (an invalid yq path/expression), matching the ticket's own reproduction.
test_failing_set_does_not_echo_its_value() {
local dir rc=0
dir="$(new_fixture)"
cp "$dir/fleetd.yaml" "$dir/pre-edit.yaml"
"$EDIT" --dry-run --set '.broker.["bad=FAKELEAK-SETVALUE' \
--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 "a --set with an invalid yq expression must exit non-zero, got 0"
cmp -s "$dir/fleetd.yaml" "$dir/pre-edit.yaml" \
|| fail "a failing --set must install nothing — the live fixture changed"
# Positive control FIRST: the path must still be in the message, or the negative assertion right
# after proves nothing (the message could simply have disappeared entirely).
assert_contains '.broker.["bad' "$RUN_OUTPUT" "the failure message must still name the PATH"
assert_not_contains "FAKELEAK-SETVALUE" "$RUN_OUTPUT" "the failure message must NEVER echo the VALUE"
}
# 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 "== acceptance criterion 14: the restore message names the directory actually searched =="
test_restore_message_names_the_searched_directory
echo "== acceptance criterion 15a: a block scalar's continuation lines are redacted =="
test_block_scalar_continuation_is_redacted
echo "== acceptance criterion 15b: a passphrase key is also recognised =="
test_passphrase_key_is_redacted
echo "== acceptance criterion 16: a failing --set must not echo its value =="
test_failing_set_does_not_echo_its_value
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'