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.
206 lines
7.3 KiB
Python
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
|