|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
| 5 | +import io |
5 | 6 | import json |
6 | 7 | import sys |
| 8 | +import zipfile |
7 | 9 | from pathlib import Path |
8 | 10 |
|
9 | 11 | import pytest |
|
14 | 16 | from flask import Flask |
15 | 17 |
|
16 | 18 | from api.export_api import export_bp |
| 19 | +from utils.jsonl_parser import parse_session |
17 | 20 |
|
18 | 21 |
|
19 | 22 | @pytest.fixture |
@@ -69,6 +72,70 @@ def test_bulk_export_empty_returns_422_json(isolated_state, tmp_path): |
69 | 72 | assert body["since"] == "all" |
70 | 73 |
|
71 | 74 |
|
| 75 | +def test_bulk_export_all_succeed_no_warnings_header(client): |
| 76 | + resp = client.post("/api/export", json={"since": "all"}) |
| 77 | + assert resp.status_code == 200 |
| 78 | + assert resp.content_type.startswith("application/zip") |
| 79 | + assert "X-Export-Warnings" not in resp.headers |
| 80 | + zf = zipfile.ZipFile(io.BytesIO(resp.data)) |
| 81 | + md_files = [name for name in zf.namelist() if name.endswith(".md")] |
| 82 | + assert len(md_files) == 2 |
| 83 | + |
| 84 | + |
| 85 | +def test_bulk_export_partial_fail_returns_warning_header(client, monkeypatch): |
| 86 | + real_parse = parse_session |
| 87 | + |
| 88 | + def flaky_parse(path: str): |
| 89 | + if path.endswith("session_def456.jsonl"): |
| 90 | + raise json.JSONDecodeError("bad", "doc", 0) |
| 91 | + return real_parse(path) |
| 92 | + |
| 93 | + monkeypatch.setattr("utils.export_engine.parse_session", flaky_parse) |
| 94 | + resp = client.post("/api/export", json={"since": "all"}) |
| 95 | + assert resp.status_code == 200 |
| 96 | + assert "X-Export-Warnings" in resp.headers |
| 97 | + warnings = json.loads(resp.headers["X-Export-Warnings"]) |
| 98 | + assert len(warnings) == 1 |
| 99 | + assert warnings[0]["session_id"] == "session_def456" |
| 100 | + assert warnings[0]["code"] == "PARSE_ERROR" |
| 101 | + zf = zipfile.ZipFile(io.BytesIO(resp.data)) |
| 102 | + assert len([name for name in zf.namelist() if name.endswith(".md")]) == 1 |
| 103 | + |
| 104 | + |
| 105 | +def test_bulk_export_all_fail_returns_422(client, monkeypatch): |
| 106 | + def always_fail(path: str): |
| 107 | + raise json.JSONDecodeError("bad", "doc", 0) |
| 108 | + |
| 109 | + monkeypatch.setattr("utils.export_engine.parse_session", always_fail) |
| 110 | + resp = client.post("/api/export", json={"since": "all"}) |
| 111 | + assert resp.status_code == 422 |
| 112 | + body = resp.get_json() |
| 113 | + assert body["code"] == "EXPORT_ALL_FAILED" |
| 114 | + assert body["since"] == "all" |
| 115 | + assert len(body["failures"]) == 2 |
| 116 | + assert {item["code"] for item in body["failures"]} == {"PARSE_ERROR"} |
| 117 | + |
| 118 | + |
| 119 | +def test_bulk_export_partial_fail_excludes_failed_from_state( |
| 120 | + client, monkeypatch, export_state_file |
| 121 | +): |
| 122 | + real_parse = parse_session |
| 123 | + |
| 124 | + def flaky_parse(path: str): |
| 125 | + if path.endswith("session_def456.jsonl"): |
| 126 | + raise json.JSONDecodeError("bad", "doc", 0) |
| 127 | + return real_parse(path) |
| 128 | + |
| 129 | + monkeypatch.setattr("utils.export_engine.parse_session", flaky_parse) |
| 130 | + resp = client.post("/api/export", json={"since": "all"}) |
| 131 | + assert resp.status_code == 200 |
| 132 | + |
| 133 | + state = json.loads(export_state_file.read_text(encoding="utf-8")) |
| 134 | + sessions = state.get("sessions", {}) |
| 135 | + assert "session_abc123" in sessions |
| 136 | + assert "session_def456" not in sessions |
| 137 | + |
| 138 | + |
72 | 139 | def test_export_state_json_fields(isolated_state): |
73 | 140 | isolated_state.write_text( |
74 | 141 | json.dumps( |
|
0 commit comments