Skip to content

Add External Test Runs API listing endpoints and new Test Runs UI (list + detail) - #348

Merged
bg-playground merged 4 commits into
mainfrom
copilot/add-test-runs-section
May 10, 2026
Merged

Add External Test Runs API listing endpoints and new Test Runs UI (list + detail)#348
bg-playground merged 4 commits into
mainfrom
copilot/add-test-runs-section

Conversation

Copilot AI commented May 10, 2026

Copy link
Copy Markdown
Contributor

This PR surfaces Playwright external run data already persisted in external_run_sessions / external_case_results by adding missing list APIs and introducing a dedicated Test Runs UI. It adds end-to-end support for browsing sessions and drilling into per-session case outcomes.

  • Backend: external results list APIs

    • Added session pagination/filtering in CRUD:
      • list_sessions(db, project_id?, status?, skip, limit) -> (sessions, total)
    • Added per-session case listing in CRUD:
      • list_case_results_for_session(db, session_id, skip, limit) -> (cases, total)
    • Added response schemas:
      • SessionListResponse
      • CaseResultListResponse
    • Added new endpoints:
      • GET /external-results/sessions
      • GET /external-results/session/{session_id}/cases
    • Extended SessionResponse with summary and included it in _session_to_response (summary=session.summary or {}), with ci_url normalized to string for response safety.
  • Backend: query efficiency

    • Optimized session case-list requirement hydration to avoid per-row lookups by batch-loading requirement links and building a test-case→requirements map.
  • Frontend: external results API client

    • Added frontend/src/api/externalResults.ts with typed models and methods:
      • listSessions(...)
      • getSession(sessionId)
      • listSessionCases(sessionId, ...)
  • Frontend: new Test Runs pages

    • Added TestRunsPage (/runs):
      • Paginated sessions table (newest first)
      • Status filter (All/Running/Passed/Failed/Aborted)
      • Status badges, branch/SHA, runner, summary counters, duration, CI link
      • Row click navigation to session detail
    • Added TestRunDetailPage (/runs/:sessionId):
      • Session header with status, git info, runner, timing, CI link, summary counters
      • Pass/fail/remaining progress bar
      • Case results table with outcome badges, duration, linked requirement count, auto-registered badge
      • Expand/collapse error rows
      • Back link to Test Runs
      • Case fetch supports full retrieval via paginated API traversal
  • App wiring

    • Added routes in App.tsx:
      • /runs
      • /runs/:sessionId
    • Added navigation item in Navigation.tsx:
      • Test Runs (between Test Cases and Manual Links)
  • Targeted backend coverage

    • Added integration tests for:
      • Session listing pagination/filtering and summary presence
      • Session case listing, ordering/pagination envelope, and 404 for unknown session
@router.get("/external-results/sessions", response_model=SessionListResponse)
async def list_external_sessions(...):
    sessions, total = await list_sessions(db, project_id=project_id, status=status, skip=skip, limit=limit)
    return SessionListResponse(
        sessions=[_session_to_response(s) for s in sessions],
        total=total,
        skip=skip,
        limit=limit,
    )
Original prompt

Overview

Add a Test Runs section to the BGSTM frontend that surfaces the external Playwright test run data already being collected by the backend. Currently the external_run_sessions and external_case_results tables are populated by the Playwright reporter but are completely invisible in the UI.

This change requires:

  1. Two new backend list endpoints (the existing API only has GET-by-ID)
  2. A new frontend API client module (externalResults.ts)
  3. Two new frontend pages (TestRunsPage.tsx, TestRunDetailPage.tsx)
  4. Wiring into App.tsx and Navigation.tsx

Backend Changes

1. Add list_sessions CRUD function

In backend/app/crud/external_results.py, add:

async def list_sessions(
    db: AsyncSession,
    *,
    project_id: UUID | None = None,
    status: RunStatus | None = None,
    skip: int = 0,
    limit: int = 25,
) -> tuple[list[ExternalRunSession], int]:
    """Return a paginated list of sessions and total count."""
    stmt = select(ExternalRunSession).order_by(ExternalRunSession.started_at.desc())
    count_stmt = select(func.count()).select_from(ExternalRunSession)
    if project_id is not None:
        stmt = stmt.where(ExternalRunSession.project_id == project_id)
        count_stmt = count_stmt.where(ExternalRunSession.project_id == project_id)
    if status is not None:
        stmt = stmt.where(ExternalRunSession.status == status)
        count_stmt = count_stmt.where(ExternalRunSession.status == status)
    stmt = stmt.offset(skip).limit(limit)
    total = (await db.execute(count_stmt)).scalar_one()
    rows = (await db.execute(stmt)).scalars().all()
    return list(rows), total

2. Add list_case_results_for_session CRUD function

In backend/app/crud/external_case_results.py, add:

async def list_case_results_for_session(
    db: AsyncSession,
    *,
    session_id: UUID,
    skip: int = 0,
    limit: int = 200,
) -> tuple[list[ExternalCaseResult], int]:
    """Return all case results for a given session, ordered by created_at asc."""
    stmt = (
        select(ExternalCaseResult)
        .where(ExternalCaseResult.session_id == session_id)
        .order_by(ExternalCaseResult.created_at.asc())
        .offset(skip)
        .limit(limit)
    )
    count_stmt = select(func.count()).select_from(ExternalCaseResult).where(ExternalCaseResult.session_id == session_id)
    total = (await db.execute(count_stmt)).scalar_one()
    rows = (await db.execute(stmt)).scalars().all()
    # Populate requirement_ids for each result
    for row in rows:
        row.requirement_ids = await _get_requirement_ids_for_test_case(db, test_case_id=row.test_case_id)
    return list(rows), total

3. Add schema for paginated session list response

In backend/app/schemas/external_results.py, add:

class SessionListResponse(BaseModel):
    """Response for GET /external-results/sessions."""
    sessions: list[SessionResponse]
    total: int
    skip: int
    limit: int

class CaseResultListResponse(BaseModel):
    """Response for GET /external-results/session/{id}/cases."""
    cases: list[CaseResultResponse]
    total: int
    skip: int
    limit: int

4. Add two new GET endpoints to backend/app/api/external_results.py

GET /external-results/sessions — paginated list of all sessions

@router.get(
    "/external-results/sessions",
    response_model=SessionListResponse,
)
async def list_external_sessions(
    project_id: UUID | None = Query(None),
    status: RunStatus | None = Query(None),
    skip: int = Query(0, ge=0),
    limit: int = Query(25, ge=1, le=100),
    db: AsyncSession = Depends(get_db),
    _auth=Depends(get_runner_or_user_auth),
) -> SessionListResponse:
    """Return a paginated list of sessions, newest first.
    Accessible by standard user JWT (any role) or runner token.
    """
    sessions, total = await list_sessions(db, project_id=project_id, status=status, skip=skip, limit=limit)
    return SessionListResponse(
        sessions=[_session_to_response(s) for s in sessions],
        total=total,
        skip=skip,
        limit=limit,
    )

GET /external-results/session/{session_id}/cases — case results for a session

@router.get(
    "/external-results/session/{session_id}/cases",
    response_model=CaseResultListResponse,
)
async def list_external_session_cases(
    session_id: UUID,
    skip: int = Query(0, ge=0),
    limit: int = Query(200, ge=1, le=500),
    db: AsyncSession = Depends(get_db),
    _auth=Depends(get_runner_or_user_auth),
) -> CaseResultListResponse:
    """Return all case results for a given session."""
    session = await get_session(db, session_id)
    if session is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"code": "session.not_found", "message": f"Session {session_id} does not exist.", "details": None},
        )
    cases, total = await list_case_results_for_sessi...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI and others added 2 commits May 10, 2026 01:30
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/294d8d86-ccd0-403f-8efe-8eea70b4325f

Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/294d8d86-ccd0-403f-8efe-8eea70b4325f

Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Copilot AI changed the title [WIP] Add Test Runs section to BGSTM frontend Add External Test Runs API listing endpoints and new Test Runs UI (list + detail) May 10, 2026
Copilot AI requested a review from bg-playground May 10, 2026 01:35
…_id to satisfy mypy Column[Any] key constraint
@bg-playground
bg-playground marked this pull request as ready for review May 10, 2026 01:55

@bg-playground bg-playground left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backend: Two new paginated list endpoints (GET /external-results/sessions and GET /external-results/session/{id}/cases) with CRUD functions, response schemas, and integration tests
Frontend: New TestRunsPage (/runs) and TestRunDetailPage (/runs/:sessionId) pages with full session/case browsing UI, wired into App.tsx and Navigation.tsx
Bug fix: mypy type annotation corrected in list_case_results_for_session (dict[Any, list[UUID]])

@bg-playground
bg-playground merged commit e3b6d58 into main May 10, 2026
12 checks passed
@bg-playground
bg-playground deleted the copilot/add-test-runs-section branch May 10, 2026 01:58
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.

2 participants