From e623a0887dbb96c3a9a66b3346b8bf80b89e90d5 Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 28 May 2026 11:57:52 +0200 Subject: [PATCH] fix(tempo,evidence): fix SystemExit crash + evidence not shown in frontend tempo: tempoapiclient raises SystemExit (BaseException) on API errors like 'User is invalid' 400 responses; except Exception never catches it, killing the uvicorn worker and causing a 500. Wrap create_worklog() to intercept BaseException and re-raise as RuntimeError so callers can catch it safely. evidence: TestOut schema was missing red_evidences / blue_evidences fields. The ORM model has evidences loaded via joinedload but they were never serialized into the API response. Add both fields to TestOut and override model_validate to split Test.evidences by team, injecting the backend-proxy download_url for each one (/api/v1/evidence/{id}/file). Co-Authored-By: Claude Sonnet 4.6 --- backend/app/schemas/test.py | 31 ++++++++++++++++++++++++++- backend/app/services/tempo_service.py | 28 +++++++++++++++++------- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/backend/app/schemas/test.py b/backend/app/schemas/test.py index e1b57f9..0d4e2a9 100644 --- a/backend/app/schemas/test.py +++ b/backend/app/schemas/test.py @@ -7,6 +7,7 @@ from pydantic import BaseModel, ConfigDict from app.domain.enums import DataClassification from app.models.enums import TestResult, TestState +from app.schemas.evidence import EvidenceOut # ── Create ────────────────────────────────────────────────────────── @@ -166,12 +167,40 @@ class TestOut(BaseModel): technique_mitre_id: str | None = None technique_name: str | None = None + # Evidences split by team (populated from the ORM relationship) + red_evidences: list[EvidenceOut] = [] + blue_evidences: list[EvidenceOut] = [] + model_config = ConfigDict(from_attributes=True) @classmethod def model_validate(cls, obj, **kwargs): - """Override to populate technique fields from the relationship.""" + """Override to populate technique and evidence fields from ORM relationships.""" if hasattr(obj, "technique") and obj.technique is not None: obj.__dict__["technique_mitre_id"] = obj.technique.mitre_id obj.__dict__["technique_name"] = obj.technique.name + + # Split evidences by team and inject the backend-proxy download URL + if hasattr(obj, "evidences") and obj.evidences is not None: + red_evs: list[EvidenceOut] = [] + blue_evs: list[EvidenceOut] = [] + for ev in obj.evidences: + ev_out = EvidenceOut( + id=ev.id, + test_id=ev.test_id, + file_name=ev.file_name, + sha256_hash=ev.sha256_hash, + uploaded_by=ev.uploaded_by, + uploaded_at=ev.uploaded_at, + team=ev.team, + notes=ev.notes, + download_url=f"/api/v1/evidence/{ev.id}/file", + ) + if ev.team and ev.team.value == "blue": + blue_evs.append(ev_out) + else: + red_evs.append(ev_out) + obj.__dict__["red_evidences"] = red_evs + obj.__dict__["blue_evidences"] = blue_evs + return super().model_validate(obj, **kwargs) diff --git a/backend/app/services/tempo_service.py b/backend/app/services/tempo_service.py index a7e9568..fc17b5c 100644 --- a/backend/app/services/tempo_service.py +++ b/backend/app/services/tempo_service.py @@ -70,15 +70,27 @@ def log_worklog( time_spent_seconds: int, description: str, ) -> dict: - """Create a worklog entry in Tempo using *user*'s personal token.""" + """Create a worklog entry in Tempo using *user*'s personal token. + + Note: tempoapiclient raises SystemExit (not Exception) on API errors, so + we intercept BaseException and re-raise as RuntimeError to keep it non-fatal. + """ tempo = get_user_tempo_client(user) - return tempo.create_worklog( - accountId=author_account_id, - issueId=jira_issue_id, - dateFrom=date, - timeSpentSeconds=time_spent_seconds, - description=description, - ) + try: + return tempo.create_worklog( + accountId=author_account_id, + issueId=jira_issue_id, + dateFrom=date, + timeSpentSeconds=time_spent_seconds, + description=description, + ) + except Exception: + raise + except BaseException as exc: + # tempoapiclient raises SystemExit on HTTP errors (e.g. 400 Bad Request). + # SystemExit is a BaseException, not Exception, so convert it so callers + # can catch it with the usual `except Exception` pattern. + raise RuntimeError(f"Tempo API error: {exc}") from exc def auto_log_test_worklog(