Skip to content

Commit 0cd165f

Browse files
fix: split parse/stats errors in export and stats routes; tighten tests
1 parent 7c6b640 commit 0cd165f

6 files changed

Lines changed: 65 additions & 11 deletions

File tree

api/export_api.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,38 @@ def export_session(project_name, session_id):
284284
)
285285

286286
fmt = request.args.get("format", "md")
287-
session = parse_session(filepath)
287+
try:
288+
session = parse_session(filepath)
289+
except Exception:
290+
current_app.logger.exception(
291+
"Failed to parse session %s for export", session_id
292+
)
293+
return error_response(
294+
ErrorCode.PARSE_ERROR,
295+
"Failed to parse session",
296+
400,
297+
)
298+
288299
rules = current_app.config.get("EXCLUSION_RULES") or []
289300
if is_session_excluded(rules, session, project_name):
290301
return error_response(
291302
ErrorCode.SESSION_NOT_FOUND,
292303
"Session not found",
293304
404,
294305
)
295-
stats = compute_stats(session)
306+
307+
try:
308+
stats = compute_stats(session)
309+
except Exception:
310+
current_app.logger.exception(
311+
"Failed to compute stats for export %s", session_id
312+
)
313+
return error_response(
314+
ErrorCode.INTERNAL_ERROR,
315+
"Failed to compute session stats",
316+
500,
317+
)
318+
296319
title_slug = slugify(session["title"], default="session")
297320

298321
if fmt == "json":

api/sessions.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ def get_session_stats(project_name, session_id):
6464

6565
try:
6666
session = parse_session(filepath)
67+
except Exception:
68+
current_app.logger.exception("Failed to parse session %s", session_id)
69+
return error_response(
70+
ErrorCode.PARSE_ERROR,
71+
"Failed to parse session",
72+
400,
73+
)
74+
75+
try:
6776
stats = compute_stats(session)
6877
return jsonify(stats)
6978
except Exception:

tests/test_api_routes.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def _boom(*_args, **_kwargs):
5656
def test_search_limit_capped_at_max(client):
5757
resp = client.get("/api/search?q=Hello&limit=9999")
5858
assert resp.status_code == 200
59-
assert len(resp.get_json()) <= 500
59+
results = resp.get_json()
60+
assert isinstance(results, list)
61+
assert len(results) <= 500
6062

6163

6264
def test_project_sessions_invalid_path_returns_400_empty_list(client):

tests/test_cli_e2e.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,17 @@ def _seed_base_dir(tmp_path: Path) -> Path:
4040
project_dir = tmp_path / "test-project"
4141
project_dir.mkdir(parents=True)
4242
dest = project_dir / "session_abc123.jsonl"
43-
dest.write_text(
44-
(FIXTURES / "session_minimal.jsonl").read_text(encoding="utf-8"),
45-
encoding="utf-8",
46-
)
43+
content = (FIXTURES / "session_minimal.jsonl").read_text(encoding="utf-8")
44+
content = content.replace("demo-project", "test-project")
45+
dest.write_text(content, encoding="utf-8")
4746
return tmp_path
4847

4948

5049
def test_cli_list_exits_zero(tmp_path):
5150
base = _seed_base_dir(tmp_path)
5251
proc = _run_cli(["list", "--base-dir", str(base)])
5352
assert proc.returncode == 0
54-
assert "test-project" in proc.stdout or "Project" in proc.stdout
53+
assert "test-project" in proc.stdout.lower()
5554

5655

5756
def test_cli_list_unknown_project_exits_zero_with_message(tmp_path):
@@ -70,6 +69,8 @@ def test_cli_stats_exits_zero(tmp_path):
7069
def test_cli_invalid_since_exits_nonzero():
7170
proc = _run_cli(["--since", "yesterday"])
7271
assert proc.returncode != 0
72+
assert "--since" in proc.stderr
73+
assert "invalid choice" in proc.stderr.lower()
7374

7475

7576
def test_cli_export_creates_output(tmp_path):

tests/test_error_propagation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,10 @@ def _boom(*args, **kwargs):
154154
monkeypatch.setattr("api.sessions.parse_session", _boom)
155155

156156
resp = client.get("/api/sessions/proj/abc/stats")
157-
assert resp.status_code == 500
157+
assert resp.status_code == 400
158158
body = resp.get_json()
159-
assert body.get("error") == "Failed to compute session stats"
159+
assert body.get("error") == "Failed to parse session"
160+
assert body.get("code") == "PARSE_ERROR"
160161
# The exception value contains a fake-secret path — must not leak.
161162
assert "/private/path" not in json.dumps(body)
162163
_assert_no_class_name_leak(json.dumps(body))

tests/test_search.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,28 @@
77

88
from tests.conftest import assert_error_response
99

10+
_SEARCH_HIT_KEYS = frozenset({
11+
"project",
12+
"session_id",
13+
"title",
14+
"role",
15+
"timestamp",
16+
"snippet",
17+
})
18+
19+
20+
def _assert_search_hits(results: list, *, max_items: int) -> None:
21+
assert isinstance(results, list)
22+
assert len(results) <= max_items
23+
for item in results:
24+
assert isinstance(item, dict)
25+
assert _SEARCH_HIT_KEYS.issubset(item.keys())
26+
1027

1128
def test_limit_integer_string(client_single):
1229
resp = client_single.get("/api/search?q=Hello&limit=10")
1330
assert resp.status_code == 200
14-
assert isinstance(resp.get_json(), list)
31+
_assert_search_hits(resp.get_json(), max_items=10)
1532

1633

1734
def test_limit_float_string(client_single):
@@ -29,6 +46,7 @@ def test_limit_non_numeric(client_single):
2946
def test_limit_default(client_single):
3047
resp = client_single.get("/api/search?q=Hello")
3148
assert resp.status_code == 200
49+
_assert_search_hits(resp.get_json(), max_items=50)
3250

3351

3452
def test_limit_whitespace_defaults(client_single):

0 commit comments

Comments
 (0)