R-306: --preflight-only writes no state
_state_put and _state_mark are no-ops under --preflight-only as well as --dry-run, so the banner "no state written" is true and the dnsmasq ownership answer is recorded only by the real install's own preflight. The log says "would be recorded at install" on a preflight-only run. scripts/test_hostinstall.py: test_preflight_only_writes_no_state (+ its control and a no-other-writer check). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -403,8 +403,12 @@ _state_has() {
|
||||
[[ -f "$STATE_FILE" ]] || return 1
|
||||
STATE_FILE="$STATE_FILE" python3 -c "import json,os,sys;f=os.environ['STATE_FILE'];d=json.load(open(f));sys.exit(0 if sys.argv[1] in d.get('completed',[]) else 1)" "$1" 2>/dev/null
|
||||
}
|
||||
# Both writers are no-ops under --dry-run AND --preflight-only (R-306): the preflight-only banner says
|
||||
# "no state written", and a value recorded by a preflight-only run (dnsmasq ownership) would otherwise be
|
||||
# baked in before the real install's own preflight. Pinned by scripts/test_hostinstall.py
|
||||
# (test_preflight_only_writes_no_state).
|
||||
_state_mark() {
|
||||
$DRY_RUN && return 0
|
||||
{ $DRY_RUN || $PREFLIGHT_ONLY; } && return 0
|
||||
mkdir -p "$STATE_DIR"
|
||||
STATE_FILE="$STATE_FILE" python3 -c "import json,os,sys;f=os.environ['STATE_FILE'];d=json.load(open(f)) if os.path.exists(f) else {'completed':[]};c=d.setdefault('completed',[]);(c.append(sys.argv[1]) if sys.argv[1] not in c else None);json.dump(d,open(f,'w'),indent=2)" "$1"
|
||||
}
|
||||
@@ -413,9 +417,9 @@ should_skip() { # returns 0 (skip) if --resume AND step already done
|
||||
return 1
|
||||
}
|
||||
# _state_put KEY VALUE — set a top-level string key in state.json (creates the file if absent).
|
||||
# Mirrors _state_mark: dry-run no-ops (writes nothing), robust JSON via python3.
|
||||
# Mirrors _state_mark: dry-run and preflight-only no-op (write nothing), robust JSON via python3.
|
||||
_state_put() {
|
||||
$DRY_RUN && return 0
|
||||
{ $DRY_RUN || $PREFLIGHT_ONLY; } && return 0
|
||||
mkdir -p "$STATE_DIR"
|
||||
STATE_FILE="$STATE_FILE" python3 -c "import json,os,sys;f=os.environ['STATE_FILE'];d=json.load(open(f)) if os.path.exists(f) else {'completed':[]};d[sys.argv[1]]=sys.argv[2];json.dump(d,open(f,'w'),indent=2)" "$1" "$2"
|
||||
}
|
||||
@@ -1965,14 +1969,14 @@ step_preflight() {
|
||||
# rule as before, applied one level finer.
|
||||
if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' dnsmasq 2>/dev/null | grep -q "install ok installed"; then
|
||||
_state_put dnsmasq_preexisting yes
|
||||
log_info " dnsmasq: already installed BEFORE Felhom — recorded; uninstall will not touch it"
|
||||
log_info " dnsmasq: already installed BEFORE Felhom — $($PREFLIGHT_ONLY && echo "would be recorded at install" || echo recorded); uninstall will not touch it"
|
||||
else
|
||||
_state_put dnsmasq_preexisting no
|
||||
log_info " dnsmasq: not present before Felhom — recorded; uninstall will remove it again if we install it"
|
||||
log_info " dnsmasq: not present before Felhom — $($PREFLIGHT_ONLY && echo "would be recorded at install" || echo recorded); uninstall will remove it again if we install it"
|
||||
fi
|
||||
if command -v dpkg-query >/dev/null 2>&1 && dpkg-query -W -f='${Status}' dnsmasq-base 2>/dev/null | grep -q "install ok installed"; then
|
||||
_state_put dnsmasq_base_preexisting yes
|
||||
log_info " dnsmasq-base: already installed BEFORE Felhom — recorded; uninstall will leave it"
|
||||
log_info " dnsmasq-base: already installed BEFORE Felhom — $($PREFLIGHT_ONLY && echo "would be recorded at install" || echo recorded); uninstall will leave it"
|
||||
else
|
||||
_state_put dnsmasq_base_preexisting no
|
||||
fi
|
||||
|
||||
@@ -11,7 +11,7 @@ functions act on are variables the test points into the temp directory. Nothing
|
||||
Where behaviour cannot be isolated, a STATIC test checks the wiring (the function is CALLED, from the
|
||||
right place) — a helper defined and never called is the seam-built-never-wired shape.
|
||||
|
||||
Rows: R-275, R-276, R-881 (uninstall residue).
|
||||
Rows: R-275, R-276, R-881 (uninstall residue); R-306 (--preflight-only writes no state).
|
||||
Run: python3 scripts/test_hostinstall.py
|
||||
"""
|
||||
import os
|
||||
@@ -330,6 +330,48 @@ def test_bundle_list_is_current():
|
||||
assert not new, "the agent bundle installs file(s) this test (and maybe the uninstall) does not know: %s" % new
|
||||
|
||||
|
||||
# ── R-306: --preflight-only writes no state ──────────────────────────────────────────────────────
|
||||
def _state_run(sb, preflight_only, dry=False):
|
||||
return sb.run(prelude(dry=dry) + func("_state_mark") + func("_state_put") + func("_state_get") +
|
||||
'PREFLIGHT_ONLY=%s\nSTATE_DIR="$SB/state"; STATE_FILE="$SB/state/state.json"\n'
|
||||
'_state_put dnsmasq_preexisting yes\n_state_mark preflight\necho "GOT=$(_state_get dnsmasq_preexisting)"\n'
|
||||
% ("true" if preflight_only else "false"))
|
||||
|
||||
|
||||
def test_preflight_only_writes_no_state():
|
||||
sb = Sandbox()
|
||||
try:
|
||||
rc, out = _state_run(sb, preflight_only=True)
|
||||
assert rc == 0, out
|
||||
assert not os.path.exists(sb.path("state", "state.json")), \
|
||||
"--preflight-only wrote state.json: %s" % open(sb.path("state", "state.json")).read()
|
||||
assert "GOT=\n" in out + "\n", out
|
||||
finally:
|
||||
sb.close()
|
||||
|
||||
|
||||
def test_install_preflight_does_write_state():
|
||||
# the control: the same helpers DO write on a real install, or the test above proves nothing.
|
||||
sb = Sandbox()
|
||||
try:
|
||||
rc, out = _state_run(sb, preflight_only=False)
|
||||
assert rc == 0, out
|
||||
assert "GOT=yes" in out, out
|
||||
assert '"preflight"' in open(sb.path("state", "state.json")).read()
|
||||
finally:
|
||||
sb.close()
|
||||
|
||||
|
||||
def test_state_json_has_no_other_writer():
|
||||
# every write must go through the two guarded helpers; a direct write would bypass the guard.
|
||||
bad = [l.strip() for l in SRC.splitlines()
|
||||
if re.search(r'>\s*"?\$STATE_FILE|json\.dump\(d,open\(f', l)
|
||||
and not re.match(r"\s*STATE_FILE=\"\$STATE_FILE\" python3 -c", l)]
|
||||
assert not bad, "state.json written outside _state_mark/_state_put: %s" % bad
|
||||
body = func("step_preflight")
|
||||
assert "_state_put dnsmasq_preexisting" in body, "the ownership record no longer goes through _state_put"
|
||||
|
||||
|
||||
def main():
|
||||
tests = [(n, f) for n, f in sorted(globals().items()) if n.startswith("test_") and callable(f)]
|
||||
fails = 0
|
||||
|
||||
Reference in New Issue
Block a user