Offer tests this project has no case for
const url = 'https://beta-api.plune.ai/v1/review-items/discovered';const options = { method: 'POST', headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'}, body: '{"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"}]}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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).
Authorizations
Section titled “Authorizations ”Request Body required
Section titled “Request Body required ”object
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
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.
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
The reporter that found it, e.g. playwright.
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.
What it did on this run, in the reporter’ own word.
The run that turned it up, once stored.
The run that turned these up. Checked against the caller’ own runs.
Examples
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" } ]}Responses
Section titled “ Responses ”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.
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
The key the server identified this test by — its highest-ranked kind.
object
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.
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.
The case a trusted source produced. Only on created.
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.
object
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.
object
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.
object
The body of a 429 that is a quota and not a throttle: the sentence, and the figures behind it.
object
What was reached and the way out.
object
The ceiling. A write that would take used past it is refused with 429.
What the project holds now, counted by the same query that refuses a write.
limit - used, never below 0.
When the oldest counted run ages out of the rolling 24 hours and a slot frees on its own. null for the two quotas only an action frees (delete cases, decide queue items), and for a run quota with no runs in the window.
Example
{ "quota": { "kind": "runsPerDay" }}Headers
Section titled “Headers ”Seconds until the window rolls over on a throttle; on a quota, until quota.resetsAt — sent only for the run quota, which has one