Skip to content

Commit 45cad6b

Browse files
authored
feat(test-classifier): record Testing Events row by default when webhook is set (#78)
Previously the helpfulness prompt + Testing Events row required the explicit --submit flag, so a plain local run never wrote to the sheet. Make it the default: on any interactive run (TTY, not CI) where METRICSAI_WEBHOOK_URL and METRICSAI_WEBHOOK_KEY are both set and tests were CLASSIFIED, prompt y/n and write the row automatically. No flag needed. - Decouple the prompt+write from --post-comment: a report-only run with no PR still records a row (empty comment fields). --submit stays as an explicit opt-in and still implies --post-comment for back-compat. - Surface write failures loudly but non-fatally: capture the webhook response body + HTTP code and print both on failure (e.g. no access to the sheet), but still exit 0 — the classification succeeded; only the sheet write missed. - Genericize the --submit-prefixed log lines to metrics: (they fire on the auto path now too). Update the dispatcher help + LOCAL_TEST_CLASSIFIER.md.
1 parent 0cf7e13 commit 45cad6b

2 files changed

Lines changed: 132 additions & 61 deletions

File tree

testing/classifier/.skills/test-classifier/scripts/test-classifier-dispatcher.sh

Lines changed: 90 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@
2828
# Posting requires --post-comment; without it the report/JSON only prints (useful
2929
# for a local dry view). Nothing is posted when nothing was triaged. Two tuning-
3030
# signal surfaces, both supported: the posted comment's 👍/👎 reaction (read by the
31-
# metricsai weekly harvest) on CI/--post-comment runs, and --submit's terminal
32-
# prompt (written straight to the Testing Events sheet) on local runs.
31+
# metricsai weekly harvest) on CI/--post-comment runs, and a terminal "helpful?"
32+
# prompt written straight to the Testing Events sheet on local runs. That prompt
33+
# fires BY DEFAULT on any interactive run when METRICSAI_WEBHOOK_URL/_KEY are set
34+
# — no --submit needed; --submit just forces it explicitly. A failed sheet write
35+
# is surfaced loudly but is non-fatal (the run still exits 0).
3336
#
3437
# Usage:
3538
# test-classifier-dispatcher.sh # no args → --unpushed (local committed+staged, report-only)
3639
# test-classifier-dispatcher.sh origin/main # bare ref → --against origin/main
3740
# test-classifier-dispatcher.sh --pr 1234 --post-comment # post the PR comment
38-
# test-classifier-dispatcher.sh --pr 1234 --submit # post + prompt "helpful?" + append a Testing Events row
41+
# test-classifier-dispatcher.sh --pr 1234 --submit # force the "helpful?" prompt + row (auto when webhook env is set)
3942
# test-classifier-dispatcher.sh --against origin/main # explicit base ref
4043
# test-classifier-dispatcher.sh --unpushed # local: committed+staged, NO PR (report-only)
4144
# test-classifier-dispatcher.sh --json-only # emit only the JSON block
@@ -91,10 +94,12 @@ SKILL_PATH_CANONICAL="${SKILLS_ROOT}/test-classifier/SKILL.md"
9194
#
9295
# --pr <number> Explicit PR number (overrides auto-discovery)
9396
# --post-comment Post ONE PR comment via gh api (omit to print only)
94-
# --submit Implies --post-comment; then (interactive only) prompts
95-
# "Was this helpful?" and appends one Testing Events row
96-
# to the Sheet with the verdict + 👍/👎. Non-TTY/CI: posts
97-
# and skips the prompt + row.
97+
# --submit Explicitly force the "Was this helpful?" prompt + the
98+
# Testing Events row, and (for back-compat) implies
99+
# --post-comment. NOTE: the prompt + row already happen
100+
# BY DEFAULT on any interactive run when the webhook env
101+
# vars are set — so --submit is only needed to also post
102+
# the comment, or to be explicit. Non-TTY/CI: skipped.
98103
# --gate Exit 1 if the result is CLASSIFIED (CI-blocking mode)
99104
# --json-only Print only the JSON block (machine consumption)
100105
# --no-run-suite Read-only INFERRED pass: do NOT run the repo's suite
@@ -503,10 +508,12 @@ if (( WANT_HELP == 1 )); then
503508
test-classifier options (in addition to the shared options above):
504509
--pr <number> Explicit PR number for posting (overrides auto-discovery).
505510
--post-comment Post ONE PR comment with the verdicts + a 👍/👎 reaction
506-
ask (omitted when --submit captures the signal locally).
507-
Omit for a local report-only run (prints, posts nothing).
508-
--submit Implies --post-comment; then (interactive only) prompts
509-
"Was this helpful?" and appends one Testing Events row.
511+
ask (omitted when the local "helpful?" prompt captures the
512+
signal). Omit for a report-only run (prints, posts nothing).
513+
--submit Force the "Was this helpful?" prompt + Testing Events row,
514+
and (back-compat) imply --post-comment. NOT usually needed:
515+
the prompt + row already fire by default on an interactive
516+
run when the webhook env vars are set (see below).
510517
--gate Exit 1 when the result is CLASSIFIED (CI-blocking mode).
511518
--json-only Print only the machine-readable JSON block.
512519
--no-run-suite Read-only INFERRED pass: predict from the diff instead of
@@ -522,7 +529,9 @@ Environment:
522529
OBSERVED is the default; set AI_RUN_SUITE=0 (or pass
523530
--no-run-suite) for a read-only INFERRED pass.
524531
METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY
525-
Required only by --submit to append the Testing Events row.
532+
When BOTH are set, an interactive run prompts "helpful?"
533+
and writes a Testing Events row automatically (no --submit
534+
needed). A failed write is surfaced but non-fatal.
526535
HELP_ADDENDUM
527536
ai_review::print_help
528537
exit 0
@@ -821,14 +830,19 @@ post_comment_to_github() {
821830
echo "[test-classifier] Comment posted (issue comment)."
822831
}
823832

824-
# ── --submit: prompt "Was this helpful?" and post one row via the webhook ──
833+
# ── Prompt "Was this helpful?" and post one Testing Events row via the webhook ─
825834
# Streamlines the manual local loop: instead of (post comment → react 👍/👎 on
826835
# GitHub → a separate weekly harvest), capture the developer's signal right after
827836
# the run and POST it to the metricsai Google Apps Script webhook — the SAME
828837
# transport metricsai uses (flat JSON body, fields aligned by header name, plus
829838
# reserved `_tab` and `_key`). No service account, no gcloud, no token expiry —
830839
# just two static env vars the developer sets once.
831840
#
841+
# This runs by DEFAULT on any interactive run when the webhook env vars are set
842+
# (see the auto_submit logic in test_classifier::run) — the developer no longer
843+
# needs to pass --submit. The explicit --submit flag remains as an opt-in and is
844+
# a no-op when auto-submit already applies.
845+
#
832846
# Fields (aligned by header name on the Apps Script side):
833847
# repo, pr, comment_id, comment_created_at, verdict, category, confidence,
834848
# thumbs_up, thumbs_down, reason (+ _tab="Testing Events", _key=<api key>)
@@ -838,10 +852,11 @@ post_comment_to_github() {
838852
# METRICSAI_WEBHOOK_KEY the "AI Metrics" API key (body _key) (required)
839853
# METRICSAI_WEBHOOK_TAB destination tab; defaults to Testing Events
840854
#
841-
# Gated: only runs interactively (TTY, not CI) — non-interactive runs post the
842-
# comment and skip the prompt + POST (no human signal to record, no hang).
843-
# Missing URL/key → records the answer to the terminal, warns, still posts the
844-
# comment.
855+
# Gated: only runs interactively (TTY, not CI) — non-interactive runs skip the
856+
# prompt + POST (no human signal to record, no hang). Missing URL/key → captures
857+
# the answer in the terminal and warns, but writes no row. A failed POST is
858+
# surfaced loudly (HTTP code + response body) but is NON-FATAL: the run exits 0
859+
# because the classification itself succeeded.
845860
#
846861
# Args: PR number, the extracted classifier JSON.
847862
submit_metrics_row() {
@@ -857,11 +872,11 @@ submit_metrics_row() {
857872
# must gate on the SAME thing: can we open /dev/tty? In CI / a real non-TTY run
858873
# there is no controlling terminal, so this still skips correctly.
859874
if [[ "${CI:-}" == "true" ]] || ! { : <>/dev/tty; } 2>/dev/null; then
860-
ai_review::info "--submit: non-interactive run (no controlling terminal) — comment posted; skipping the helpfulness prompt + metrics row." >&2
875+
ai_review::info "metrics: non-interactive run (no controlling terminal) — skipping the helpfulness prompt + Testing Events row." >&2
861876
return 0
862877
fi
863878
if ! command -v python3 &>/dev/null; then
864-
ai_review::warn "--submit: python3 not found; cannot build the metrics row — skipping it." >&2
879+
ai_review::warn "metrics: python3 not found; cannot build the Testing Events row — skipping it." >&2
865880
return 0
866881
fi
867882

@@ -886,7 +901,7 @@ submit_metrics_row() {
886901
;;
887902
*)
888903
# Empty Enter or anything unrecognized = skip (don't guess a verdict).
889-
ai_review::info "--submit: skipped (no y/n answer) — comment still posted." >&2
904+
ai_review::info "metrics: skipped (no y/n answer) — nothing recorded to the sheet." >&2
890905
return 0
891906
;;
892907
esac
@@ -895,7 +910,7 @@ submit_metrics_row() {
895910
local webhook_key="${METRICSAI_WEBHOOK_KEY:-}"
896911
local webhook_tab="${METRICSAI_WEBHOOK_TAB:-Testing Events}"
897912
if [[ -z "${webhook_url}" || -z "${webhook_key}" ]]; then
898-
ai_review::warn "--submit: METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY not set — recorded your answer but didn't post a row." >&2
913+
ai_review::warn "metrics: METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY not set — captured your answer but didn't post a row." >&2
899914
ai_review::log " Export both (the metricsai webhook URL + the AI Metrics API key) to enable the sink." >&2
900915
return 0
901916
fi
@@ -942,14 +957,28 @@ print(json.dumps(dict(zip(keys, sys.argv[1:1+len(keys)]))))
942957
# it can 405 on the final Drive hop even though the row landed, so we must NOT
943958
# use `-f -L` (that reports a false failure). Capture the first-hop status and
944959
# treat 200/302 as success; don't follow the redirect.
945-
local http_code
946-
http_code="$(curl -sS -o /dev/null -w '%{http_code}' -X POST "${webhook_url}" \
960+
# Capture BOTH the response body and the first-hop status so a real failure
961+
# (e.g. the Apps Script denying access to the sheet, an auth error, a 4xx/5xx)
962+
# is surfaced verbatim rather than reduced to a bare code. Body and status are
963+
# split on a trailing sentinel line we ask curl to append via -w.
964+
local resp http_code
965+
resp="$(curl -sS -X POST "${webhook_url}" \
947966
-H "Content-Type: application/json" \
948-
-d "${body}" 2>/dev/null || echo "000")"
967+
-d "${body}" -w $'\n__HTTP_CODE__:%{http_code}' 2>&1 || true)"
968+
http_code="$(printf '%s' "${resp}" | sed -n 's/.*__HTTP_CODE__:\([0-9]*\).*/\1/p' | tail -1)"
969+
[[ -n "${http_code}" ]] || http_code="000"
970+
local resp_body
971+
resp_body="$(printf '%s' "${resp}" | sed 's/__HTTP_CODE__:[0-9]*$//' | sed '/^$/d')"
949972
if [[ "${http_code}" == "200" || "${http_code}" == "302" ]]; then
950-
ai_review::ok "--submit: posted a row to the metrics webhook (tab=${webhook_tab}, verdict=${verdict:-?}, $([ "${thumbs_up}" = 1 ] && echo 👍 || echo 👎))."
973+
ai_review::ok "Recorded a Testing Events row (tab=${webhook_tab}, verdict=${verdict:-?}, $([ "${thumbs_up}" = 1 ] && echo 👍 || echo 👎))."
951974
else
952-
ai_review::warn "--submit: webhook POST failed (HTTP ${http_code}; your answer was not recorded). Check METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY." >&2
975+
# Loud, but non-fatal: the classification itself succeeded, so we still
976+
# exit 0. Surface the HTTP code AND the response body so the user can see
977+
# WHY it failed (e.g. no access to the sheet) — don't swallow it.
978+
ai_review::warn "Testing Events row was NOT recorded — webhook POST failed (HTTP ${http_code})." >&2
979+
ai_review::warn " Your y/n answer was captured locally but did not reach the sheet." >&2
980+
[[ -n "${resp_body}" ]] && ai_review::warn " Webhook response: ${resp_body}" >&2
981+
ai_review::warn " Check METRICSAI_WEBHOOK_URL / METRICSAI_WEBHOOK_KEY and that you have access to the target sheet." >&2
953982
fi
954983
}
955984

@@ -1047,11 +1076,30 @@ test_classifier::run() {
10471076
;;
10481077
esac
10491078

1079+
# ── Decide whether to capture the helpfulness signal (the Testing Events row).
1080+
# Historically this required the explicit --submit flag. Now it is the DEFAULT
1081+
# on any interactive run when the metricsai webhook is configured: if both
1082+
# METRICSAI_WEBHOOK_URL and METRICSAI_WEBHOOK_KEY are set AND we have a
1083+
# controlling terminal AND tests were actually classified, we prompt y/n and
1084+
# write the row — no --submit needed. --submit stays as an explicit opt-in
1085+
# (and is a harmless no-op when auto-submit already applies). CI / non-TTY runs
1086+
# never prompt (submit_metrics_row itself guards on /dev/tty). A failed write
1087+
# is surfaced loudly but never fails the run — the classification still stands.
1088+
local auto_submit=0
1089+
if [[ -n "${METRICSAI_WEBHOOK_URL:-}" && -n "${METRICSAI_WEBHOOK_KEY:-}" ]] \
1090+
&& [[ "${CI:-}" != "true" ]] && { : <>/dev/tty; } 2>/dev/null \
1091+
&& [[ "${result}" == "CLASSIFIED" ]]; then
1092+
auto_submit=1
1093+
fi
1094+
local do_submit=0
1095+
(( SUBMIT == 1 || auto_submit == 1 )) && do_submit=1
1096+
10501097
# ── Post ONE PR comment with the verdicts. ────────────────────────────────
10511098
# --post-comment is what CI passes to actually post; omit it for a local dry
10521099
# view (the JSON/report still prints to stdout). Nothing is posted when nothing
10531100
# was triaged (NO_ACTION). The comment carries the 👍/👎 reaction ask on a
1054-
# CI/--post-comment run; a local --submit run omits it (signal via the prompt).
1101+
# CI/--post-comment run; when we capture the signal via the terminal prompt
1102+
# (--submit or auto-submit) the ask is omitted (the prompt is the signal).
10551103
if (( POST_COMMENT == 1 )); then
10561104
if [[ "${result}" == "NO_ACTION" ]]; then
10571105
ai_review::info "Result is NO_ACTION — nothing to triage, so no PR comment is posted."
@@ -1069,20 +1117,26 @@ test_classifier::run() {
10691117
ai_review::log " <!-- AI_CLASSIFIER_JSON_END -->"
10701118
exit 1
10711119
fi
1072-
# Include the 👍/👎 reaction ask UNLESS this is a local --submit run (which
1073-
# already captures the signal via its terminal prompt). CI / plain
1074-
# --post-comment → ask (1); --submit → no ask (0). Both surfaces stay alive.
1120+
# Include the 👍/👎 reaction ask UNLESS we're capturing the signal via the
1121+
# terminal prompt (--submit or auto-submit). CI / plain --post-comment →
1122+
# ask (1); prompt-capturing run → no ask (0). Both surfaces stay alive.
10751123
local want_reaction_ask=1
1076-
(( SUBMIT == 1 )) && want_reaction_ask=0
1124+
(( do_submit == 1 )) && want_reaction_ask=0
10771125
local comment_body
10781126
comment_body="$(render_pr_comment_body "${json_block}" "${want_reaction_ask}")"
10791127
post_comment_to_github "${AI_REVIEW_PR_NUMBER}" "${comment_body}"
1128+
fi
1129+
fi
10801130

1081-
# --submit: prompt for the helpfulness signal and append a Testing Events
1082-
# row (interactive only; no-ops cleanly in CI / non-TTY).
1083-
if (( SUBMIT == 1 )); then
1084-
submit_metrics_row "${AI_REVIEW_PR_NUMBER}" "${json_block}"
1085-
fi
1131+
# ── Capture the helpfulness signal + write the Testing Events row. ─────────
1132+
# Runs on --submit OR auto-submit, INDEPENDENT of whether a comment was posted:
1133+
# a local report-only run with no PR still prompts and writes a row (with empty
1134+
# comment fields). Guarded internally on TTY + CLASSIFIED; no-ops in CI.
1135+
if (( do_submit == 1 )) && [[ "${result}" == "CLASSIFIED" ]]; then
1136+
local json_block_submit
1137+
json_block_submit="$(extract_classifier_json "${classifier_output}")"
1138+
if [[ -n "${json_block_submit}" ]]; then
1139+
submit_metrics_row "${AI_REVIEW_PR_NUMBER:-}" "${json_block_submit}"
10861140
fi
10871141
fi
10881142

0 commit comments

Comments
 (0)