Skip to content

A test that was weakened to pass is not a pass.

Graylint AI turns plain-English requirements into real Playwright tests, runs them against your app with self-healing, and tells you exactly what it had to relax to get a green. Most tools show you the green.

ci gate, stagingexit 1

Gate blocked this build

  • 2 failed test(s)
  • 1 weakened pass(es) blocked by policy
  • pass rate 60.0% < 100% minimum

Every reason comes from the same pure function that decides the verdict, so the console, the JUnit report and the exit code can never disagree about why a build stopped.

You describe the check. It writes the test.

No DSL to learn and no source code to hand over. You write the flow the way you would explain it to a new hire, and the engine returns runnable Playwright that you own.

what you writeqa.yaml flow
After signing in, go to /tasks/new.
Fill 'Task title' with 'QA smoke task'.
Click 'Save task'.
Verify the text 'Task created' appears.
what it runspytest + playwright
def test_task_create(page: Page):
    login(page)
    page.goto(f"{BASE_URL}/tasks/new")
    page.get_by_label("Task title").fill("QA smoke task")
    page.get_by_role("button", name="Save task").click()
    expect(page.get_by_text("Task created")).to_be_visible()

Before that test executes, a 44-rule sanitizer repairs the mistakes language models make writing Playwright. When a repair relaxes an assertion, the run is recorded as weakened rather than passed.

Six stages, and it tells you which of them ran.

A stage that never executed is reported as grey, never quietly folded into a green summary. This is the same pipeline the console draws live while a run is in flight.

  1. 1SupervisorReads the requirement and routes it.
  2. 2GeneratorWrites Gherkin, then Playwright code.
  3. 3ExecutorRuns it in a real browser, heals locators.
  4. 4TriageRoot-causes failures, finds duplicates.
  5. 5RegressionPicks the suites the change touches.
  6. 6ReporterEmits JUnit, the verdict, the exit code.

Four results. Each one means exactly what it says.

These colours carry no decorative meaning anywhere in the product. A green accent on something that is not a genuine pass would be a bug, not a style choice.

pass
The assertion the requirement asked for ran, and held.
pass (weakened)
The sanitizer relaxed an assertion so the test could run. It went green, but it no longer proves what you asked. Policy can block the build on this alone.
fail
The assertion ran and did not hold. The reason is named, not summarised.
did not run
The stage never executed. It is never painted green to fill the row.

We broke a running app on purpose, five times.

A suite that always passes is worthless, and no amount of describing it proves otherwise. So the application was mutated with git checkout and the approved suite replayed against each break. Left: what changed. Right: what the engine said, verbatim.

  1. T0

    Nothing changed, control run

    Suite is green

    4 passed · 0 failed · exit 0

  2. M1

    Renamed the heading “All Tasks” to “Task Overview”

    The two flows that assert it fail

    2 failed: login, tasks-list

    Assertion failed: Heading ‘All Tasks’ should be visible

  3. M2

    Changed a seeded task from “Write spec” to “Draft requirements”

    One flow fails, login recovers

    1 failed: tasks-list

    Task list should contain the text ‘Write spec’

  4. M3

    Renamed the selector id="profile-user" to profile-username

    One flow fails, tasks-list recovers

    1 failed: profile

    Timeout waiting for locator("#profile-user")

  5. M4

    Restored every mutation

    Green returns

    4 passed · 0 failed · exit 0

Every mutation produced the exact expected failure, named the exact broken assertion, and left unrelated flows passing. Tests ran in four to six seconds each against a real browser.

Then we tried to make it lie.

Two ways a test suite fakes a result: it passes against the wrong thing, or it treats a dead environment as a broken feature. Both were tried deliberately.

Point the gate at a valid but wrong application

The suite was aimed at a different working app instead of its own.

fail, 0 of 4 passed, exit 1

Point it at a dead URL

Nothing listening. A broken environment is not a broken feature and must not be reported as one.

infra_error, exit 2

One manifest. Then it runs itself.

Credentials are referenced by environment variable name, never written down. The policy block is what the gate enforces, and it is the only place a build-blocking rule lives.

qa.yaml
product: taskdesk
environments:
  staging: https://staging.example.internal
auth:
  username_ref: TASKDESK_USER   # env var name, never a password
  password_ref: TASKDESK_PASS
flows:
  - id: task-create
    text: "After signing in, go to /tasks/new. Fill 'Task title'
           with 'QA smoke task'. Click 'Save task'. Verify the
           text 'Task created' appears."
    tags: [regression, feature]
policy:
  block_on: [failed, weakened]
  min_pass_rate: 100
  1. Live test

    Describe one check in plain English. The engine writes it, runs it, and returns a verdict in about a minute. For trying an idea.

  2. Baseline suite

    Hand it the manifest. Every flow becomes a generated, executed and reviewed test. What passes review becomes your approved suite.

  3. CI gate

    The approved suite replays on every change. Nothing is regenerated, so nothing drifts, and a blocked build names its reasons.

The tests are yours, including the day you stop paying us.

Every approved test exports as plain pytest and Playwright and runs anywhere Python does. Results come back as standard JUnit XML. The gate speaks in process exit codes your pipeline already understands.

CI gate exit codes and their meaning
ExitMeaning
0The suite met your policy. Ship it.
1The gate blocked the build. Every reason is listed.
2The run could not complete. Infrastructure, not your feature.

Questions worth asking

What does "pass (weakened)" mean?
Before a generated test runs, a 44-rule sanitizer repairs the mistakes language models make when writing Playwright code. Some of those repairs relax an assertion to let the test execute. When that happens the result is recorded as pass (weakened) and the relaxed assertion is named. It is never counted as a clean pass, and your gate policy can block the build on weakened passes alone.
How do you know the generated tests are real and not always-green?
By mutation testing. A live application was deliberately broken five times: a renamed heading, changed seed data, a renamed selector. The approved suite replayed against each mutation. Every mutation produced exactly the expected failure, named the exact broken assertion, and left unrelated flows passing. Restoring the application restored the green run.
What happens if the application under test is unreachable?
The gate exits 2 with an infra_error verdict. An unreachable target is never reported as a test failure, because a broken environment and a broken feature call for different responses from whoever is on call.
Am I locked into the platform?
No. Every approved test exports as plain pytest plus Playwright and runs anywhere Python does. The CI gate verdict is a pure function over coverage metrics and your policy, and it reports through standard JUnit XML and process exit codes.
Which exit codes does the CI gate use?
Exit 0 when the suite passes your policy. Exit 1 when the gate blocks the build, with every blocking reason named. Exit 2 when the run could not be completed for infrastructure reasons.
Does it need my application’s source code?
No. Flows are described in plain English in a qa.yaml manifest and executed against a running environment through a real browser. Credentials are referenced by environment variable name and never stored in the manifest.

Find out what your green is hiding.

Graylint AI is available to a small number of teams while we work with each one directly. Tell us what you are testing and we will come back to you.