Good morning, Maya Here’s how your tests did this week, Oct 31 – Nov 6.
3/4
Almost set up — connect CI to run on every change Your tests already run locally. One step left to run them on every push and pull request.
Hide Install the CLI (done) Record your first test (done) Add a safety policy (done) 4 Connect CI 446
96.2%
↑ 0.6 pts vs last week · 429 of 446 passed
17
by the safety door — none reached a real service
23
Run activity Runs per day by result, last 7 days
Chart Table
Passed Policy blocked Failed100 75 50 25 0
Oct 31 · 42 runs39 passed1 blocked2 failedNov 1 · 55 runs53 passed1 blocked1 failedNov 2 · 49 runs46 passed1 blocked2 failedNov 3 · 72 runs70 passed1 blocked1 failedNov 4 · 64 runs62 passed0 blocked2 failedNov 5 · 88 runs85 passed2 blocked1 failedNov 6 · 76 runs74 passed1 blocked1 failed
Oct 31 Nov 1 Nov 2 Nov 3 Nov 4 Nov 5 Nov 6
Safety door this week Every request your tests tried to make
Open policy Most often stopped
POST */refunds* rule block-destructive-refunds 9 POST /admin/settings rule block-admin-mutations 5 POST webhook.site host on the blocked list 3 Recent runs Local and cloud runs, newest first
All runs Latest signed receipt run_40549cd06baf · Nov 6, 08:40
Valid chainMerkle root e2114ac9fab789cb…a8f6aece Signature HMAC-SHA256 · 3c05ca9c…0a24 Ledger 5 blocks · 1 request blocked · 2 stand-in keys
Runs Every test run — on laptops, in CI and on cloud runners. Each one leaves a signed receipt.
Showing 10 of 446 runs this week Previous Next
All runsCheckout payment Failed at step 17Run #1039 · Nov 6, 09:31 · triggered by CI pipeline on pull request #128
What happened, in plain words The test logged in, filled the cart and applied the SAVE20 coupon. When it clicked Pay now , your server answered with an error (500), so the order confirmation never appeared. Along the way the safety door stopped a refund request and an unknown website, exactly as your policy says. One renamed field was found again automatically.
Timeline 18 Requests & policy 24 X-Ray 1 Checkpoints 2 Evidence 61
01 Open the checkout page staging.shop.test/checkout 0.9s 02 Log in to the sandbox button “Log In to Sandbox” 0.4s 03 Save point · post_auth cookies, storage, URL and DOM hash saved Checkpoint saved 0.2s 04 Add “Trail runner” to the cart button “Add to cart” 0.3s 05 Save point · cart_ready ready to fork variations from here Checkpoint saved 0.2s 06 Type coupon SAVE20 #coupon not found → coupon-input (94% match) Locator healed 0.3s 07 Apply the coupon button “Apply Coupon” 0.4s 08 Total shows $50.00 text matched in cart-total 0.1s 09 Choose the saved card radio “Visa ending 4242” 0.3s 10 Saved cards loaded GET api.stripe.com/v1/payment_methods → 200 Stand-in key swapped 0.6s 11 Refund attempt is refused POST api.stripe.com/v1/refunds → 403, as expected Stopped by policy 0.1s 12 Fill the shipping address 4 fields 1.2s 13 Pick “Standard shipping” select “Delivery speed” 0.2s 14 Order summary is visible region “Order summary” 0.1s 15 Accept the terms checkbox “I agree” 0.2s 16 Click “Pay now” button “Pay now” 0.5s 17 Order confirmation is visible timed out after 4000 ms · POST /api/checkout → 500 Failed — see X-Ray 4.0s 18 URL is /order/complete skipped after the failure — Context Suite Checkout payment Spec tests/checkout.spec.ts Target staging.shop.test/checkout Branch fix/checkout-retry Commit e612af4 Browser Chromium · headless · 1280×800 Posted to pull request #128 ✕ Checkout payment failed at step 17 of 18 — POST /api/checkout returned 500.
2 requests stopped by policy · 1 locator healed · evidence signed
Tests Your suites and the specs inside them — and how steady each one has been.
Suites · 5 Sample suite groups Checkout 40 /42 2 failing · 1 flaky test Authentication 30 /31 1 failing Account settings 23 /24 1 failing Cart & pricing 17 /18 1 flaky test Notes app 6 /6 All passing
Checkout Cart, coupons, payment and confirmation on staging.shop.test
Run this suiteStability is the share of runs that failed and then passed on the same commit over the last 30 days.
Analytics Are tests getting steadier, faster and safer? Every number below follows the filters.
Last 7 days Last 30 days Last 90 days
EnvironmentAll staging preview SuiteAll suites Checkout Authentication Account settings Cart & pricing Pass rate
96.8%
↑ 1.1 pts vs previous 30 days
Flaky rate
1.9%
↓ 0.4 pts fewer reruns
Median run
41s
↓ 6% faster than before
Requests stopped
61
0 reached a real service
Pass rate Share of runs that passed, last 30 days
98% 95% 92%
95.1 %Oct 8 95.8 %Oct 11 96.4 %Oct 14 95.2 %Oct 17 96.9 %Oct 20 97.3 %Oct 23 96.1 %Oct 26 96.8 %Oct 29 97.6 %Nov 1 97.4 %Nov 6
97.4 %
Oct 8 Oct 23 Nov 6
How long runs take Number of runs by duration, last 30 days
38 runs · <10s 76 runs · 10–20s 172 runs · 20–30s 265 runs · 30–40s 311 311 runs · 40–50s 92 runs · 50–60s 25 runs · >60s
<10s 10–20s 20–30s 30–40s 40–50s 50–60s >60s
Median 41s · runs that restore a checkpoint skip their setup and land in the shortest buckets.
What the safety door stopped Stopped requests by reason, last 30 days
Open policy POST */refunds* rule block-destructive-refunds 38
POST /admin/settings rule block-admin-mutations 21
POST webhook.site blocked host 13
GET cdn.segment.io not on the allowed list 8
POST */wipe* secret blockedPaths 4
How broken locators were found again Healed steps by matching method, last 30 days
Review heals Same test id exact_testid 38
Accessible name aria_fuzzy 29
Visible text semantic_text 17
Page structure structural_xpath 8
Similarity score ml_heuristic 4
Heals below the 80% confidence threshold are never applied — they fail the step and appear in review.
Flakiest tests Tests that failed and then passed on the same commit
Test Runs Flaky rate Checkout › applies saved payment method 52 3.8% Account › refreshes session after login 47 2.1% Cart › restores warm checkpoint 61 1.6% Checkout › pays with a new card 58 1.2%
Record a test Click through your app like a customer would. AirlockQA writes down each step and turns it into a Playwright test.
✓ Record 2 Review steps 3 Save as a test Recorded steps checkout-flow · 8 steps · 14 requests seen
trace_65fb1d16 Open http://127.0.0.1:3900 goto domcontentloaded Click “Log In to Sandbox” testId login-btn text Save point · post_auth cookies storage DOM hash Click “Add to cart” css [data-testid=add-to-cart] Save point · cart_filled cookies storage DOM hash Type “SAVE20” into the coupon box testId coupon-input Click “Apply Coupon” testId apply-coupon-btn text Check the total shows $50.00 testId cart-total text $50.00 Generated Playwright test tests/checkout.spec.ts · no AI tokens used
Fallback locators on import { test, expect } from '@playwright/test' ;
import { AirlockRunner, airlockLocator } from 'airlockqa' ;
test.describe('checkout-flow' , () => {
let airlock: AirlockRunner;
test.beforeEach(async ({ page, context }) => {
airlock = new AirlockRunner();
await airlock.initialize(context, page);
});
test('checkout-flow' , async ({ page }) => {
// 1 · open the shop
await page.goto('http://127.0.0.1:3900' );
// 2 · log in (falls back to text if the test id changes)
const login = await airlockLocator(page, { testId: 'login-btn' , text: 'Log In to Sandbox' });
await login.click();
await airlock.checkpoint('post_auth' , page);
// 4–7 · cart and coupon
await (await airlockLocator(page, { css: '[data-testid="add-to-cart"]' })).click();
await airlock.checkpoint('cart_filled' , page);
await (await airlockLocator(page, { testId: 'coupon-input' })).fill('SAVE20' );
await (await airlockLocator(page, { testId: 'apply-coupon-btn' })).click();
// 8 · the total should read $50.00
const total = await airlockLocator(page, { testId: 'cart-total' , text: '$50.00' });
await expect(total).toBeVisible({ timeout: 4000 });
});
}); Save to tests/checkout.spec.tsor npx airlock compile trace.json -o tests/checkout.spec.ts Recordings Saved in .airlock/traces — commit them alongside your tests
Recording Steps Requests Recorded Status
checkout-flow-trace.json 8 14 Nov 5 Compiled
puter-notes-trace.json 6 5 Nov 6 Not compiled yet
Self-healing When a button is renamed or moved, AirlockQA finds it again so the run can continue — then asks a person before the fix becomes part of your test.
Healed this week
23
runs kept going instead of failing
Average match
91%
confidence across applied heals
Waiting for review
3
accept to update the spec
Couldn’t heal
2
best match below the threshold
Review queue Checkout › applies the SAVE20 coupon
Step 6 · type into a field · seen in run #1039 · React
Same test id · 94% Found instead
getByTestId('coupon-input')
Match 94 %
before <input id="coupon" name="coupon">
after <input data-testid="coupon-input" name="promo">
Accept and update specReject Account › updates the display name
Step 3 · click · seen in run #1038 · React
Accessible name · 88% Was looking for
button.btn-save
Found instead
getByRole('button', { name: 'Save changes' })
Match 88 %
before <button class="btn-save">Save</button>
after <button class="button primary">Save changes</button>
Accept and update specReject Checkout › guest checkout
Step 9 · click · seen in run #1035 · Vue
Visible text · 81% Was looking for
text=Continue as guest
Found instead
getByText('Checkout as guest')
Match 81 %
before <a href="/guest">Continue as guest</a>
after <button>Checkout as guest</button>
Accept and update specReject How AirlockQA finds it again Clues are tried in this order. The more precise the clue, the higher the match.
1 Same test id exact_testid 2 Accessible name and role aria_fuzzy 3 Visible text semantic_text 4 Place in the page structural_xpath 5 Overall similarity ml_heuristic Healing log Every attempt is saved to .airlock/dataset/mutations.jsonl — the same data airlock dataset shows
When Test · step Change Match Outcome Nov 6, 09:31 Checkout · step 6 #coupon → coupon-input 94% Applied · in review Nov 6, 08:12 Account · step 3 button.btn-save → role button “Save changes” 88% Applied · in review Nov 5, 17:40 Cart · step 4 #apply → .btn-apply 62% Not applied · below threshold Nov 5, 11:02 Auth · step 2 login-btn → login-button 97% Accepted by Jordan Nov 4, 15:26 Checkout · step 12 input[name=zip] → label “Postcode” 90% Accepted by Maya
Checkpoints Save points for your app. Capture a moment — signed in, cart filled — then start new tests from it, side by side, without repeating the setup.
Saved in every checkpoint Cookies, local storage, session storage, the page URL and a DOM snapshot with its hash.
Not saved Your server’s data, background jobs, or process and VM memory. Branches share the same backend.
Saved checkpoints .airlock/checkpoints cart_ready step 5 From Checkout payment URL /cart Holds 3 cookies · 4 storage keys DOM hash c41e08bd… Saved Nov 6, 09:31 9.2 KB post_auth step 3 From Checkout payment URL /account Holds 3 cookies · 3 storage keys DOM hash 7f3a91c2… Saved Nov 6, 09:31 9.2 KB e2e_cart_ready step 5 From Warm state branching URL /cart Holds 3 cookies · 4 storage keys DOM hash a90d77e1… Saved Nov 6, 08:46 9.2 KB e2e_post_auth step 3 From Warm state branching URL /account Holds 3 cookies · 3 storage keys DOM hash 1b6fe3a0… Saved Nov 6, 08:46 9.2 KB
Latest branch results 4 branches · 4 at a time · run 1 cart_ready
setup skipped in every branch · saved 24.8s of repeated setup
Apply coupon SAVE20 Passed 1.4s
Reached /checkout/review with a $50.00 total
assertions 3/3 · ledger 4be1c9…
Expired coupon Handled 1.1s
Showed “This coupon has expired” and kept the full price
assertions 3/3 · ledger 91c07a…
Guest checkout Failed 1.6s
Expected /checkout/guest but landed on /login
assertions 2/3 · ledger 7d2a18…
Pay with saved card Passed 1.3s
Card ending 4242 preselected, stand-in key swapped at the door
assertions 3/3 · ledger e05f3b…
Request policy Your tests’ safety door. Decide what the test browser may reach, and give it stand-in keys so real ones never enter the browser.
When no rule matches What should happen to a request headed somewhere you haven’t listed?
Block itRecommended. Unknown sites never hear from your tests. Let it throughOnly rules and the blocked list stop requests. "defaultAction": "BLOCK "Try a request Pick one to see what the safety door would do with your current policy.
GET api.stripe.com POST api.stripe.com POST webhook.site GET cdn.segment.io GET localhost:3000
POST https://api.stripe.com/v1/refunds
Stopped Refunds are never allowed from a test. Rule 'block-destructive-refunds' blocked path '/v1/refunds' Stand-in keys The test browser only ever sees the stand-in. The real value is added at the door — and only for the hosts and methods listed.
Add keyName Stand-in the browser sees Real value Only for hosts Methods Never on paths STRIPE_KEY ph_sec_stripe_test_placeholder In your runner · never shownapi.stripe.com, localhost, 127.0.0.1 GET, POST */refunds*, */transfers*, */wipe* API_TOKEN ph_sec_test_dummy_token In your runner · never shownapi.example.com GET, POST /v1/delete*, /v1/refund*
Rules Checked top to bottom. The first rule that matches decides.
BLOCK block-destructive-refunds Prevent accidental refunds or wipes during test runs any host · */refunds*, */wipe*, */delete-all* 9 this weekBLOCK block-admin-mutations Role tests may read admin pages but never change them staging.shop.test · POST · /admin/* 5 this weekALLOW allow-stripe-reads Payment lookups are safe to make with the stand-in key api.stripe.com · GET 412 this weekBLOCK limit-upload-size Stop unusually large uploads leaving the browser any host · payload over 1 MB 0 this weekHosts Allowed
localhost 127.0.0.1 api.stripe.com staging.shop.test + Add host Always blocked
pastebin.com webhook.site transfer.sh + Add host The safety door covers requests made by the test browser. Calls your own servers make aren’t routed through it.
Recent decisions Every decision is also written to the run’s signed ledger
See runs Time Decision Request Reason 10:18:42 Stopped POST staging.shop.test/admin/settings Rule block-admin-mutations blocked path /admin/settings 09:31:27 Stopped POST api.stripe.com/v1/refunds Secret policy blocks mutations on /v1/refunds (matches */refunds*) 09:31:22 Key swapped GET api.stripe.com/v1/payment_methods STRIPE_KEY stand-in replaced on an allowed host 09:31:19 Cleaned GET staging.shop.test/api/cart 1 real secret removed from the response before the page saw it 09:31:05 Stopped POST webhook.site/a1b2c3 Host webhook.site is on the blocked list
Evidence Every run writes a step-by-step log, links each entry to the one before it, and signs the result. Change a single entry and the receipt stops checking out.
Latest receipt
run_40549cd06baf Nov 6, 2026 · 08:40:27 UTC · local run · 393 ms
Valid chain
Merkle root e2114ac9fab789cbffd7cebba2fab189 58a23ec41bbdc0af7a0f3f1ba8f6aece
Signature · HMAC-SHA256 3c05ca9c533fb6903624f8188ec698b9 13c3bfad569c8c8562b74efcc2ef0a24
Signing key airlock-signer-key-id-01 2
stand-in keys configured
Not measured
real secrets in sandbox
The ledger, link by link Each block stores the hash of the block before it. Try changing one to see why tampering shows.
Tamper with block #2 EXEC_START
Run began · checkout-flow
prev 00000000 hash 4f1c2a9e
SECRET_SWAP
STRIPE_KEY stand-in replaced
prev 4f1c2a9e hash 9b02d7c1
EGRESS_BLOCKED
POST /v1/refunds stopped
prev 9b02d7c1 hash c7e95b30
ASSERTION_EVAL
Total shows $50.00 · passed
prev c7e95b30 hash 21ad8f64
EXEC_COMPLETE
Run finished in 393 ms
prev 21ad8f64 hash e8c6c259
Every block points at the hash of the one before it, and the Merkle root of all blocks is signed. Nothing has changed since signing. Verify a bundle Checks the signature with your key and recomputes the Merkle root — the same check as airlock audit.
latest-attestation.jsonVerify Where this evidence can help an audit AirlockQA produces evidence. It doesn’t assess or certify controls — your auditor does.
CC6.1 Logical access stand-in keys, host allow-lists Not assessed CC6.6 Boundary protection blocked and allowed request records Not assessed CC7.1 Change verification signed results per commit Not assessed CC8.1 Non-repudiation hash chain and signature Not assessed All receipts Stored in .airlock/compliance · keep them with your build records
Receipt Created Merkle root Chain File run_40549cd06baf Nov 6, 08:40 e2114ac9…6aece Valid Download run_b6277fcda718 Nov 5, 21:04 7a90b2e4…c311f Valid Download run_e8c6c2594903 Nov 5, 13:21 d4417c08…92ab0 Valid Download run_8dc3cd21a56f Nov 5, 09:15 0be3a6d1…71fe4 Valid Download
Example identifiers are not signed evidence. Cryptographic proof is verified against the SHA-256 Merkle root.
Time warp PREVIEW See what your app shows days from now — trials ending, coupons expiring, renewal reminders — without waiting for the calendar.
What moves: the clock that JavaScript’s Date reads inside the test page, including pages it opens next. What doesn’t: browser timers, your servers, databases, cron jobs and certificates keep real time — so server-side expiry needs its own test.
Move the page clock Page time now
Nov 6
2026 · 10:42
After the warp
Nov 20
2026 · 10:42
Jump ahead by
+1 hour +1 day +14 days +30 days
Apply to the test pageReset to real time $ npx airlock warp "+14 days "
What the test page shows staging.shop.test/account · trial started Oct 30
staging.shop.test/account
Your free trial has ended
Upgrade to keep your projects and run history.
TimeWarpResult
scope browser-page
previousTimeIso 2026-11-06T10:42:00.000Z
targetTimeIso 2026-11-20T10:42:00.000Z
deltaMs 1209600000
affectedPageAPIs Date, Date.now()
ledger TIMEWARP_ADVANCED
Good uses — and one to avoid for now The page computes “days left” from Date — a perfect fit.
Coupon and sale end dates
Check the countdown and the “expired” message shown in the browser.
Your server decides when a plan ends, and its clock doesn’t move. Test that on the server side.
Fixtures PREVIEW Save an outside service’s answer once, then replay it in every run — so a test doesn’t fail just because that service had a bad day.
Fixtures answer requests made inside the test browser . Your servers’ own calls aren’t captured, and a request with no saved answer currently goes out to the real network — every miss is listed below so nothing slips by unnoticed.
Mode for the next run Record answers Let requests through once and save what comes back. Replay saved answers Matching requests get the saved answer instantly. Block requests with no saved answer Strict mode is on the roadmap
Saved answers
5
1 synthetic
Served this week
1,140
requests answered locally
Misses
2
went to the real network
Now replaying
GET /v1/rates · 200
Saved answers Pick one to see exactly what the browser receives
Method Request Status Size Used in
GET /v1/payment_methods api.stripe.com 200 2.1 KB 312 runs GET /v1/rates?base=USD api.exchangerate.test 200 1.4 KB 488 runs POST /v1/payment_intents api.stripe.com synthetic 200 0.9 KB 205 runs GET /flags cdn.featureflags.test 200 0.6 KB 96 runs GET /v1/shipping/quote api.shipping.test 200 0.8 KB 41 runs Saved response GET api.exchangerate.test/v1/rates?base=USD
{
"base": "USD",
"date": "2026-11-03",
"rates": { "EUR": 0.92, "GBP": 0.79, "INR": 83.4 }
} Recorded Nov 3. Pinned so prices in tests never drift with real rates.
Misses — no saved answer, sent to the real network Record these, or add the host to your blocked list in Request policy
GET api.analytics.test/v1/track run #1042 · step 9 Block this host GET maps.test/v2/geocode?q=… run #1039 · step 12 Record next time
Chaos report PREVIEW Find buttons that could charge, submit or send twice — before a customer double-clicks or a network hiccup retries.
Today the check watches one real click and looks for an idempotency key on the request it sends. Duplicate, dropped-connection and 502 scenarios are described, not executed yet — each row says which.
What to check Run check again npx airlock chaos http://localhost:3000Pay now
order #5521 · $49.00 order #5522 · $49.00 · duplicate?
What a double-click could do without an idempotency key Risk indicated
Checkout can be sent twice POST /api/checkout went out without an Idempotency-Key header. If a customer double-clicks or the browser retries, your server can’t tell the two apart.
Suggested fix
Send a unique Idempotency-Key with each checkout request, and have the server return the first result for any repeat.
This is a warning sign, not proof of a double charge — your server may already guard against repeats in another way.
request headers seen
content-type application/json
authorization Bearer ph_sec_stripe… (stand-in)
traceparent 00-4bf92f35…-01
idempotency-key — missing
Scenarios Report chaos_1c8f · Nov 6, 10:52 · observation only
Observed we watched it happenRisk indicated a warning signSimulated described, not runScenario Status Measured What it means 1 · One real click Observed 412 ms · HTTP 200 Clicked “Pay now” once and saw the checkout request leave the browser. 2 · Duplicate request Risk indicated Not executed No idempotency key on the request, so a repeat couldn’t be recognised. No second request was sent. 3 · Dropped connection Simulated Not executed Run a controlled connection-interruption test and check retry and form-state behaviour. 4 · 502 and slow webhook Simulated Not executed Test the 502 and delayed-webhook path with a controlled server fixture.
Bug capsules PREVIEW When something breaks in a test environment, a capsule keeps the last moments before the error. AirlockQA turns it into a Playwright test that reproduces the bug.
Prototype — keep real customer data out. Emails, card numbers, tokens and obvious secrets are scrubbed on a best-effort basis, but capsules aren’t encrypted and storage can still hold sensitive values. Use staging or preview data only.
Captured · 3 cap_7f2a91 Nov 6, 09:12 TypeError: Cannot read properties of undefined (reading 'total') staging /checkout 38 eventscap_19bd04 Nov 5, 16:40 Unhandled promise rejection: 409 Conflict preview /cart 22 eventscap_c03e77 Nov 4, 11:08 ReferenceError: couponState is not defined staging /checkout 15 eventsHow a capsule is made
Your test app keeps a small rolling log of recent clicks, inputs, requests and console messages. When an error isn’t caught, it saves that log as a capsule with a checksum. Teleport replays the steps in a fresh browser and writes a repro test. Checkout crashed after applying a coupon twice TypeError: Cannot read properties of undefined (reading 'total')
Checksum valid
Environment staging · app 2.14.0
Page /checkout
Browser Chromium · 1280×800
Checksum sha256:5be0…a91c Scrubbed before saving
[REDACTED_EMAIL] × 2 [REDACTED_PAYMENT_CARD] × 1 [REDACTED_JWT_TOKEN] × 1 Review: 3 storage keys, 2 cookies kept
The last moments before the error −4.8s navigation /checkout −4.1s dom_input input[name=email] → [REDACTED_EMAIL] −3.6s dom_input input[name=coupon] → "SAVE20" −3.2s dom_click button “Apply coupon” −2.9s network_req POST /api/cart/coupon → 200 −1.4s dom_click button “Apply coupon” (again) −1.1s console warn: coupon already applied 0.0s uncaught_error TypeError: Cannot read properties of undefined (reading 'total') · checkout.js:212 Repro test npx airlock teleport cap_7f2a91 .json -o tests/repro-bug.spec.ts
Run the reproimport { test, expect } from '@playwright/test' ;
test('repro: cap_7f2a91 on /checkout ' , async ({ page }) => {
const errors: string[] = [];
page.on('pageerror' , (e) => errors.push(e.message));
await page.goto('https://staging.shop.test/checkout ' );
await page.getByLabel('Coupon' ).fill('SAVE20' );
await page.getByRole('button' , { name: 'Apply coupon' }).click();
await page.getByRole('button' , { name: 'Apply coupon' }).click();
expect(errors).toContain("Cannot read properties of undefined (reading 'total') " );
});
Connect your pipeline Run AirlockQA on every push and pull request, and get the result — with its signed receipt — right on the pull request.
Cloud API reachable · api.qbrin.cloud1 · Where should tests run? Local --local Your CI runner’s own browser. Free and keyless — nothing leaves your machine. Cloud plan --cloud Hosted runners. CI submits the run with a project API key. Enterprise AIRLOCK_CLOUD_URL Same CLI, pointed at your own deployment and its own keys. $ bun add --dev airlockqa
$ bunx airlock test --cloud 3 · Store the key as a CI secret Secret name AIRLOCK_API_KEY GitHub → Settings → Secrets → Actions The CLI sends the key only as an HTTPS Bearer token — never in the run body or a command argument. Don’t expose the secret to workflows triggered by pull requests from forks. Real app secrets stay in your runner; only stand-in values travel to the cloud. 4 · Add the workflow.github/workflows/airlockqa.yml
Copy name : AirlockQA
on : [push, pull_request]
jobs :
browser-tests :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : oven-sh/setup-bun@v2
- run : bun install && bunx playwright install --with-deps chromium
- run : bunx airlock test --cloud
env :
AIRLOCK_API_KEY : ${{ secrets.AIRLOCK_API_KEY }}
- run : bunx airlock audit5 · See results on your pull requestsairlockqa bot
commented on #131 All 3 suites passed · 72 of 72 steps · 38s
2 requests stopped by policy · 1 locator healed (needs review) · receipt e2114ac9… signed
API keys Project-scoped. Shown once, stored as a hash.
Create keyKey Project Last used Actions GitHub Actions air_live_7Hk2… acme-web 2 min ago Revoke Nightly regression air_live_c81d… acme-web Nov 6, 02:00 Revoke Old laptop key air_live_02fe… acme-admin Revoked Oct 21 —
Cloud usage today acme-web 64 / [LIMIT] runs
Whole workspace 91 / [LIMIT] runs
Each parallel worker and each retry counts as a run. Limits reset at midnight UTC; the CLI warns when 20% or less remains.
API contract Today the cloud API accepts one call. History, usage and key endpoints are planned.
POST /v1/runs
Authorization: Bearer air_live_••••
{
"suiteName" : "checkout-flow" ,
"targetUrl" : "https://staging.shop.test" ,
"phantomConfig" : { "STRIPE_KEY" : "ph_sec_stripe_test_placeholder" },
"options" : { "browser" : "chromium" , "headless" : true }
}
202 Accepted x-ratelimit-remaining: 36
{ "runId" : "run_7c1e09" , "status" : "queued" }
429 Too Many Requests example
{ "code" : "DAILY_QUOTA_EXCEEDED" , "quota" : { "scope" : "workspace" , "resetAt" : "2026-11-07T00:00:00Z" } }
EXECUTION FLOW · THREE.JS ARCHITECTURE
From commit to verified attestation
↻ Replay
⌘ CI runner
⇄ Airlock Cloud API
▣ Browser runner
✓ Run response
Three.js CI to attestation flow
POST
/v1/runs
· Cloud run submission endpoint
Settings Workspace details, people, run defaults and how evidence is signed. Everything here maps to airlock.config.json.
General Members Run defaults Configuration file Evidence signing Plan General Projects
acme-web3 suites acme-admin1 suite notes-app1 suite + New project Person Role Last active MC Maya Chen maya@acme.test Admin Now JL Jordan Lee jordan@acme.test Editor Today, 09:54 PN Priya Nair priya@acme.test Viewer Nov 5 CI pipeline service · uses key air_live_7Hk2… Runner 2 min ago
Run defaults Used by every run unless a suite overrides them
Run browsers without a window Faster in CI. Turn off to watch a run locally. browser.headless Safety door on Every browser request passes the request policy. proxy.enabled Add accessible-name fallbacks to compiled tests Gives each step a second way to find its element. compiler.emitAriaFallbacks Self-heal broken locators Applies a fix at 80% match or higher, then asks for review. healer.autoHeal Save every heal to the dataset Powers the healing log and airlock dataset. healer.saveMutations airlock.config.json Real secret values are never shown or uploaded — they stay in your runner.
Matches the schema{
"projectName" : "Acme SaaS QA" ,
"baseUrl" : "https://staging.shop.test" ,
"browser" : { "headless" : true , "viewport" : { "width" : 1280, "height" : 800 } },
"proxy" : { "enabled" : true , "port" : 8899 },
"secrets" : {
"STRIPE_KEY" : { "real" : "•••• kept in your runner" , "phantom" : "ph_sec_stripe_test_placeholder" , … }
},
"policy" : { "defaultAction" : "BLOCK" , … },
"compiler" : { "defaultTimeoutMs" : 10000, "emitAriaFallbacks" : true },
"healer" : { "autoHeal" : true , "confidenceThreshold" : 0.8, "saveMutations" : true },
"forker" : { "maxParallelForks" : 5 },
"compliance" : { "outputDir" : "./.airlock/compliance" , "generateHtmlReport" : true }
} Download file Copy
Evidence signing How each run’s receipt is sealed
HMAC-SHA256 One shared key signs and verifies. Simple for a single team. Ed25519 A private key signs; anyone with the public key can verify.
Key ID airlock-signer-key-id-01
Receipts folder ./.airlock/compliance
Last rotated Oct 30, 2026 by Maya Chen Rotate key Never use the sample key from the docs outside a demo.Cloud plan Early access [YOUR PRICE] / month · daily runs: [YOUR LIMIT] per repository
Running locally is always free and doesn’t count toward cloud usage.