Skip to Content
webagent docs
Guides

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.

Open as .md

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 localhost or 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-uat is missing, the start call fails with not_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

StepCallReply
StartPOST $API_URL/v1/skills/smoke-uat/run with {"inputs": {"url": "..."}, "session": {"record": true}}202 with runId, sessionId, status: "running"
Wait and readGET $API_URL/v1/skill-runs/{runId}/report.json until verdict is not runningThe report: verdict, summary, steps, findings, consoleErrors, networkFailures, evidence
KeepOpen evidence.reportSignedUrlThe 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:

verdictMeaningExit code in the script below
passEvery step and check passed.0
failThe test ran and found a problem. Read summary and the failed steps.1
errorThe test could not run to a result: the run was cancelled, denied a permission, or the browser failed.2
runningStill 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.

scripts/smoke.sh
#!/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 ;;
esac

To 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 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

.github/workflows/smoke.yml
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: ignore

Add 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.

smoke.mjs
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 fail in CI is the moment to read summary, the failed steps and the saved report file.

On this page