Smoke tests in CI
Run a webagent smoke test after every deploy from a CI job with curl or the SDK, and fail the job when the test does not pass.
On GitHub, the Lookover QA Action runs a whole tests.yaml and comments on the pull request: see Run QA in CI in 5 minutes. This page is the version with plain API calls, for any CI system.
A smoke test in CI is three calls: start a run, wait for the report, set the exit code from its verdict. This page uses the smoke-uat skill. It loads your page, checks for console errors, failed network calls, broken links and basic accessibility problems, and can follow plain-language steps and check what is on the page afterwards.
What you need
- A public URL of the app you just deployed. The cloud browser cannot open
localhostor a private CI address. See Test a local app. - An API key. Create one on the Keys page. The member role is enough to run skills. Make a separate key for CI, so you can revoke it alone. Give it the CI scope and, if you like, an expiry: see API keys.
- The key stored as a CI secret, here
WEBAGENT_API_KEY. Never write it in the repository or print it in the log. - The skills seeded on the deployment you use. If
smoke-uatis missing, the start call fails withnot_found. Ask your admin.
$API_URL in the examples is your API host, https://api.us.webagent.lookover.ai once your workspace is live there. If your team gave you another URL, use that.
The three calls
| Step | Call | Reply |
|---|---|---|
| Start | POST $API_URL/v1/skills/smoke-uat/run with {"inputs": {"url": "..."}, "session": {"record": true}} | 202 with runId, sessionId, status: "running" |
| Wait and read | GET $API_URL/v1/skill-runs/{runId}/report.json until verdict is not running | The report: verdict, summary, steps, findings, consoleErrors, networkFailures, evidence |
| Keep | Open evidence.reportSignedUrl | The HTML report with screenshots of failed steps. The link works for 5 minutes. |
Authenticate with Authorization: Bearer <key> or x-api-key: <key>.
The verdict decides the exit code:
verdict | Meaning | Exit code in the script below |
|---|---|---|
pass | Every step and check passed. | 0 |
fail | The test ran and found a problem. Read summary and the failed steps. | 1 |
error | The test could not run to a result: the run was cancelled, denied a permission, or the browser failed. | 2 |
running | Still going. Read again. | (waits) |
A fail is a finding about your app, or about the test. An error means the test could not finish. Keep the two apart in your CI so nobody chases a bug that is not there. The outcome field has the raw skill status (passed, failed, error, cancelled, denied).
GET /v1/skill-runs/{runId} is still there for the raw run record, and GET /v1/skill-runs/{runId}/report redirects to the HTML report.
Console errors and failed network calls come from the session log, which is saved when the session ends. A report read the moment the run ends can say in notes that the log is not available. The script reads the report once more after 5 seconds when evidence.logsPath is missing.
A script with curl
This script reads the report from report.json. It was run against a local API with a pass (exit 0), a failed expectation and a missing page (both exit 1). A failed download of the HTML report does not change the exit code.
#!/usr/bin/env bash
# Needs: curl, jq. Env: WEBAGENT_API_KEY, TARGET_URL. Optional: API_URL, EXPECT_TEXT.
set -euo pipefail
API_URL="${API_URL:-https://api.us.webagent.lookover.ai}"
auth=(-H "Authorization: Bearer ${WEBAGENT_API_KEY}")
started=$(curl -sS --fail-with-body -X POST "$API_URL/v1/skills/smoke-uat/run" \
"${auth[@]}" -H "Content-Type: application/json" \
-d "$(jq -n --arg url "$TARGET_URL" --arg text "${EXPECT_TEXT:-}" \
'{inputs: ({url: $url} + (if $text != "" then {expectText: $text} else {} end)), session: {record: true}}')")
run_id=$(jq -r .runId <<<"$started")
echo "run: $run_id"
read_report() { curl -sS --fail-with-body "$API_URL/v1/skill-runs/$run_id/report.json" "${auth[@]}"; }
# Wait up to 10 minutes for a verdict.
for _ in $(seq 1 120); do
report=$(read_report)
verdict=$(jq -r .verdict <<<"$report")
[ "$verdict" != "running" ] && break
sleep 5
done
# The session log is saved a moment after the run ends: read once more if it is not there yet.
if [ "$verdict" != "running" ] && [ "$(jq -r '.evidence.logsPath // empty' <<<"$report")" = "" ]; then
sleep 5
report=$(read_report)
fi
echo "verdict: $verdict"
jq -r '.summary' <<<"$report"
jq -r '.steps[] | select(.status == "fail") | "failed step \(.index + 1) \(.title): \(.error // "no reason given")"' <<<"$report"
jq -r '.consoleErrors[] | "console: \(.text)"' <<<"$report"
jq -r '.networkFailures[] | "network: \(.method // "") \(.url) \(.status // .error // "")"' <<<"$report"
# The report link lives for 5 minutes: save the file now. A failed download must not hide the verdict.
link=$(jq -r '.evidence.reportSignedUrl // empty' <<<"$report")
if [ -n "$link" ]; then
curl -sS -o smoke-report.html "$link" || echo "could not save the HTML report" >&2
fi
case "$verdict" in
pass) exit 0 ;;
fail) exit 1 ;;
running) echo "still running after 10 minutes" >&2; exit 3 ;;
*) exit 2 ;;
esacTo add plain-language steps and what must be true afterwards, extend inputs. The script above sets expectText from EXPECT_TEXT. For the other checks, expectUrl and expectNoText, add them the same way:
{
"inputs": {
"url": "https://staging.example.com/login",
"steps": ["type \"qa-user@example.com\" into Email", "type \"<TEST_PASSWORD>\" into Password", "click Sign in"],
"expectText": "Welcome",
"expectUrl": "/dashboard",
"expectNoText": "Invalid password"
}
}Step text is stored with the run. Use a throwaway test account, never a real password. Skill runs cannot use vault logins yet, see Signed-in sites. A broken link on the start page fails the run. To report it as a finding only, add "strictLinks": false to inputs.
To retry the start call without making a second run, send an Idempotency-Key header. The same key and body replay the first answer.
GitHub Actions
name: Smoke test
on:
workflow_dispatch:
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Run the smoke test
run: bash scripts/smoke.sh
env:
WEBAGENT_API_KEY: ${{ secrets.WEBAGENT_API_KEY }}
TARGET_URL: https://staging.example.com
- name: Keep the report
if: always()
uses: actions/upload-artifact@v4
with:
name: smoke-report
path: smoke-report.html
if-no-files-found: ignoreAdd the key under Settings, Secrets and variables, Actions, as WEBAGENT_API_KEY. Pass the URL of the deploy you want to test. The job fails when the script exits with a code other than 0.
If your CI starts the app inside the job, the app is on a private address. Either deploy it first to a public preview URL, or put a tunnel in front of it for the length of the job. A tunnel makes the app public for that time, so use test data only.
With the SDK
The TypeScript SDK wraps the same calls. It is a private package today, so use it only if your team has access. See SDK.
import { WebagentClient } from "@lookover-ai/webagent-sdk";
const client = new WebagentClient({
apiKey: process.env.WEBAGENT_API_KEY,
// baseUrl: "https://...", // set this when your API host is not the one of your key's region
});
const { runId } = await client.runSkill("smoke-uat", {
inputs: { url: process.env.TARGET_URL, expectText: "Welcome" },
session: { record: true },
});
await client.waitForSkillRun(runId, { timeoutMs: 600_000 });
// The session log is saved a moment after the run ends: wait before reading console and network errors.
await new Promise((r) => setTimeout(r, 5_000));
const report = await client.getSkillRunReport(runId);
console.log(report.verdict, report.summary);
for (const s of report.steps.filter((s) => s.status === "fail")) console.log("failed:", s.title, s.error);
for (const e of report.consoleErrors) console.log("console:", e.text);
for (const n of report.networkFailures) console.log("network:", n.method, n.url, n.status ?? n.error);
process.exit(report.verdict === "pass" ? 0 : report.verdict === "fail" ? 1 : 2);waitForSkillRun polls every second by default and throws a timeout error when timeoutMs passes. The default is 5 minutes. getSkillRunReport(runId) returns the report from GET /v1/skill-runs/:id/report.json. For a task, use getTaskReport(taskId). They need SDK 0.3.0.
Tips
- Run the test after the deploy has finished, against the new URL.
- Keep CI keys apart from your personal key. Revoke a CI key when a job log leaks it.
- Limit one run to one question. Several short runs are easier to read than one long one.
- A
failin CI is the moment to readsummary, the failed steps and the saved report file.
Run QA in CI in 5 minutes
Run your .webagent/tests.yaml on every pull request with the Lookover QA GitHub Action. The job passes or fails on the result and one comment on the pull request shows what broke.
TypeScript SDK
The @lookover-ai/webagent-sdk package. How to get it today, what it can do and what it cannot.