Skip to content

Accept reporter-supplied requirement external IDs on external case result creation - #324

Merged
bg-playground merged 4 commits into
mainfrom
copilot/add-requirement-external-ids
May 8, 2026
Merged

Accept reporter-supplied requirement external IDs on external case result creation#324
bg-playground merged 4 commits into
mainfrom
copilot/add-requirement-external-ids

Conversation

Copilot AI commented May 8, 2026

Copy link
Copy Markdown
Contributor

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

    • Added requirement_external_ids: list[str] | None to CaseResultCreate
    • Added auto_register_requirements: bool = False to opt into stub requirement creation
    • Added Pydantic validation to trim each external ID and reject empty / whitespace-only entries with 422
  • Case-result CRUD: external ID resolution

    • Resolved requirement_external_ids against requirements.external_id
    • Built the final link set as the deduplicated union of:
      • submitted requirement_ids
      • UUIDs resolved from requirement_external_ids
    • Preserved no-op behavior for duplicate (test_case_id, requirement_id) links
    • When auto_register_requirements=true, creates stub Requirement rows with:
      • external_id=<submitted id>
      • title=<submitted id>
      • description="Auto-registered from external ID <submitted id>"
      • type=FUNCTIONAL, priority=MEDIUM, status=DRAFT
    • Exposed unresolved external IDs as a transient unresolved_requirement_external_ids attribute for audit wiring
  • API audit details: additive only

    • Extended create-case audit details to include the following keys only when requirement_external_ids was submitted and non-empty:
      • requirement_external_ids_submitted
      • unresolved_requirement_external_ids
      • auto_register_requirements
    • Kept the existing audit shape intact for callers that do not use the new fields
  • Integration coverage

    • Added focused tests for:
      • known external ID → resolves and links
      • unknown external ID + auto_register_requirements=false → dropped, audited as unresolved
      • unknown external ID + auto_register_requirements=true → stub requirement created and linked
      • mixed UUID + external-ID payloads → deduplicated union
      • null / [] external IDs → no-op, no audit pollution
      • whitespace-only entries → 422
      • idempotent re-POST → existing case returned, no duplicate links inserted
  • Spec updates

    • Updated external-results spec § c.4 to document:
      • requirement_external_ids
      • auto_register_requirements
      • default unresolved-ID behavior vs. auto-register mode
    • Updated § d to state that links produced from resolved external IDs follow the same duplicate no-op rule as direct UUID links

Example 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 CaseResultCreate and 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

  • Field name: requirement_external_ids: list[str] | None — additive to CaseResultCreate, alongside the existing requirement_ids: list[UUID].
  • Resolution: Look up each external ID against requirements.external_id (already unique=True, indexed).
  • Auto-register flag: auto_register_requirements: bool = False — additive to CaseResultCreate. Default false. Unknown IDs are dropped + recorded in audit-log details under unresolved_requirement_external_ids. When true, unknown IDs auto-create stub Requirement rows and link them.
  • Smoke gets requirements pre-created in bootstrap.py in a later PR — auto_register=true stays as a "danger zone" feature, default-off. Do NOT modify scripts/smoke/bootstrap.py in this PR.

Scope

1. Schema changes — backend/app/schemas/external_results.py

Add to CaseResultCreate:

requirement_external_ids: list[str] | None = Field(
    default=None,
    description="Reporter-supplied external IDs. Each is resolved against requirements.external_id; "
                "unresolved IDs are dropped (or auto-registered if auto_register_requirements=True) and "
                "recorded in the audit-log details.",
)
auto_register_requirements: bool = Field(
    default=False,
    description="If true, unknown requirement_external_ids cause stub Requirement rows to be created "
                "and linked. Default false — unknown IDs are dropped silently (with audit-log diagnostics).",
)

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 [].

CaseResultResponse does NOT need to change — it already returns requirement_ids: list[UUID]. The resolved UUIDs join the response list naturally.

2. CRUD changes — backend/app/crud/external_case_results.py (or wherever create_case_result lives)

In create_case_result, after the case-result row is committed but before audit-log emission:

  1. Build the resolution set from payload.requirement_ids (UUIDs already) and the resolved-to-UUID list from payload.requirement_external_ids.
  2. For each external ID in payload.requirement_external_ids:
    • Look up Requirement by external_id.
    • If found → add UUID to the resolution set.
    • If not found:
      • If payload.auto_register_requirements=True: create a stub Requirement row 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.
      • Else: append external_id to a separate unresolved_external_ids: list[str] accumulator.
  3. Deduplicate the resolution set; create RequirementTestCaseLink rows for any new (case_result_id, requirement_id) pairs that don't already exist. Existing duplicates are no-ops.
  4. Return the case_result with both:
    • the populated requirement_ids list (resolved UUIDs)
    • a new 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.py

In create_external_case_result, the audit-log details block already includes a snapshot of relevant fields. Add to the audit details (only when requirement_external_ids was non-empty in the payload):

"requirement_external_ids_submitted": payload.requirement_external_ids,
"unresolved_requirement_external_ids": getattr(case_result, "unresolved_requirement_external_ids", []),
"auto_register_requirements": payload.auto_register_requirements,

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.py

Mirror the structure of existing case-result tests. Cover at minimum:

  • Known external ID → case persists, requirement_ids includes the resolved UUID, RequirementTestCaseLink row exists.
  • Unknown external ID + auto_register=False → case persists, link skipped, audit-log details includes unresolved_requirement_external_ids: ["REQ-MISSING"].
  • Unknown external ID + auto_register=True → case persists, new stub Requirement row created with external_id="REQ-MISSING", link present, requirement_ids includes its UUID.
  • Mixed: both requirement_ids (UUIDs) and requirement_external_ids (strings) → union ...

This pull request was created from Copilot chat.

Copilot AI and others added 3 commits May 8, 2026 20:21
Copilot AI changed the title [WIP] Implement reporter-supplied requirement external IDs in CaseResultCreate Accept reporter-supplied requirement external IDs on external case result creation May 8, 2026
Copilot AI requested a review from bg-playground May 8, 2026 20:25
@bg-playground

Copy link
Copy Markdown
Owner

LGTM ✅ — merging despite the unrelated Playwright E2E Tests red.

Why the failing check is not blocking

The failing test is frontend/tests/e2e/rbac.spec.ts:89 ("admin can access user management page without 403 or login redirect"). It is provably unrelated to this PR's changes:

Files touched by #324 Affects RBAC / /admin/users / auth?
backend/app/api/external_results.py (audit-log details, additive) No
backend/app/crud/external_case_results.py (external-ID resolution) No
backend/app/schemas/external_results.py (new optional requirement_external_ids field) No
backend/tests/test_external_results_requirement_links.py (new tests) No
docs/specs/external_results_v1.md (spec wording) No

Zero touches to frontend/**, RBAC code, or auth/admin seeding. The failure pattern (admin → /admin/users → page renders text matching /403|forbidden|access denied/i) reproduces consistently across all three retries, which means it's deterministic in the current CI environment — not a flake intermittently triggered by this PR.

For comparison: PR #322 (the projects API) passed Playwright cleanly at its merge SHA f56af4f. PR #323 was docs-only. The regression appeared on main HEAD ac5dd0b — somewhere between #322 merging and now — and is not caused by anything here.

What's being done about it

Tracking the regression separately as a new issue (forthcoming, title: [bug] frontend e2e: RBAC admin /admin/users test sees forbidden text). Likely either an over-broad assertion regex in the test (/403|forbidden|access denied/i will match a lot of benign text) or a real /admin/users page-render issue introduced post-#322. Will triage there.

Acceptance check on this PR

Item Status
requirement_external_ids field accepted on CaseResultCreate, additive
auto_register_requirements flag, default false
Whitespace/empty-string entries → 422
Known external IDs resolve and produce RequirementTestCaseLink rows
Unknown IDs + auto_register=False → dropped, audited under unresolved_requirement_external_ids
Unknown IDs + auto_register=True → stub Requirement rows with external_id populated, link created
Mixed UUID + external-ID payloads → deduplicated union
Audit-log details only includes the three new keys when requirement_external_ids was non-empty (no pollution)
Idempotent re-POST → existing case returned, no duplicate links
Spec § c.4 updated; §d traceability-links subsection mentions external-ID resolution
External Results contract smoke job green
Existing case-result tests still pass

Strong execution on the audit-details non-pollution pattern — gating the three new keys on if payload.requirement_external_ids: keeps the audit shape stable for callers that don't use the new fields. Nice.

Next in the #316 chain (for tracking)

  1. ✅ This PR (BGSTM-side resolution + audit + spec)
  2. ⏳ Reporter PR in bg-playground/bgstm-playwright-frameworks — read bgstm:requirement annotations and transmit them
  3. ⏳ Smoke pin-bump PR — bump reporter SHA in external-results-smoke.yml, pre-create REQ-CRM-HOMEPAGE in bootstrap.py, re-enable the deferred assert.py requirement-link assertion. Closes [v0.2] BGSTMReporter should transmit bgstm:requirement annotations and BGSTM should resolve external→UUID for case-result linking #316.

Merging this. 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v0.2] BGSTMReporter should transmit bgstm:requirement annotations and BGSTM should resolve external→UUID for case-result linking

2 participants