From adf2e9c5ff88d63aa03872c53bfe08cee7017bc3 Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Fri, 31 Jul 2026 10:12:27 -0600 Subject: [PATCH] fix(import): guard both detail reads and pin the status contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-on to #391. The detail endpoint's second metadata read sat outside the not-found guard, so an import removed between the two reads raised instead of returning import_not_found. Both reads now share the guard; status resolution stays outside it, since it touches no filesystem. Adds the server-side coverage the change was missing. The JSON fixtures pin the client derivation, but nothing asserted that the endpoint itself returns status, error and error_stage — the half a later refactor drops silently. Two tests now pin it: an unfinished import reads running, and a failed one carries its error and stage through. --- CHANGELOG.md | 4 ++ solstone/apps/import/routes.py | 15 ++++--- solstone/apps/import/tests/test_routes.py | 54 +++++++++++++++++++++++ 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 581bf2756..bc949ef74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ Format adapted from [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), al ## [Unreleased] +### Fixed + +- an import you open while it is still working now says it is running. the import history listed it correctly, but opening that same import showed it as failed until it finished — on every file type, and most noticeable on longer imports. the page now also keeps up on its own while the import runs, instead of waiting for a reload. reported and fixed by ha-ye. + ## [1.0.20] - 2026-07-30 ### Added diff --git a/solstone/apps/import/routes.py b/solstone/apps/import/routes.py index cc101f60e..34f16312d 100644 --- a/solstone/apps/import/routes.py +++ b/solstone/apps/import/routes.py @@ -910,16 +910,17 @@ def import_detail_api(timestamp: str) -> Any: journal_root=Path(state.journal_root), timestamp=timestamp, ) + # Resolve status the same way the history list does, so an in-progress + # import (task_id present, no imported.json yet) reads as "running" + # rather than falling through to "failed". Both filesystem reads stay + # inside the guard: an import removed between them is not found, not a 500. + import_data = build_import_info( + journal_root=Path(state.journal_root), + timestamp=timestamp, + ) except FileNotFoundError: return error_response(IMPORT_NOT_FOUND, detail="Import not found") - # Resolve status the same way the history list does, so an in-progress - # import (task_id present, no imported.json yet) reads as "running" - # rather than falling through to "failed". - import_data = build_import_info( - journal_root=Path(state.journal_root), - timestamp=timestamp, - ) resolution = resolve_import_status(import_data) result["status"] = resolution.status result["error"] = resolution.error diff --git a/solstone/apps/import/tests/test_routes.py b/solstone/apps/import/tests/test_routes.py index 011caaf06..58da0a3f9 100644 --- a/solstone/apps/import/tests/test_routes.py +++ b/solstone/apps/import/tests/test_routes.py @@ -189,6 +189,60 @@ def test_import_missing_detail_api_still_returns_not_found(client): assert response.get_json()["reason_code"] == "import_not_found" +def _seed_import_json(journal: Path, timestamp: str, payload: dict) -> None: + import_dir = journal / "imports" / timestamp + import_dir.mkdir(parents=True, exist_ok=True) + (import_dir / "import.json").write_text( + json.dumps(payload) + "\n", + encoding="utf-8", + ) + + +def test_import_detail_api_reports_running_for_an_unfinished_import(client): + import importlib + + import_routes = importlib.import_module("solstone.apps.import.routes") + journal = Path(import_routes.state.journal_root) + _seed_import_json( + journal, + "20260101_120000", + {"original_filename": "in-progress.pdf", "task_id": "123"}, + ) + + response = client.get("/app/import/api/20260101_120000") + + assert response.status_code == 200 + body = response.get_json() + assert body["status"] == "running" + assert body["error"] is None + assert body["error_stage"] is None + + +def test_import_detail_api_reports_failed_with_error_and_stage(client): + import importlib + + import_routes = importlib.import_module("solstone.apps.import.routes") + journal = Path(import_routes.state.journal_root) + _seed_import_json( + journal, + "20260101_130000", + {"original_filename": "broken.pdf", "task_id": "456"}, + ) + _seed_imported_json( + journal, + "20260101_130000", + {"error": "bad ", "error_stage": "writing"}, + ) + + response = client.get("/app/import/api/20260101_130000") + + assert response.status_code == 200 + body = response.get_json() + assert body["status"] == "failed" + assert body["error"] == "bad " + assert body["error_stage"] == "writing" + + def test_import_detail_api_path_resolves(client): adapter = client.application.url_map.bind("localhost") -- 2.51.2