Skip to Content

Agentアクション

Agentアクションは、自然言語で書いたタスクからWebアプリケーションをテストします。テスターと呼ぶモデルが実際のChromeでアプリケーションを操作してタスクを行い、そのあと別に呼び出したモデル、ジャッジが、ページに表示された内容から期待どおりかどうかを判定します。Probeを実行する環境にChromeかChromiumが必要で、モデルはそのプロバイダーのAPIキーで呼び出します。

外部アクションとしてmozership/probe-agent で公開しています。ワークフローが初めて使うときにProbeがダウンロードし、固定したコミットのaction.ymlが示すSHA-256と一致する実行ファイルだけを実行します。Probe v1.21.0以降が必要です。

このアクションは、ページが何をするかも、モデルが何をクリックするかも判定できないため、ガードに従いません。--read-onlyや--allow-hostの下では、--allow-actionで指定しない限り、このアクションを使うステップを拒否します。テスターはallow_hostsの範囲にとどめます。ほかのホストのページは開かず、クリックでほかのホストに移ったときはブラウザを戻します。

モードは2つあります。

  • シナリオ(taskとexpect): テスターがタスクを行い、ジャッジが期待どおりかを判定します。マージ前のように、通るべきことを確かめる用途に向きます。
  • ペルソナ(personaとgoal): テスターがペルソナになりきって目標を目指し、途中で気づいた問題を書き留めます。レビュアーが、ペルソナが目標にたどり着けたか、そして書き留めた内容とブラウザの記録のうちどれを指摘とするかを判断します。ステージング環境で繰り返し実行するなど、誰もテストを書いていないところの問題を探す用途に向きます。

決定論的なステップの代わりではありません。ステータスやDBの行、メールのように厳密に書けることはそちらで確かめ、マージの関門にします。agentアクションは、それらでは書けないこと、たとえばユーザーが最後までたどり着けるか、データや設定が積み重なったステージングでもアプリケーションが持ちこたえるかを確かめるためのものです。ペルソナのテストをrepeatでステージングに対して回せば、人と同じようにアプリケーションを使い続けます。

基本的な構文

ワークフローでは、アクションを完全なコミットSHAで固定します。各リリース のノートの先頭に、コピーして使うアクションとコミットがあります。ここでの例では、actionsで一度だけ名前を付けています。これにはProbe v1.24.0以降が必要です。それより前のバージョンでは、usesごとにアクションを省略せずに書きます。

name: Sign-up actions: agent: github.com/mozership/probe-agent@4d0d6ccee6165624283b22fa0541b02d17e50ac5 # v0.2.0 secrets: - ANTHROPIC_API_KEY - TEST_PASSWORD jobs: - name: sign-up steps: - name: A new user signs up uses: agent with: url: https://staging.example.com/ task: | As a new user of this service, sign up with the email address {secret:EMAIL} and the password {secret:PASSWORD}. expect: | The account is created, and the dashboard greets the user. secrets: EMAIL: "qa+{{random_str(8)}}@example.com" PASSWORD: "{{TEST_PASSWORD}}" timeout: 5m test: res.verdict == "pass"

ペルソナのテストでは、taskとexpectの代わりにpersonaとgoalを書きます。

- name: A first-time visitor writes a first note uses: agent with: url: https://staging.example.com/ persona: | Carol, who has never used a notes app and is not technical. She signs up with {secret:EMAIL} and the password {secret:PASSWORD}. goal: Create an account, and write a first note that says "Buy milk". secrets: EMAIL: "qa+{{random_str(8)}}@example.com" PASSWORD: "{{TEST_PASSWORD}}" timeout: 5m test: len(filter(res.findings, .severity == "high" && .verified)) == 0

タスクやペルソナは日本語で書いても構いません。判定の理由や要約は、タスクやゴールと同じ言語で返ります。

自然言語で書いたテストは決定的ではありません。同じステップでも、実行のたびに違う手順をたどったり、違う判定になったりすることがあります。できるところは、アカウントが作成されたことをhttpやdbのステップで確かめるなど、決定的なステップをあとに続けて確認してください。また1回の結果を信じるのではなく、repeatで複数回の実行のうち何回通ったかを数えてください。

仕組み

  1. Chromeがurlを開きます。
  2. テスターにはtaskとexpect、そしてブラウザのツールを渡します。テスターはページをrefの付いた要素の木として読み、クリックし、入力し、フォームを埋め、スクリーンショットを撮り、最後にfinishを呼びます。
  3. ジャッジには、タスク、期待、ステップ、ページに表示されたテキスト、最後のページとそのスクリーンショット、コンソールのエラーと失敗したリクエストを渡します。ジャッジはページから引用してpassかfailを返します。
  4. 引用はそれぞれ、ページに表示された内容の中から探します。ページに表示されたものを何も引用していないpassは採らず、判定はblockedになります。

テスターには判定せずに見たものを説明するよう指示しています。作業をしていない呼び出しが判定を下すためです。

ペルソナのテストでは、テスターは問題に気づいたらすぐに書き留めます。レビュアーはジャッジの代わりに、記録に加えて書き留めた内容と最も遅かったリクエストを受け取ります。記録が裏付ける内容だけを残し、テスター自身の操作ミスによるものは除き、サーバーのエラーのようにテスターが書き留めていないが記録に表れているものを加えます。指摘はそれぞれ記録を引用し、引用が記録に見つかったものはverifiedになります。

パラメータ

以下はwithの直下に書きます。

パラメータ型デフォルト説明
urlString必須ブラウザが最初に開くページ。httpかhttps
taskStringシナリオでは必須テスターが行うこと
expectStringシナリオでは必須終わったあとに成り立っているべきこと
personaStringペルソナでは必須テスターが演じる人物。何を知っていて、どう振る舞うか
goalStringペルソナでは必須ペルソナが目指すこと
secretsObjectなしモデルに見せない値を名前ごとに指定する。taskやpersonaには{secret:NAME}と書く。シークレットを参照
allow_hostsList、または文字列urlのホストテスターが開いてよいホスト。host、host:port、*.domainのいずれか。リスト、またはカンマ区切りの文字列
providerStringanthropicanthropic、openai、xai、gemini、sakuraのいずれか。モデルを参照
modelStringanthropicではclaude-opus-5-5、それ以外では必須モデルのID
effortStringmediumlow、medium、high、xhigh、maxのいずれか
base_urlStringプロバイダーのものプロバイダーのAPIのURL。プロキシを通すときなどに指定する
visionBooleansakuraではfalse、それ以外ではtrueモデルが画像を読めるかどうか。anthropicでは指定できない
max_stepsInteger40テスターがブラウザを呼び出せる回数
timeout期間の文字列、または秒数4mジャッジを含むテスト全体の制限時間。ステップのtimeout(指定しなければ5m)より短くする
headlessBooleantrueウィンドウを表示せずにChromeを動かす
window_w、window_hInteger1280、800ビューポートの大きさ(ピクセル)
cacheStringなし通ったシナリオテストのステップを保存し、そこから再生するファイル。リプレイを参照
cache_modeStringautoauto、read、strict、refreshのいずれか。リプレイを参照
evidence_dirString一時ディレクトリスクリーンショットとレポートを書き出す場所。テストごとにディレクトリを分ける

モデル

このアクションはClaude向けに作っています。ClaudeはClaude APIのブラウザツールセットでブラウザを操作するよう学習しています。ほかのプロバイダーのモデルには、同じブラウザのツールをアクション独自のツールとして渡すため、ジャッジ、ペルソナのテスト、リプレイを含めて同じテストを実行できます。タスクをどれだけうまくこなせるかはモデルによります。

APIキーはProbeの環境から読みます。Probeの出力で値を隠すよう、ワークフローのsecretsに書いてください。

providerAPIキー補足
anthropicANTHROPIC_API_KEYデフォルト
openaiOPENAI_API_KEYResponses APIで呼び出し、OpenAI側には何も保存しない。maxはxhighとして送る
xaiXAI_API_KEYChat Completionsで呼び出す。maxはxhighとして送る
geminiGEMINI_API_KEYChat Completions互換のAPIで呼び出す。xhighとmaxはhighとして送る
sakuraSAKURA_AI_ENGINE_TOKENさくらのAI Engine。Chat Completionsで呼び出す。モデルごとに受け付けるパラメータが違うためeffortは送らず、visionをtrueにしない限り画像も渡さない。gpt-oss-120bのようにツールを呼び出せるモデルを選ぶ
secrets: - OPENAI_API_KEY jobs: - name: sign-up steps: - name: A new user signs up uses: agent with: provider: openai model: gpt-5.4 url: https://staging.example.com/ task: Sign up with {secret:EMAIL} and the password {secret:PASSWORD}. expect: The dashboard greets the user.

画像を読めないモデルは、ページを要素とテキストだけで読みます。スクリーンショットのツールは渡さず、ページ上の座標ではなく要素を操作し、ジャッジにも最後のページを画像として見せません。スクリーンショット自体は証拠として保存します。

レスポンスオブジェクト

フィールド型説明
res.modeStringscenarioかpersona
res.verdictStringpass、fail、または判定できなかったときのblocked。ペルソナのテストでは、ペルソナが目標にたどり着けたかどうか
res.findingsListペルソナのテストで見つかったアプリケーションの問題。重いものから順に並ぶ。title、severity(high、medium、low)、category(error、functional、usability、performance、content、accessibility)、expected、observed、evidence、steps、url、screenshot、そして根拠のどれかが記録に見つかったかを示すverified
res.reasonStringジャッジが述べた判定の理由
res.evidenceListジャッジの引用。quoteと、それがページに表示されていたかを示すverified
res.stoppedStringテスターの止まり方。done、blocked、max_steps、timeout、no_finish、refused、またはcache_mode: strictで記録が合わなくなったときのstale
res.summaryStringテスターが行ったこと、見たことの要約
res.stepsListブラウザの呼び出しごとのindex、tool、input、resultかerror、url、screenshot、テスターのnotes
res.url、res.titleStringテスターが最後にブラウザを置いたページ
res.console_errorsListページがコンソールに書いたエラー
res.network_errorsList失敗したリクエストと、400以上で応答されたリクエスト。method、url、status、error
res.cacheObjectcacheを指定したときの使われ方。path、mode、status(miss、hit、partial)、再生したステップ数のreplayed、記録が止まった位置と理由のdiverged_atとreason、アプリケーションが記録と合わなくなって止まったかを示すstale、その実行を記録したかを示すsaved
res.metricsObject実行どうしを比べるための、モデルに左右されない数値。steps、duration_ms、requests、failed_requests、console_errors、時間のかかった上位10件のリクエストのslowest_requests(method、url、status、duration_ms)
res.usageObjectモデルへのリクエスト数requestsと、使ったトークン数
res.modelStringモデルのID
res.filepathsObjectステップごとのスクリーンショットを含むMarkdownのレポートreport、結果のJSONtrace、最後のページのscreenshot
res.reportStringProbeがステップの下に表示する数行
statusInteger0。判定はステップのtestで確かめる

blockedはfailと分けています。CAPTCHAやタスクに書かれていない情報のように、アプリケーションの誤りではなく、テスターが先に進めなかったことを表します。

リプレイ

毎回同じ手順で通るシナリオテストなら、毎回モデルに手順を考えさせる必要はありません。cacheを指定すると、通った実行のステップをファイルに保存し、次の実行ではモデルを使わずにそれを再生します。ページに表示された内容は引き続き判定するため、ジャッジだけは呼び出します。

with: url: https://staging.example.com/ task: Sign up with {secret:EMAIL} and the password {secret:PASSWORD}. expect: The dashboard greets the new user. cache: .probe-agent/signup.json cache_mode: "{{CACHE_MODE ?? 'auto'}}"

リプレイは次のように進みます。

  1. 記録が始まったページから始まるときだけ再生します。開始URLがログインなどにリダイレクトすることがあるためです。
  2. ステップが操作した要素を、button "Create account"のようにロールと名前で探し直します。同じロールと名前の要素がほかにもあった場合はその数も数え、同じ数のうちの同じ順番の要素を選びます。数が違えば曖昧として、推測せずに再生を止めます。
  3. ステップのたびに、記録したときと同じルートに着いたこと、記録にないアラートが出ていないことを確かめます。ルートはページのパスとクエリで、IDやトークン、日付のようにレコードごとに発行される値は1つとして扱います。/orders/42と/orders/7は同じルートで、/companiesと/companies?tab=notesは別のルートです。
  4. モデルを使わずに終わるか、止まったところからテスターに引き継ぎます。引き継ぐときは、何を再生し、なぜ止まったかをテスターに伝えます。テストが通れば、その実行をあらためて記録します。

タスクに書かれた値や、名前のようにテスターが考えた値は、再生でもう一度入力します。画面に表示されたコードや日付のように、テスターが画面から読み取った可能性のある値は、その実行に固有の値です。そのステップは値を保存せずに記録し、再生はそこでテスターに引き継ぎます。

記録はタスク、期待、開始URLに結びついていて、どれかが変われば再生しません。シークレットはプレースホルダーのまま、urlと同じホストのURLはパスとして保存するため、あるホストで作った記録を別のホストで再生できます。記録するのはステップであってモデルではないので、あるモデルで記録したテストを別のモデルで再生できます。記録が変わったときだけ書き込むので、コミットした記録は再生しているあいだ変更されません。ペルソナのテストは毎回違う道をたどるためのものなので、cacheは指定できません。

cache_mode再生記録記録が合わなくなったとき
autoする通った実行で記録が変わるときテスターが続ける
readするしないテスターが続ける
strictするしないモデルを使わずにblocked(stopped: stale)にする
refreshしない通った実行

記録ファイルをコミットし、CIではstrictで実行してください。記録が合わなくなったとき、誰かが記録し直すまで毎回テスターの費用を払うのではなく、テストを失敗させるためです。記録し直すのは、変更を人が確かめられる場所でautoを使って行います。

シークレット

モデルには、シークレットの場所が{secret:NAME}と見えます。モデルがプレースホルダーを入力すると、ブラウザには実際の値を渡します。ページからモデルに見せる内容とアクションが返す内容では、それぞれの値を大文字小文字を問わずプレースホルダーに置き換えて戻します。アプリケーションがメールアドレスを小文字で表示することがあるためです。指定されていないシークレットのプレースホルダーは、入力する文字列ではなく、モデルに伝えるエラーになります。

CIでの実行

  • ランナーにChromeかChromiumをインストールし、headless: trueのままにします。
  • APIキーはCIのシークレットから設定し、ワークフローのsecretsに書きます。
  • evidence_dirをアーティファクトとしてアップロードすると、失敗したテストのレポートとスクリーンショットを読めます。
  • cacheを指定したテストはcache_mode: strictで実行します。記録が合っているあいだ、モデルには判定だけを頼みます。
更新日時