|
7 | 7 |
|
8 | 8 | - GET /api/sessions/<project>/<id> (api/sessions.py) |
9 | 9 | - GET /api/sessions/<project>/<id>/stats (api/sessions.py) |
10 | | -- GET /api/projects (per-session card error_detail) |
| 10 | +- GET /api/projects/<project>/sessions (api/projects.py — per-session card error_detail) |
11 | 11 |
|
12 | 12 | This file exercises each via Flask test_client with a payload that triggers |
13 | 13 | the failure path, asserts a 500 (or 200 for projects, since the per-session |
@@ -168,24 +168,38 @@ def _boom(*args, **kwargs): |
168 | 168 |
|
169 | 169 | class TestGetProjectsErrorCard: |
170 | 170 |
|
171 | | - def test_per_session_error_card_omits_error_detail(self, tmp_path, client): |
172 | | - # One project with one unparseable session file → the per-session |
173 | | - # card should have error: True but no error_detail string. |
174 | | - _write_session(tmp_path, "myproj", "deadbeef-aaaa-bbbb-cccc-000000000000", "totally not jsonl\n") |
| 171 | + def test_per_session_error_card_omits_error_detail(self, tmp_path, client, monkeypatch): |
| 172 | + # parse_session is tolerant of malformed lines, so to exercise the |
| 173 | + # except branch deterministically (the one that builds the error |
| 174 | + # card), monkeypatch it to raise — same pattern as the session-level |
| 175 | + # tests above. |
| 176 | + _write_session(tmp_path, "myproj", "deadbeef-aaaa-bbbb-cccc-000000000000", "{}") |
| 177 | + |
| 178 | + def _boom(*args, **kwargs): |
| 179 | + raise KeyError("internal_secret_field_id") |
| 180 | + |
| 181 | + # api/projects.py imports parse_session inside the handler body, |
| 182 | + # so patch the source module rather than the consumer. |
| 183 | + monkeypatch.setattr("utils.jsonl_parser.parse_session", _boom) |
175 | 184 |
|
176 | 185 | resp = client.get("/api/projects/myproj/sessions") |
177 | | - # Response shape varies — accept 200 with a list, 200 with a dict, |
178 | | - # or any error code as long as it doesn't leak class names. |
| 186 | + # Pin the response shape so a future wrapper change (e.g. {"sessions": [...]}) |
| 187 | + # doesn't silently turn this test green by skipping the per-row scan. |
| 188 | + assert resp.status_code == 200 |
179 | 189 | body = resp.get_json() |
180 | | - body_text = json.dumps(body) if body is not None else "" |
181 | | - _assert_no_class_name_leak(body_text) |
182 | | - # If there's a per-row error, error_detail must NOT be present |
183 | | - if isinstance(body, list): |
184 | | - for row in body: |
185 | | - if isinstance(row, dict) and row.get("error"): |
186 | | - assert "error_detail" not in row, ( |
187 | | - "Per-session error card still includes error_detail (issue #25)" |
188 | | - ) |
| 190 | + assert isinstance(body, list), ( |
| 191 | + f"Expected JSON array of session cards; got {type(body).__name__}" |
| 192 | + ) |
| 193 | + _assert_no_class_name_leak(json.dumps(body)) |
| 194 | + error_rows = [r for r in body if isinstance(r, dict) and r.get("error")] |
| 195 | + assert error_rows, "Expected at least one per-session error card from the forced parse failure" |
| 196 | + for row in error_rows: |
| 197 | + assert "error_detail" not in row, ( |
| 198 | + "Per-session error card still includes error_detail (issue #25)" |
| 199 | + ) |
| 200 | + # The exception's args include "internal_secret_field_id" — must not |
| 201 | + # appear anywhere in the response. |
| 202 | + assert "internal_secret_field_id" not in json.dumps(body) |
189 | 203 |
|
190 | 204 |
|
191 | 205 | # --------------------------------------------------------------------------- |
|
0 commit comments