diff --git a/wizard-work/spark/CHECK b/wizard-work/spark/CHECK index 86e2339..5269219 100755 --- a/wizard-work/spark/CHECK +++ b/wizard-work/spark/CHECK @@ -27,7 +27,8 @@ python3 --version 2>&1 | grep -q "Python" || exit 10 # Check git commit pushed git -C /root/troupe-standup rev-parse ed959f0 >/dev/null 2>&1 || exit 11 -# Q81: the profile-health suite. One cell per graded behaviour (9 cells at 2026-10-26). +# Q81: the profile-health suite. One cell per graded behaviour (9 cells at 2026-10-26, +# 14 after Q83 pins the five config.yaml/profile-home refusals). # Resolves its own directory from $0, so this leg grades THIS checkout, not a host path. SPARK_DIR="$(cd "$(dirname "$0")" && pwd)" suite_out="$(python3 -m unittest discover -s "$SPARK_DIR" -p 'test_hermes_profile_health.py' 2>&1)" diff --git a/wizard-work/spark/test_hermes_profile_health.py b/wizard-work/spark/test_hermes_profile_health.py index 9f14c29..505b37b 100644 --- a/wizard-work/spark/test_hermes_profile_health.py +++ b/wizard-work/spark/test_hermes_profile_health.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Cells for wizard-work/spark/hermes_profile_health.py (Q81). +"""Cells for wizard-work/spark/hermes_profile_health.py (Q81 cells; Q83 adds five). Every cell drives the tool as a subprocess and grades its exit code, so the thing under test is the CLI contract (0 clean / 1 named faults / 2 ungradable), not an @@ -7,6 +7,10 @@ internal function. Fixtures are built in a fresh temp dir per cell; nothing here reads or writes a real profile home, and no host path is referenced: the tool is located from this file's own directory. +Q83: the five cells from test_config_missing_exits_1 to test_profile_home_is_a_file_exits_2 +pin the config.yaml refusals and the profile-home refusal that the Q81 nine left unpinned; +each names the mutant that kills it in its docstring. + Run: python3 -m unittest discover -s wizard-work/spark -p test_hermes_profile_health.py """ import os @@ -121,6 +125,53 @@ class ProfileHealthTest(unittest.TestCase): self.assertIn("no profile named", r.stderr) self.assertEqual(r.stdout.strip(), "", "rc 2 must not print a verdict") + # ---- Q83: the five branches Q81's nine cells left unpinned ----------------- + def test_config_missing_exits_1(self): + """Killed by M9 (refusal deleted: the is_file() branch relabels a MISSING config as + 'exists but is not a regular file' at the same rc 1 — the sweep's only live red + becomes a lie) and by M9b (both refusals deleted -> rc 2, ungradable). + """ + home = self.make_home(config=None) + r = self.run_tool("--profile", str(home)) + self.assertEqual(r.returncode, 1, r.stdout + r.stderr) + self.assertIn("config.yaml missing", r.stdout) + self.assertNotIn("Profile is healthy", r.stdout) + self.assertEqual(r.stderr.strip(), "", "rc 1 is a verdict, not a refusal") + + def test_config_as_directory_exits_1(self): + """Killed by M10 (the is_file() refusal deleted -> rc 2, a graded fault lost).""" + home = self.make_home(config=None) + (home / "config.yaml").mkdir() + r = self.run_tool("--profile", str(home)) + self.assertEqual(r.returncode, 1, r.stdout + r.stderr) + self.assertIn("config.yaml exists but is not a regular file", r.stdout) + self.assertEqual(r.stderr.strip(), "", "rc 1 is a verdict, not a refusal") + + def test_config_no_document_exits_1(self): + """Killed by M11 (the 'parses to no document' grade deleted -> NoneType wording).""" + home = self.make_home(config="# only comments\n# and nothing else\n") + r = self.run_tool("--profile", str(home)) + self.assertEqual(r.returncode, 1, r.stdout + r.stderr) + self.assertIn("config.yaml parses to no document", r.stdout) + self.assertNotIn("not a mapping", r.stdout, "a null document is not a non-mapping") + + def test_config_empty_mapping_exits_1(self): + """Killed by M12 ('if not data:' -> 'if False:' -> rc 0, healthy).""" + home = self.make_home(config="{}\n") + r = self.run_tool("--profile", str(home)) + self.assertEqual(r.returncode, 1, r.stdout + r.stderr) + self.assertIn("config.yaml is an empty mapping", r.stdout) + self.assertNotIn("Profile is healthy", r.stdout) + + def test_profile_home_is_a_file_exits_2(self): + """Killed by M13 (the is_dir() refusal deleted -> rc 1, five named faults).""" + home = self.base / "not-a-home" + home.write_text("", encoding="utf-8") + r = self.run_tool("--profile", str(home)) + self.assertEqual(r.returncode, 2, r.stdout + r.stderr) + self.assertIn("profile home is not a directory", r.stderr) + self.assertEqual(r.stdout.strip(), "", "rc 2 must not print a verdict") + def test_profile_name_resolves_under_root(self): """Killed by M8 (a bare NAME taken as a relative path).""" root = self.base / "profiles"