Originally created by: fu351
Three small fast-follow fixes for doberman decision-log-prune (added in [#461]).
1. The resolved-AUTH predicate didn't match what auth writers actually persist. _RESOLVED_DECISIONS_PREDICATE in src/doberman/storage/log.py:84-86 only treated auth_result IN ('approved', 'denied', 'executed') as a resolved outcome. But the real writers of auth_result never persist those exact three values for the common case: proxy/executor.py:715,747,780 persist the auth tier/approval-method name (e.g. "soft_confirm", "local_auth", "<method.name>", "<method.name>+elevation" — see auth/provider.py:189-198), executor.py:766,775,886,894 and the hosthooks persist "blocked", and executor.py's error path persists "error". The comment above the predicate already stated the correct intent ("an AUTH row remains eligible only when its challenge already produced an explicit outcome... a missing auth_result is deliberately kept") — the enumerated literal list just didn't implement it. Fixed to (final_verdict <> 'AUTH' OR auth_result IS NOT NULL), which is exactly "any explicit outcome, pending stays".
2. test_decision_log_prune_reports_count_only was calendar-fragile. _seed_auth_secret_read (in tests/integration/test_cli_views.py) hardcoded a _NOW = 2026-06-08 timestamp on the Decision/SecurityObject it builds, but neither of those values is what lands in the persisted ts column the prune predicate compares — record_decision's now keyword is (src/doberman/storage/log.py:172-198, record["ts"] = now.isoformat()), and the seed helper never passed it, so it silently fell back to the real wall clock every time. That happened to make the prune test self-consistent today, but it's an accident of the current default, not something the test asserts — a future change to that fallback (e.g. deriving it from decided_at instead) would make the row's stored ts snap back to the stale _NOW and eventually push it past the --older-than-days 90 cutoff, breaking the "0 row(s)" assertion. Made it robust either way: _seed_auth_secret_read now takes an optional ts parameter and threads it into record_decision(..., now=ts) too, and the prune test seeds with datetime.now(timezone.utc) explicitly instead of relying on an implicit fallback.
3. Misleading help text. src/doberman/cli/main.py:1917 said "this many days old or older", but prune_decisions (src/doberman/storage/log.py:302-303,323, ts < cutoff) keeps a row exactly at the cutoff — it's a strict <, not <=. Reworded to "older than this many days (a row exactly at the cutoff is kept)".
src/doberman/storage/log.py: fix _RESOLVED_DECISIONS_PREDICATE to auth_result IS NOT NULL; update the comment and prune_decisions docstring to describe "any non-NULL auth_result" instead of enumerating three literal values.tests/integration/test_decision_log.py: add test_prune_deletes_auth_rows_with_any_recorded_outcome, seeding soft_confirm/blocked/None AUTH rows and asserting only the two non-NULL ones are pruned.tests/integration/test_cli_views.py: give _seed_auth_secret_read an optional ts parameter that also flows into record_decision(..., now=ts); test_decision_log_prune_reports_count_only now seeds with datetime.now(timezone.utc) instead of relying on an unstated fallback.src/doberman/cli/main.py: correct the --older-than-days help text to describe the strict cutoff.changelog.d/502.md: user-facing changelog fragment.Follow-up to [#461] (#213).
🤖 Generated with Claude Code
Ticket changed by: fu351