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.
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.
- 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 \"[email protected]\" into Email", "type the 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.