agnes-the-ai-analyst/tests/test_session_health.py
ZdenekSrotyr c8de0e0f64
release: 0.53.2 — diagnose silent-capture check + urllib3 2.7.0 + flaky-test fix (#270)
Three bundled improvements:

- #244 — new `agnes diagnose` check compares SessionStart events
  (~/.claude/projects/<encoded>/*.jsonl) against agnes-push uploaded
  log entries inside a 7-day window. Surfaces a warning when the gap
  exceeds 3, hinting at silently-broken capture-session — previously
  detectable only weeks after the fact.

- Dependabot — bumps transitive urllib3 from 1.26.20 to 2.7.0 to close
  5 advisories (4 high, 1 medium). kbcstorage 0.9.5 still pins
  urllib3<2.0.0 upstream; overridden via [tool.uv] override-dependencies
  since the SDK works fine against 2.x in practice (Client + Tables
  both flow through requests, which supports both lines).

- #252 — fix flaky test_scratch_dir_cleaned_up_after_failed_extraction
  by redirecting tempfile.tempdir to a per-test tmp_path. Pre-#252 the
  test scanned the shared system tmp dir and a sibling store test in
  another pytest-xdist worker could trip the assertion mid-window.

Closes #244. Closes #252.
2026-05-12 18:28:04 +02:00

206 lines
7.3 KiB
Python

"""Regression coverage for cli.lib.session_health.capture_session_health.
Issue #244 — flag silently-broken `agnes capture-session` by comparing
session files in `~/.claude/projects/<encoded>/` against entries in
`<workspace>/.claude/agnes-sessions-uploaded.txt` within a sliding
window.
"""
from __future__ import annotations
import os
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
def _set_home(monkeypatch, tmp_path):
"""Override the module-level ``_PROJECTS_DIR`` (evaluated once at
import via ``Path.home()``) so the check reads from a controlled
``~/.claude/projects/`` tree under tmp_path."""
import cli.lib.claude_sessions as cs
monkeypatch.setattr(cs, "_PROJECTS_DIR", tmp_path / ".claude" / "projects")
def _make_session_file(home: Path, workspace: Path, name: str, age_days: float) -> Path:
"""Write an empty jsonl into one of the candidate encoded dirs and
backdate its mtime."""
# Use variant-a encoding (slash→dash) — matches the macOS-friendly
# form cli/lib/claude_sessions.py emits first.
encoded = str(workspace.resolve()).replace("/", "-")
target = home / ".claude" / "projects" / encoded
target.mkdir(parents=True, exist_ok=True)
f = target / name
f.write_text("{}\n", encoding="utf-8")
# Backdate mtime
age = time.time() - (age_days * 86400)
os.utime(f, (age, age))
return f
def _append_uploaded_log(workspace: Path, when: datetime, transcript_path: str) -> None:
(workspace / ".claude").mkdir(parents=True, exist_ok=True)
log = workspace / ".claude" / "agnes-sessions-uploaded.txt"
line = f"{when.strftime('%Y-%m-%dT%H:%M:%SZ')}\t{transcript_path}\n"
with open(log, "a", encoding="utf-8") as f:
f.write(line)
def test_no_sessions_returns_info(tmp_path, monkeypatch):
"""Fresh workspace with no SessionStart events → info, not warning."""
workspace = tmp_path / "ws"
workspace.mkdir()
_set_home(monkeypatch, tmp_path / "home")
(tmp_path / "home").mkdir()
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace)
assert r["status"] == "info"
assert r["expected_sessions"] == 0
assert r["uploaded_entries"] == 0
def test_aligned_counts_returns_ok(tmp_path, monkeypatch):
"""SessionStart events match uploaded-log entries → ok."""
workspace = tmp_path / "ws"
workspace.mkdir()
home = tmp_path / "home"
home.mkdir()
_set_home(monkeypatch, home)
# 3 recent sessions
for i in range(3):
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=2)
now = datetime.now(timezone.utc)
for i in range(3):
_append_uploaded_log(workspace, now - timedelta(days=2, hours=i),
f"/path/s{i}.jsonl")
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace)
assert r["status"] == "ok"
assert r["expected_sessions"] == 3
assert r["uploaded_entries"] == 3
def test_silent_breakage_returns_warning(tmp_path, monkeypatch):
"""SessionStart events ≫ uploaded entries (delta > threshold) → warning."""
workspace = tmp_path / "ws"
workspace.mkdir()
home = tmp_path / "home"
home.mkdir()
_set_home(monkeypatch, home)
# 10 recent SessionStart events
for i in range(10):
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=2)
# only 2 uploads — capture-session silently dropped 8
now = datetime.now(timezone.utc)
for i in range(2):
_append_uploaded_log(workspace, now - timedelta(days=1), f"/p{i}.jsonl")
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace)
assert r["status"] == "warning"
assert r["expected_sessions"] == 10
assert r["uploaded_entries"] == 2
assert "capture-session may be silently failing" in r["detail"]
def test_older_sessions_outside_window_ignored(tmp_path, monkeypatch):
"""Sessions outside the window must not count toward expected."""
workspace = tmp_path / "ws"
workspace.mkdir()
home = tmp_path / "home"
home.mkdir()
_set_home(monkeypatch, home)
# 5 ancient sessions (60d ago) + 1 recent
for i in range(5):
_make_session_file(home, workspace, f"old{i}.jsonl", age_days=60)
_make_session_file(home, workspace, "recent.jsonl", age_days=2)
now = datetime.now(timezone.utc)
_append_uploaded_log(workspace, now - timedelta(days=2), "/p/recent.jsonl")
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace, window_days=7)
assert r["status"] == "ok"
assert r["expected_sessions"] == 1
assert r["uploaded_entries"] == 1
def test_uploaded_entries_outside_window_ignored(tmp_path, monkeypatch):
"""Old uploaded-log entries don't count even if SessionStart count is high."""
workspace = tmp_path / "ws"
workspace.mkdir()
home = tmp_path / "home"
home.mkdir()
_set_home(monkeypatch, home)
for i in range(10):
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=1)
# 8 uploads but ancient — outside window
now = datetime.now(timezone.utc)
for i in range(8):
_append_uploaded_log(workspace, now - timedelta(days=60),
f"/p{i}.jsonl")
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace, window_days=7)
assert r["status"] == "warning"
assert r["expected_sessions"] == 10
assert r["uploaded_entries"] == 0
def test_threshold_respected(tmp_path, monkeypatch):
"""Delta within threshold stays ok (a couple unsynced sessions is fine)."""
workspace = tmp_path / "ws"
workspace.mkdir()
home = tmp_path / "home"
home.mkdir()
_set_home(monkeypatch, home)
for i in range(5):
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=1)
now = datetime.now(timezone.utc)
# 3 uploads of 5 events → delta=2, threshold=3 → still ok
for i in range(3):
_append_uploaded_log(workspace, now - timedelta(days=1), f"/p{i}.jsonl")
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace, window_days=7, threshold=3)
assert r["status"] == "ok"
assert r["expected_sessions"] == 5
assert r["uploaded_entries"] == 3
def test_malformed_uploaded_log_lines_skipped(tmp_path, monkeypatch):
"""Garbage in uploaded-log doesn't crash the check; only well-formed
timestamped lines count."""
workspace = tmp_path / "ws"
workspace.mkdir()
(workspace / ".claude").mkdir()
home = tmp_path / "home"
home.mkdir()
_set_home(monkeypatch, home)
for i in range(3):
_make_session_file(home, workspace, f"s{i}.jsonl", age_days=1)
log = workspace / ".claude" / "agnes-sessions-uploaded.txt"
now = datetime.now(timezone.utc)
log.write_text(
"totally bogus line\n"
"\n" # blank
"no-tab-just-a-path\n"
f"{(now - timedelta(days=1)).strftime('%Y-%m-%dT%H:%M:%SZ')}\t/p.jsonl\n"
"not-a-timestamp\tstill-has-a-tab\n",
encoding="utf-8",
)
from cli.lib.session_health import capture_session_health
r = capture_session_health(workspace, window_days=7, threshold=3)
assert r["expected_sessions"] == 3
assert r["uploaded_entries"] == 1