Files
troupe-standup/wizard-work/spark/test_hermes_profile_health.py
ledger fe0991738a spark: Q83 -- five cells pin the config.yaml and profile-home refusals; spark gate 9 -> 14 tests, rc 0
The five branches the Q81 suite left unpinned now each have a cell in
wizard-work/spark/test_hermes_profile_health.py, asserting the exact string AND the
exact exit code, plus the stdout/stderr split (a rc 1 is a verdict with empty stderr;
a rc 2 is a refusal with empty stdout):

  test_config_missing_exits_1           rc 1 'config.yaml missing'
  test_config_as_directory_exits_1      rc 1 'config.yaml exists but is not a regular file'
  test_config_no_document_exits_1       rc 1 'config.yaml parses to no document'
  test_config_empty_mapping_exits_1     rc 1 'config.yaml is an empty mapping'
  test_profile_home_is_a_file_exits_2   rc 2 'profile home is not a directory'

Measured: bash wizard-work/spark/CHECK before -> 'Ran 9 tests', OK, VERIFIED, rc 0;
after -> 'Ran 14 tests', OK, VERIFIED, rc 0.

Mutant battery (q83_mutants.py, q83-mutants.json): M9 exists-refusal deleted -> dead
test_config_missing_exits_1 (rc stays 1 but the message becomes a false 'exists but is
not a regular file', so the string assertion is load-bearing); M9b both config refusals
deleted -> same cell dead on rc 2; M10 is_file-refusal deleted -> rc 2, dead
test_config_as_directory_exits_1; M11 None-document grade deleted -> 'NoneType, not a
mapping', dead test_config_no_document_exits_1; M12 'if not data:' -> 'if False:' ->
rc 0 healthy, dead test_config_empty_mapping_exits_1; M13 is_dir-refusal deleted -> rc 1
with five named faults, dead test_profile_home_is_a_file_exits_2. Baseline copy: rc 0,
Ran 14, 0 dead. hermes_profile_health.py is not touched.

Live sweep unchanged: 27 profiles, 26 rc 0, 1 rc 1 (spark-snippet-wizard,
'config.yaml missing').
2026-10-27 00:54:56 +00:00

194 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""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
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
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
TOOL = Path(os.path.dirname(os.path.abspath(__file__))) / "hermes_profile_health.py"
DIRS = ("skills", "plugins", "cron", "memories")
GOOD_CONFIG = "model: deepseek-v4.1-flash\nprovider: deepseek\n"
class ProfileHealthTest(unittest.TestCase):
"""One cell per graded behaviour; each names the mutant that kills it."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory(prefix="q81cell-")
self.base = Path(self._tmp.name)
self.addCleanup(self._tmp.cleanup)
# ---- fixture -----------------------------------------------------------
def make_home(self, name="home", *, dirs=DIRS, dirs_as_files=(), config=GOOD_CONFIG,
config_bytes=None):
home = self.base / name
home.mkdir(parents=True, exist_ok=True)
if config_bytes is not None:
(home / "config.yaml").write_bytes(config_bytes)
elif config is not None:
(home / "config.yaml").write_text(config, encoding="utf-8")
for d in dirs:
if d in dirs_as_files:
(home / d).write_text("", encoding="utf-8")
else:
(home / d).mkdir(exist_ok=True)
return home
def run_tool(self, *args, env_extra=None, drop_hermes_home=True):
env = dict(os.environ)
env.pop("HERMES_PROFILES_DIR", None)
if drop_hermes_home:
env.pop("HERMES_HOME", None)
env.update(env_extra or {})
return subprocess.run(
[sys.executable, str(TOOL), *args],
cwd=str(self.base),
env=env,
capture_output=True,
text=True,
)
# ---- cells -------------------------------------------------------------
def test_healthy_profile_exits_0(self):
"""Killed by M5 (an extra expected directory nobody has) and M3 (rc 2/1 swap)."""
home = self.make_home()
r = self.run_tool("--profile", str(home))
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
self.assertIn("Profile is healthy", r.stdout)
self.assertNotIn("Issues", r.stdout)
def test_missing_directory_exits_1(self):
"""Killed by M4 (the missing-directory branch suppressed)."""
home = self.make_home(dirs=("skills", "plugins", "cron"))
r = self.run_tool("--profile", str(home))
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
self.assertIn("memories/ missing", r.stdout)
self.assertNotIn("Profile is healthy", r.stdout)
def test_absent_profile_dir_exits_2(self):
"""Killed by M3 (absent home graded as a fault, rc 1, instead of ungradable)."""
r = self.run_tool("--profile", str(self.base / "does-not-exist"))
self.assertEqual(r.returncode, 2, r.stdout + r.stderr)
self.assertIn("does not exist", r.stderr)
self.assertEqual(r.stdout.strip(), "", "rc 2 must not print a verdict")
def test_skills_as_regular_file_exits_1(self):
"""Killed by M1 (is_dir() weakened back to exists())."""
home = self.make_home(dirs_as_files=("skills",))
r = self.run_tool("--profile", str(home))
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
self.assertIn("skills exists but is not a directory", r.stdout)
def test_empty_config_exits_1(self):
"""Killed by M2 (the empty-config grade removed)."""
home = self.make_home(config="")
r = self.run_tool("--profile", str(home))
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
self.assertIn("config.yaml is empty", r.stdout)
def test_invalid_yaml_exits_1(self):
"""Killed by M2 (empty-only) and M6 (config graded by exists() alone)."""
home = self.make_home(config="model: [unclosed\n - :\n\t:bad\n")
r = self.run_tool("--profile", str(home))
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
self.assertIn("not valid YAML", r.stdout)
def test_config_not_a_mapping_exits_1(self):
"""Killed by M6 (no parse, so a list config reads healthy)."""
home = self.make_home(config="- just\n- a\n- list\n")
r = self.run_tool("--profile", str(home))
self.assertEqual(r.returncode, 1, r.stdout + r.stderr)
self.assertIn("not a mapping", r.stdout)
def test_no_profile_names_nothing_exits_2(self):
"""Killed by M7 (an implicit default home invented instead of refusing)."""
r = self.run_tool()
self.assertEqual(r.returncode, 2, r.stdout + r.stderr)
self.assertIn("ungradable", r.stderr)
# the refusal must name the missing input, not a missing directory: an invented
# default home also exits 2 on this host, and that mutant must not read clean here.
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"
home = root / "volt"
home.mkdir(parents=True)
(home / "config.yaml").write_text(GOOD_CONFIG, encoding="utf-8")
for d in DIRS:
(home / d).mkdir()
r = self.run_tool("--profile", "volt", "--profiles-root", str(root))
self.assertEqual(r.returncode, 0, r.stdout + r.stderr)
self.assertIn(str(home), r.stdout)
# and HERMES_HOME alone, with no --profile, reaches the same home
r2 = self.run_tool(env_extra={"HERMES_HOME": str(home)}, drop_hermes_home=False)
self.assertEqual(r2.returncode, 0, r2.stdout + r2.stderr)
self.assertIn(str(home), r2.stdout)
if __name__ == "__main__":
unittest.main(verbosity=2)