はじめに
UIテストにAIが入り込む経路は、いまのところ大きく二つある。
ひとつは「テストコードをAIに書かせる」、もうひとつは「テスト結果のトリアージをAIに任せる」。
どちらもそれ単体では成立するが、片方だけを切り出して評価しても、テスト自動化が抱える本来の課題には届かない気がしていた。
特に気になっていたのは、Playwrightで書かれたテストの 偽陽性(PASSすべきなのにFAIL) と 偽陰性(FAILすべきなのにPASS) を、誰がどのタイミングで判別するのか、という構造の話だった。
LLMトリアージは賢く見えるが、入力に何を渡すかで原理的な限界が決まる。
本当に偽陰性まで掴めているのか、それとも見えている範囲だけを上手に説明しているのか。
この記事は、その疑問を確かめるために組んだ TypeScript ベースのパイプラインを、1回まるごと通した記録になる。
技術スタックは TypeScript で統一した:
- サンプルアプリ: TypeScript + Express + EJS + SQLite
- UIテスト: Playwright(テストコードはAIに書かせる)
- CI: ローカル Jenkins(Docker Compose)
- パーサー / 差分分類 / 偽陰性候補検出: TypeScript
- AIトリアージ: OpenAI Responses API(最初は
gpt-4.1-mini、追検証でgpt-5.4) - 評価: Evalite(Vitest ベース)
ソース一式は GitHub に置いてある: ui-test-analytics
なお、本記事は「制約環境で作っていた自作テスト分析基盤を、TypeScriptで再構築する」というシリーズの1本目にあたる。
ダッシュボード化と改善サイクルの話は次の記事で扱う。
検証観点
このパイプラインで確かめたかったのは、次の点だ。
- AIに書かせたPlaywrightテストは、罠を仕込んだ環境でどう壊れるか
- 失敗テストに対するLLMトリアージは、偽陽性と偽陰性をどこまで言い当てるか
- 「テストはPASSしているが実際には壊れている」をパイプライン側でどう拾うか
- Evalite で精度を CSV 蓄積する流れは、CI に組み込めるか
- AIに任せる範囲と、構造で受け止めるべき範囲の境界はどこにあるか
「AIに書かせて、AIにトリアージさせて、AIに評価させる」と言いたいわけではない。
むしろ、AIをパイプラインの構成要素として置いたときに、 どこで原理的に届かないか を数字で見ることが目的だった。
罠を仕込んだサンプルアプリ
サンプルアプリ(Phantom Brew)はEC風の6ページ構成で、6種類の偽陽性トラップと6種類の偽陰性トラップを意図的に仕込んでいる。アプリ実装の詳細はGitHubの仕様書に置いた。記事に必要な情報だけ表で抜く。
| ID | 罠の種類 | 偽陽性/偽陰性 | 検知に必要な条件 |
|---|---|---|---|
| FP-2 | SSEステータス遷移の非決定タイミング | 偽陽性 | タイミングに依存しない待ち方 |
| FP-3 | A/Bテストでフォームレイアウトが2系統 | 偽陽性 | DOM構造に依存しないセレクタ |
| FP-4 | 全APIに10秒20リクエストでレート制限 | 偽陽性 | テスト実行ペースの設計 |
| FN-1 | 税計算の丸め誤差(商品ごとに Math.round) |
偽陰性 | 3商品以上で期待値を厳密比較 |
| FN-2 | 価格ソート後にカードの href が更新されない |
偽陰性 | ソート後にクリックして遷移先を検証 |
| FN-3 | クーポンUIは表示変化、合計は再計算されない | 偽陰性 | 適用前後の合計を比較 |
| FN-4 | 注文がちょうど10件のとき空の2ページ目が出る | 偽陰性 | 境界値テスト |
| FN-5 | 数量+ボタン連打で競合(最後のレスポンスが上書き) | 偽陰性 | 高速連続クリック |
| FN-6 | サーバー側のメールバリデーション欠如 | 偽陰性 | 不正値での送信検証 |
偽陽性は「テスト側の書き方や環境でどうにかできるはずなのに失敗するケース」、偽陰性は「テストが書けていなければ気付けないケース」として整理している。
このうち、偽陰性側は「正しく書ければ検知できるが、普通に書くとPASSが返ってくる」点が肝になる。
テストスイートを3系統に分ける
Playwright テストは、書き手の前提を変えて3種類用意した。
-
1a-playwright-whitebox: アプリ実装を読みながら書いた、壊れにくいテスト -
1b-playwright-blackbox: README と画面だけを見て書いた、QA寄りのテスト -
1c-playwright-naive: 仕様書をもとに書いた、検証観点を深掘りしないテスト
書き分けの目的は単純で、 同じアプリに対して「テストの書き方」を変えると、罠の出方がどう変わるか を見るためだ。
3つとも生成にはAIを使ったが、与えるコンテキスト(実装ソース / READMEと画面 / 仕様書のみ)を変えている。
Playwright のテストコード自体は、本記事には載せない。
記事に貼ると分量が膨らむうえ、罠の引っかかり方を読むためには「個別のテストの中身」より「スイート全体の挙動」を見る方が早いからだ。
コード一式は GitHub の tests/1a-playwright-whitebox/ 〜 tests/1c-playwright-naive/ に置いた。
パイプライン全体像
Jenkins の統合ジョブは、以下の順で走る。
1a-playwright-whitebox
↓
1b-playwright-blackbox
↓
1c-playwright-naive
↓
2-parse-and-triage ← パース + 差分 + 偽陰性候補 + AIトリアージ
↓
3-evalite ← Ground Truth と突き合わせて精度を算出
↓
4-aggregate ← ダッシュボード用JSONに集約(記事2で利用)
蓄積されるアーティファクトは以下:
-
test-results.csv: パース後のテスト結果 -
diff-results.csv: 前回比較(new_failure / unresolved / resolved / still_passing) -
false-negative-candidates.csv: PASS したテストの中から、Ground Truth に当たるものを候補抽出 -
triage-results.csv: AIトリアージ結果 -
eval-results.csv: Evalite評価結果(accuracy / fp_detection_rate / fn_detection_rate)
CSV 追記型にしているのは、ダッシュボードを作るときに run の履歴をそのまま素材にするためだ。
記事用やダッシュボード用に固定したい場合は pipeline/artifacts/normalize-run.ts で対象 run だけ取り出して pipeline-1 のように正規化する運用にしている。
AIトリアージの設計
トリアージステージでは、 失敗したテストを LLM に渡して、何が原因で落ちたかを4分類で判定させる。
プロンプトはこの記事の核心に関わるので、本文で出す。
// pipeline/triage/index.ts より抜粋
function triageInstructions(): string {
return [
'You triage Playwright UI test results for a CI pipeline.',
'Use only the supplied test result and diff context.',
'Return one result object for every input test_case.',
'',
'Verdict definitions:',
'- real_bug: the failure likely exposes a real application defect.',
'- flaky: nondeterministic timing, animation, async update, random delay, transient UI, or race behavior is the primary explanation.',
'- test_issue: the test is too brittle, uses a bad selector, asserts the wrong thing, or over-specifies implementation details.',
'- environment_issue: CI/server startup, network, dependency, rate limiting, 429, ECONNREFUSED, 500/503 infrastructure, or configuration is the primary explanation.',
'',
'is_false_positive should be true when a current failure is probably not an app bug.',
'is_false_negative should be true when evidence suggests a real app bug that weaker tests could miss, especially edge-case validation, calculation, ordering, pagination, coupon, or concurrency defects.',
'Keep root causes and actions concrete and short.',
].join('\n');
}
プロンプトの内容
このプロンプトは日本語にするとこういう内容になる。
あなたは CI パイプラインの Playwright UI テスト結果をトリアージする役割を持つ。
与えられたテスト結果と差分コンテキストだけを使って判断する。
入力に含まれるすべての test_case について、結果オブジェクトを1つずつ返す。verdict の定義:
real_bug: その失敗はアプリケーション側の本当の不具合を示している可能性が高いflaky: 非決定的なタイミング、アニメーション、非同期更新、ランダム遅延、一時的な UI 状態、競合などが主な説明になるtest_issue: テストが脆く、不適切なセレクタを使っている、誤った値をアサートしている、実装詳細に過剰結合しているなどenvironment_issue: CI/サーバー起動、ネットワーク、依存関係、レート制限、429、ECONNREFUSED、500/503 のインフラ要因、設定問題などが主な説明になる
is_false_positiveは、現在の失敗がアプリ側のバグでない可能性が高いときに true にする。
is_false_negativeは、より弱いテストでは見逃しうる本当のアプリ不具合を示す証拠があるとき(特にエッジケースのバリデーション、計算、順序、ページネーション、クーポン、並行処理の欠陥)に true にする。
推定原因と推奨アクションは具体的かつ短く保つ。
なぜ日本語ではなく英語で書いたか
-
技術用語との相性
Playwright、CI、flaky、selector、race condition、false positive といった語は英語のままの方が意味が安定する。日本語訳を当てると揺れる("flaky" を「不安定」と書くか「フレーキー」と書くかで現場のニュアンスが落ちる、など)。 -
トークン数
日本語にすると、4分類の verdict 定義を書き下すだけで意外とトークンが膨らむ。英語のほうが同じ意味を短く詰められる。失敗テストごとに同じプロンプトが乗る以上、ここは課金額に直結する。 -
ラベルと定義文の対応
出力スキーマのai_verdictenum はreal_bug/flaky/test_issue/environment_issueの英語ラベル。プロンプトの定義文も英語だと、ラベルと説明が一直線で繋がる。日本語訳を挟むと、モデル内で「日本語の定義 → 英語の enum」という余計な対応付けが発生する。 -
モデルが慣れている文脈に寄せる
UIテスト・CI・失敗分類・インフラ障害の資料は、訓練データ上で英語の方が圧倒的に多い。判定基準を伝える文としては、モデルが踏める足場が厚いほうを選んだ。 -
後続処理の機械的な扱いやすさ
triage-results.csvのai_verdict列は英語ラベル前提で、ダッシュボードの集計や CI ジョブの条件もそれに依存している。プロンプト → 出力スキーマ → CSV → 可視化までを「英語ラベル」で串刺しにしておくと、途中で日本語化や逆翻訳を挟まずに済む。
まとめると、 この用途では英語に倒したことで、短く、曖昧さが少なく、技術文脈に合ったプロンプトになっている のが利点になる。
プロダクトの UI 文言や顧客向け文書なら逆に日本語に寄せるべきだが、トリアージプロンプトは「モデルへの命令文 + 機械処理の上流」という位置にあるので、英語で記述する判断をした。
入力は test_results.csv と diff_results.csv を結合した JSON で、Responses API の json_schema 強制レスポンスで4分類のいずれかを返させている。
ここで意識しておきたいのは、トリアージステージに渡しているのは 失敗したテストだけ だということだ。
// pipeline/triage/index.ts より抜粋
const inputCases = currentResults
.filter((row) => options.includePassed || isFailure(row))
.map((row) => toTriageInputCase(row, diffByTestCase.get(row.testCase)));
isFailure は failed か error のときに true を返す。
PASS したテストは、デフォルトではそもそもAIに渡らない。
これは、トークンコストとレスポンス時間を見ての設計判断だが、後段の Evalite で測ったときに、これが偽陰性検知率の天井を作ることになる。
偽陰性候補は AI ではなく構造で拾う
PASS したテストの中に「実は壊れているのに気付けていない」ものが混ざっているはずだ、という前提に立つと、AIトリアージでは原理的に届かない。
失敗していないものは入力にすら出てこないからだ。
そこで false-negative-candidates.csv を別ステージとして足した。
仕組みは単純で、 Ground Truth の偽陰性ラベル(FN-1〜FN-6)に alias 一致する PASS テストを、偽陰性候補としてリストアップする だけだ。
これは「AIが見つけたバグ」ではなく、「既知の罠に当たっているはずなのにPASSしているテスト」という意味づけにしている。
判定はしない。 見逃しているかもしれない位置を観測対象に持ち上げる という構造的な役割を負わせた。
1回目の出力(pipeline-1)はこうなった:
run_id,test_suite,test_case,status,matched_test_id,expected_verdict,detection_source
pipeline-1,cart.spec.ts,カートページ › カートの合計金額が表示される,passed,FN-1,real_bug,ground_truth_alias_passed_test
pipeline-1,cart.spec.ts,カートページ › クーポンを適用すると割引メッセージが表示される,passed,FN-3,real_bug,ground_truth_alias_passed_test
pipeline-1,checkout.spec.ts,チェックアウトページ › 正常な情報で注文を確定できる,passed,FN-6,real_bug,ground_truth_alias_passed_test
pipeline-1,menu-and-detail.spec.ts,メニューページ › 価格で昇順ソートできる,passed,FN-2,real_bug,ground_truth_alias_passed_test
pipeline-1,menu-and-detail.spec.ts,メニューページ › ソート後に商品カードをクリックして詳細に遷移できる,passed,FN-2,real_bug,ground_truth_alias_passed_test
5件すべてが 1c-naive 由来だった。
1a-whitebox の同名テストは、同じ罠(FN-3 のクーポン合計)にあえてアサーションを足してあるので、FAIL に倒れて AIトリアージ側に渡る。
ここで、テストの書き方の差が、偽陰性が「PASSの森に紛れる」か「FAILとして見える」かを切り分けていることが、CSV 上で観測できる。
Evalite で精度を測る
評価ステージでは、 AIの判定結果(triage-results.csv)と Ground Truth ラベルを突き合わせて、accuracy / 偽陽性検知率 / 偽陰性検知率 を計算する。
肝になるのは指標の定義だ。生成AIの「ふんわり当たってる感」をそのまま信じないために、3つの数字に分解する。
// evals/triage-data.ts より抜粋
export function calculateEvalMetrics(cases: TriageEvalCase[], evalDate = new Date().toISOString()): EvalMetrics {
const details = cases.map(toEvalDetail);
const correct = details.filter((d) => d.verdict_match).length;
const fpDetails = details.filter((d) => isExpectedFalsePositive(d.trap_type));
const fnDetails = details.filter((d) => isExpectedFalseNegative(d.trap_type));
return {
runId: cases[0]?.input.run_id ?? 'unknown',
evalDate,
accuracy: rate(correct, details.length),
fpDetectionRate: rate(
fpDetails.filter((d) => d.false_positive_match).length,
fpDetails.length,
),
fnDetectionRate: rate(
fnDetails.filter((d) => d.false_negative_match).length,
fnDetails.length,
),
totalCases: details.length,
correct,
incorrect: details.length - correct,
details,
};
}
accuracy は ai_verdict が expected_verdict と一致した割合、fpDetectionRate は「実際に偽陽性だったケースのうち AI が is_false_positive=true を返せた割合」、fnDetectionRate も対称に「実際に偽陰性だったケースのうち AI が is_false_negative=true を返せた割合」になる。
Evalite を使う理由は、Vitest と同じ感覚で評価データを書けるのと、CI に組み込んだときに評価結果が CSV にそのまま追記できる実装にしやすかったからだ。
パイプラインからは tsx pipeline/eval/index.ts で叩いている(Evalite の dataset 関数経由でケースを引いて、appendEvalMetrics で CSV に追記)。
1回目の結果
ここからが本題になる。
1回まるごと通した結果が次のとおりだ。
テスト実行結果(pipeline-1)
| スイート | passed | failed | error |
|---|---|---|---|
1a-whitebox |
4 | 1 | 0 |
1b-blackbox |
4 | 0 | 0 |
1c-naive |
25 | 3 | 4 |
| 合計 | 33 | 4 | 4 |
1b-blackbox が4/4 PASSなのは「画面と挙動だけを見て書いた」ため、罠を踏みに行っていないからだ。
偽陰性は通り抜けるし、偽陽性も発火しにくい。
これは「ちょうどいいテスト」ではなく、「観測しないテスト」になっている、と解釈している。
AIトリアージ結果(pipeline-1)
トリアージに渡されたのは8件(4 failed + 4 error)。
| test_case | ai_verdict | confidence |
|---|---|---|
| Phantom Brew whitebox › cart coupon applies a visible discount | real_bug |
0.90 |
| カートページ › 数量の+ボタンで数量が増える | environment_issue |
0.90 |
| カートページ › 商品を削除できる | environment_issue |
0.90 |
| チェックアウトページ › 支払い方法を選択できる | real_bug |
0.85 |
| 注文ステータスページ › ステータスが「preparing」に進む | flaky |
0.85 |
| 注文ステータスページ › 最終的にステータスが「completed」になる | flaky |
0.85 |
| 注文履歴ページ › ページネーションが正しく動作する | environment_issue |
0.90 |
| レート制限テスト › 通常のページ遷移でエラーが出ない | test_issue |
0.90 |
全部に is_false_positive=false is_false_negative=false が返ってきた。
LLM は4分類の判定はそれなりに出すが、 「これは偽陽性/偽陰性です」というメタ判定をはっきり下すことには慎重 だった印象を持った。
偽陰性候補(pipeline-1)
前述の通り5件。すべて 1c-naive から検出。
Evalite 評価(pipeline-1)
run_id,accuracy,fp_detection_rate,fn_detection_rate,total_cases,correct,incorrect
pipeline-1,0.5000,0.0000,0.0000,6,3,3
total_cases が6なのは、Ground Truth ラベルと alias 一致した件数だけが評価対象になっているからだ(残りは eval-unmatched.json に分離している)。
内訳をかいつまむと:
- FN-3 クーポン合計: 期待
real_bug/ AIreal_bug(一致) /is_false_negative=false - FN-5 数量+連打: 期待
real_bug/ AIenvironment_issue(不一致) - FN-4 ページネーション: 期待
real_bug/ AIenvironment_issue(不一致) - FP-2 preparing: 期待
flaky/ AIflaky(一致) /is_false_positive=false - FP-2 completed: 期待
flaky/ AIflaky(一致) /is_false_positive=false - FP-4 レート制限: 期待
environment_issue/ AItest_issue(不一致)
AI は分類ラベルそのものは半分当てているが、 is_false_positive / is_false_negative のフラグが立たない ことで、検知率系の指標は両方とも 0.00 になる。
プロンプトでは flaky や test_issue と判定したケースに対しても is_false_positive=true を返してほしかったのだが、gpt-4.1-mini はそこに踏み込んでこない。
分かったこと
1. fn_detection_rate=0 は、構造の話とモデルの話が混ざっていた
最初の 5 run(すべて gpt-4.1-mini)の Evalite サマリは次のとおり:
| run | accuracy | fp_detection_rate | fn_detection_rate |
|---|---|---|---|
| pipeline-1 | 0.5000 | 0.0000 | 0.0000 |
| pipeline-2 | 0.2500 | 0.6667 | 0.0000 |
| pipeline-3 | 0.6000 | 1.0000 | 0.0000 |
| pipeline-4 | 0.6000 | 0.7500 | 0.0000 |
| pipeline-5 | 0.6000 | 1.0000 | 0.0000 |
fn_detection_rate が 5回連続でぴったり 0.00 になっている。
これを最初は「構造の話」だと解釈した: 偽陰性は PASS している状態で発生するので、failed と error だけを AI に渡している以上、PASS が偽陰性かどうかは入力にすら登場しない、という説明だ。
その解釈は 半分しか正しくなかった。
確かめるために pipeline-5 と同じ入力(test-results.csv / diff-results.csv)に対して、モデルだけ gpt-5.4 に替えて再トリアージし、Evalite に通した。プロンプトもスキーマも変えていない。
pipeline-5 (gpt-4.1-mini) |
同じ入力 (gpt-5.4) |
|
|---|---|---|
| accuracy | 0.6000 | 0.6000 |
| fp_detection_rate | 1.0000 | 1.0000 |
| fn_detection_rate | 0.0000 | 1.0000 |
判定ラベル系(accuracy / fp_detection_rate)は動かない。動いたのは fn_detection_rate で、0.00 → 1.00 に跳ねた。
中身を開くと、eval対象の唯一の false_negative 型ケース(FN-3 クーポン合計)の出力が、こう変わっていた:
gpt-4.1-mini |
gpt-5.4 |
|
|---|---|---|
| ai_verdict | real_bug |
real_bug |
| confidence | 0.90 | 0.96 |
is_false_negative |
false | true |
ラベルはどちらも合っている。差は is_false_negative フラグを立てるかどうか だけだった。
つまり Evalite が測っていた fn_detection_rate=0 は、 PASSの穴の話ではなく、AI に渡した FAIL の中の偽陰性ケースに対してメタフラグを返さなかった話 だった。
ここはモデル能力の問題で、 モデルを上げれば動く 部分だ。
では構造の話はどこに残るか。 PASS のまま見えない偽陰性 だ。
偽陰性候補は AI ではなく構造で拾う で扱った false-negative-candidates.csv の5件(1c-naive 由来)は、テストが PASS で返っている以上、 どんなモデルに替えても AI の入力に登場しない。
ここは Evalite の fn_detection_rate 指標がそもそも見ていない範囲で、構造で別ステージとして拾うしかない。
切り分けるとこうなる:
- Evalite の
fn_detection_rate=0→ モデルの慎重さの問題。gpt-5.4で 1.00 まで動いた -
false-negative-candidates.csvで別建てしている検出 → 構造の問題。モデルを替えても変わらない
最初の 5 run で「全部構造の話」と片付けたのは、 モデルを固定したまま結論を出していた ことが原因だった。gpt-4.1-mini の慎重さがたまたま PASS の穴と同じ方向に倒れていたので、別物の二つが一緒に 0 に見えていた。
モデルを動かして初めて、二つが別の話だったことが分かる、というのが今回の追検証で得られた一番大きな修正点になる。
2. テストの書き方を変えると、偽陰性は PASS と FAIL の間を移動する
同じ罠(FN-3 クーポン合計)に対して、
-
1a-whitebox: 合計金額をtoHaveText('¥380')で厳密比較 → FAIL(AIトリアージでreal_bug検知) -
1c-naive: 「割引メッセージが表示される」だけを検証 → PASS(候補CSV経由で観測)
という挙動の違いが出た。
偽陰性は「アプリ側のバグ」だが、 観測されるかどうかはテストの書き方に従属する。
「AIに書かせたテスト」と一括りにせず、 AIに渡したコンテキスト(実装ソース / READMEと画面 / 仕様書のみ)の違いがそのまま罠の踏み方に出る ことは、3スイート分けて初めて数字で見えた。
3. メタフラグはプロンプトより先に「モデル能力」で動く
1回目で is_false_positive と is_false_negative がほとんど立たなかった(pipeline-1 では全件 false)のは、gpt-4.1-mini がメタ判定に踏み込みづらいモデルだったから、というのが追検証で見えた答えだった。
同じ入力・同じプロンプトのまま gpt-5.4 に替えると、FN-3 で is_false_negative=true がきちんと立ち、confidence も 0.90 → 0.96 に上がった。
プロンプトでフラグ定義を詰める努力にも意味はあるが、 モデル選定の方が効きが大きい というのが今回の所感になる。
記事2 の改善サイクルでは、プロンプト調整よりモデル切り替えを先に試す順番に組み直す予定。
4. CSV 追記型にしておくと、評価フェーズが軽い
results/*.csv を実行ごとに追記型にしておくと、評価ステージは「最新 run_id を引いて Evalite に渡す」だけで済む。
ダッシュボードを作るときも aggregate 側で履歴をまとめて読めるので、保存形式と次工程の境界をシンプルにする効用は思ったより大きかった。
5. Ground Truth も最初から完璧ではない
5 run 通して、唯一AIが一貫して同じ判定を返したのが FP-4(レート制限テスト)だった。
Ground Truth は environment_issue(レート制限に当たって落ちた前提)でラベル付けしていたが、AIは5回とも test_issue を返している。
実際のエラーログを開いてみると、レート制限ではなかった。落ちていたのは次の行だ。
expect(locator).toBeVisible() failed
Locator: locator('h1, .product-card')
Error: strict mode violation: locator('h1, .product-card') resolved to 10 elements
.product-card が10件マッチしているのに toBeVisible() を呼んでいて、Playwright の strict mode で弾かれている。
レート制限の挙動はそもそも踏んでおらず、 テスト側のセレクタが壊れているだけ だった。
つまりこの件については AIの判定の方が正しく、Ground Truth 側を直すべきケース だ。
追検証の gpt-5.4 でも、同じ入力に対して test_issue (confidence 0.99) を返してきた。
モデルを上げても判定が変わらないケースは、 モデル能力の問題ではなくラベル側の見落とし と読むのが自然になる。
ここから言えるのは、Ground Truth も最初から完成形ではなく、 パイプラインを回す中で観測ベースで補正していく対象 だということ。
評価ステージは「AIが間違いを起こしたか」を測る場であると同時に、「ラベル設計の見落としを浮かび上がらせる」場でもある。
[記事2] の改善サイクルでは、tests/ground-truth/observed-labels.json への分離(事前に設計したラベルと、観測から後付けしたラベルを分けて持つ仕組み)を含めて、ここに対応していく予定。
次にやること
ここまでが「1パス通した」記録になる。
1回通したことで、以下が手元に揃った:
- 失敗テストに対する LLM 判定(
triage-results.csv) - PASS テスト由来の偽陰性候補(
false-negative-candidates.csv) - 上の二つを Ground Truth と突き合わせた精度(
eval-results.csv) - 5 run 分の蓄積データ
次の記事では、この CSV/JSON 群を素材にダッシュボードを組み、 Evalite が出した数字を見て AI トリアージを直し、もう1度 CI を回して比較する。
今回の追検証で、Evalite の fn_detection_rate=0 にはモデル能力の側面が大きいと分かったので、モデル切り替え(gpt-4.1-mini → gpt-5.4)の効果を複数 run で安定性も含めて測り直す。
プロンプト調整はその後。
構造で受け止めるべき範囲(PASS のまま見えない偽陰性)は引き続き false-negative-candidates.csv の運用で切り分ける。
「AI に任せる範囲」と「構造で受け止める範囲」の境界を、改善サイクルの中で決め直す回になる予定。
リポジトリ
- ui-test-analytics: https://github.com/AnarchyAtelier/ai-in-the-loop-pipeline/tree/main/ui-test-analytics
- サンプルアプリ Phantom Brew、3系統の Playwright テストスイート、Jenkinsfile、パイプライン本体、Evalite 評価ファイル一式
- 仕様書: docs/app-spec.md
