Implement External Results v1 case-result API (create/update/get) with auto-upsert, transition guards, traceability linking, and audit coverage - #317
Conversation
… and tests Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/434f0dd2-8f2f-440c-b288-e95f447e8ea3 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/434f0dd2-8f2f-440c-b288-e95f447e8ea3 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/434f0dd2-8f2f-440c-b288-e95f447e8ea3 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/434f0dd2-8f2f-440c-b288-e95f447e8ea3 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
…esolution Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/434f0dd2-8f2f-440c-b288-e95f447e8ea3 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/434f0dd2-8f2f-440c-b288-e95f447e8ea3 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
|
The CI failure here is the classic Alembic + PG enum double-create bug. Backend container exits during In
Required fixAdd case_outcome_enum = postgresql.ENUM(
"started", "passed", "failed", "skipped", "flaky", "aborted",
name="case_outcome",
create_type=False, # ← add this; manual .create(checkfirst=True) below handles creation
)
case_outcome_enum.create(op.get_bind(), checkfirst=True)That's the only change needed in the migration to unblock CI. Also please fix (correctness, before re-running CI)The model-side enum in outcome = Column(Enum(CaseStatus, values_callable=_enum_values), nullable=False)SQLAlchemy will derive a name from the Python enum class (something like Please change to: outcome = Column(
Enum(
CaseStatus,
name="case_outcome",
values_callable=_enum_values,
create_type=False,
),
nullable=False,
)This matches the precedent set by One reasonability check (no code change expected — just confirm)
Once those two changes land, CI should be green. Thanks! |
…odel enum name Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/a476c823-ef02-40ac-a4db-b6221128af09 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Addressed in I updated the migration to set I also validated the downgrade/upgrade round-trip on a fresh Postgres container:
Round-trip completed successfully. |
case_outcomeenum creation pathcreate_type=Falseon the PG enumname="case_outcome"andcreate_type=Falsefor parity with migrationupgrade head,downgrade -1,upgrade head) with PostgreSQLOriginal prompt
Implement the case-result endpoints for the External Results v1 contract. Closes #303 (parent: #291).
This is the case-result link in the v0.1 contract chain:
The Pydantic schemas, audit-write helper, and the parent session model already exist on
main— seebackend/app/schemas/external_results.py,backend/app/crud/audit_log.py::write_audit, andbackend/app/models/external_results.py::ExternalRunSession. Don't re-define any of those. Build on them.Endpoints
All under
/api/v1/external-results:POST/caseexternal_results:write201 Created+CaseResultResponse(or200 OKon idempotent collision)PATCH/case/{case_result_id}external_results:write200 OK+CaseResultResponseGET/case/{case_result_id}200 OK+CaseResultResponseMirror the auth pattern in the existing session endpoints (
backend/app/api/external_results.py):Depends(require_runner_scope("external_results:write"))._get_session_authfrom the same module if it can be parameterized cleanly, otherwise factor it into a shared dependency inapp/auth/dependencies.pynamedget_runner_or_user_auth. Don't duplicate the helper inline — that's the kind of drift we're trying to avoid.Schemas — already merged, do NOT modify
backend/app/schemas/external_results.pyalready defines:CaseResultCreate— hassession_id,test_case_id?,external_id?,title,outcome,duration_ms,error_message?,requirement_ids: list[UUID], plus amodel_validatorthat enforces "at least one oftest_case_idorexternal_idmust be provided"CaseResultResponse— hasid,session_id,test_case_id?,external_id?,title,outcome,duration_ms,error_message?,requirement_ids: list[UUID],created_at,auto_registeredCaseResultUpdate— hasoutcome?,duration_ms?,error_message?CaseOutcomeenum —passed | failed | skipped | flakyThese are frozen for v0.1. Don't edit them.
Auto-upsert rules
test_case_idprovided + exists intest_casestableauto_registered=False.test_case_idprovided + does not exist404withcode: case.test_case_not_foundexternal_idprovided(project_id, external_id)intest_cases. Found → link,auto_registered=False. Not found → create newtest_casesrow withauto_registered=Trueand link.422(already enforced byCaseResultCreate.model_validator; let Pydantic handle it — don't duplicate)The session's
project_idcomes from the parentexternal_run_sessionsrow. Resolvesession_id → project_idvia a single SELECT before the upsert. Ifsession_iddoesn't exist, return404 case.session_not_found.For the auto-upsert path, the new
test_casesrow needs sensible defaults. Look atbackend/app/models/test_case.pyfor required fields. Required minimums:id: new UUIDproject_id: from sessionexternal_id: from payloadtitle: from payloadauto_registered:True(add this column to the test-case model if it doesn't exist; it's referenced in the response and acceptance criteria —CaseResultResponse.auto_registeredexists in the schema already, but verify the model has anauto_registeredcolumn. If not, add via migration in this PR — single column, default false, server-side default. Keep migration minimal.)Important:
CaseResultResponse.auto_registeredis per-case-result, not per-test-case. It reflects "did this POST trigger an auto-creation." Ifexternal_idlooked up an existing test case, returnauto_registered=Falseeven if that test case happens to have been auto-registered by an earlier call.Status transitions
started→ any: allowedpassed/failed/skipped→ onlyflaky(re-classification window): allowedflaky→flaky: allowed (idempotent)aborted→ anything: blocked (terminal)Violations on
PATCH /case/{id}return409withcode: case.transition.invalidand a structureddetails: { current_status, requested_status }. Match the pattern inapp/crud/external_results.py::finish_session_dbfor the error envelope.Traceability auto-link
If
requirement_ids: [uuid, ...]is provided onPOST /case:requirement_id, write an idempotent row into thetraceabilitytable linking the test case (the resolved or auto-created on...This pull request was created from Copilot chat.