Skip to content

Approve or reject an item, or correct a rejection reason

PATCH
/v1/review-items/{id}
curl --request PATCH \
--url https://beta-api.plune.ai/v1/review-items/2489E9AD-2EE2-8E00-8EC9-32D5F69181C0 \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "state": "rejected", "rejectionCategory": "duplicate", "note": "Цей самий сценарій уже покриває кейс «Оформлення замовлення з порожнім кошиком»." }'

The verdict on one entry — the review item page (api.ts → reviewItem); a verdict over a whole page goes to POST /v1/review-items/decisions. Approving a proposed case is the one road by which a machine-made case becomes a real one (ADR 0010); approving a discovered test creates its case, or puts back the deleted case that still holds its key. A rejection must say why, in a category the agent can act on plus a note, and a corrected reason is refused half-done. An approval that would make a case past the project’s ceiling is refused whole with a 429: the item stays as it was (waiting, or rejected if it had been) and nothing is written. This holds for the case that comes back from the bin as well as for a new one.

id
required
string format: uuid
Media type application/json
One of:
RejectWithReason
object
state
required
string
Allowed values: rejected
rejectionCategory
required

Why a proposal or a result was turned down. A closed list of six: each value maps to one correction the generating agent can act on, which is what makes the reasons aggregatable instead of a pile of prose. Wire values are these English keys; the Ukrainian labels live in the SPA and never reach a machine client.

string
Allowed values: duplicate incorrect out-of-scope low-value not-testable other
note
required

The nuance the category cannot hold. Trimmed first, then measured: 10 to 2000 characters, and at least two words of letters — 12345678910 is long enough and says nothing. The bounds are a product number reviewed without a migration, so they are enforced on the route rather than in the database.

string
>= 10 characters <= 2000 characters
Examples

Reject with the full pair

{
"state": "rejected",
"rejectionCategory": "duplicate",
"note": "Цей самий сценарій уже покриває кейс «Оформлення замовлення з порожнім кошиком»."
}

Updated

Media type application/json

One entry of the review queue. Always starts pending; a reviewer moves it on.

object
schemaVersion
required
integer
Allowed values: 1
id
required
string format: uuid
resultId

Present on an entry disputing a RESULT. Exactly one of resultId, proposedCase and discoveredTest is present: an entry with two asks two questions at once, and one with none asks nothing.

string format: uuid
proposedCase

Present on an entry proposing a new case. Mutually exclusive with the other two.

object
stableId
required

Content-derived identity; the dedup key.

string
title
required
string
<= 300 characters
execution
required
string
Allowed values: auto manual
steps
required
Array<string>
>= 1 items <= 20 items
expected
required
string
<= 2000 characters
origin
required
One of:
object
runId
required
string
mode
required
string
Allowed values: explore design api
target
required
string
technique
string
<= 32 characters
priority
string
source
object
kind
required
string
Allowed values: url openapi-operation requirement pasted-text
value
required
string
>= 1 characters <= 100 characters
rationale
string
>= 1 characters <= 400 characters
lint

What the pre-review checks found, written before the proposal was queued (#1289). It marks the proposal. Only placeholder stops anything: a proposal whose title, steps or expected result still holds {{...}}, [TODO]/[TBD]/[FIXME]/[XXX] (or the same in <...>) or a run of three or more underscores cannot be approved (409 naming the places), and that check is read off the reviewer’s edited version when there is one. Absent means nothing was found.

Array<object>
object
rule
required
string
Allowed values: title-vague step-empty expected-vague duplicate-case duplicate-proposal placeholder
message
required
string
url
required

Where the rule is explained.

string
runCost
object
tokens
required
integer
cases
required
integer
verdict

Absent means never executed — NOT “no result yet”. blocked: no answer from the target.

string
Allowed values: passed failed blocked
externalKeys

What a match resolves through. Absent is a normal proposal, not a degraded one: an agent that names no key gets an entry with no comparison, which is exactly what a genuinely new case gets.

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 testomat zephyr
value
required
string
<= 1024 characters
editedCase

The reviewer’s corrected version of the proposal (#1878), beside proposedCase, which stays what the agent said. Present only after an edit; approving the item makes the case from it. Left off the page of the listing (editedBy and editedAt stay).

object
title
required
string
<= 300 characters
steps
required
Array<string>
>= 1 items <= 20 items
expected
required
string
<= 2000 characters
editedBy

The user id of whoever saved the version in editedCase.

string
editedAt

When editedCase was saved.

string format: date-time
discoveredTest

Present on an entry reporting a test the caller RUNS and this project has no case for. Mutually exclusive with the other two.

object
keys
required

The identity. Order does not matter — the server ranks them and the highest-ranked kind is what deduplication reads, so two adapters sending the same keys in a different order do not produce two entries about one test.

Array<object>
>= 1 items <= 8 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 testomat zephyr
value
required
string
<= 1024 characters
title
required
string
<= 300 characters
source
required

The reporter that found it, e.g. playwright.

string
<= 32 characters
specRef
required

Where the test lives, e.g. e2e/checkout.spec.ts:12. Required while everything descriptive is optional: an approved discovery becomes an automated case, and that shape needs a spec to point at.

string
<= 400 characters
rawStatus

What it did on this run, in the reporter’ own word.

string
<= 64 characters
platformRunId

The run that turned it up, once stored.

string
state
required

changes_requested (#1276): a reviewer asked the author for changes (changeRequest says what). It is the item’s own state, not a mark on a reviewer: it cannot be approved until the author re-submits, which puts the item back to pending.

string
Allowed values: pending changes_requested approved rejected
revision

Raised by every re-submission; absent is 1. An approval counts only for the revision it was given on.

integer
>= 1
reviewers

The user ids asked to review the item - the project’s default reviewer, when they could be asked.

Array<string>
defaultReviewerSkipped

The default reviewer who was NOT asked because they no longer have the right to review - the list says who was passed over.

string
approvals

Who approved which revision; the policy’s approvalsRequired counts those of the current revision.

Array<object>
object
userId
required
string
at
required
string format: date-time
revision
required
integer
changeRequest

The open request for changes (present with state: changes_requested); the author’s re-submission clears it.

object
by
required
string
at
required
string format: date-time
note
required
string
revision
required
integer
rejectionCategory

Present on every item rejected after R4, and absent forever on rejections stored before it — those are never migrated and must keep reading without error. An item that is pending or approved usually has none, but MAY still carry one: a rejection returned to review keeps the pair in the row as a draft, and the next move to rejected must supply it in the request anyway. Read it as “meaningful only while state is rejected”, not as “absent otherwise”.

string
Allowed values: duplicate incorrect out-of-scope low-value not-testable other
note
string
createdAt
required
string format: date-time
updatedAt
required
string format: date-time
Example
{
"schemaVersion": 1,
"proposedCase": {
"execution": "auto",
"origin": {
"mode": "explore"
},
"source": {
"kind": "url"
},
"lint": [
{
"rule": "title-vague"
}
],
"verdict": "passed",
"externalKeys": [
{
"kind": "playwright-id"
}
]
},
"discoveredTest": {
"keys": [
{
"kind": "playwright-id"
}
]
},
"state": "pending",
"rejectionCategory": "duplicate"
}

Invalid body, unknown state, or an incomplete reason on a write that leaves the item rejected. Each missing half is named separately, and an unknown category is answered with the list of allowed ones.

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

No / invalid token or session

Not found or not yours

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

The verdict cannot be applied as the item stands: review policy, a case already in the Bin, or (#1923) a proposal whose title, steps or expected result still holds an unfinished placeholder ({{...}}, [TODO], [TBD], [FIXME], [XXX], the same in <...>, or ___). The error starts with unfinished placeholders and lists the places (title, step N, expected); edit the proposal, then approve again.

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

Two refusals share this status, and what to do about them is opposite. Rate-limited — the per-route throttle, or the per-minute request limit of the plan an API token counts by (#1442, #2092): Retry-After carries the seconds until the window rolls over, and waiting that long is the way out. Quota reached — a ceiling on what the project holds (runs in the last 24 hours, test cases, items waiting in the review queue): { error, quota }. A retry straight away frees nothing, so do not loop on it; the message says what makes room — deleting cases, working the queue down, an operator raising the limit — and the run quota, a rolling 24 hours, frees its oldest runs as they age out. Tell the two apart by the body: a 429 whose body has a quota object is a quota, one without it is a throttle. quota carries kind, limit, used, remaining and resetsAt: the figures behind the sentence, and for the run quota the instant its oldest run ages out (#1258). Retry-After is sent on a quota only where there is such an instant (the run quota: the whole seconds, at least 1, until resetsAt); the case, review-queue and project quotas free only by an action, have resetsAt: null, and send no Retry-After. GET /v1/quotas reads the same figures without a refusal.

Media type application/json
One of:
object
error
required
string
Example
{
"quota": {
"kind": "runsPerDay"
}
}
Retry-After
integer

Seconds until the window rolls over on a throttle; on a quota, until quota.resetsAt — sent only for the run quota, which has one