Skip to content

Draft from code

An automated case is a test in your repository, and it usually has a title and nothing else. Draft from code reads that test’s code and proposes two texts for the case — a description and the preconditions — which you take, edit or throw away. Nothing is written until you decide, and the model never touches a case itself: it answers a question, and a person writes the answer.

The feature is off until an owner of the project connects a provider with a key of their own, and it is off for manual and eval cases, which have no test code.

Only an owner of the project changes this page. A member reads the status, the provider, the model and the language under one line that says Only an owner of the project can change AI. The form, before anything is connected:

Settings → AI of Webshop before anything is connected: the Provider switch with Anthropic picked, OpenRouter and Custom address beside it; the API key field with the line Stored encrypted and never shown again.; the Model list with claude-sonnet-4-5 — $3 / $15 · recommended and the line Price per 1M tokens: $3 in, $15 out.; the Language list with English; the unticked box I understand test code goes to this provider on every request.; and the Connect button. Settings → AI of Webshop before anything is connected: the Provider switch with Anthropic picked, OpenRouter and Custom address beside it; the API key field with the line Stored encrypted and never shown again.; the Model list with claude-sonnet-4-5 — $3 / $15 · recommended and the line Price per 1M tokens: $3 in, $15 out.; the Language list with English; the unticked box I understand test code goes to this provider on every request.; and the Connect button.
The connect form of an owner.
  • Provider — Anthropic, OpenRouter or Custom address. A custom address is any OpenAI-compatible endpoint; it must be an https:// address that resolves to a public one, and a refusal names the reason (the scheme, or an internal address) and never the address it resolved to.
  • API key — written once, never shown again: it is stored encrypted, and the page afterwards says only who saved it and when. There is no way to read it back; to change it you replace it.
  • Model — a list of the models Plune knows the price of, each with its price per million tokens in and out, the recommended one first (for Anthropic, claude-sonnet-4-5). Other model… lets you type a name — letters, digits and . _ : / @ + -, no spaces, 200 characters at most — and Calls of a model outside the list may show an unknown price.. A custom address has no list: you name the model.
  • Language — the language the draft is written in: English, Українська, Polski, Deutsch, Français or Español.
  • Consent — the box I understand test code goes to this provider on every request. must be ticked before Connect works. It is meant literally, and the next section says exactly what goes.

Connect with a gap and each field that lacks something is marked, says why, and takes the focus in the order of the form. Once connected, the page shows what the owner can do about it:

Settings → AI of Webshop after Connect: Status on with a Disable button; Provider Anthropic; Model claude-sonnet-4-5; Language English; API key — Credentials saved with the date, by Alex Morgan — with the buttons Replace credentials and Remove credentials. Settings → AI of Webshop after Connect: Status on with a Disable button; Provider Anthropic; Model claude-sonnet-4-5; Language English; API key — Credentials saved with the date, by Alex Morgan — with the buttons Replace credentials and Remove credentials.
Connected: the facts, the switch, and the key's two actions.

Disable and Enable switch drafts for the whole project without losing the key; a project that is off says AI is off for this project. Replace credentials asks for a new key alone — the provider, model and language stay, and the key saved keeps working until the new one is saved. Remove credentials asks first and takes the provider, the model and the language with the key: drafts stop till AI is connected again. A deployment where the operator has not set AI up says AI is not available on this platform instead of the form.

An owner can connect one fallback provider on the same page, under Fallback provider. It is optional, and it is tried only when the primary provider fails for a reason on the provider’s side (see when it is tried). The section is empty until the primary is connected: When the primary provider does not answer, the draft is tried with a fallback.

  • Connect fallback asks for the same things as the primary — provider, model, a key of its own (Fallback API key, stored encrypted and never shown again) — and for its own box, I understand test code goes to this provider only when the primary fails. It must be ticked before Save fallback works.
  • The fallback cannot be the primary: the same provider, address and model is refused with The fallback matches the primary. Choose another provider or model. The same provider with another model is allowed.
  • Once connected, the owner sees the provider, the model and who saved the key and when, with Replace credentials (a new key alone), Change model and Remove fallback. Removing asks first, erases the fallback’s key and keeps the costs already recorded. Switching AI off for the project switches off both providers.
  • A member sees one line, Fallback: provider · model, and nothing if there is none — never a key or an address. Only an owner in a session changes the fallback; a token cannot.
  • The last problem of the fallback is shown to the owner on its own line, with its date, apart from the primary’s; a fallback that saved a draft does not clear the primary’s last problem.

The code sent to the fallback is masked exactly as for the primary, and a custom address of the fallback goes through the same check.

Privacy. The code goes to a second company only if an owner connected a fallback and ticked its own box, and only after the primary failed. Plune has no provider of its own. The privacy page lists what else is held and who handles it.

On an automated case that has a code snapshot, Draft from code stands in the head, with the provider and the model under it.

The head of the case page of slots show the next seven days: the breadcrumb e2e › checkout › delivery.spec.ts, the priority high, the name, the tags automated and draft, Edit, ⋯ and the button Draft from code; under it the line via Anthropic · claude-sonnet-4-5, then created by Alex Morgan, the Unassigned select, and the tag delivery with the line Tags come from the test name in code. The head of the case page of slots show the next seven days: the breadcrumb e2e › checkout › delivery.spec.ts, the priority high, the name, the tags automated and draft, Edit, ⋯ and the button Draft from code; under it the line via Anthropic · claude-sonnet-4-5, then created by Alex Morgan, the Unassigned select, and the tag delivery with the line Tags come from the test name in code.
The button and the provider and model it will ask.

When it is not available, the case page says why in that place:

You readBecause
AI drafts are off for this project — ask an owner to turn them on. (with an AI settings link for an owner)nothing is connected, the project is switched off, or the platform has no AI
This case has no code snapshot yet — it will after the next import.the case came without the code of its test
Drafts from code don’t exist for this kind of case — it has no test code.a manual or eval case
nothing at allyou are a reader of the project

With a fallback connected, the line under the button reads Primary: provider · model. If it fails — fallback: provider · model.

Pressing the button asks the provider and takes you to the draft’s page, which says it can take up to 2 minutes: the primary gets 60 s, and then the fallback, if it is tried, gets its own 60 s. Leaving it is safe: while a draft waits for you, the case page shows Your draft is waiting for your decision. with Open, to the member who asked.

One request carries five things: the code of that one test, the name of the case without its tags, its tags, the model and the language. That is all. Not the other tests of the file, not its imports, helpers or fixtures, not the case’s existing description or preconditions, not any run or result.

Before it leaves Plune the code is masked twice:

  1. by form — what looks like a secret wherever it sits (a bearer token, a private key, and the other patterns the platform already masks in failure text);
  2. by place — a string that looks ordinary but stands where a credential belongs: assigned to a name containing password, token, secret or api key, passed to a call of such a name, typed with fill into a field named like one, or the user and password of a URL. Each becomes a marker such as ‹password›.

The model’s answer is checked the same way on the way back: it must be exactly a description and a precondition of at most 16 384 bytes each, and anything else — a missing field, an extra one, text that does not parse, a field too long — is an unusable answer and no draft.

What the model could not see is said under the draft, in plain words, from the code itself: the names the test only uses — a fixture or helper it does not declare itself and that is not one of Playwright’s built-in fixtures (page, context, request, browser, browserName) or one of the language and test-runner globals every snapshot may call (test, expect, String, JSON, console and the like) — and whether the test is skipped.

the draft never saw: openSlots, isSunday; the test can be skipped conditionally at runtime.

or the draft saw everything it uses directly. when there is nothing to name, with the test is skipped unconditionally. added for a test.skip or test.fixme at the top. A draft written without sight of openSlots is a guess about what it does, and this line is where you learn to check it.

The fallback is tried once, in the same request, only after the primary failed for one of these:

The primaryFallback tried
was rate-limited by its provideryes
was unavailableyes
sent an answer that could not be usedyes
gave no answer in 60 syes
rejected the credentialsno
had its custom address rejectedno

A rejected key or address is a fault of the setup, not of the provider, and a fallback that quietly covered it would hide a broken primary. The page then says The primary provider rejected the credentials. The fallback was not tried: this is a setup failure, not the provider’s., and the owner sees the problem on Settings → AI. With no fallback connected nothing changes: one refusal, no second attempt.

When the fallback made the draft, the page says so before you accept: Drafted by the fallback provider: provider · model. The primary (provider · model) failed: reason. When both failed, one refusal names both: The draft failed. Primary — provider · model: reason. Fallback — provider · model: reason. The case was not changed. Try again in a few minutes. A member’s refusal carries no addresses. The reasons are rate limit reached, unavailable, an answer we could not use, no answer within 60 seconds, credentials rejected and address rejected.

The draft page for slots show the next seven days: Back to case, the heading Draft for «slots show the next seven days», the line You asked on a date and the draft waits for your decision until the same time the next day; Description with Take this ticked, Existing (case had none) on the left and Draft (editing) on the right in a text box holding Checks that the delivery step offers a slot for each of the next seven days and that every slot can be picked.; Preconditions with Take this unticked, Existing (case had none) and the Draft A cart with one deliverable item and a saved address.; the unseen line; the price line 1840 in · 212 out · $0.0087 via claude-sonnet-4-5; and Reject and Accept. The draft page for slots show the next seven days: Back to case, the heading Draft for «slots show the next seven days», the line You asked on a date and the draft waits for your decision until the same time the next day; Description with Take this ticked, Existing (case had none) on the left and Draft (editing) on the right in a text box holding Checks that the delivery step offers a slot for each of the next seven days and that every slot can be picked.; Preconditions with Take this unticked, Existing (case had none) and the Draft A cart with one deliverable item and a saved address.; the unseen line; the price line 1840 in · 212 out · $0.0087 via claude-sonnet-4-5; and Reject and Accept.
Two fields, the first taken and being edited; the second left.

For each field the draft proposed, Existing stands on the left ((case had none) when it was empty) and the Draft on the right, with a Take this box. The frame stands on the side that will be kept.

  • Take this ticked turns the draft into a text box — Draft (editing) — and what you leave in it is what is written. Unticked, the case keeps its own text for that field.
  • A box you tick and then clear takes nothing: an empty text is not a decision to erase.
  • A text over 16 384 bytes is refused before anything is sent: Too long to save — 16384 bytes is the most a field can hold. Accept waits.
  • Accept writes only the ticked fields, with the text each box holds at that moment, and returns you to the case. Reject writes nothing: the case is not touched, and no version of it is made.
  • Only the member who asked decides. Anybody else who opens the page reads Someone else is deciding a draft for this case. — none of the draft’s text is sent to them.

After an accept the case remembers where the text came from, and the page says it under the field:

The case page of slots show the next seven days after Accept: the breadcrumb, the head with the button Draft from code, via Anthropic · claude-sonnet-4-5, created by Alex Morgan, the Unassigned select, the tag delivery, an empty Defects section, then Description with the accepted text Checks that the delivery step offers a slot for each of the next seven days and that every slot can be picked. and, under it, Drafted by claude-sonnet-4-5 · date · accepted by Alex Morgan · date. The case page of slots show the next seven days after Accept: the breadcrumb, the head with the button Draft from code, via Anthropic · claude-sonnet-4-5, created by Alex Morgan, the Unassigned select, the tag delivery, an empty Defects section, then Description with the accepted text Checks that the delivery step offers a slot for each of the next seven days and that every slot can be picked. and, under it, Drafted by claude-sonnet-4-5 · date · accepted by Alex Morgan · date.
The origin line, shown to every role.

The line ends with · drafted from a previous version of the code when the test has changed since, and with · edited by … · date once a person edits the field. When the fallback wrote the draft, the line says · by the fallback provider, provider and names its model. Markdown export is unchanged.

  • The code changed since you asked: a notice, The code changed since this draft was requested., above the comparison. It warns and does not refuse.
  • A field changed after the draft was requested — somebody edited the description while you read it: Accept is refused for the whole accept, nothing is written, the Existing side shows the text as it is now with This field changed after this draft was requested., and you accept again, seeing it.
  • The case kept being saved by someone else while yours was written, three attempts in a row: The case was saved by someone else again and again while the draft was being accepted. Nothing was written and the draft is still waiting: check the text below, then accept again.
  • A day passes. A draft nobody decided on within 24 hours expires and its text is deleted; the page then says This draft is no longer available. Asking again replaces any draft of that case that was waiting, whoever asked for it.
The draft page at 375 px wide: Back to case, the heading Draft for «slots show the next seven days», the line about when it was asked and until when it waits; Description with Take this unticked, Existing (case had none), an arrow and the Draft; Preconditions the same way; then the unseen line and the start of the price line. The draft page at 375 px wide: Back to case, the heading Draft for «slots show the next seven days», the line about when it was asked and until when it waits; Description with Take this unticked, Existing (case had none), an arrow and the Draft; Preconditions the same way; then the unseen line and the start of the price line.
The draft at 375 px: the two sides stand one above the other.
LimitWindow
One person30 requeststhe last hour, in one project
One project200 requeststhe last 24 hours
One case1 at a timeuntil it answers, or after 90 s
One requestwaits 60 s for the providerthen it is recorded as aborted (with a fallback, another 60 s for it: up to 2 minutes)

Every request counts, whether it succeeded, failed or was aborted. A request that used the fallback still counts once for the person and the project: the second attempt is not a second request. The window is a sliding one: the wait named in a refusal is the real time until enough requests have left it, rounded up — minutes up to an hour, then hours — and the API sends the same number in Retry-After.

The head of the case page of slots show the next seven days after thirty requests in an hour: the line You've reached the draft limit — try again in 60 minutes. in place of the button, and under it the notice Your draft is waiting for your decision. with Open. The head of the case page of slots show the next seven days after thirty requests in an hour: the line You've reached the draft limit — try again in 60 minutes. in place of the button, and under it the notice Your draft is waiting for your decision. with Open.
The person's limit reached: the button is gone and the sentence says when it returns.

You read You’ve reached the draft limit — try again in 60 minutes. for your own window and This project has reached its draft limit — try again in … for the project’s. The API answers 429 with code: member_limit or project_limit.

Under every draft stands the price of the call, as the provider counted it:

LineMeans
1840 in · 212 out · $0.0087 via claude-sonnet-4-5tokens in and out, and the cost — the model’s price per million tokens, applied to this call
1840 in · 212 out · price unknown · via <model>the provider reported the tokens, and Plune has no price for this model: a model outside the list, or any model of a custom address
tokens not reported · price unknown · via <model>the provider said nothing about the tokens

With a fallback, each attempt is its own recorded call, priced by its own model: a request that used both pays for both, and the tokens the primary counted before it failed are not lost. Under a draft the fallback made, the line shows both attempts.

An unknown price is never shown as zero: a call with no price is not a free call. A failed call whose answer arrived and could not be used was billed by the provider, and is counted like a draft.

The month’s use, for an owner, is on Settings → AI under the key:

Settings → AI of Webshop with a red notice Provider rejected the key on a date over Status on, Provider Anthropic, Model claude-sonnet-4-5, Language English and the key's actions; under a line, Calls this month 1 succeeded · 1 failed · 0 aborted and Known cost this month $0.01 · 0 calls with unknown price. Settings → AI of Webshop with a red notice Provider rejected the key on a date over Status on, Provider Anthropic, Model claude-sonnet-4-5, Language English and the key's actions; under a line, Calls this month 1 succeeded · 1 failed · 0 aborted and Known cost this month $0.01 · 0 calls with unknown price.
Calls this month, and the known cost, apart from the calls whose price is unknown.

Calls this month counts succeeded, failed and aborted calls. Known cost this month is the sum of the calls with a price, and beside it, as its own number, how many calls had tokens and no price — the two are never folded into one. A line Of them by the fallback says how many of the month’s calls were attempts of the fallback.

A call that ends badly takes you to the draft’s page with one sentence and Ask again. The same reason goes to the owner on Settings → AI, with its date, above the facts — a rejected key in red, the others as a notice. The primary and the fallback each keep their own last problem, so one does not hide the other. A timeout is the one that is not reported there: it is not the provider’s fault.

The draft page for slots show the next seven days: Back to case, the heading, and a card The provider rejected the saved credentials. with the button Ask again. The draft page for slots show the next seven days: Back to case, the heading, and a card The provider rejected the saved credentials. with the button Ask again.
A refused key.
You readWhat it meansWhat to do
The provider rejected the saved credentials.the provider answered that the key is not accepted, or is not allowed to use this modelan owner: Replace credentials in Settings → AI; then Ask again
The provider is rate-limiting this project right now.the provider’s own limit, not Plune’swait, then Ask again
The provider was unavailable — try again shortly.an outage or a network error on the provider’s sideAsk again shortly
The provider sent back something we could not use.the answer was not the two fields, was too long, or was cut offAsk again; the tokens are counted
The custom address was rejected.a custom address that now resolves to an internal addressan owner: remove the credentials and connect again with a public https:// address
The draft timed out — the manual path still works.no answer in 60 s; the call is recorded as abortedAsk again, or write the texts by hand

A saved key the server can no longer decrypt is answered 409 — the saved AI provider key cannot be read — the project owner has to save it again — and goes to the owner as a rejected key.

Drafts are asked for and decided by a person — a token gets 403 — but any token reads.

GET /v1/test-cases/{id}/draft → { "available": true, "provider": "anthropic", "model": "claude-sonnet-4-5", "pending": { … } | null }
POST /v1/test-cases/{id}/draft → the call: its state, the two texts, tokens, cost
PATCH /v1/test-cases/{id}/draft { "state": "accepted", "fields": { "description": "…" } } or { "state": "rejected" }

available is false with a reason: ai_disabled, provider_not_connected, platform_not_configured, wrong_case_type or no_code_snapshot. A call is in_progress, awaiting_decision, accepted, rejected, expired, failed (with its failureKind) or aborted; the texts of a draft nobody took are cleared when it ends, and the tokens and the cost stay.