Skip to content

Offer tests this project has no case for

POST
/v1/review-items/discovered
curl --request POST \
--url https://beta-api.plune.ai/v1/review-items/discovered \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{ "runId": "6f1c2b3a-0000-4000-8000-0000000000d1", "discovered": [ { "keys": [ { "kind": "playwright-id", "value": "pw-cart" } ], "title": "cart adds an item", "source": "playwright", "specRef": "e2e/checkout.spec.ts:12", "rawStatus": "passed" } ] }'

A runner reports the tests it ran that resolved to no case. Each becomes a queue entry of a third kind — one that testifies a case is missing rather than describing one — and a reviewer decides whether the project tracks it. Approving creates an automated case and attaches the keys, which is what makes the next run resolve instead of offering the same test again. A source the project trusts (trustedDiscoverySources, ADR 0030) is the exception: its tests become cases at once, with no queue entry. A request that would take the project past its case ceiling, or the queue past its own, with what it adds is refused whole with a 429 before anything is written; when another request uses the room in between, the 429 can come after some of the trusted cases were made, and sending the request again writes the rest (a test with a case is known).

Media type application/json
object
discovered
required
Array<object>
>= 1 items <= 500 items

A test the caller runs that this project has no case for. It carries NO steps and no expected result, and that is the shape rather than a gap: a reporter sees a test’ RESULT, never its source, so it testifies that a case is missing instead of describing one. Approving the entry creates an automated case pointing at specRef and attaches every key, so the next run resolves instead of discovering the same test again.

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
runId

The run that turned these up. Checked against the caller’ own runs.

string
Examples
Example one

One test a Playwright suite ran and Plune has no case for

{
"runId": "6f1c2b3a-0000-4000-8000-0000000000d1",
"discovered": [
{
"keys": [
{
"kind": "playwright-id",
"value": "pw-cart"
}
],
"title": "cart adds an item",
"source": "playwright",
"specRef": "e2e/checkout.spec.ts:12",
"rawStatus": "passed"
}
]
}

The batch was read. Read the verdicts, not only the status — 200 does NOT mean every test was queued, and a caller that assumes it will re-offer refused tests for ever or go quiet about a real gap.

Media type application/json
object
results
required
Array<object>

What became of ONE offered test. Read it per test: most of a repeated batch is already known or already refused, and a status line cannot say which.

object
key
required

The key the server identified this test by — its highest-ranked kind.

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
outcome
required

queued — a new entry is waiting for a reviewer. duplicate — an entry about this test already exists. refused — a reviewer has already declined it, and the answer outlives the run that provoked it. known — a case already holds this key, so nothing should have been offered; resolve before discovering. created — the project trusts this source, so the case was made without asking; count these, because they are the ones a reporter would otherwise report as nothing happening.

string
Allowed values: queued duplicate refused known created
id

The queue entry, when there is one. On created it is the id of the case that was just made (the same as testCaseId), so the result of this run for the test can be posted against it right away.

string format: uuid
testCaseId

The case a trusted source produced. Only on created.

string format: uuid
Example
{
"results": [
{
"key": {
"kind": "playwright-id"
},
"outcome": "queued"
}
]
}

Nothing was stored. Either the list is missing or exceeds the ceiling, or a test carries a field this kind of entry does not have — steps and an expected result are REFUSED by name rather than dropped, because a client that sent them believes they were stored.

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

No / invalid token or session

The runId is not one of the caller’ runs. Nothing is stored — answering 201 to a partly wrong request would leave rows missing the one field nobody would think to look for 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