A mirror for the suite

Twinrova

Turn real customer sessions into a small set of canonical end-to-end tests.

The recorder is lit. The canon is still being cut.

The gate

Customers already walk the product.

Twinrova keeps those walks. When the canon is cut, the walks that repeat become Playwright the team owns. A failing test catches a bug. That is a side effect. The suite is what lets an agent change the product and know the real paths still happen.

Owned tests

Ordinary Playwright, committed by the team and run in their CI. Twinrova is not the test runtime.

Intent

The check names the goal. When the clicks change and the goal does not, the how can move. When the goal stops happening, the test stays failed.

Masked in the page

A password leaves as a class called secret. The characters typed into the field do not leave with it.

The twin streams

Two records. One canon.

A single script tag writes both. Only one of them becomes the test.

Fire

The action log

Written in the live page, at the moment of the action. This is the source of the tests. Mousemove, hover, and scroll are dropped. A burst of keystrokes collapses into one fill.

  • navigate
  • click
  • fill
  • select
  • check
  • press
  • submit

The locator stack prefers a test id, then role and accessible name, then label. A value is stored as a class: empty, email, secret, text, number, or choice.

Ice

The replay

Standard rrweb, with inputs masked, so a person can watch the session that stood for the rest. It is kept for a short window.

The tests still emit if that recording has already expired. The replay is how a human reviews the example. It is not how the suite is generated.

Funnels, heatmaps, and a session player are not the product. The replay is one blob behind the example.

The corridor

Seven chambers.

Only Record is lit. The rest of the corridor is drawn, and it is not built.

  1. 01

    Record

    Lit

    Two streams leave the browser. The action log is kept. The replay expires.

  2. 02

    Normalize

    Uncut

    Cut a session on a navigation, an idle gap, or a route change. Template the ids out of the path.

  3. 03

    Fingerprint

    Uncut

    A token of verb, route template, and the best stable locator. Role and name outrank a CSS path.

  4. 04

    Cluster

    Uncut

    Exact matches first. Near duplicates next. The model only sees representatives, and only to judge a merge.

  5. 05

    Name

    Uncut

    The model names the bucket and proposes assertions. The steps come from the medoid trace. The model does not invent them.

  6. 06

    Review

    Uncut

    A person accepts the assertion. A draft is not part of CI. Merging the spec is the gate.

  7. 07

    Emit

    Uncut

    One Playwright file per accepted run. Recorded emails, cards, and passwords never appear in it.

The vow

Do not heal a failed outcome.

The how may move

Same intent, different locator, or an extra step, and the cohort still reaches the accepted outcome. Open a drift update. A person accepts it.

The goal may not

If checkout no longer reaches confirmation, the test fails. Nothing rewrites the assertion to match the new page.

Frequency, not lines

Rank a use case by how often customers do it. A rare branch that covers many lines is not the path that makes the money.

The file emit is meant to leave. This one is not generated yet.
// canon: checkout-guest-promo
// support: 1204 sessions ยท accepted 2026-10-25
import { test, expect } from "@playwright/test";

test("guest checks out with a promo code", async ({ page }) => {
  await page.goto("/cart");
  await page.getByRole("button", { name: "Checkout" }).click();
  await page.getByLabel("Promo code").fill(process.env.CANON_PROMO_CODE ?? "PROMO");
  await page.getByRole("button", { name: "Apply" }).click();
  await page.getByRole("button", { name: "Pay now" }).click();
  await expect(page.getByRole("heading", { name: "Order confirmed" })).toBeVisible();
});

What is lit

Record a session tonight.

Clustering and Playwright emit are not built. The recorder is. About nine installs in ten are done by an agent, with one config file and no browser. That contract is the plan. These commands run now.

On a site you already have open

Nothing is uploaded. The log stays in the tab until you copy it.

npm install
npm run snippet

Paste the output into the DevTools console, use the site, then:

copy(JSON.stringify(twinrova.dump(), null, 2))

On this machine

A preview page, and sessions written to .twinrova/sessions/.

npm start
npm run twinrova -- sessions

The preview talks to 127.0.0.1 only. A password field shows up as valueClass: "secret". The characters do not.

The procedure, including what is never sent, is AGENTS.md.