From 25edb10447fc6839d3b6a51e1324b62a5128bf83 Mon Sep 17 00:00:00 2001 From: vidaks <39301796+vidaks@users.noreply.github.com> Date: Thu, 4 Jun 2026 09:46:01 +0200 Subject: [PATCH] test: cover the maintain crash-email path; document RESOLVE_RETRY_DAYS Add unit tests asserting cmd_maintain's `finally` still sends the heartbeat email when the run crashes before a summary is composed (and sends nothing when --email is off). This is the safety-critical path that had only inspection coverage. Add COLOPHON_RESOLVE_RETRY_DAYS to .env.example and note that the SMTP block now also feeds the `maintain --email` daily summary, not just weekly oversight. --- .env.example | 8 +++++- tests/test_maintain_email.py | 51 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 tests/test_maintain_email.py diff --git a/.env.example b/.env.example index 26866ad..b61b05e 100644 --- a/.env.example +++ b/.env.example @@ -19,11 +19,17 @@ COLOPHON_HARDCOVER_KEY= # Set the API key to call Anthropic directly; leave unset to use the `claude` CLI. ANTHROPIC_API_KEY= +# Re-query a cached-unresolvable mis-seed after N days (0 = never; the default). +# A book is also re-queried automatically once its title/author changes. +# COLOPHON_RESOLVE_RETRY_DAYS=0 + # --- State (defaults are repo-relative; set absolute paths for a service) --- # COLOPHON_DB=/var/lib/colophon/colophon.db # COLOPHON_REPORTS=/var/lib/colophon/reports -# --- Weekly oversight email (sent ONLY on DRIFT/REVIEW) --- +# --- Email (host SMTP relay) --- +# Used by `oversight` (sent ONLY on DRIFT/REVIEW) and by `maintain --email` +# (a one-page summary sent after every run, as a daily heartbeat). SMTP_HOST= SMTP_PORT=587 SMTP_USER= diff --git a/tests/test_maintain_email.py b/tests/test_maintain_email.py new file mode 100644 index 0000000..4baf1df --- /dev/null +++ b/tests/test_maintain_email.py @@ -0,0 +1,51 @@ +"""maintain CLI email paths — no network, no SMTP, no writes. Run: python -m unittest -v + +Covers the safety-critical guarantee: a crash before the summary is composed must +still send the heartbeat email (the `finally` in cmd_maintain). The happy and +aborted render paths are covered by the render smoke + live runs. +""" +import argparse +import unittest + +from colophon import cli, maintain, oversight + + +def _args(**kw): + d = {"limit": 20, "min_conf": 0.9, "apply": False, "force": False, "email": True} + d.update(kw) + return argparse.Namespace(**d) + + +def _boom(*a, **k): + raise RuntimeError("grimmory unreachable") + + +class CrashStillEmails(unittest.TestCase): + def setUp(self): + self.sent = [] + self._send, self._run = oversight.send_email, maintain.run_maintain + oversight.send_email = lambda subject, body: (self.sent.append((subject, body)), (True, "stub"))[1] + + def tearDown(self): + oversight.send_email, maintain.run_maintain = self._send, self._run + + def test_crash_before_summary_still_emails(self): + # run_maintain itself swallows phase failures, but an unexpected throw must + # not eat the heartbeat: the finally sends a [CRASH] notice, then re-raises. + maintain.run_maintain = _boom + with self.assertRaises(RuntimeError): + cli.cmd_maintain(_args(email=True), None, None) + self.assertEqual(len(self.sent), 1) + subject, body = self.sent[0] + self.assertIn("[CRASH]", subject) + self.assertIn("journalctl", body) + + def test_crash_without_email_flag_sends_nothing(self): + maintain.run_maintain = _boom + with self.assertRaises(RuntimeError): + cli.cmd_maintain(_args(email=False), None, None) + self.assertEqual(self.sent, []) + + +if __name__ == "__main__": + unittest.main()