From e21eb37e8f41c8299c050eae94d9773dca98428e Mon Sep 17 00:00:00 2001 From: Keith Raghubar Date: Wed, 28 Oct 2026 18:16:13 -0400 Subject: [PATCH] Give doctor's --config axis flag its own attribute (4.0.0-B2) The --config axis flag and the loaded config both lived on args.config. Bare `sysforge doctor` read the loaded dict as the flag and ran only the config axis; `doctor system` read the flag's False as an already-loaded config and ran every axis against {}. The flag now stores to config_axis; its spelling is unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/release-notes/unreleased.md | 4 ++++ sysforge/doctor.py | 9 ++++---- tests/test_doctor.py | 35 ++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/release-notes/unreleased.md b/docs/release-notes/unreleased.md index ff69fb60..c4a7fe7d 100644 --- a/docs/release-notes/unreleased.md +++ b/docs/release-notes/unreleased.md @@ -33,3 +33,7 @@ https://keepachangelog.com/en/1.1.0/ ## Fixed - **`4.0.0-B1` — `build --failed` no longer rebuilds a package you removed.** Replacing a failed `ethtool-git` with `pacman -S ethtool` left its failure recorded, so `build --failed` queued `ethtool-git` again. A removal cleared the failure only when sysforge read the pacman hook's record of that transaction, and that record can be gone by then. A failed package that sysforge tracks but that is no longer installed now counts as removed too, in `update`, `state failed`, `doctor --state` and `build --failed`; `update` checks it before pruning the entry. A package whose first build failed was never installed and is still retried. A failure left over from before this fix is cleared with `sysforge state failed --clear PKGBASE`. + +--- + +- **`4.0.0-B2` — bare `sysforge doctor` runs the full sweep again, and `doctor system` reads your config.** The `--config` axis flag and the loaded config shared one attribute. Bare `sysforge doctor` took the loaded config for the flag and checked only the live config settings, so it printed one section and stopped. `sysforge doctor system` took the flag's unset value for a loaded config and ran every axis against an empty one, so the toolchain, cache, graphics and storage checks ignored your settings. The flag now has its own attribute; its spelling is unchanged. diff --git a/sysforge/doctor.py b/sysforge/doctor.py index bb5b841d..e29bbf00 100644 --- a/sysforge/doctor.py +++ b/sysforge/doctor.py @@ -1184,7 +1184,7 @@ def _collect_integrity_findings(args) -> diag.AxisResult: "gfxperf": "gfxperf", "pacman": "pacman", "state": "state", - "config": "config", + "config_axis": "config", "boot": "boot", "restart": "restart", "storage": "storage", @@ -1708,7 +1708,8 @@ def _add_system_axis_flags(p): help="Check sysforge's own state integrity: recorded build failures, an " "interrupted stage sentinel from a prior run, and build_state drift " "vs the live pacman database. Read-only (does not recover).") - p.add_argument("--config", action="store_true", + # dest: args.config holds the loaded config (4.0.0-B2). + p.add_argument("--config", action="store_true", dest="config_axis", help="Check the live config files for settings sysforge retired or does " "not know (the same report as `sysforge config check`).") p.add_argument("--boot", action="store_true", @@ -1791,7 +1792,7 @@ def add_parser(cls, sub): def pre_check(self, args) -> PreCheckResult: # cmd_doctor_system reads args.config; the cli.py wrapper used to set it. - if not hasattr(args, "config") or args.config is None: + if getattr(args, "config", None) is None: args.config = _load_config() or {} return PreCheckResult() @@ -1890,7 +1891,7 @@ def pre_check(self, args) -> PreCheckResult: return PreCheckResult(blocker=( "--json is a read-only report; it cannot be combined with " "--apply, which rebuilds packages"), exit_code=2) - if not hasattr(args, "config") or args.config is None: + if getattr(args, "config", None) is None: args.config = _load_config() or {} # Split targets by provenance so the rust axis can collapse # broad-selector targets into one survey finding (2.6.1-F1). diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 3a7fd8ff..30cedde8 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -2215,3 +2215,38 @@ def test_toolchain_axis_reports_non_graphics_consumer_symbols(monkeypatch): assert not any("gallium" in m for m in messages) assert "libllvm_consumers" in res.roster.ran + + +# --------------------------------------------------------------------------- +# The --config axis flag vs the loaded config (4.0.0-B2) +# --------------------------------------------------------------------------- +# Both lived on ``args.config``: bare `doctor` read the loaded dict as the flag +# and ran only the config axis, and `doctor system` read the flag's False as +# "already loaded" and ran every axis against an empty config. + +_LIVE_CONFIG = {"defaults": {"toolchain": "llvm"}} + + +def _pre_checked(monkeypatch, *argv): + from cli_args import cli_args + monkeypatch.setattr(doctor, "_load_config", lambda: dict(_LIVE_CONFIG)) + args = cli_args(*argv) + args.verb_cls().pre_check(args) + return args + + +@pytest.mark.parametrize("argv", [("doctor",), ("doctor", "system")]) +def test_full_sweep_runs_every_default_axis_with_the_loaded_config(monkeypatch, argv): + args = _pre_checked(monkeypatch, *argv) + + assert doctor._resolve_axis_names(args) == [ + n for n in doctor._SYSTEM_AXIS_ORDER + if n not in doctor._OPT_IN_AXES and n != "abi"] + assert args.config == _LIVE_CONFIG + + +def test_config_flag_selects_only_the_config_axis(monkeypatch): + args = _pre_checked(monkeypatch, "doctor", "system", "--config") + + assert doctor._resolve_axis_names(args) == ["config"] + assert args.config == _LIVE_CONFIG