> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.nunu.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Update Test

> Apply a partial update to a test case. Uses optimistic concurrency control via expected_version.

<Info>
  **Permission Required**: `project:edit-tests`
</Info>

Apply a partial update to a test case. Updates are protected by an optimistic
version check — supply the `version` you fetched from
[Get Test](/api-reference/tests/get-test) as `expected_version`. If the test
has been modified since, the update fails with a `409 Conflict` and the latest
version is returned so the caller can rebase.

The `patch` field is deep-merged into the current test body. To replace a list
(e.g. `tags` or `player_order`), include the full new list — arrays are
replaced, not merged.

## Path Parameters

<ParamField path="testId" type="string" required>
  The test ID (UUID)
</ParamField>

## Query Parameters

<ParamField query="include_shared_collection_steps" type="boolean" default="true">
  Whether shared collection steps are expanded inline in the response.
</ParamField>

## Request Body

<ParamField body="expected_version" type="integer" required>
  The version returned by the most recent [Get Test](/api-reference/tests/get-test).
  Must be a non-negative integer.
</ParamField>

<ParamField body="patch" type="object" required>
  Partial test body. Object fields are deep-merged; arrays are replaced wholesale.
  Cannot change the test `id`.
</ParamField>

<ParamField body="shared_collection_steps_mode" type="string" default="error">
  How to handle shared collection items that contain inline steps in the patch:
  `error` (reject) or `drop` (silently strip the inline steps).
</ParamField>

<ParamField body="folder_path" type="string">
  New folder placement. `null` or `"/"` moves the test to the project root.
  Mutually exclusive with `folder_id`. Omit both fields to leave folder unchanged.
</ParamField>

<ParamField body="folder_id" type="string">
  Pre-resolved folder UUID. Mutually exclusive with `folder_path`.
</ParamField>

## Patching the time budget

Time budgets live at the test level (`min_seconds` / `max_seconds` on the
test body) — see [Create Test](/api-reference/tests/create-test#test-level-time-budget)
for the per-framework rules. Patches accept the same fields with the same
constraints:

* `verification`: only `max_seconds` is allowed; sending `min_seconds` is
  rejected with `400`.
* `discovery` / `task`: either bound (or both) may be patched. The merged
  result must satisfy `min_seconds <= max_seconds`; e.g. patching only
  `min_seconds` to a value above the current `max_seconds` is rejected.

## Response

Returns the updated test (same shape as [Get Test](/api-reference/tests/get-test))
on success.

On version conflict, returns `409` with:

```json theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
{
  "error": "version conflict",
  "expected_version": 3,
  "latest_version": 5
}
```

<RequestExample>
  ```bash cURL (rename test) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X PATCH "https://nunu.ai/api/v1/project/your-project-id/tests/a5060a07-d386-4269-98ce-b33fb894201e" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "expected_version": 4,
      "patch": { "name": "Sanity smoke v2", "tags": ["smoke", "regression"] }
    }'
  ```

  ```bash cURL (move folder) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X PATCH "https://nunu.ai/api/v1/project/your-project-id/tests/a5060a07-d386-4269-98ce-b33fb894201e" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "expected_version": 4,
      "patch": {},
      "folder_path": "regression/full"
    }'
  ```

  ```bash cURL (raise budget on a discovery test) theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  curl -X PATCH "https://nunu.ai/api/v1/project/your-project-id/tests/a5060a07-d386-4269-98ce-b33fb894201e" \
    -H "X-Api-Key: YOUR_API_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "expected_version": 4,
      "patch": { "max_seconds": 14400 }
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  {
    "id": "a5060a07-d386-4269-98ce-b33fb894201e",
    "project_id": "test",
    "name": "Sanity smoke v2",
    "version": 5,
    "tags": ["smoke", "regression"],
    "test_type": "verification",
    "...": "..."
  }
  ```

  ```json 409 Conflict theme={"theme":{"light":"catppuccin-latte","dark":"catppuccin-mocha"}}
  {
    "error": "version conflict",
    "expected_version": 4,
    "latest_version": 5
  }
  ```
</ResponseExample>


## OpenAPI

````yaml PATCH /project/{projectId}/tests/{testId}
openapi: 3.1.0
info:
  title: nunu.ai API
  description: >-
    The nunu.ai Public API allows you to programmatically interact with your
    projects, manage test runs, and upload builds.
  version: 1.0.0
servers:
  - url: https://nunu.ai/api/v1
security:
  - apiKeyAuth: []
paths:
  /project/{projectId}/tests/{testId}:
    parameters:
      - name: projectId
        in: path
        required: true
        schema:
          type: string
        description: >-
          The project ID. You can copy it from the project settings page or the
          project URL in the dashboard.
    patch:
      tags:
        - Tests
      summary: Update Test
      description: >-
        Apply a partial update to a test case. Uses optimistic concurrency
        control via expected_version.
      operationId: updateTest
      parameters:
        - name: testId
          in: path
          required: true
          description: The test ID (UUID)
          schema:
            type: string
            format: uuid
        - name: include_shared_collection_steps
          in: query
          description: Whether shared collection steps are expanded inline
          schema:
            type: boolean
            default: true
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTestRequest'
      responses:
        '200':
          description: Test updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TestCase'
        '400':
          description: Bad Request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Forbidden - missing project:edit-tests
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Test not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: Version conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VersionConflictError'
components:
  schemas:
    UpdateTestRequest:
      type: object
      required:
        - expected_version
        - patch
      properties:
        expected_version:
          type: integer
          minimum: 0
          description: Version returned by the most recent Get Test.
        patch:
          type: object
          description: >-
            Partial test body. Object fields are deep-merged; arrays are
            replaced wholesale.
        shared_collection_steps_mode:
          type: string
          enum:
            - error
            - drop
          default: error
        folder_path:
          type: string
          nullable: true
        folder_id:
          type: string
          format: uuid
    TestCase:
      type: object
      description: >-
        Full test case body. Shape varies by test_type (verification, discovery,
        task); see the docs page for the exact shape.
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
        name:
          type: string
        version:
          type: integer
        tags:
          type: array
          items:
            type: string
        incident_auto_approve:
          type: boolean
        test_type:
          type: string
          enum:
            - verification
            - discovery
            - task
        quarantined:
          type: boolean
        testrail:
          type: object
          nullable: true
        source:
          type: string
          enum:
            - repository
            - bot
        min_seconds:
          type: integer
          minimum: 0
          maximum: 19800
          description: >-
            Test-level minimum runtime in seconds. Allowed for `discovery` and
            `task` only; sending this on a `verification` test is rejected with
            400. Defaults when omitted: discovery=3600 (1h), task=0. Must be ≤
            `max_seconds` (validated against the effective budget after defaults
            are applied).
        max_seconds:
          type: integer
          minimum: 0
          maximum: 19800
          description: >-
            Test-level maximum runtime in seconds. Hard ceiling for any single
            run is 19800 (5.5h). Defaults when omitted: verification=19800
            (5.5h), discovery=10800 (3h), task=19800 (5.5h).
        players_by_key:
          type: object
          additionalProperties: true
        player_order:
          type: array
          items:
            type: string
    Error:
      type: object
      properties:
        error:
          type: string
        code:
          type: string
        details:
          type: object
    VersionConflictError:
      type: object
      properties:
        error:
          type: string
          example: version conflict
        expected_version:
          type: integer
        latest_version:
          type: integer
  securitySchemes:
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Api-Key
      description: API key for authentication

````