✅ fresh
last synced 2026-10-24T20:09:49.281395+00:00 · coverage 100% (
run-report)Validation by Beadloom
doc_sync— same source assync-check.
Run report
The run report as a value of closed vocabularies, and the notification text rendered from it in the language the caller names. No field can carry a password.
Source
src/horizon_tracker/domain/report.py
Specification
Purpose
Every run ends in exactly one report to the owner. The report must say what happened, when, on whose working-day verdict and why, and it must be impossible for it to carry a credential. A value with no free-text field gives both properties by construction.
How it works
RunReport holds the run kind, an Outcome, an aware datetime, the VerdictSource (or None for a run that asks no calendar: a --force run or an absence skip), a Reason, an optional screenshot Path, planned_at, the drawn moment the run was due at, and absence, the absence a skip was for. planned_at is set for every scheduled run, whether or not it reached its window: a skip on a day off or a run started after its window names its moment as well. Only a --force run leaves it None. __post_init__ checks the type of every field and refuses a value of the wrong type with TypeError, rather than trusting the caller.
render(report, language) builds the message text from the message catalogue (messages) in language. A failure in English, with HT_TIMEZONE=Europe/Berlin as an example zone:
Check-in failed
Time: 21.10.2026 07:39:12 CEST, planned for 07:38
Production calendar: isdayoff.ru
Reason: the desktop did not openHT_LANGUAGE=ru renders the same four catalogue keys in Russian. Only the words change; the times and their format stay as they are. The ru text of the headline above, for instance, is Вход не удался; every ru text holds Cyrillic, every en text none, and the catalogue's own test keeps it so.
The first line is the run kind and the outcome, one catalogue key per pair (report.checkin.failed). The time line carries the actual time with the abbreviation of the configured zone (CEST for Europe/Berlin in summer, for example), followed by the planned moment (planned for HH:MM in English, the report.time_planned key) when planned_at is set. The calendar line names the source, one catalogue key per source (report.calendar.<source>): Production calendar: isdayoff.ru, Production calendar: unavailable, the Mon–Fri rule decided, or Production calendar: not asked, manual run --force for a --force run. The reason line is present only when there is a reason. The formatted times and the days of an absence are the only values the report puts into the text; every word comes from the catalogue.
A day-level skip (RunReport.is_day_level_skip) is a skip for the day itself: the outcome is SKIPPED and the reason is NON_WORKING_DAY or an absence reason. Its notice has no time line, since nothing was going to run that day; planned_at stays in the report. A skipped holiday in Russian:
Вход пропущен
Производственный календарь: isdayoff.ru
Причина: выходнойAn absence skip has no calendar line either, because the calendar was not asked. Its reason line carries the absence's first and last day, dd.mm without the year, joined by an en dash (one day when both are the same):
Check-in skipped
Reason: vacation (03.11–14.11)The screenshot never appears in the text: the notifier attaches it beside the text. A failure carries the screenshot of the step that failed; a success carries the use case's photo of the step it completed (the opened desktop, the launch list without the session). Either may be None, and the report then goes out as text.
Invariants
- No field holds free text: the outcome and the reason come from closed vocabularies, so a rendered message cannot quote an exception or a password.
RunReport.atcarries a time zone; a naivedatetimeraisesValueError.- A reason is present exactly when the outcome is not
SUCCEEDED; any other combination raisesValueError. - An absence is present exactly when the reason is an absence reason, and then the reason names the absence's kind, the outcome is
SKIPPEDandverdict_sourceisNone; any other combination raisesValueError. - Every
Reasonhas a phrase in every language; an exception message is never a reason. - An English notification holds no Cyrillic letter, for every kind, outcome, source, reason and planned moment.
API
Module src/horizon_tracker/domain/report.py:
Outcome—StrEnum:SUCCEEDED,FAILED,SKIPPED.Reason—StrEnum:NON_WORKING_DAY,TOO_EARLY,WINDOW_MISSED,ALREADY_LOGGED_OFF,PORTAL_UNREACHABLE,LOGIN_REJECTED,DESKTOP_NOT_FOUND,DESKTOP_DID_NOT_OPEN,LOGOFF_NOT_CONFIRMED,UNEXPECTED, and the absence reasonsON_VACATION,ON_SICK_LEAVE,ON_DAY_OFF.ABSENCE_REASONS— read-only mappingAbsenceKind -> Reason;absence_reason(kind: AbsenceKind) -> Reasonreads it.absence_span(absence: Absence) -> str— an absence's days without a year, as the reason line gives them:05.11for one day,03.11–14.11(an en dash) for a range. The probe names the next absence with it.RunReport(kind, outcome, at, verdict_source, reason=None, screenshot=None, planned_at=None, absence=None)— frozen; validated at construction as described above.is_day_level_skip: boolis the property the time line and the morning-only notice are decided by.planned_at: time | Noneis the drawn moment the run was due at, local wall time, set whether or not the run reached its window. It isNonefor a--forcerun. A value of any other type is refused withTypeError.render(report: RunReport, language: Language) -> str— the notification text inlanguage; there is no default, so every caller names the language it renders in. Whenplanned_atis set, the time line carries it beside the actual time:Time: 20.10.2026 07:56:12 CEST, planned for 07:43.
Testing
Tests: tests/unit/domain/test_report.py, each text test in both languages. The module is inside the mutation gate.
Scenarios:
tests/acceptance/features/production_calendar.feature:A skipped non-working day is reported without a time lineA day the calendar marks as a holiday is skipped with a noticeWhen the calendar is unreachable the weekday rule decides and says so
tests/acceptance/features/notifications.feature:A failed run sends the reason and a screenshotA notification never contains the password
tests/acceptance/features/morning_checkin.feature:Morning check-in reports success with the recorded time
tests/acceptance/features/language.feature:A notification is rendered in the configured languageThe default language is English
Dependencies
Derived from the imports on reindex.
- Depends on:
absence,messages,schedule,working-day-calendar - Used by:
cli,evening-checkout,horizon-playwright-client,morning-checkin,ports,probe,telegram-notifier,windowed-run
Parent
domain — domain README