components:
  parameters:
    Id:
      in: path
      name: id
      required: true
      schema:
        format: uuid
        type: string
    Limit:
      in: query
      name: limit
      schema:
        default: 20
        maximum: 100
        minimum: 1
        type: integer
    Offset:
      in: query
      name: offset
      schema:
        default: 0
        minimum: 0
        type: integer
  responses:
    EnqueueFailed:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
      description: Background-worker enqueue failed (the resource was compensated)
    Forbidden:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
      description: Caller is not owner|admin
    NotFound:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
      description: Unknown or foreign resource
    Ok:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/OkEnvelope'
      description: Acknowledged
    Unauthorized:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
      description: Missing/invalid token, or insufficient role
    ValidationError:
      content:
        application/json:
          schema:
            allOf:
              - $ref: '#/components/schemas/ErrorEnvelope'
              - properties:
                  details:
                    type: object
                type: object
      description: Request validation failed (Zod) — `details` carries the flattened issues
  schemas:
    ApiKey:
      properties:
        createdAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        keyPrefix:
          description: Display prefix only — the full key is never readable again.
          example: tb_live_0123…
          type: string
        lastUsedAt:
          format: date-time
          nullable: true
          type: string
        name:
          type: string
        projectId:
          format: uuid
          nullable: true
          type: string
        revokedAt:
          format: date-time
          nullable: true
          type: string
        scope:
          enum:
            - read
            - read_write
          type: string
      required:
        - id
        - name
        - keyPrefix
        - scope
        - createdAt
      type: object
    ChatConversation:
      properties:
        created_at:
          format: date-time
          type: string
        created_by:
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        project_id:
          format: uuid
          nullable: true
          type: string
        title:
          nullable: true
          type: string
      required:
        - id
        - project_id
        - created_by
        - title
        - created_at
      type: object
    ChatMessage:
      properties:
        content:
          description: >-
            User/assistant text; for role=tool the serialized tool result `{
            tool, ok, result?, error? }` (ChatToolResultContentSchema).
          type: string
        conversation_id:
          format: uuid
          type: string
        created_at:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        role:
          enum:
            - user
            - assistant
            - tool
          type: string
        tool_calls:
          description: >-
            Tool calls of an assistant turn resp. the one call a tool-result row
            answers; `id` is the provider tool_use_id.
          items:
            properties:
              id:
                type: string
              input: {}
              name:
                type: string
            required:
              - id
              - name
            type: object
          nullable: true
          type: array
      required:
        - id
        - conversation_id
        - role
        - content
        - tool_calls
        - created_at
      type: object
    Config:
      properties:
        createdAt:
          format: date-time
          type: string
        data:
          description: Per-type payload; secret fields are masked as `***` on reads.
          type: object
        id:
          format: uuid
          type: string
        name:
          type: string
        overrides:
          description: Per-environment override blocks (secrets masked on reads).
          type: object
        projectId:
          format: uuid
          type: string
        type:
          enum:
            - credentials
            - credentials_totp
            - email
            - file
          type: string
        updatedAt:
          format: date-time
          type: string
      required:
        - id
        - projectId
        - name
        - type
        - data
        - overrides
        - createdAt
        - updatedAt
      type: object
    CrawlAccepted:
      properties:
        crawl_id:
          format: uuid
          type: string
      required:
        - crawl_id
      type: object
    CrawlSession:
      description: >-
        One crawl of the project with its lifecycle status (processing |
        complete | failed | cancelled), discovered page count and timing.
      properties:
        endedAt:
          format: date-time
          nullable: true
          type: string
        error:
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        intent:
          nullable: true
          type: string
        maxDepth:
          nullable: true
          type: integer
        maxPages:
          nullable: true
          type: integer
        pagesFound:
          type: integer
        startedAt:
          format: date-time
          type: string
        status:
          enum:
            - processing
            - complete
            - failed
            - cancelled
          type: string
      required:
        - id
        - status
        - maxDepth
        - maxPages
        - intent
        - pagesFound
        - error
        - startedAt
        - endedAt
      type: object
    Environment:
      properties:
        baseUrl:
          format: uri
          type: string
        createdAt:
          format: date-time
          type: string
        defaultHeaders:
          additionalProperties:
            type: string
          type: object
        defaultVars:
          items:
            $ref: '#/components/schemas/TestVar'
          type: array
        id:
          format: uuid
          type: string
        isProduction:
          type: boolean
        kind:
          enum:
            - standard
            - preview
          type: string
        name:
          type: string
        projectId:
          format: uuid
          type: string
      required:
        - id
        - projectId
        - name
        - baseUrl
        - kind
        - isProduction
        - defaultVars
        - defaultHeaders
        - createdAt
      type: object
    EnvironmentCreate:
      properties:
        base_url:
          format: uri
          type: string
        default_headers:
          additionalProperties:
            type: string
          type: object
        default_vars:
          items:
            $ref: '#/components/schemas/TestVar'
          type: array
        is_production:
          default: false
          type: boolean
        kind:
          default: standard
          enum:
            - standard
            - preview
          type: string
        name:
          minLength: 1
          type: string
      required:
        - name
        - base_url
      type: object
    EnvironmentUpdate:
      minProperties: 1
      properties:
        base_url:
          format: uri
          type: string
        default_headers:
          additionalProperties:
            type: string
          type: object
        default_vars:
          items:
            $ref: '#/components/schemas/TestVar'
          type: array
        is_production:
          type: boolean
        kind:
          enum:
            - standard
            - preview
          type: string
        name:
          minLength: 1
          type: string
      type: object
    ErrorEnvelope:
      description: Failure envelope — every non-2xx JSON response body.
      properties:
        error:
          type: string
        ok:
          enum:
            - false
          type: boolean
      required:
        - ok
        - error
      type: object
    HealEvent:
      properties:
        autoApplied:
          type: boolean
        confidence:
          description: numeric(3,2) column ⇒ string on the wire, e.g. "0.85"
          type: string
        createdAt:
          format: date-time
          type: string
        failureClass:
          type: string
        id:
          format: uuid
          type: string
        newSelector:
          nullable: true
          type: string
        oldSelector:
          nullable: true
          type: string
        runId:
          format: uuid
          type: string
        stepIndex:
          type: integer
        userDecision:
          enum:
            - approved
            - rejected
            - null
          nullable: true
          type: string
      required:
        - id
        - runId
        - stepIndex
        - failureClass
        - confidence
        - autoApplied
        - createdAt
      type: object
    HealMetrics:
      properties:
        auto_apply_rate:
          type: number
        by_day:
          items:
            properties:
              applied:
                type: integer
              attempts:
                type: integer
              date:
                format: date
                type: string
              rejected:
                type: integer
            required:
              - date
              - attempts
              - applied
              - rejected
            type: object
          type: array
        flaky_rate:
          type: number
        manual_approval_count:
          type: integer
        total_heals:
          type: integer
        window:
          enum:
            - 7d
            - 30d
          type: string
      required:
        - window
        - total_heals
        - auto_apply_rate
        - manual_approval_count
        - flaky_rate
        - by_day
      type: object
    Incident:
      properties:
        affected:
          items:
            enum:
              - api
              - worker
              - browserbase
            type: string
          type: array
        body:
          type: string
        id:
          format: uuid
          type: string
        resolvedAt:
          format: date-time
          nullable: true
          type: string
        severity:
          enum:
            - minor
            - major
            - critical
          type: string
        startedAt:
          format: date-time
          type: string
        status:
          enum:
            - investigating
            - identified
            - monitoring
            - resolved
          type: string
        title:
          type: string
      required:
        - id
        - severity
        - status
        - title
        - body
        - affected
        - startedAt
      type: object
    Issue:
      properties:
        fingerprint:
          description: Deterministic hash over test case + failure class + step.
          type: string
        firstSeenAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        kind:
          enum:
            - test_failure
            - console_error
            - a11y
          type: string
        lastSeenAt:
          format: date-time
          type: string
        occurrences:
          minimum: 1
          type: integer
        payload:
          description: Structured failure context of the latest occurrence.
          nullable: true
          type: object
        projectId:
          format: uuid
          type: string
        sampleRunId:
          description: Latest run that exhibited the failure.
          format: uuid
          nullable: true
          type: string
        severity:
          enum:
            - minor
            - major
            - critical
          type: string
        status:
          enum:
            - active
            - ignored
            - resolved
          type: string
        title:
          type: string
      required:
        - id
        - projectId
        - fingerprint
        - kind
        - severity
        - status
        - title
        - occurrences
        - firstSeenAt
        - lastSeenAt
      type: object
    NotificationRule:
      properties:
        channel:
          enum:
            - slack
            - email
          type: string
        createdAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        onlyFailures:
          type: boolean
        planId:
          format: uuid
          nullable: true
          type: string
        projectId:
          format: uuid
          nullable: true
          type: string
        target:
          nullable: true
          type: string
      required:
        - id
        - channel
        - onlyFailures
        - createdAt
      type: object
    OkEnvelope:
      description: Plain acknowledgement body.
      properties:
        ok:
          enum:
            - true
          type: boolean
      required:
        - ok
      type: object
    PlanCapError:
      description: 402 body when a requested value exceeds the org's plan cap.
      properties:
        error:
          type: string
        limit:
          type: integer
        ok:
          enum:
            - false
          type: boolean
        upgradeHint:
          type: string
      required:
        - ok
        - error
        - upgradeHint
        - limit
      type: object
    PlanRun:
      properties:
        createdAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        parentPlanRunId:
          format: uuid
          nullable: true
          type: string
        planId:
          format: uuid
          nullable: true
          type: string
        projectId:
          format: uuid
          type: string
        runMode:
          enum:
            - ci
            - nightly
          type: string
        trigger:
          enum:
            - manual
            - cron
            - ci
            - rerun
          type: string
      required:
        - id
        - projectId
        - trigger
        - runMode
        - createdAt
      type: object
    PlanRunStatus:
      properties:
        counts:
          properties:
            cancelled:
              type: integer
            error:
              type: integer
            failed:
              type: integer
            passed:
              type: integer
            queued:
              type: integer
            running:
              type: integer
            skipped:
              type: integer
            total:
              type: integer
          required:
            - total
            - queued
            - running
            - passed
            - failed
            - error
            - cancelled
            - skipped
          type: object
        created_at:
          format: date-time
          type: string
        parent_plan_run_id:
          format: uuid
          nullable: true
          type: string
        plan_id:
          format: uuid
          nullable: true
          type: string
        plan_run_id:
          format: uuid
          type: string
        result:
          enum:
            - passed
            - failed
            - null
          nullable: true
          type: string
        run_mode:
          enum:
            - ci
            - nightly
          type: string
        status:
          enum:
            - running
            - done
          type: string
        trigger:
          enum:
            - manual
            - cron
            - ci
            - rerun
          type: string
      required:
        - plan_run_id
        - trigger
        - run_mode
        - created_at
        - counts
        - status
        - result
      type: object
    Project:
      description: >-
        A project with page/run counts. Settings fields (heal threshold, GitHub
        mapping, run budgets) appear on single-project reads only.
      properties:
        appId:
          format: uuid
          nullable: true
          type: string
        baseUrl:
          format: uri
          type: string
        captchaSolvingEnabled:
          type: boolean
        crawlCron:
          nullable: true
          type: string
        createdAt:
          format: date-time
          type: string
        deletedAt:
          format: date-time
          nullable: true
          type: string
        devicePreset:
          oneOf:
            - properties:
                locale:
                  pattern: ^[a-z]{2}(-[A-Z]{2})?$
                  type: string
                preset:
                  enum:
                    - desktop
                    - tablet
                    - mobile
                  type: string
                timezone:
                  type: string
              required:
                - preset
              type: object
            - properties:
                locale:
                  pattern: ^[a-z]{2}(-[A-Z]{2})?$
                  type: string
                preset:
                  enum:
                    - custom
                  type: string
                timezone:
                  type: string
                viewport:
                  properties:
                    height:
                      maximum: 1024
                      minimum: 320
                      type: integer
                    width:
                      maximum: 1280
                      minimum: 320
                      type: integer
                  required:
                    - width
                    - height
                  type: object
              required:
                - preset
                - viewport
              type: object
            - nullable: true
        generateGapTests:
          type: boolean
        githubInstallationId:
          nullable: true
          type: string
        githubRepo:
          nullable: true
          type: string
        healAutoApplyThreshold:
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        name:
          type: string
        orgId:
          type: string
        pageCount:
          type: integer
        prFeedbackEmojiReactionEnabled:
          type: boolean
        prFeedbackReviewContextChars:
          nullable: true
          type: integer
        prFeedbackVerdictEnabled:
          type: boolean
        runBudgetMinutes:
          nullable: true
          type: integer
        runCount:
          type: integer
        stateLifetimeMinutes:
          nullable: true
          type: integer
        stepTimeoutSeconds:
          nullable: true
          type: integer
        testDataRetentionDays:
          nullable: true
          type: integer
        testDataRetentionPolicy:
          enum:
            - 30d
            - 90d
            - custom
            - default
          type: string
        updatedAt:
          format: date-time
          type: string
        vercelProtectionBypassConfigured:
          type: boolean
      required:
        - id
        - orgId
        - name
        - baseUrl
        - createdAt
        - updatedAt
        - deletedAt
        - pageCount
        - runCount
        - appId
      type: object
    ProjectUsage:
      description: >-
        Run count and LLM cost (USD, SUM of runs.cost_usd) of the project since
        the start of the current UTC calendar month, plus the project's daily
        LLM cap (null = unset). Browser-agent costs (Stagehand act()) are NOT
        included until the Browserbase billing API is integrated.
      properties:
        dailyCapUsd:
          nullable: true
          type: number
        llmCostUsdThisMonth:
          type: number
        runsThisMonth:
          type: integer
      required:
        - runsThisMonth
        - llmCostUsdThisMonth
        - dailyCapUsd
      type: object
    RemoteTunnel:
      properties:
        createdAt:
          format: date-time
          type: string
        createdBy:
          nullable: true
          type: string
        expiresAt:
          format: date-time
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        localPort:
          maximum: 65535
          minimum: 1
          type: integer
        projectId:
          format: uuid
          type: string
        status:
          enum:
            - active
            - expired
            - revoked
          type: string
        tunnelUrl:
          format: uri
          type: string
      required:
        - id
        - projectId
        - localPort
        - tunnelUrl
        - status
        - createdBy
        - expiresAt
        - createdAt
      type: object
    RemoteTunnelCreate:
      properties:
        expires_at:
          format: date-time
          type: string
        local_port:
          maximum: 65535
          minimum: 1
          type: integer
        project_id:
          format: uuid
          type: string
        tunnel_url:
          format: uri
          type: string
      required:
        - project_id
        - local_port
        - tunnel_url
      type: object
    RemoteTunnelStatusUpdate:
      properties:
        status:
          enum:
            - active
            - expired
            - revoked
          type: string
      required:
        - status
      type: object
    RunAccepted:
      properties:
        device_matrix_id:
          format: uuid
          type: string
        run_id:
          format: uuid
          type: string
      required:
        - run_id
      type: object
    RunAgentStep:
      properties:
        action:
          nullable: true
          type: string
        createdAt:
          format: date-time
          type: string
        instruction:
          type: string
        pageExcerpt:
          nullable: true
          type: string
        pageUrl:
          nullable: true
          type: string
        reasoning:
          nullable: true
          type: string
        stepIndex:
          type: integer
        success:
          type: boolean
      required:
        - stepIndex
        - instruction
        - action
        - reasoning
        - success
        - pageUrl
        - pageExcerpt
        - createdAt
      type: object
    RunArtifact:
      properties:
        byteSize:
          type: integer
        createdAt:
          format: date-time
          type: string
        download_url:
          type: string
        id:
          format: uuid
          type: string
        stepIndex:
          nullable: true
          type: integer
        type:
          description: Artifact kind (screenshot, dom, log, video, …)
          type: string
        when:
          enum:
            - pre
            - post
          nullable: true
          type: string
      required:
        - id
        - type
        - byteSize
        - createdAt
        - stepIndex
        - when
        - download_url
      type: object
    RunDetail:
      description: >-
        A run with its artifacts (presigned download URLs), the executed
        test-case DSL and derived per-step results.
      properties:
        agentSteps:
          items:
            $ref: '#/components/schemas/RunAgentStep'
          type: array
        artifacts:
          items:
            $ref: '#/components/schemas/RunArtifact'
          type: array
        costUsd:
          type: string
        deviceMatrixId:
          nullable: true
          type: string
        devicePreset:
          description: >-
            Resolved device preset the run executed with (source:
            run|testCase|project|default); null for runs predating the feature.
            notApplied lists configured fields without an init param in the
            session mode (locale/timezone on Browserbase).
          nullable: true
          properties:
            locale:
              pattern: ^[a-z]{2}(-[A-Z]{2})?$
              type: string
            notApplied:
              items:
                enum:
                  - locale
                  - timezone
                type: string
              type: array
            preset:
              enum:
                - desktop
                - tablet
                - mobile
                - custom
              type: string
            source:
              enum:
                - run
                - testCase
                - project
                - default
              type: string
            timezone:
              type: string
            viewport:
              properties:
                height:
                  maximum: 1024
                  minimum: 320
                  type: integer
                width:
                  maximum: 1280
                  minimum: 320
                  type: integer
              required:
                - width
                - height
              type: object
          required:
            - source
            - preset
            - viewport
          type: object
        downloadResults:
          description: >-
            File downloads verified via assert.download steps (filename + size);
            null when no download was verified.
          items:
            properties:
              contentType:
                type: string
              filename:
                type: string
              sizeBytes:
                minimum: 0
                type: integer
              stepIndex:
                minimum: 0
                type: integer
            required:
              - stepIndex
              - filename
              - sizeBytes
            type: object
          maxItems: 10
          nullable: true
          type: array
        endedAt:
          format: date-time
          nullable: true
          type: string
        environmentId:
          nullable: true
          type: string
        error:
          nullable: true
          type: string
        exploratoryFindings:
          items:
            $ref: '#/components/schemas/RunExploratoryFinding'
          nullable: true
          type: array
        failureClass:
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        kind:
          type: string
        liveViewUrl:
          description: >-
            Short-lived Browserbase live-view URL; non-null only while the run
            is running. Resolved freshly on every request.
          nullable: true
          type: string
        modelTier:
          description: >-
            Resolved LLM model tier the run executed with (tier: fast|thorough,
            source: run|testCase|project|default); null for runs predating the
            feature.
          nullable: true
          properties:
            source:
              enum:
                - run
                - testCase
                - project
                - default
              type: string
            tier:
              enum:
                - fast
                - thorough
              type: string
          required:
            - tier
            - source
          type: object
        outputValues:
          additionalProperties:
            type: string
          description: >-
            Output values saved via output.save steps; populated for passed runs
            only, secret-masked. {} = no outputs.
          type: object
        planRunId:
          nullable: true
          type: string
        projectId:
          format: uuid
          type: string
        startedAt:
          format: date-time
          type: string
        status:
          enum:
            - queued
            - running
            - passed
            - failed
            - error
            - cancelled
            - skipped
          type: string
        stepResults:
          items:
            properties:
              postArtifactId:
                nullable: true
                type: string
              preArtifactId:
                nullable: true
                type: string
              status:
                enum:
                  - passed
                  - failed
                  - skipped
                type: string
              stepIndex:
                minimum: 0
                type: integer
            required:
              - stepIndex
              - status
              - preArtifactId
              - postArtifactId
            type: object
          type: array
        testCase:
          nullable: true
          properties:
            dsl:
              properties:
                name:
                  minLength: 1
                  type: string
                steps:
                  items:
                    oneOf:
                      - properties:
                          op:
                            enum:
                              - navigate
                            type: string
                          url:
                            type: string
                        required:
                          - op
                          - url
                        type: object
                      - properties:
                          op:
                            enum:
                              - click
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                        type: object
                      - properties:
                          op:
                            enum:
                              - fill
                            type: string
                          selector:
                            minLength: 1
                            type: string
                          value:
                            type: string
                        required:
                          - op
                          - selector
                          - value
                        type: object
                      - properties:
                          op:
                            enum:
                              - select
                            type: string
                          selector:
                            minLength: 1
                            type: string
                          value:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                          - value
                        type: object
                      - properties:
                          op:
                            enum:
                              - hover
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                        type: object
                      - properties:
                          ms:
                            exclusiveMinimum: true
                            minimum: 0
                            type: integer
                          op:
                            enum:
                              - wait
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          contains:
                            type: string
                          equals:
                            type: string
                          op:
                            enum:
                              - assert.url
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          op:
                            enum:
                              - assert.visible
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                        type: object
                      - properties:
                          contains:
                            type: string
                          equals:
                            type: string
                          op:
                            enum:
                              - assert.text
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                        type: object
                      - properties:
                          equals:
                            minimum: 0
                            type: integer
                          op:
                            enum:
                              - assert.count
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                          - equals
                        type: object
                      - properties:
                          config:
                            minLength: 1
                            type: string
                          op:
                            enum:
                              - totp
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                          - config
                        type: object
                      - properties:
                          equals:
                            type: integer
                          lessThan:
                            type: integer
                          op:
                            enum:
                              - assert.status
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          max:
                            minimum: 0
                            type: integer
                          op:
                            enum:
                              - assert.consoleErrors
                            type: string
                        required:
                          - op
                          - max
                        type: object
                      - properties:
                          instruction:
                            minLength: 1
                            type: string
                          op:
                            enum:
                              - act
                            type: string
                        required:
                          - op
                          - instruction
                        type: object
                      - properties:
                          name:
                            pattern: ^[\w.-]{1,64}$
                            type: string
                          op:
                            enum:
                              - output.save
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - name
                        type: object
                      - properties:
                          config:
                            minLength: 1
                            type: string
                          default:
                            enum:
                              - pdf
                              - png
                            type: string
                          op:
                            enum:
                              - upload
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                        type: object
                      - properties:
                          fromContains:
                            minLength: 1
                            type: string
                          inbox:
                            default: run
                            enum:
                              - run
                              - project
                            type: string
                          op:
                            enum:
                              - email.waitFor
                            type: string
                          subjectContains:
                            minLength: 1
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          fromContains:
                            minLength: 1
                            type: string
                          inbox:
                            default: run
                            enum:
                              - run
                              - project
                            type: string
                          op:
                            enum:
                              - email.fillOtp
                            type: string
                          selector:
                            minLength: 1
                            type: string
                          subjectContains:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - selector
                        type: object
                      - properties:
                          fromContains:
                            minLength: 1
                            type: string
                          inbox:
                            default: run
                            enum:
                              - run
                              - project
                            type: string
                          linkContains:
                            minLength: 1
                            type: string
                          op:
                            enum:
                              - email.openLink
                            type: string
                          subjectContains:
                            minLength: 1
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          attribute:
                            minLength: 1
                            type: string
                          op:
                            enum:
                              - navigate.dynamic
                            type: string
                          path:
                            minLength: 1
                            type: string
                          relativeTo:
                            enum:
                              - current
                            type: string
                          selector:
                            minLength: 1
                            type: string
                          source:
                            enum:
                              - clipboard
                              - domAttribute
                            type: string
                          urlTemplate:
                            minLength: 1
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          filenameContains:
                            minLength: 1
                            type: string
                          minBytes:
                            minimum: 0
                            type: integer
                          op:
                            enum:
                              - assert.download
                            type: string
                        required:
                          - op
                        type: object
                      - properties:
                          body:
                            type: string
                          config:
                            minLength: 1
                            type: string
                          extract:
                            additionalProperties:
                              minLength: 1
                              type: string
                            default: {}
                            type: object
                          op:
                            enum:
                              - api.call
                            type: string
                          save:
                            default: false
                            type: boolean
                          url:
                            type: string
                        required:
                          - op
                          - config
                        type: object
                      - properties:
                          op:
                            enum:
                              - api.expect
                            type: string
                          status:
                            type: integer
                          statusLessThan:
                            type: integer
                        required:
                          - op
                        type: object
                      - properties:
                          config:
                            minLength: 1
                            type: string
                          op:
                            enum:
                              - auth.basic
                            type: string
                        required:
                          - op
                          - config
                        type: object
                      - properties:
                          apiName:
                            pattern: ^[\w.-]{1,64}$
                            type: string
                          attribute:
                            minLength: 1
                            type: string
                          contains:
                            type: string
                          equals:
                            type: string
                          op:
                            enum:
                              - api.dataAssert
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - apiName
                          - selector
                        type: object
                      - properties:
                          attribute:
                            minLength: 1
                            type: string
                          name:
                            pattern: ^[\w.-]{1,64}$
                            type: string
                          op:
                            enum:
                              - clipboard.copy
                            type: string
                          selector:
                            minLength: 1
                            type: string
                        required:
                          - op
                          - name
                          - selector
                        type: object
                      - properties:
                          name:
                            pattern: ^[\w.-]{1,64}$
                            type: string
                          op:
                            enum:
                              - clipboard.paste
                            type: string
                        required:
                          - op
                          - name
                        type: object
                      - properties:
                          failOn:
                            default: mismatch
                            enum:
                              - match
                              - mismatch
                            type: string
                          instruction:
                            minLength: 1
                            type: string
                          op:
                            enum:
                              - assert.vision
                            type: string
                        required:
                          - op
                          - instruction
                        type: object
                  type: array
                tags:
                  default: []
                  items:
                    type: string
                  type: array
                vars:
                  default: []
                  items:
                    properties:
                      name:
                        minLength: 1
                        type: string
                      secret:
                        default: false
                        type: boolean
                      value:
                        type: string
                    required:
                      - name
                      - value
                    type: object
                  type: array
              required:
                - name
                - steps
              type: object
            executionMode:
              enum:
                - steps
                - goal
              type: string
            id:
              format: uuid
              type: string
            name:
              type: string
          required:
            - id
            - name
            - dsl
          type: object
        testCaseId:
          format: uuid
          nullable: true
          type: string
        vars:
          items:
            properties:
              name:
                minLength: 1
                type: string
              secret:
                default: false
                type: boolean
              value:
                type: string
            required:
              - name
              - value
            type: object
          type: array
        verdict:
          description: >-
            Structured assessment verdict (errorClass transport|user|infra|none,
            confidence low|medium|high, key observations, expected/actual); null
            for runs predating the verdict step.
          nullable: true
          properties:
            actual:
              type: string
            confidence:
              enum:
                - low
                - medium
                - high
              type: string
            errorClass:
              enum:
                - transport
                - user
                - infra
                - none
              type: string
            expected:
              type: string
            keyObservations:
              items:
                type: string
              maxItems: 8
              type: array
            suggestion:
              minLength: 1
              type: string
            summary:
              minLength: 1
              type: string
          required:
            - errorClass
            - confidence
            - summary
            - expected
            - actual
            - keyObservations
            - suggestion
          type: object
      required:
        - id
        - projectId
        - testCaseId
        - kind
        - status
        - startedAt
        - endedAt
        - costUsd
        - vars
        - planRunId
        - deviceMatrixId
        - environmentId
        - error
        - failureClass
        - artifacts
        - testCase
        - stepResults
        - exploratoryFindings
        - agentSteps
        - liveViewUrl
        - outputValues
        - devicePreset
        - modelTier
        - downloadResults
        - verdict
      type: object
    RunEmail:
      properties:
        id:
          format: uuid
          type: string
        receivedAt:
          format: date-time
          type: string
        recipient:
          type: string
        sender:
          type: string
        subject:
          type: string
        textBody:
          type: string
      required:
        - id
        - recipient
        - sender
        - subject
        - textBody
        - receivedAt
      type: object
    RunEmailList:
      properties:
        items:
          items:
            $ref: '#/components/schemas/RunEmail'
          type: array
        total:
          type: integer
      required:
        - items
        - total
      type: object
    RunExploratoryFinding:
      properties:
        countInRun:
          exclusiveMinimum: true
          minimum: 0
          type: integer
        findingKind:
          enum:
            - console_error
            - broken_interaction
            - http_error
          type: string
        normalizedMessage:
          type: string
        normalizedUrl:
          type: string
        sample:
          type: string
        severity:
          enum:
            - minor
            - major
            - critical
          type: string
      required:
        - findingKind
        - severity
        - normalizedMessage
        - normalizedUrl
        - sample
        - countInRun
      type: object
    RunList:
      properties:
        items:
          items:
            $ref: '#/components/schemas/RunListItem'
          type: array
        limit:
          type: integer
        offset:
          type: integer
        total:
          type: integer
      required:
        - items
        - total
        - offset
        - limit
      type: object
    RunListItem:
      properties:
        costUsd:
          type: string
        createdBy:
          nullable: true
          type: string
        endedAt:
          format: date-time
          nullable: true
          type: string
        error:
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        kind:
          type: string
        projectId:
          format: uuid
          type: string
        projectName:
          type: string
        startedAt:
          format: date-time
          type: string
        status:
          enum:
            - queued
            - running
            - passed
            - failed
            - error
            - cancelled
            - skipped
          type: string
        testCaseId:
          format: uuid
          nullable: true
          type: string
        testCaseName:
          nullable: true
          type: string
      required:
        - id
        - projectId
        - projectName
        - testCaseId
        - testCaseName
        - kind
        - status
        - startedAt
        - endedAt
        - costUsd
        - error
        - createdBy
      type: object
    RunMetrics:
      properties:
        byDay:
          description: One zero-filled entry per calendar day of the window, ascending.
          items:
            allOf:
              - $ref: '#/components/schemas/RunMetricsAggregate'
              - properties:
                  date:
                    format: date
                    type: string
                required:
                  - date
                type: object
          type: array
        totals:
          $ref: '#/components/schemas/RunMetricsAggregate'
        window:
          enum:
            - 7d
            - 30d
          type: string
      required:
        - window
        - totals
        - byDay
      type: object
    RunMetricsAggregate:
      properties:
        costUsd:
          description: Sum of runs.cost_usd, cast to a JSON number server-side.
          type: number
        error:
          type: integer
        failed:
          type: integer
        p50DurationMs:
          description: Median duration over finished runs; null without any.
          nullable: true
          type: number
        p95DurationMs:
          nullable: true
          type: number
        passRate:
          description: passed / finished (passed+failed+error); 0 when nothing finished.
          type: number
        passed:
          type: integer
        total:
          type: integer
      required:
        - total
        - passed
        - failed
        - error
        - passRate
        - p50DurationMs
        - p95DurationMs
        - costUsd
      type: object
    RunOwner:
      properties:
        id:
          nullable: true
          type: string
      required:
        - id
      type: object
    RunOwnerList:
      properties:
        items:
          items:
            $ref: '#/components/schemas/RunOwner'
          type: array
      required:
        - items
      type: object
    TestCase:
      properties:
        createdAt:
          format: date-time
          type: string
        createdBy:
          description: >-
            Clerk user id of who created the test case (GAP-057); null for
            system-generated (AI / burn-in / seed) and pre-0082 rows.
          nullable: true
          type: string
        dependsOnId:
          format: uuid
          nullable: true
          type: string
        dsl:
          description: >-
            Stored DSL payload `{ steps, vars, tags }` (name lives in its own
            column).
          type: object
        flakyScore:
          description: numeric(3,2) column ⇒ string on the wire, e.g. "0.25"
          type: string
        groupId:
          description: >-
            The test group this case belongs to (GAP-002, migration 0086); null
            for ungrouped and pre-0086 rows.
          format: uuid
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        name:
          type: string
        parentId:
          format: uuid
          nullable: true
          type: string
        projectId:
          format: uuid
          type: string
        quarantined:
          type: boolean
        status:
          enum:
            - draft
            - active
            - archived
          type: string
        tags:
          items:
            type: string
          type: array
      required:
        - id
        - projectId
        - name
        - dsl
        - tags
        - status
        - flakyScore
        - quarantined
        - createdAt
      type: object
    TestCaseDsl:
      properties:
        name:
          minLength: 1
          type: string
        steps:
          items:
            $ref: '#/components/schemas/TestStep'
          type: array
        tags:
          items:
            type: string
          type: array
        vars:
          items:
            $ref: '#/components/schemas/TestVar'
          type: array
      required:
        - name
        - steps
      type: object
    TestCaseRevision:
      properties:
        actor:
          nullable: true
          type: string
        changedAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        newData:
          description: jsonb snapshot after the change
          nullable: true
          type: object
        oldData:
          description: jsonb snapshot before the change (null for pre-0028 rows)
          nullable: true
          type: object
        operation:
          type: string
      required:
        - id
        - operation
        - actor
        - changedAt
      type: object
    TestGroup:
      description: One node of the hierarchical test-group taxonomy (GAP-002).
      properties:
        createdAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        name:
          type: string
        parentGroupId:
          description: The parent group; null = a root group.
          format: uuid
          nullable: true
          type: string
        position:
          type: integer
        projectId:
          format: uuid
          type: string
      required:
        - id
        - projectId
        - parentGroupId
        - name
        - position
        - createdAt
      type: object
    TestGroupCreate:
      properties:
        name:
          maxLength: 200
          minLength: 1
          type: string
        parent_group_id:
          description: Nest under this group; absent/null = a root group.
          format: uuid
          nullable: true
          type: string
        position:
          default: 0
          minimum: 0
          type: integer
      required:
        - name
      type: object
    TestGroupTreeNode:
      description: A group plus its recursive children and its direct case count.
      properties:
        caseCount:
          description: Test cases directly in this group (NOT including descendants).
          type: integer
        children:
          items:
            $ref: '#/components/schemas/TestGroupTreeNode'
          type: array
        id:
          format: uuid
          type: string
        name:
          type: string
        parentGroupId:
          format: uuid
          nullable: true
          type: string
        position:
          type: integer
      required:
        - id
        - parentGroupId
        - name
        - position
        - caseCount
        - children
      type: object
    TestGroupUpdate:
      minProperties: 1
      properties:
        name:
          maxLength: 200
          minLength: 1
          type: string
        parent_group_id:
          description: Re-parent under this group; null re-parents to the root.
          format: uuid
          nullable: true
          type: string
        position:
          minimum: 0
          type: integer
      type: object
    TestItem:
      description: >-
        One unified "My items" row (GAP-057): a test case or a test plan owned
        by the caller, with its project context for linking back.
      properties:
        createdAt:
          format: date-time
          type: string
        id:
          format: uuid
          type: string
        kind:
          enum:
            - test_case
            - test_plan
          type: string
        name:
          type: string
        projectId:
          format: uuid
          type: string
        projectName:
          type: string
      required:
        - id
        - kind
        - name
        - projectId
        - projectName
        - createdAt
      type: object
    TestPlan:
      properties:
        createdAt:
          format: date-time
          type: string
        createdBy:
          description: >-
            Clerk user id of who created the plan (GAP-057); null for
            system-generated and pre-0082 rows.
          nullable: true
          type: string
        cron:
          nullable: true
          type: string
        enabled:
          type: boolean
        environmentId:
          format: uuid
          nullable: true
          type: string
        id:
          format: uuid
          type: string
        lastRunAt:
          format: date-time
          nullable: true
          type: string
        name:
          type: string
        nextRunAt:
          format: date-time
          nullable: true
          type: string
        projectId:
          format: uuid
          type: string
        runMode:
          enum:
            - ci
            - nightly
          type: string
        tags:
          items:
            type: string
          type: array
      required:
        - id
        - projectId
        - name
        - runMode
        - tags
        - enabled
        - createdAt
      type: object
    TestPlanCreate:
      properties:
        cron:
          nullable: true
          type: string
        enabled:
          default: true
          type: boolean
        environment_id:
          format: uuid
          nullable: true
          type: string
        name:
          minLength: 1
          type: string
        run_mode:
          default: nightly
          enum:
            - ci
            - nightly
          type: string
        tags:
          items:
            minLength: 1
            type: string
          type: array
        test_case_ids:
          items:
            format: uuid
            type: string
          type: array
      required:
        - name
      type: object
    TestPlanUpdate:
      minProperties: 1
      properties:
        cron:
          nullable: true
          type: string
        enabled:
          type: boolean
        environment_id:
          format: uuid
          nullable: true
          type: string
        name:
          minLength: 1
          type: string
        run_mode:
          enum:
            - ci
            - nightly
          type: string
        tags:
          items:
            minLength: 1
            type: string
          type: array
        test_case_ids:
          items:
            format: uuid
            type: string
          type: array
      type: object
    TestStep:
      additionalProperties: true
      description: >
        One DSL step — a discriminated union on `op`. Fields besides `op` depend
        on the operation (see TestStepSchema in
        packages/shared/src/schemas/test-dsl.ts).
      properties:
        op:
          enum:
            - navigate
            - click
            - fill
            - select
            - hover
            - wait
            - assert.url
            - assert.visible
            - assert.text
            - assert.count
            - totp
          type: string
      required:
        - op
      type: object
    TestVar:
      properties:
        name:
          minLength: 1
          type: string
        secret:
          default: false
          description: When true, the value is masked (`***`) in artifacts.
          type: boolean
        value:
          type: string
      required:
        - name
        - value
      type: object
    Webhook:
      properties:
        active:
          type: boolean
        createdAt:
          format: date-time
          type: string
        events:
          items:
            enum:
              - run.completed
              - crawl.completed
            type: string
          type: array
        id:
          format: uuid
          type: string
        projectId:
          format: uuid
          nullable: true
          type: string
        secret:
          description: Masked as `***` on every read; plaintext only in the POST response.
          type: string
        url:
          format: uri
          type: string
      required:
        - id
        - url
        - events
        - active
        - createdAt
      type: object
  securitySchemes:
    apiKeyAuth:
      description: >
        Org API key created via POST /api-keys, sent as a bearer token:
        `Authorization: Bearer tb_live_…`. Keys with scope `read` may only issue
        GET requests; `read_write` keys act as admin.
      scheme: bearer
      type: http
    bearerAuth:
      bearerFormat: JWT
      description: >
        Clerk session JWT of a user with an active organization. Org and role
        are resolved from the verified token — never from headers.
      scheme: bearer
      type: http
info:
  description: >
    REST API for qa-clone (testsbot.com). All JSON error responses use the
    envelope `{ ok: false, error }`; success bodies are either the documented
    resource shape or `{ ok: true, data? }`. The `/projects*` and `/runs*` paths
    of this document are generated from the zod-openapi route definitions in the
    API source; the remaining paths are maintained by hand and drift-gated in CI
    (see docs/runbooks/openapi-drift.md).
  title: qa-clone API
  version: 0.2.0
openapi: 3.0.3
paths:
  /api-keys:
    get:
      description: Includes revoked keys; never the hash or plaintext.
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    items:
                      $ref: '#/components/schemas/ApiKey'
                    type: array
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: API keys of the org
        '403':
          $ref: '#/components/responses/Forbidden'
      summary: List API keys of the org (admin)
      tags:
        - API Keys
    post:
      description: >
        The plaintext key (`tb_live_…`) is embedded in THIS response only —
        afterwards it is unrecoverable by design.
      requestBody:
        content:
          application/json:
            schema:
              properties:
                name:
                  maxLength: 200
                  minLength: 1
                  type: string
                project_id:
                  format: uuid
                  type: string
                scope:
                  enum:
                    - read
                    - read_write
                  type: string
              required:
                - name
                - scope
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/ApiKey'
                      - properties:
                          key:
                            description: The plaintext API key (`tb_live_…`)
                            example: tb_live_0123456789abcdef0123456789abcdef
                            type: string
                        required:
                          - key
                        type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: API key created (plaintext included exactly once)
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create an API key (admin)
      tags:
        - API Keys
  /api-keys/{id}:
    delete:
      description: Sets `revoked_at` (idempotent) — no hard delete.
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Revoke an API key (admin)
      tags:
        - API Keys
  /billing/checkout:
    post:
      requestBody:
        content:
          application/json:
            schema:
              properties:
                plan:
                  enum:
                    - pro
                    - team
                  type: string
              required:
                - plan
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      url:
                        format: uri
                        type: string
                    required:
                      - url
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Checkout session URL
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
      summary: Create a Stripe Checkout session
      tags:
        - Billing
  /billing/portal:
    post:
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      url:
                        format: uri
                        type: string
                    required:
                      - url
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Portal session URL
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: No Stripe customer for the org — complete checkout first
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Stripe is not configured
      summary: Create a Stripe Billing-Portal session
      tags:
        - Billing
  /billing/spending-limit:
    patch:
      description: >
        Sets the org-wide monthly LLM spend limit in USD enforced as a hard 402
        on POST /runs. A null value clears the ceiling (no limit). Admin only.
      requestBody:
        content:
          application/json:
            schema:
              properties:
                limitUsd:
                  description: USD ceiling, or null to clear the limit.
                  format: double
                  minimum: 0
                  nullable: true
                  type: number
              required:
                - limitUsd
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      limitUsd:
                        format: double
                        nullable: true
                        type: number
                    required:
                      - limitUsd
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Limit stored
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Invalid spending limit
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Caller is not an org admin
      summary: Set or clear the org-wide monthly spend ceiling (admin only)
      tags:
        - Billing
  /billing/subscription:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      limits:
                        properties:
                          projects:
                            nullable: true
                            type: integer
                          runsPerMonth:
                            nullable: true
                            type: integer
                        required:
                          - projects
                          - runsPerMonth
                        type: object
                      overage:
                        description: >
                          GAP-070 soft metered-overage estimate. tracked is the
                          running count reported to Stripe; estimated is the
                          live overage (executions beyond runsPerMonth, 0 when
                          unlimited). Display-only — metered billing is inert
                          until a Stripe metered product is configured.
                        properties:
                          estimated:
                            minimum: 0
                            type: integer
                          tracked:
                            minimum: 0
                            type: integer
                        required:
                          - tracked
                          - estimated
                        type: object
                      plan:
                        enum:
                          - free
                          - pro
                          - team
                        type: string
                      usage:
                        properties:
                          projects:
                            minimum: 0
                            type: integer
                          runsThisMonth:
                            minimum: 0
                            type: integer
                        required:
                          - projects
                          - runsThisMonth
                        type: object
                    required:
                      - plan
                      - usage
                      - limits
                      - overage
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Subscription state of the org
      summary: Current plan, usage and limits
      tags:
        - Billing
  /billing/usage-records:
    post:
      description: >
        GAP-070. Given the org's executionCount for the current month, computes
        the overage beyond the plan's runsPerMonth quota, reports those units to
        Stripe as metered usage (with an idempotency key), and increments the
        running overage_runs_tracked counter. This is SOFT overage billing
        ALONGSIDE the still-enforced hard monthly quota. INERT (503) until a
        Stripe metered Price/Product is configured in the Stripe dashboard and a
        subscription-item id has been captured via webhook.
      requestBody:
        content:
          application/json:
            schema:
              properties:
                executionCount:
                  minimum: 0
                  type: integer
              required:
                - executionCount
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      overage:
                        minimum: 0
                        type: integer
                      plan:
                        enum:
                          - free
                          - pro
                          - team
                        type: string
                      reported:
                        type: boolean
                      tracked:
                        minimum: 0
                        type: integer
                    required:
                      - plan
                      - overage
                      - reported
                      - tracked
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Overage computed (and reported when metered billing is configured)
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Hard execution ceiling reached — upgrade required
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Stripe-side failure reporting the usage record
        '503':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Stripe metered billing is not configured
      summary: Report soft metered execution overage to Stripe
      tags:
        - Billing
  /chat/conversations:
    post:
      description: >-
        Creates a conversation, optionally pinned to a project. The project pin
        scopes the assistant's system-prompt context and the LLM cost cap.
      requestBody:
        content:
          application/json:
            schema:
              properties:
                project_id:
                  format: uuid
                  type: string
              type: object
        required: false
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChatConversation'
          description: Conversation created
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Start a chat conversation
      tags:
        - Chat
  /chat/conversations/{id}/messages:
    get:
      description: >-
        Full chronological history including assistant tool calls and tool
        results.
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/ChatMessage'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Messages, oldest first
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Read the conversation history
      tags:
        - Chat
    parameters:
      - $ref: '#/components/parameters/Id'
    post:
      description: >-
        Persists the user message, runs the LLM tool loop (create/generate/ list
        test cases, trigger/read runs — max 5 tool iterations) and answers
        synchronously with every message the turn produced (user, assistant,
        tool results). Tool failures come back as tool-result messages, never as
        a 5xx; a cost-cap breach ends the turn with an assistant hint message.
      requestBody:
        content:
          application/json:
            schema:
              properties:
                content:
                  maxLength: 8000
                  minLength: 1
                  type: string
              required:
                - content
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/ChatMessage'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: All messages created by this turn, oldest first
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: >-
            The LLM upstream is unavailable (the user message is already
            persisted)
      summary: Send a message to the assistant
      tags:
        - Chat
  /configs/{id}:
    delete:
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete a config
      tags:
        - Configs
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      description: >
        `data`/`overrides` are validated against the STORED row's type. A secret
        field echoing the mask `***` keeps the stored value.
      requestBody:
        content:
          application/json:
            schema:
              minProperties: 1
              properties:
                data:
                  type: object
                name:
                  minLength: 1
                  type: string
                overrides:
                  description: Per-environment override blocks keyed by environment id.
                  type: object
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Config'
          description: The updated config (secrets masked)
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update a config
      tags:
        - Configs
  /environments/{id}:
    delete:
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete an environment
      tags:
        - Environments
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnvironmentUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Environment'
          description: The updated environment
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update an environment
      tags:
        - Environments
  /feedback:
    post:
      requestBody:
        content:
          application/json:
            schema:
              properties:
                message:
                  type: string
                rating:
                  enum:
                    - up
                    - down
                  type: string
              required:
                - rating
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      id:
                        format: uuid
                        type: string
                    required:
                      - id
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Feedback stored
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Submit product feedback
      tags:
        - Feedback
  /github/callback:
    get:
      parameters:
        - in: query
          name: installation_id
          required: true
          schema:
            type: string
        - in: query
          name: state
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  installation_id:
                    type: string
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - installation_id
                type: object
          description: Installation persisted on the project
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: GitHub App installation callback
      tags:
        - GitHub
  /github/install:
    get:
      parameters:
        - in: query
          name: project_id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '302':
          description: Redirect to the GitHub App installation page (state = project id)
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Start the GitHub App installation flow
      tags:
        - GitHub
  /heals/{id}/approve:
    post:
      description: >
        Owner|admin only. Applies the suggested selector to the test-case DSL
        and dispatches a verification run.
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Approve a heal suggestion (admin)
      tags:
        - Heals
  /heals/{id}/reject:
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Reject a heal suggestion (admin)
      tags:
        - Heals
  /health:
    get:
      description: >-
        Reports whether the API can serve database-backed traffic. Answers 200
        only when the database responds; 503 when it does not, with `ok` in
        lockstep with the HTTP status. This is the endpoint for uptime
        monitoring and the status page — use `/live` for orchestrator probes.
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  db_ok:
                    example: true
                    type: boolean
                  ok:
                    example: true
                    type: boolean
                type: object
          description: Service is ready — the database responded
        '503':
          content:
            application/json:
              schema:
                properties:
                  db_ok:
                    example: false
                    type: boolean
                  ok:
                    example: false
                    type: boolean
                type: object
          description: >-
            Service is NOT ready — the database is unreachable or did not answer
            within the ping deadline. The process itself is still serving HTTP.
      security: []
      summary: Readiness probe
      tags:
        - Health
  /integrations:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  slack:
                    properties:
                      channel:
                        type: string
                      connected:
                        type: boolean
                    required:
                      - connected
                    type: object
                required:
                  - slack
                type: object
          description: Per-integration connection state
      summary: Integration status of the org
      tags:
        - Integrations
  /integrations/slack:
    delete:
      description: Idempotent — disconnecting an already-disconnected org is ok.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          $ref: '#/components/responses/Forbidden'
      summary: Disconnect Slack (admin)
      tags:
        - Integrations
  /integrations/slack/callback:
    get:
      parameters:
        - in: query
          name: code
          required: true
          schema:
            type: string
        - in: query
          name: state
          required: true
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  ok:
                    enum:
                      - true
                    type: boolean
                  teamId:
                    type: string
                required:
                  - ok
                  - teamId
                type: object
          description: Token exchanged and stored for the org
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Slack OAuth callback
      tags:
        - Integrations
  /integrations/slack/install:
    get:
      responses:
        '302':
          description: Redirect to Slack's authorize URL (state = org id)
      summary: Start the Slack OAuth flow
      tags:
        - Integrations
  /issues/{id}:
    patch:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              properties:
                status:
                  enum:
                    - active
                    - ignored
                    - resolved
                  type: string
              required:
                - status
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Issue'
          description: The updated issue
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update the triage status of an issue
      tags:
        - Issues
  /legal/{doc}:
    get:
      parameters:
        - in: path
          name: doc
          required: true
          schema:
            enum:
              - terms
              - privacy
              - dpa
            type: string
        - in: query
          name: format
          schema:
            default: md
            enum:
              - md
              - pdf
            type: string
      responses:
        '200':
          content:
            application/pdf:
              schema:
                format: binary
                type: string
            text/markdown:
              schema:
                type: string
          description: The document as Markdown (default) or PDF
        '404':
          $ref: '#/components/responses/NotFound'
      security: []
      summary: Serve a legal document (public)
      tags:
        - Legal
  /live:
    get:
      description: >-
        Reports only that the process is up and serving HTTP — it performs no
        database call and stays 200 during a database outage. This is what the
        Fly http_service check polls; probing the DB-aware `/health` there would
        let a database fault evict the machine from the load balancer.
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  ok:
                    example: true
                    type: boolean
                type: object
          description: Process is alive
      security: []
      summary: Liveness probe
      tags:
        - Health
  /metrics/heal:
    get:
      parameters:
        - in: query
          name: window
          schema:
            default: 7d
            enum:
              - 7d
              - 30d
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    $ref: '#/components/schemas/HealMetrics'
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Aggregated heal metrics
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Heal metrics of the org
      tags:
        - Metrics
  /metrics/runs:
    get:
      description: >
        Pass rate, p50/p95 duration and cost per calendar day plus whole-window
        totals. Days without runs are zero-filled so the series always spans the
        window; percentile durations only consider finished runs (ended_at set)
        and are null for days without one.
      parameters:
        - in: query
          name: window
          schema:
            default: 7d
            enum:
              - 7d
              - 30d
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    $ref: '#/components/schemas/RunMetrics'
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Aggregated run metrics
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Run execution metrics of the org as a daily time series
      tags:
        - Metrics
  /notification-rules:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/NotificationRule'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Rules of the org, newest first
        '403':
          $ref: '#/components/responses/Forbidden'
      summary: List notification rules (admin)
      tags:
        - Notification Rules
    post:
      requestBody:
        content:
          application/json:
            schema:
              additionalProperties: false
              properties:
                channel:
                  enum:
                    - slack
                    - email
                  type: string
                only_failures:
                  default: true
                  type: boolean
                plan_id:
                  format: uuid
                  type: string
                project_id:
                  format: uuid
                  type: string
                target:
                  description: >-
                    Webhook-URL override for `slack` / address override for
                    `email`
                  type: string
              required:
                - channel
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationRule'
          description: Rule created
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create a notification rule (admin)
      tags:
        - Notification Rules
  /notification-rules/{id}:
    delete:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete a notification rule (admin)
      tags:
        - Notification Rules
  /onboarding/events:
    post:
      requestBody:
        content:
          application/json:
            schema:
              properties:
                event_type:
                  enum:
                    - signup
                    - first_crawl
                    - first_test_generated
                    - first_run
                  type: string
              required:
                - event_type
              type: object
        required: true
      responses:
        '201':
          $ref: '#/components/responses/Ok'
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Record an onboarding funnel milestone
      tags:
        - Onboarding
  /onboarding/sample-project:
    post:
      description: >-
        Provisions the demo sample project with a canonical DSL test case for
        the caller's org. Idempotent: a second call returns the existing project
        with 200 instead of creating a duplicate.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '201':
          $ref: '#/components/responses/Ok'
      summary: Create (or return) the org's idempotent sample project
      tags:
        - Onboarding
  /plan-runs/{id}:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanRunStatus'
          description: Aggregated plan-run state with per-status member counts
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Get a plan run (CI polling contract)
      tags:
        - Test Plans
  /plan-runs/{id}/rerun:
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
        - in: query
          name: scope
          schema:
            default: failed
            enum:
              - failed
              - all
            type: string
      responses:
        '202':
          content:
            application/json:
              schema:
                properties:
                  plan_run_id:
                    format: uuid
                    type: string
                required:
                  - plan_run_id
                type: object
          description: Rerun dispatched as a new plan run
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCapError'
          description: Run limit of the org's plan reached
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Predecessor still running, or nothing to rerun
        '502':
          $ref: '#/components/responses/EnqueueFailed'
      summary: Rerun failed (or all) member cases
      tags:
        - Test Plans
  /privacy/data-delete:
    post:
      description: 30-day confirmation/cancel window before the deletion runs.
      responses:
        '202':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      graceDays:
                        type: integer
                      requestId:
                        format: uuid
                        type: string
                      scheduledFor:
                        format: date-time
                        type: string
                      status:
                        enum:
                          - pending_confirmation
                        type: string
                    required:
                      - requestId
                      - status
                      - graceDays
                      - scheduledFor
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Deletion request recorded
      summary: Request org data deletion (GDPR Art. 17)
      tags:
        - Privacy
  /privacy/data-export:
    post:
      responses:
        '202':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      requestId:
                        format: uuid
                        type: string
                      status:
                        enum:
                          - queued
                        type: string
                    required:
                      - requestId
                      - status
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Export request queued
      summary: Request an org data export (GDPR Art. 20)
      tags:
        - Privacy
  /projects:
    get:
      description: >-
        All projects of the active org, newest first. Soft-deleted projects are
        excluded.
      responses:
        '200':
          content:
            application/json:
              schema:
                items:
                  $ref: '#/components/schemas/Project'
                type: array
          description: Projects of the org
      summary: List projects
      tags:
        - Projects
    post:
      requestBody:
        content:
          application/json:
            schema:
              properties:
                app_id:
                  format: uuid
                  type: string
                base_url:
                  format: uri
                  type: string
                name:
                  minLength: 1
                  type: string
              required:
                - name
                - base_url
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
          description: Project created
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Validation error
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCapError'
          description: Project limit of the org's plan reached (planGate)
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: app_id does not belong to this org (GAP-054)
      summary: Create a project
      tags:
        - Projects
  /projects/{id}:
    delete:
      description: >-
        Requires the owner|admin role. Sets `deleted_at`; run/artifact history
        stays intact.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
          description: Project soft-deleted
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Caller is not owner|admin
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown, foreign or already deleted project
      summary: Soft-delete a project (admin)
      tags:
        - Projects
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
          description: The project incl. settings fields
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown, foreign or soft-deleted project
      summary: Get a project
      tags:
        - Projects
    patch:
      description: >-
        Partial update; at least one field is required. `run_budget_minutes`
        above the org's plan cap answers 402.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      requestBody:
        content:
          application/json:
            schema:
              properties:
                app_id:
                  format: uuid
                  nullable: true
                  type: string
                base_url:
                  format: uri
                  type: string
                captcha_solving_enabled:
                  type: boolean
                crawl_cron:
                  minLength: 1
                  nullable: true
                  type: string
                device_preset:
                  oneOf:
                    - properties:
                        locale:
                          pattern: ^[a-z]{2}(-[A-Z]{2})?$
                          type: string
                        preset:
                          enum:
                            - desktop
                            - tablet
                            - mobile
                          type: string
                        timezone:
                          type: string
                      required:
                        - preset
                      type: object
                    - properties:
                        locale:
                          pattern: ^[a-z]{2}(-[A-Z]{2})?$
                          type: string
                        preset:
                          enum:
                            - custom
                          type: string
                        timezone:
                          type: string
                        viewport:
                          properties:
                            height:
                              maximum: 1024
                              minimum: 320
                              type: integer
                            width:
                              maximum: 1280
                              minimum: 320
                              type: integer
                          required:
                            - width
                            - height
                          type: object
                      required:
                        - preset
                        - viewport
                      type: object
                    - nullable: true
                generate_gap_tests:
                  type: boolean
                heal_auto_apply_threshold:
                  maximum: 1
                  minimum: 0
                  nullable: true
                  type: number
                name:
                  minLength: 1
                  type: string
                pr_feedback_emoji_reaction_enabled:
                  type: boolean
                pr_feedback_review_context_chars:
                  minimum: 50
                  nullable: true
                  type: integer
                pr_feedback_verdict_enabled:
                  type: boolean
                run_budget_minutes:
                  minimum: 1
                  nullable: true
                  type: integer
                state_lifetime_minutes:
                  maximum: 10080
                  minimum: 5
                  nullable: true
                  type: integer
                step_timeout_seconds:
                  maximum: 120
                  minimum: 5
                  nullable: true
                  type: integer
                test_data_retention_days:
                  maximum: 3650
                  minimum: 1
                  nullable: true
                  type: integer
                test_data_retention_policy:
                  enum:
                    - 30d
                    - 90d
                    - custom
                    - default
                  type: string
                vercel_protection_bypass_secret:
                  minLength: 1
                  nullable: true
                  type: string
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
          description: The updated project
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Validation error
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCapError'
          description: Requested run budget exceeds the plan cap
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown, foreign or soft-deleted project
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: app_id does not belong to this org (GAP-054)
      summary: Update a project
      tags:
        - Projects
  /projects/{id}/configs:
    get:
      description: Secret fields inside `data` and `overrides` are masked as `***`.
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/Config'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Configs of the project (secrets masked)
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List configs of a project
      tags:
        - Configs
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              properties:
                data:
                  description: >
                    Shape depends on `type` — credentials {username, password},
                    credentials_totp {username, password, totp_secret}, email
                    {address, password?}, file {file_name, url?, r2_key?,
                    content_type?} (at least one of url/r2_key; r2_key must live
                    under `configs/<projectId>/`).
                  type: object
                name:
                  minLength: 1
                  type: string
                type:
                  enum:
                    - credentials
                    - credentials_totp
                    - email
                    - file
                  type: string
              required:
                - name
                - type
                - data
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Config'
          description: Config created (secrets masked)
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create a config
      tags:
        - Configs
  /projects/{id}/configs/file-upload-url:
    post:
      description: >
        Returns a short-lived presigned PUT URL plus the `r2_key`
        (`configs/<projectId>/<uuid>/<file_name>`) to persist on a `file` config
        after the client uploaded the file directly to storage. `size_bytes` is
        validated against the 25 MB limit.
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              properties:
                content_type:
                  minLength: 1
                  type: string
                file_name:
                  minLength: 1
                  type: string
                size_bytes:
                  minimum: 1
                  type: integer
              required:
                - file_name
                - content_type
                - size_bytes
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  expires_in:
                    type: integer
                  r2_key:
                    type: string
                  upload_url:
                    type: string
                required:
                  - upload_url
                  - r2_key
                  - expires_in
                type: object
          description: Presigned upload target
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
        '503':
          description: File uploads unavailable (storage not configured)
      summary: Mint a presigned upload URL for a file config
      tags:
        - Configs
  /projects/{id}/crawls:
    get:
      description: >-
        The project's crawl history, newest first (limit 20). Replaces guessing
        crawl completion from page counts — `status` is the real lifecycle.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                items:
                  $ref: '#/components/schemas/CrawlSession'
                type: array
          description: Crawl sessions of the project
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown or foreign project
      summary: List crawl sessions
      tags:
        - Projects
    post:
      description: >-
        Enqueues a BFS crawl of the project's base URL with the background
        worker. The body is optional — an empty body starts an anonymous default
        crawl.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      requestBody:
        content:
          application/json:
            schema:
              properties:
                auth_test_case_id:
                  format: uuid
                  type: string
                intent:
                  maxLength: 500
                  minLength: 3
                  type: string
                max_actions:
                  maximum: 1000
                  minimum: 1
                  type: integer
                max_depth:
                  maximum: 10
                  minimum: 1
                  type: integer
                max_pages:
                  maximum: 1000
                  minimum: 1
                  type: integer
              type: object
        required: false
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CrawlAccepted'
          description: Crawl enqueued
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Validation error
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown or foreign project
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: auth_test_case_id does not belong to this project
      summary: Start a crawl
      tags:
        - Projects
  /projects/{id}/crawls/cancel:
    post:
      description: Stops every running crawl of the project at its next step boundary.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
          description: Cancellation enqueued
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown or foreign project
      summary: Cancel in-flight crawls
      tags:
        - Projects
  /projects/{id}/environments:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/Environment'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Environments of the project
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List environments of a project
      tags:
        - Environments
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnvironmentCreate'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Environment'
          description: Environment created
        '400':
          $ref: '#/components/responses/ValidationError'
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCapError'
          description: Per-project environment limit of the org's plan reached
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create an environment
      tags:
        - Environments
  /projects/{id}/github:
    patch:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              properties:
                installation_id:
                  minLength: 1
                  type: string
                repo:
                  example: acme/webshop
                  pattern: ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$
                  type: string
              required:
                - installation_id
                - repo
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  installation_id:
                    type: string
                  ok:
                    enum:
                      - true
                    type: boolean
                  repo:
                    type: string
                required:
                  - ok
                  - installation_id
                  - repo
                type: object
          description: Mapping persisted
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Map a GitHub repository to a project
      tags:
        - GitHub
  /projects/{id}/issues:
    get:
      description: >
        One issue per recurring fingerprint: failed tests (test case + failure
        class + step) and console errors (normalized message + source URL,
        GAP-051). Recurrences bump `occurrences` and `lastSeenAt` instead of
        creating new rows. Optionally filtered by triage status and/or kind
        (both filters combine).
      parameters:
        - $ref: '#/components/parameters/Id'
        - in: query
          name: status
          schema:
            enum:
              - active
              - ignored
              - resolved
            type: string
        - in: query
          name: kind
          schema:
            enum:
              - test_failure
              - console_error
              - a11y
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/Issue'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Issues of the project, newest activity first
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthorized'
      summary: List deduplicated failure issues of a project
      tags:
        - Issues
  /projects/{id}/remote-tunnels:
    get:
      description: >
        Tunnels map a developer's local port to a public tunnel URL exposed by
        an EXTERNAL transport (ngrok / cloudflared / localtunnel). The platform
        only stores the mapping; a run targets the tunnel URL as its effective
        base.
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/RemoteTunnel'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: The project's remote tunnels, newest first
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List a project's registered remote tunnels
      tags:
        - Remote Tunnels
    post:
      description: >
        Stores a (local_port → public tunnel_url) mapping. The actual tunnel
        process is external — the platform never establishes it. `project_id` in
        the body must match the path project.
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoteTunnelCreate'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteTunnel'
          description: Remote tunnel registered
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
      summary: Register a remote tunnel for a project (admin only)
      tags:
        - Remote Tunnels
  /projects/{id}/test-cases:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
        - in: query
          name: include_archived
          schema:
            default: false
            type: boolean
        - description: >-
            Owner filter (GAP-057): a Clerk user id (created_by) or the literal
            "system" (created_by IS NULL). Absent ⇒ all owners.
          in: query
          name: owner
          schema:
            type: string
        - description: >-
            Group filter (GAP-002): a test-group uuid (group_id) or the literal
            "ungrouped" (group_id IS NULL). Absent ⇒ all groups.
          in: query
          name: group
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/TestCase'
                    type: array
                  limit:
                    type: integer
                  offset:
                    type: integer
                  total:
                    type: integer
                required:
                  - items
                  - total
                  - offset
                  - limit
                type: object
          description: Test cases of the project (archived hidden by default)
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List test cases of a project
      tags:
        - Test Cases
  /projects/{id}/test-cases/generate:
    post:
      description: >
        Generates up to `count` DSL test cases for the project's base URL (or an
        explicit `url`) from a natural-language goal and persists them as
        drafts.
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              properties:
                count:
                  default: 1
                  maximum: 5
                  minimum: 1
                  type: integer
                goal:
                  maxLength: 2000
                  minLength: 3
                  type: string
                url:
                  format: uri
                  type: string
              required:
                - goal
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/TestCase'
                    type: array
                type: object
          description: Generated test cases
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Generate test cases with AI
      tags:
        - Test Cases
  /projects/{id}/test-groups:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
        - description: >-
            A test-group uuid lists that group's direct children; absent lists
            the project's root groups (parent_group_id IS NULL).
          in: query
          name: parent
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/TestGroup'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: The matching groups, ordered by (position, name)
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List a project's root test groups (or a parent's direct children)
      tags:
        - Test Groups
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestGroupCreate'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestGroup'
          description: Test group created
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create a test group (admin only)
      tags:
        - Test Groups
  /projects/{id}/test-groups/tree:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  tree:
                    items:
                      $ref: '#/components/schemas/TestGroupTreeNode'
                    type: array
                required:
                  - tree
                type: object
          description: The nested tree (sidebar source)
        '404':
          $ref: '#/components/responses/NotFound'
      summary: The project's nested group tree with per-group case counts
      tags:
        - Test Groups
  /projects/{id}/test-plans:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
        - description: >-
            Owner filter (GAP-057): a Clerk user id (created_by) or the literal
            "system" (created_by IS NULL). Absent ⇒ all owners.
          in: query
          name: owner
          schema:
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/TestPlan'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Test plans of the project
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List test plans of a project
      tags:
        - Test Plans
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestPlanCreate'
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestPlan'
          description: Test plan created
        '400':
          $ref: '#/components/responses/ValidationError'
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCapError'
          description: Test-plan limit of the org's plan reached
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create a test plan
      tags:
        - Test Plans
  /projects/{id}/usage:
    get:
      description: >-
        Run count and LLM cost of the project since the start of the current UTC
        calendar month, plus the configured daily LLM cap. Browser-agent costs
        (Stagehand act()) are not included.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectUsage'
          description: Usage of the project in the current UTC month
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown, foreign or soft-deleted project
      summary: Project usage this month
      tags:
        - Projects
  /remote-tunnels/{id}:
    delete:
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete a remote tunnel (admin only)
      tags:
        - Remote Tunnels
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      description: >
        Flip the status (active → revoked/expired). The port and URL are
        immutable once registered — re-register to change them.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RemoteTunnelStatusUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RemoteTunnel'
          description: The updated remote tunnel
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update a remote tunnel's lifecycle status (admin only)
      tags:
        - Remote Tunnels
  /runs:
    get:
      description: >-
        Paginated run history, newest first; filterable by project, test case,
        plan run, status, result (passed|failed|skipped), owner (Clerk user id
        or 'system') and a case-insensitive name search (q).
      parameters:
        - in: query
          name: projectId
          required: false
          schema:
            format: uuid
            type: string
        - in: query
          name: testCaseId
          required: false
          schema:
            format: uuid
            type: string
        - in: query
          name: planRunId
          required: false
          schema:
            format: uuid
            type: string
        - in: query
          name: deviceMatrixId
          required: false
          schema:
            format: uuid
            type: string
        - in: query
          name: status
          required: false
          schema:
            enum:
              - queued
              - running
              - passed
              - failed
              - error
              - cancelled
              - skipped
            type: string
        - in: query
          name: result
          required: false
          schema:
            enum:
              - passed
              - failed
              - skipped
            type: string
        - in: query
          name: owner
          required: false
          schema:
            minLength: 1
            type: string
        - in: query
          name: q
          required: false
          schema:
            maxLength: 200
            minLength: 1
            type: string
        - in: query
          name: limit
          required: false
          schema:
            default: 20
            maximum: 100
            minimum: 1
            type: integer
        - in: query
          name: offset
          required: false
          schema:
            default: 0
            minimum: 0
            nullable: true
            type: integer
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunList'
          description: Runs matching the filters
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Invalid filter/pagination query
      summary: List runs
      tags:
        - Runs
    post:
      description: >-
        Inserts a queued run for the test case and enqueues it with the
        background worker.
      requestBody:
        content:
          application/json:
            schema:
              properties:
                device_preset:
                  oneOf:
                    - properties:
                        locale:
                          pattern: ^[a-z]{2}(-[A-Z]{2})?$
                          type: string
                        preset:
                          enum:
                            - desktop
                            - tablet
                            - mobile
                          type: string
                        timezone:
                          type: string
                      required:
                        - preset
                      type: object
                    - properties:
                        locale:
                          pattern: ^[a-z]{2}(-[A-Z]{2})?$
                          type: string
                        preset:
                          enum:
                            - custom
                          type: string
                        timezone:
                          type: string
                        viewport:
                          properties:
                            height:
                              maximum: 1024
                              minimum: 320
                              type: integer
                            width:
                              maximum: 1280
                              minimum: 320
                              type: integer
                          required:
                            - width
                            - height
                          type: object
                      required:
                        - preset
                        - viewport
                      type: object
                device_presets:
                  items:
                    oneOf:
                      - properties:
                          locale:
                            pattern: ^[a-z]{2}(-[A-Z]{2})?$
                            type: string
                          preset:
                            enum:
                              - desktop
                              - tablet
                              - mobile
                            type: string
                          timezone:
                            type: string
                        required:
                          - preset
                        type: object
                      - properties:
                          locale:
                            pattern: ^[a-z]{2}(-[A-Z]{2})?$
                            type: string
                          preset:
                            enum:
                              - custom
                            type: string
                          timezone:
                            type: string
                          viewport:
                            properties:
                              height:
                                maximum: 1024
                                minimum: 320
                                type: integer
                              width:
                                maximum: 1280
                                minimum: 320
                                type: integer
                            required:
                              - width
                              - height
                            type: object
                        required:
                          - preset
                          - viewport
                        type: object
                  maxItems: 4
                  minItems: 1
                  type: array
                environment_id:
                  format: uuid
                  type: string
                model_tier:
                  enum:
                    - fast
                    - thorough
                  type: string
                test_case_id:
                  format: uuid
                  type: string
                vars:
                  items:
                    properties:
                      name:
                        minLength: 1
                        type: string
                      secret:
                        default: false
                        type: boolean
                      value:
                        type: string
                    required:
                      - name
                      - value
                    type: object
                  type: array
              required:
                - test_case_id
              type: object
        required: true
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunAccepted'
          description: Run queued
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Validation error
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Run limit of the org's plan reached (planGate)
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: environment_id does not belong to the test case's project
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Run committed but could not be enqueued (marked as error)
      summary: Trigger a test run
      tags:
        - Runs
  /runs/owners:
    get:
      description: >-
        Distinct owners of the org's runs (for the run-list owner filter): one
        entry per Clerk user id plus a single { id: null } System entry when at
        least one system-triggered run exists.
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunOwnerList'
          description: Distinct run owners of the org
      summary: List run owners
      tags:
        - Runs
  /runs/{id}:
    get:
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunDetail'
          description: The run incl. artifacts, DSL and step results
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown or foreign run
      summary: Get a run
      tags:
        - Runs
  /runs/{id}/cancel:
    post:
      description: >-
        Requests cancellation of a queued or running run via the background
        worker.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OkEnvelope'
          description: Cancellation enqueued
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown or foreign run
        '409':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Run already finished
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Cancellation could not be enqueued
      summary: Cancel a run
      tags:
        - Runs
  /runs/{id}/emails:
    get:
      description: >-
        Mails delivered to the run's system-managed inbox addresses (text view
        only), newest first.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunEmailList'
          description: Received emails of the run
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: Unknown or foreign run
      summary: List a run's received test-inbox emails
      tags:
        - Runs
  /runs/{id}/heals:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/HealEvent'
                    type: array
                required:
                  - items
                type: object
          description: Heal events of the run (empty for foreign orgs)
      summary: List heal events of a run
      tags:
        - Heals
  /runs/{id}/stream:
    get:
      description: >-
        Server-sent events: `status` on every status change, `step` per
        completed DSL step, terminal `done`. An unknown run yields a single
        `error` event.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            format: uuid
            type: string
      responses:
        '200':
          description: >-
            text/event-stream of status/step/done events (see
            runStreamEventSchema in routes/runs.ts)
      summary: Stream live run events (SSE)
      tags:
        - Runs
  /status/incidents:
    get:
      description: >
        Anonymous endpoint for status.testsbot.com — open incidents plus
        everything resolved within the visibility window, newest first.
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  incidents:
                    items:
                      $ref: '#/components/schemas/Incident'
                    type: array
                required:
                  - incidents
                type: object
          description: Current incidents
      security: []
      summary: List current incidents (public)
      tags:
        - Status
    post:
      requestBody:
        content:
          application/json:
            schema:
              additionalProperties: false
              properties:
                affected:
                  items:
                    enum:
                      - api
                      - worker
                      - browserbase
                    type: string
                  type: array
                body:
                  minLength: 1
                  type: string
                severity:
                  default: minor
                  enum:
                    - minor
                    - major
                    - critical
                  type: string
                status:
                  enum:
                    - investigating
                    - identified
                    - monitoring
                    - resolved
                  type: string
                title:
                  minLength: 1
                  type: string
              required:
                - title
                - status
                - body
                - affected
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                properties:
                  id:
                    format: uuid
                    type: string
                required:
                  - id
                type: object
          description: Incident created
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
      summary: Open an incident (admin)
      tags:
        - Status
  /status/incidents/{id}:
    patch:
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              additionalProperties: false
              minProperties: 1
              properties:
                resolved_at:
                  format: date-time
                  nullable: true
                  type: string
                status:
                  enum:
                    - investigating
                    - identified
                    - monitoring
                    - resolved
                  type: string
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Incident'
          description: The updated incident
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update an incident (admin)
      tags:
        - Status
  /test-cases:
    post:
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/TestCaseDsl'
                - properties:
                    parent_id:
                      format: uuid
                      type: string
                    project_id:
                      format: uuid
                      type: string
                  required:
                    - project_id
                  type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestCase'
          description: Test case created
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Create a test case
      tags:
        - Test Cases
  /test-cases/{id}:
    delete:
      description: 'Soft-delete — sets `status: archived`; run history keeps its FK.'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Archive a test case
      tags:
        - Test Cases
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestCase'
          description: The test case
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Get a test case
      tags:
        - Test Cases
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      description: >
        Partial update; at least one field is required. `depends_on_id` is
        validated against same-project membership and cycle freedom; `null`
        clears the dependency.
      requestBody:
        content:
          application/json:
            schema:
              minProperties: 1
              properties:
                depends_on_id:
                  format: uuid
                  nullable: true
                  type: string
                dsl:
                  description: TestCaseDsl without the row-level `name` (steps, vars, tags)
                  properties:
                    steps:
                      items:
                        $ref: '#/components/schemas/TestStep'
                      type: array
                    tags:
                      items:
                        type: string
                      type: array
                    vars:
                      items:
                        $ref: '#/components/schemas/TestVar'
                      type: array
                  type: object
                name:
                  minLength: 1
                  type: string
                status:
                  enum:
                    - draft
                    - active
                    - archived
                  type: string
                tags:
                  items:
                    type: string
                  type: array
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestCase'
          description: The updated test case
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update a test case
      tags:
        - Test Cases
  /test-cases/{id}/duplicate:
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestCase'
          description: The copy (status `draft`, name suffixed with "(copy)")
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Duplicate a test case
      tags:
        - Test Cases
  /test-cases/{id}/quarantine:
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Quarantine a flaky test case
      tags:
        - Test Cases
  /test-cases/{id}/refine:
    post:
      description: >
        Regenerates the steps from the persisted `goal`/`expected result`
        (P5-W23-T03) and/or an ad-hoc `instruction`. Returns the current and the
        proposed DSL WITHOUT persisting anything — accepting happens via `PATCH
        /test-cases/{id}` with the proposed `dsl`.
      parameters:
        - $ref: '#/components/parameters/Id'
      requestBody:
        content:
          application/json:
            schema:
              properties:
                instruction:
                  description: Optional revision instruction in natural language.
                  maxLength: 1000
                  minLength: 3
                  type: string
              type: object
        required: false
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  current:
                    $ref: '#/components/schemas/TestCaseDsl'
                  proposed:
                    $ref: '#/components/schemas/TestCaseDsl'
                required:
                  - current
                  - proposed
                type: object
          description: Current and proposed DSL — a pure proposal, no DB write
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: >-
            Nothing to regenerate from (no stored goal and no instruction), or
            the stored DSL is invalid
        '502':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
          description: The LLM could not produce a schema-valid revision
      summary: Refine a test case with AI (proposal only)
      tags:
        - Test Cases
  /test-cases/{id}/revisions:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/TestCaseRevision'
                    type: array
                  limit:
                    type: integer
                  offset:
                    type: integer
                  total:
                    type: integer
                required:
                  - items
                  - total
                  - offset
                  - limit
                type: object
          description: Audit-log revision history, newest first
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List revisions of a test case
      tags:
        - Test Cases
  /test-cases/{id}/unquarantine:
    post:
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Lift the quarantine of a test case
      tags:
        - Test Cases
  /test-groups/{id}:
    delete:
      description: >
        Deletes the group and its whole subtree (cascade). Member test cases are
        orphaned (group_id set to NULL), never deleted.
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete a test group (admin only)
      tags:
        - Test Groups
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      description: >
        Partial update; at least one field is required. `parent_group_id` is
        validated against same-project membership and cycle freedom (a group may
        not be re-parented under itself or one of its descendants); `null`
        re-parents the group to the root.
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestGroupUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestGroup'
          description: Updated test group
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update a test group — rename, re-parent or reorder (admin only)
      tags:
        - Test Groups
  /test-items:
    get:
      description: >
        The caller's own test cases AND test plans across the whole org, newest
        first, paginated. The owner is always the verified caller — there is no
        cross-user view, so this route takes no owner parameter.
      parameters:
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/TestItem'
                    type: array
                  limit:
                    type: integer
                  offset:
                    type: integer
                  total:
                    type: integer
                required:
                  - items
                  - total
                  - offset
                  - limit
                type: object
          description: The caller's owned test cases and plans
      summary: List the caller's owned test cases and plans ("My items")
      tags:
        - Test Items
  /test-plans/{id}:
    delete:
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete a test plan
      tags:
        - Test Plans
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/TestPlan'
                  - properties:
                      explicitMembers:
                        items:
                          properties:
                            position:
                              type: integer
                            testCaseId:
                              format: uuid
                              type: string
                          required:
                            - testCaseId
                            - position
                          type: object
                        type: array
                      resolvedMemberIds:
                        items:
                          format: uuid
                          type: string
                        type: array
                    required:
                      - explicitMembers
                      - resolvedMemberIds
                    type: object
          description: The plan incl. explicit members and resolved member ids
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Get a test plan
      tags:
        - Test Plans
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TestPlanUpdate'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestPlan'
          description: The updated plan
        '400':
          $ref: '#/components/responses/ValidationError'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update a test plan
      tags:
        - Test Plans
  /test-plans/{id}/runs:
    get:
      parameters:
        - $ref: '#/components/parameters/Id'
        - $ref: '#/components/parameters/Offset'
        - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/PlanRun'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Plan runs, newest first
        '404':
          $ref: '#/components/responses/NotFound'
      summary: List runs of a test plan
      tags:
        - Test Plans
    post:
      description: >
        Dispatches the plan's resolved member cases as individual runs grouped
        under one plan run. Gated by the org's monthly run budget (402).
      parameters:
        - $ref: '#/components/parameters/Id'
      responses:
        '202':
          content:
            application/json:
              schema:
                properties:
                  plan_run_id:
                    format: uuid
                    type: string
                required:
                  - plan_run_id
                type: object
          description: Plan run dispatched
        '402':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanCapError'
          description: Run limit of the org's plan reached
        '404':
          $ref: '#/components/responses/NotFound'
        '502':
          $ref: '#/components/responses/EnqueueFailed'
      summary: Run a test plan now
      tags:
        - Test Plans
  /user-preferences:
    get:
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      tourCompleted:
                        type: boolean
                    required:
                      - tourCompleted
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Per-user preferences (keyed by the verified Clerk user)
      summary: Read the caller's preferences
      tags:
        - User Preferences
    patch:
      requestBody:
        content:
          application/json:
            schema:
              properties:
                tour_completed:
                  type: boolean
              required:
                - tour_completed
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    properties:
                      tourCompleted:
                        type: boolean
                    required:
                      - tourCompleted
                    type: object
                  ok:
                    enum:
                      - true
                    type: boolean
                required:
                  - ok
                  - data
                type: object
          description: Updated preferences
        '400':
          $ref: '#/components/responses/ValidationError'
      summary: Update the caller's preferences
      tags:
        - User Preferences
  /webhooks:
    get:
      description: The HMAC secret is masked as `***` on every read.
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  items:
                    items:
                      $ref: '#/components/schemas/Webhook'
                    type: array
                  total:
                    type: integer
                required:
                  - items
                  - total
                type: object
          description: Webhooks of the org
        '403':
          $ref: '#/components/responses/Forbidden'
      summary: List outbound webhooks (admin)
      tags:
        - Webhooks
    post:
      description: >
        The HMAC secret (`whsec_…`) is generated server-side and returned in
        THIS response only; every later read masks it.
      requestBody:
        content:
          application/json:
            schema:
              additionalProperties: false
              properties:
                events:
                  items:
                    enum:
                      - run.completed
                      - crawl.completed
                    type: string
                  minItems: 1
                  type: array
                project_id:
                  format: uuid
                  type: string
                url:
                  format: uri
                  type: string
              required:
                - url
                - events
              type: object
        required: true
      responses:
        '201':
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Webhook'
                  - properties:
                      secret:
                        description: HMAC signing secret (`whsec_…`), shown once
                        type: string
                    required:
                      - secret
                    type: object
          description: Webhook created (plaintext secret included exactly once)
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Create an outbound webhook (admin)
      tags:
        - Webhooks
  /webhooks/{id}:
    delete:
      responses:
        '200':
          $ref: '#/components/responses/Ok'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Delete an outbound webhook (admin)
      tags:
        - Webhooks
    parameters:
      - $ref: '#/components/parameters/Id'
    patch:
      requestBody:
        content:
          application/json:
            schema:
              additionalProperties: false
              properties:
                active:
                  type: boolean
                events:
                  items:
                    enum:
                      - run.completed
                      - crawl.completed
                    type: string
                  minItems: 1
                  type: array
                url:
                  format: uri
                  type: string
              type: object
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Webhook'
          description: The updated webhook (secret masked)
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
      summary: Update an outbound webhook (admin)
      tags:
        - Webhooks
security:
  - bearerAuth: []
  - apiKeyAuth: []
servers:
  - description: Production
    url: https://api.testsbot.com
  - description: Local
    url: http://localhost:3001
tags:
  - name: Health
  - name: Projects
  - name: Test Cases
  - name: Runs
  - name: Heals
  - name: Metrics
  - name: Test Plans
  - name: Test Groups
  - name: Test Items
  - name: Environments
  - name: Remote Tunnels
  - name: Configs
  - name: API Keys
  - name: Webhooks
  - name: Integrations
  - name: Notification Rules
  - name: Status
  - name: Billing
  - name: Feedback
  - name: Onboarding
  - name: Chat
  - name: User Preferences
  - name: Legal
  - name: Privacy
  - name: GitHub
