Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
51 changes: 51 additions & 0 deletions tests/test_maintain_email.py
Original file line number Diff line number Diff line change
@@ -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()
Loading