# One number per parallel test

> Make SMS tests repeatable: mark the inbox before you send, give each parallel worker its own number, budget the whole test and report the stage that failed.

Most flaky SMS tests read an old code or another worker's code. Mark the inbox before you send, and give each parallel worker its own number.

## Mark the inbox before you send

Reading the latest message can return yesterday's code. Take a mark just before your app sends the text, then wait for the first message after it.

```typescript title="Mark, send, wait"
const number = phonekit.number(testPhoneNumber)
const after = await number.mark()
await page.getByRole("button", { name: "Send code" }).click()
const receipt = await number.waitForMessage({ after, timeoutMs: 120_000 })
if (!receipt.message.otp) throw new Error("Message arrived without a code")
await page.getByLabel("Verification code").fill(receipt.message.otp)
await page.getByRole("button", { name: "Verify" }).click()
await expect(page.getByRole("heading", { name: "Your account" })).toBeVisible()
```

The [Playwright guide](https://www.phonekit.io/docs/guides/playwright) adds the imports, key and setup. `mark()` doesn't send or clear anything. It records where the inbox is now.

## Give each parallel worker its own number

A mark only says when a text arrived, not which test asked for it. Two workers sharing a number can read each other's codes:

| Step | Worker A          | Worker B             |
| ---- | ----------------- | -------------------- |
| Mark | Marks the inbox   | Marks the same point |
| Send | Asks for A's code | Asks for B's code    |
| Read | May get B's code  | May get B's code     |

Give each worker that runs at the same time its own number, or run those tests one after another. Count overlapping branches and reruns when you size the pool.

The account needs the same care. A signup test can't register the same identity forever, so reset it, clean it up, or test sign-in with an existing account.

## Budget the whole test

Set the wait's `timeoutMs` inside the runner's overall timeout, with room for page setup and the final check. The deadline covers polling and retries, so the test fails with a clear receiving error instead of hanging.

A resend is a separate action in your app. Only add one if the test covers resend, since it can create competing codes and use up your sender's rate limit.

## Report the stage that failed

| What you see                        | Check first                                 |
| ----------------------------------- | ------------------------------------------- |
| Key or number unavailable           | Key scope, CI secret and assigned number    |
| Your app rejects the send           | App response and sender settings            |
| Nothing arrives before the deadline | Sender delivery status, number, environment |
| Text arrives with no code found     | Message template and extracted code         |
| Code entered but rejected           | Account state, expiry, competing requests   |
| Verified but the assertion fails    | Page readiness and expected final state     |

Log the stage, timing, request IDs and outcome. Leave message bodies and codes out of logs. Anyone with access to the number can open the inbox when they need the text itself.

## Questions

### Should we retry the whole test?

Find the failing stage first. A retry sends a second code while the first may still be arriving. If you keep retries, reset the account and take a new mark for each attempt.

### How do we skip a text that isn't ours?

Inspect the receipt from waitForMessage. If it belongs to something else, wait again from receipt.cursor. A wait can't tell which request sent a text, so separate numbers are the simplest way to keep parallel work apart.

### How long should the deadline be?

Base it on how long your texts usually take, then leave room for page setup and the final check. There is no universal SMS delivery time. Report a timeout as a receiving failure, not a rejected code.

---

Source: https://www.phonekit.io/learn/one-number-per-test
