Accept reporter-supplied requirement external IDs on external case result creation - #324
Conversation
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/16143958-4342-4233-89bd-610c3abaff17 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/16143958-4342-4233-89bd-610c3abaff17 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/16143958-4342-4233-89bd-610c3abaff17 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
|
LGTM ✅ — merging despite the unrelated Why the failing check is not blockingThe failing test is
Zero touches to For comparison: PR #322 (the projects API) passed Playwright cleanly at its merge SHA What's being done about itTracking the regression separately as a new issue (forthcoming, title: Acceptance check on this PR
Strong execution on the audit-details non-pollution pattern — gating the three new keys on Next in the #316 chain (for tracking)
Merging this. 🚀 |
This PR implements the BGSTM half of requirement external-ID ingestion for
POST /api/v1/external-results/case. Reporters can now submit requirement external IDs alongside UUIDs; BGSTM resolves them to requirement links, optionally auto-registers missing requirements, and records unresolved IDs in audit details without changing the response shape.Schema: additive request fields
requirement_external_ids: list[str] | NonetoCaseResultCreateauto_register_requirements: bool = Falseto opt into stub requirement creation422Case-result CRUD: external ID resolution
requirement_external_idsagainstrequirements.external_idrequirement_idsrequirement_external_ids(test_case_id, requirement_id)linksauto_register_requirements=true, creates stubRequirementrows with:external_id=<submitted id>title=<submitted id>description="Auto-registered from external ID <submitted id>"type=FUNCTIONAL,priority=MEDIUM,status=DRAFTunresolved_requirement_external_idsattribute for audit wiringAPI audit details: additive only
requirement_external_idswas submitted and non-empty:requirement_external_ids_submittedunresolved_requirement_external_idsauto_register_requirementsIntegration coverage
auto_register_requirements=false→ dropped, audited as unresolvedauto_register_requirements=true→ stub requirement created and linkednull/[]external IDs → no-op, no audit pollution422Spec updates
requirement_external_idsauto_register_requirementsExample request:
{ "session_id": "550e8400-e29b-41d4-a716-446655440000", "external_id": "suite > login > should redirect to dashboard", "title": "should redirect to dashboard", "outcome": "passed", "duration_ms": 1234, "requirement_ids": ["c1234567-89ab-cdef-0123-456789abcdef"], "requirement_external_ids": ["REQ-LOGIN-001", "REQ-LOGIN-002"], "auto_register_requirements": false }Original prompt
Goal
Implement the BGSTM half of issue #316 — accept reporter-supplied requirement external IDs on
CaseResultCreateand resolve them to UUID requirement links.This is PR 1 of 2 for #316. The reporter-side change (in
bg-playground/bgstm-playwright-frameworks) will be a separate PR after this one merges. Do NOT auto-close #316 from this PR — it stays open until the reporter PR + smoke pin-bump PR also land.Decisions already made — do not re-open
requirement_external_ids: list[str] | None— additive toCaseResultCreate, alongside the existingrequirement_ids: list[UUID].requirements.external_id(alreadyunique=True, indexed).auto_register_requirements: bool = False— additive toCaseResultCreate. Default false. Unknown IDs are dropped + recorded in audit-log details underunresolved_requirement_external_ids. Whentrue, unknown IDs auto-create stubRequirementrows and link them.bootstrap.pyin a later PR —auto_register=truestays as a "danger zone" feature, default-off. Do NOT modifyscripts/smoke/bootstrap.pyin this PR.Scope
1. Schema changes —
backend/app/schemas/external_results.pyAdd to
CaseResultCreate:Add a Pydantic v2 validator that strips whitespace and rejects empty strings within the list (422), but allows the list itself to be omitted or
[].CaseResultResponsedoes NOT need to change — it already returnsrequirement_ids: list[UUID]. The resolved UUIDs join the response list naturally.2. CRUD changes —
backend/app/crud/external_case_results.py(or wherevercreate_case_resultlives)In
create_case_result, after the case-result row is committed but before audit-log emission:payload.requirement_ids(UUIDs already) and the resolved-to-UUID list frompayload.requirement_external_ids.payload.requirement_external_ids:Requirementbyexternal_id.payload.auto_register_requirements=True: create a stubRequirementrow with reasonable defaults (title=external_id,description=f"Auto-registered from external ID {external_id}",type=FUNCTIONAL,priority=MEDIUM,status=DRAFT,external_id=external_id). Add new UUID to resolution set.unresolved_external_ids: list[str]accumulator.RequirementTestCaseLinkrows for any new (case_result_id, requirement_id) pairs that don't already exist. Existing duplicates are no-ops.requirement_idslist (resolved UUIDs)unresolved_requirement_external_ids: list[str]attribute (set as a transient attribute, not persisted) so the API layer can surface it in audit details.3. API layer —
backend/app/api/external_results.pyIn
create_external_case_result, the audit-logdetailsblock already includes a snapshot of relevant fields. Add to the auditdetails(only whenrequirement_external_idswas non-empty in the payload):Don't break the existing audit shape — these are additive keys. Existing smoke and tests should keep passing.
4. Tests — new file
backend/tests/test_external_results_requirement_links.pyMirror the structure of existing case-result tests. Cover at minimum:
requirement_idsincludes the resolved UUID,RequirementTestCaseLinkrow exists.unresolved_requirement_external_ids: ["REQ-MISSING"].Requirementrow created withexternal_id="REQ-MISSING", link present,requirement_idsincludes its UUID.requirement_ids(UUIDs) andrequirement_external_ids(strings) → union ...This pull request was created from Copilot chat.