Skip to content

Copy a case beside the original

POST
/v1/test-cases/{id}/duplicate
curl --request POST \
--url https://beta-api.plune.ai/v1/test-cases/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0/duplicate \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "suiteId": "6f1c2b3a-0000-4000-8000-0000000000a1", "position": 0 }'

A new case with the same texts, priority and suite, titled «Copy of …» (its @tags come along), as a draft, right under its original (D30, D31). External keys and evalId stay with the original — they are what makes a case itself — and an automated case has no copy: its identity is the spec, so the test is duplicated in code. The body is optional and says where the copy goes instead — the dashboard sends it when a selection is dropped with Alt held.

id
required
string format: uuid
Media type application/json

Where the copy goes (D31). Optional, and so is each field: without a body the copy sits right under its original; a suiteId alone puts it last there; position is the slot among that suite’s cases, past the end meaning last.

object
suiteId

A suite, never a folder — the same rule as filing a case; null puts the copy outside any suite.

string format: uuid
nullable
position
integer
Examples
Example placed

The copy first in another suite

{
"suiteId": "6f1c2b3a-0000-4000-8000-0000000000a1",
"position": 0
}

The copy

Media type application/json
One of:

A stored test case — the metadata plus the body of its type.

object
schemaVersion
required

Accepted: 1 or 2. Responses are always 2 — a v1 record is upgraded on read.

integer
Allowed values: 1 2
id
required
string format: uuid
projectId
required
string
title
required

Carries its @tag tail — the one source of tags (ADR 0032). The dashboard shows the clean title and the tags as chips.

string
state
required

The case’s place in its lifecycle. in_review is a case a proposal produced and nobody has confirmed yet; detached is one whose source stopped being reported. Who may move it where depends on whether the caller is a session or a token.

string
Allowed values: draft in_review active detached deprecated
status
required

Derived from state for clients on v1, never stored. in_review answers as draft and detached as active — v1 has no word for either.

string
Allowed values: draft active archived
stateReason

Why the case last moved.

string
allowedStates

Only when reading ONE case: the states the caller may move it to from here, per the lifecycle table and who the caller is - a session is a person, a bearer token is automation. A screen renders exactly these as actions and never lists moves itself. The current state is not included.

Array<string>
Allowed values: draft in_review active detached deprecated
stateChangedAt

When it last moved. Absent on a case that has not moved.

string format: date-time
suiteId

The suite this case is filed under, if any. Always a suite, never a folder (AC-03).

string
priority
required

Five levels in rank order — low < normal < important < high < critical. Metadata: changing it does not bump the text version.

string
Allowed values: low normal important high critical
position
required

Order among the cases of its suite (or the unfiled ones) — dense, 0-based, renumbered on every write that places a case (AC-05).

integer
tags
required

Read from the title by the server, each once, in first-seen order. Derived: sent back, never taken on the way in as a field of its own — tags on a write rewrites the title.

Array<string>
description

Free text beside the steps. Part of what a result was produced against, so a change bumps the text version (AC-09). Absent when never set. 16 384 bytes of UTF-8 at most.

string
precondition

What must hold before the steps. Same rules as description.

string
examples

The parameters table (D7). Content like the two texts: a change bumps the version.

object
params
required
Array<string>
>= 1 items <= 16 items
rows
required
Array<Array<string>>
>= 1 items <= 100 items
externalKeys

The identifiers this case has elsewhere (D2) — attached on every read; absent when it has none.

Array<object>
<= 32 items

An identifier another tool already uses for this test, so a report can find its case without anyone typing a Plune id. The order of kind is load-bearing: it is the ranking that decides which key wins when several match different cases.

object
kind
required
string
Allowed values: playwright-id path-title allure-history cairn-stable eval-id qase testrail
value
required
string
<= 1024 characters
evalId

The id of this case’s eval in plune.yaml. An ingested run attaches its results to the case carrying the same id (ADR 0010); absent for cases the CLI does not run.

string
createdBy

Who wrote it (B8-team T3): the userId of the caller of POST /v1/test-cases or of a duplicate. Set by the server, never taken from a body, never changed by a PATCH. Absent on a case from before names were kept and on one the review queue created. Resolve it through GET /v1/members.

string
assigneeId

Who is to run it (B8-team T4): the userId of a member of the project. Set by PATCH {assigneeId} or the bulk assign action, never on create; null on a PATCH takes it away; leaving the project takes it away too. Absent means unassigned. GET /v1/test-cases?assignee=me|none|<userId> filters on it.

string
provenance

Where the case came from, present only on one an approved proposal created (B-1). Read through that proposal rather than stored on the case, so it answers for every case ever created this way and can never disagree with the queue entry beside it. Absent on a case a person wrote — and absent, too, when the proposal named neither field: both mean nothing was reported.

object
source
object
kind
required
string
Allowed values: url openapi-operation requirement
value
required
string
>= 1 characters <= 100 characters
technique

The test-design technique the supplier named, as they named it.

string
<= 32 characters
createdAt
required
string format: date-time
updatedAt
required
string format: date-time
type
required
string
Allowed values: manual
steps
required
Array<object>
object
action
required
string
expected
required
string
Example
{
"schemaVersion": 1,
"title": "Checkout — empty cart @smoke @checkout",
"state": "draft",
"status": "draft",
"allowedStates": [
"draft"
],
"priority": "low",
"tags": [
"smoke",
"checkout"
],
"examples": {
"params": [
"Amount",
"Outcome"
],
"rows": [
[
"10.00",
"pending"
],
[
"0.00",
"refused"
]
]
},
"externalKeys": [
{
"kind": "playwright-id"
}
],
"provenance": {
"source": {
"kind": "url"
}
},
"type": "manual"
}

A body that is not JSON, a position below 0, or a suiteId that is a folder or not the caller’s

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

No / invalid token or session

No such case in the caller’s project

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

The case is automated — identity is the spec

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}

Rate-limited: either the per-route throttle or a tenant quota. Retry-After carries the seconds until the window rolls over.

Media type application/json
object
error
required
string
Example generated
{
"error": "example"
}
Retry-After
integer

Seconds until the window rolls over