Skip to content

Commit 6b9c91f

Browse files
authored
test-assistants.sh: treat a NONE sentinel run into prose as no blocker (#37)
An assistant that writes the BLOCKER line as the NONE sentinel glued straight into an explanatory sentence with no separator (Cursor was observed writing 'NONEThe background action_search.py run was stopped after it hung') meant NONE and merely broke the one-line contract. classify() checked the whole value against is_none, so the run-on read as a real blocker and the run false-FAILed even though STATUS was WORKING and its scripts ran. Normalize a blocker that leads with the NONE sentinel glued to a capital letter (the '^NONE[A-Za-z]' signature) to NONE, but only when the model did not self-report BLOCKED. A genuine 'None of the actions could be discovered' keeps its space and is left intact, so real blockers still fail. Two parser tests cover both directions.
1 parent 6aa6eb9 commit 6b9c91f

2 files changed

Lines changed: 82 additions & 7 deletions

File tree

test-assistants.sh

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,11 @@ blocker_category() {
525525
# that ignored the slug suffix authored the same workflow name and churned the
526526
# tenant. Categorised separately so it cannot be read as an assistant problem.
527527
grep -qiE 'name already exists|already in use|duplicate (workflow|definition)' <<< "$t" && { echo dupname; return; }
528+
# A server-side 5xx from the import/release API (Internal Server Error, trace-id
529+
# for support). The workflow validated locally; the tenant API failed the import
530+
# itself. Categorised separately from a skills fault so a run of API 500s on
531+
# complex workflows (a known platform behaviour) is legible and trackable.
532+
grep -qiE 'internal server error|HTTP 50[0-9]\b|\b50[0-9] (internal server|bad gateway|service unavailable)|import failed.*(internal server|50[0-9])' <<< "$t" && { echo api500; return; }
528533
echo other
529534
}
530535

@@ -541,20 +546,30 @@ blocker_category() {
541546
# there doing nothing", and it read a clean timeout as success.
542547
classify() {
543548
local log="$1" rc="$2" body status skills raw_cmds raw_blocker cmds detail cat
544-
# Claude streams stream-json, so its report arrives inside an escaped JSON string.
545-
# Expanding \n puts the labels and the blockquoted skill text back at line start,
546-
# where the patterns below expect them. The second sed drops the JSON tail that
547-
# follows the closing quote, which would otherwise be read as part of BLOCKER. No-ops
548-
# on plain-text logs.
549-
body=$(sed -e 's/\\n/\
550-
/g' -e 's/"[]}].*$//' "$log" 2>/dev/null | grep -v '^[[:space:]]*>')
549+
# Claude and Cursor stream stream-json, so the report arrives inside an escaped
550+
# JSON string. The first sed expands \n to restore line structure. The second, run
551+
# as a separate process so it sees the already-split lines individually, drops the
552+
# JSON structure that trails the closing quote: `"}]}` from a content array or `","`
553+
# from a result-level string (Cursor's final `result` event closes the report with
554+
# `","session_id":...`). A single sed with two -e expressions would apply the trim to
555+
# the original long line before the split, matching the pervasive `","` in JSON prose
556+
# and killing the whole report. No-ops on plain-text logs.
557+
body=$(sed 's/\\n/\
558+
/g' "$log" 2>/dev/null | sed 's/"[]}),].*$//' | grep -v '^[[:space:]]*>')
551559

552560
# An account-level block — quota or subscription exhausted — is not a skills or
553561
# harness fault and cannot be fixed by re-running, so treat it as an environment SKIP
554562
# (like a missing CLI), not a failure. Anchored on assistant billing phrasing so it
555563
# cannot match a skill doc's own "rate limit" guidance.
556564
grep -qiE "quota reached|quota exceeded|upgrade your subscription|subscription (required|expired|to increase)|insufficient (credits|quota)|out of (credits|quota)" <<< "$body" && { echo "SKIP|account|account quota/subscription limit reached||"; return; }
557565

566+
# A transient backend error — the assistant's own model service is momentarily
567+
# busy ("Our servers are experiencing high traffic right now, please try again in
568+
# a minute"). Not a skills or harness fault and it clears on a retry, so treat it
569+
# as an environment SKIP like a quota block, not a failure. Anchored on
570+
# backend-busy phrasing so it cannot match a skill doc's own throttling guidance.
571+
grep -qiE "experiencing high traffic|our servers are (experiencing|busy|overloaded)|temporarily (unavailable|overloaded)|(server|service) is (busy|overloaded)|please try again in a (minute|moment|few)|overloaded_error" <<< "$body" && { echo "SKIP|transient|assistant backend busy — retryable||"; return; }
572+
558573
# A Python traceback for a missing dependency is decisive: the venv was never built
559574
# (the SessionStart hook is Claude-only) or the script was run outside python.sh.
560575
grep -qiE "ModuleNotFoundError|No module named '(falconpy|yaml|tomli)'" <<< "$body" && { echo "FAIL|deps|missing Python dependency (venv not built?)||"; return; }
@@ -598,6 +613,15 @@ classify() {
598613
grep -qi 'BLOCK' <<< "$status" && status=WORKING
599614
fi
600615

616+
# A model sometimes writes the NONE sentinel straight into an explanatory
617+
# sentence with no separator ("NONEThe background job was stopped") — it meant
618+
# NONE and merely broke the one-line contract. The glued capital letter is the
619+
# signature; a genuine "None of the actions could be discovered" keeps its space
620+
# and is left intact. Only when the model did not self-report BLOCKED.
621+
if [[ "$raw_blocker" =~ ^[Nn][Oo][Nn][Ee][A-Za-z] ]] && ! grep -qi 'BLOCK' <<< "$status"; then
622+
raw_blocker=NONE
623+
fi
624+
601625
# A real blocker is the result, whatever else the assistant managed to do.
602626
if grep -qi 'BLOCK' <<< "$status" || ! is_none "$raw_blocker"; then
603627
cat=$(blocker_category "$raw_blocker")
@@ -766,6 +790,11 @@ report_one() { # name bin source rc elapsed
766790
local ebody; ebody=$(grep -v '^[[:space:]]*>' "$log" 2>/dev/null)
767791
rwf=$(clean "$(report_field WORKFLOW <<< "$ebody")" 60)
768792
rdef=$(clean "$(report_field DEFINITION <<< "$ebody")" 60)
793+
# Persist the claims so a standalone `--judge` — a separate process, where the
794+
# in-memory RESULTS is gone — can still match by the authoritative definition id
795+
# instead of falling back to the workflow name.
796+
mkdir -p "$LOG_DIR/e2e/$bin"
797+
printf '%s\t%s\n' "$rwf" "$rdef" > "$LOG_DIR/e2e/$bin/claim.tsv"
769798
fi
770799
RESULTS+=("$name|$status|$category|$detail|$elapsed|$source|$rskills|$rwf|$rdef")
771800
# An environment SKIP (account/quota) is "could not test", like a missing CLI — it is
@@ -893,6 +922,9 @@ judge_one() { # name bin claimed_workflow claimed_definition
893922
JUDGED+=("$name|NOYAML|||"); return
894923
fi
895924
app_name=$(sed -n 's/^name:[[:space:]]*//p' "$wf" | head -1)
925+
# Strip surrounding quotes so a quoted `name: '...'` still matches the tenant's
926+
# plain name (the tenant stores the unquoted value).
927+
app_name="${app_name#[\"\']}"; app_name="${app_name%[\"\']}"
896928
[ -n "$FOUND_OUTSIDE" ] && notes="authored outside its working directory: ${wf/#$HOME/\~}"
897929

898930
# Pipeline-stage markers, read from the authored YAML.
@@ -947,6 +979,12 @@ run_judge() {
947979
[ "$rn" = "$name" ] && break
948980
rwf=""; rdef=""
949981
done
982+
# Standalone --judge runs in a separate process from --e2e, so RESULTS is empty
983+
# and the loop above found nothing. Reload the claims --e2e persisted to disk so
984+
# the match can key on the authoritative definition id, not just the name.
985+
if [ -z "$rdef" ] && [ -r "$LOG_DIR/e2e/$bin/claim.tsv" ]; then
986+
IFS=$'\t' read -r rwf rdef < "$LOG_DIR/e2e/$bin/claim.tsv"
987+
fi
950988
judge_one "$name" "$bin" "$rwf" "$rdef"
951989
done
952990
}

test-verdict-parser.sh

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ BLOCKER: NONE"
111111
expect_verdict "account quota exhaustion is an environment skip" 1 "SKIP|account" \
112112
"Error: Individual quota reached. Please upgrade your subscription to increase your limits. Resets in 75h."
113113

114+
# A transient "servers are busy, try again" backend error is retryable and not a
115+
# skills fault — an environment SKIP, not a failure.
116+
expect_verdict "transient backend busy is an environment skip" 1 "SKIP|transient" \
117+
"Error: Our servers are experiencing high traffic right now, please try again in a minute."
118+
114119
expect_verdict "self-reported blocker wins even with a script OK" 0 "FAIL|auth" \
115120
"FUSION-REPORT
116121
STATUS: WORKING
@@ -125,6 +130,24 @@ SKILLS: skills/authoring/SKILL.md
125130
COMMANDS: action_search.py => OK
126131
BLOCKER: ran out of time on the 60-second harness limit"
127132

133+
# A model that writes the NONE sentinel glued straight into an explanatory
134+
# sentence ("NONEThe background job was stopped") meant NONE and merely broke the
135+
# one-line contract. The glued capital is the signature; a real "None of the
136+
# actions could be discovered" (with its space) stays a blocker.
137+
expect_verdict "NONE run straight into prose is not a blocker" 0 "PASS|ok" \
138+
"FUSION-REPORT
139+
STATUS: WORKING
140+
SKILLS: skills/authoring/SKILL.md
141+
COMMANDS: action_search.py => OK, action_search.py --search VirusTotal => FAIL: hung
142+
BLOCKER: NONEThe background action_search.py --search VirusTotal run was stopped after it hung"
143+
144+
expect_verdict "a real None-prefixed blocker with a space still fails" 0 "FAIL|other" \
145+
"FUSION-REPORT
146+
STATUS: WORKING
147+
SKILLS: skills/authoring/SKILL.md
148+
COMMANDS: action_search.py => OK
149+
BLOCKER: None of the actions could be discovered from the tenant"
150+
128151
expect_verdict "no report and a timeout rc is stalled" 124 "FAIL|stalled" \
129152
"I started reading the authoring skill and then"
130153

@@ -173,6 +196,19 @@ SKILLS: skills/deployment/SKILL.md
173196
COMMANDS: import_workflows.py => OK
174197
BLOCKER: NONE"
175198

199+
# Cursor's stream-json emits a final `result` event whose report string closes with
200+
# `","session_id":...` (quote-comma) rather than `"}`/`"]`, so the body sed's tail
201+
# stripper misses it and the trailing JSON is glued onto the last report field. The
202+
# per-field strip in report_field must cut it, or a clean deploy reads as a blocker.
203+
expect_verdict "e2e report with a glued json result-event tail still deploys" 0 "PASS|deployed" \
204+
'FUSION-REPORT
205+
STATUS: DONE
206+
WORKFLOW: NG-SIEM Parallel Threat Intel Enrichment-agent
207+
DEFINITION: ee7a24ee80d7487abbd6185275123b29
208+
SKILLS: skills/deployment/SKILL.md
209+
COMMANDS: import_workflows.py => OK
210+
BLOCKER: NONE","session_id":"416af865-a95b-4451-9263-85e03611ef70","usage":{"outputTokens":10540}}'
211+
176212
E2E=0
177213

178214
echo "== report_field / blocker_category / is_none =="
@@ -188,6 +224,7 @@ blocker_category "No module named 'yaml'" | grep -qx deps; check "blocker_catego
188224
blocker_category "403 Forbidden from the API" | grep -qx auth; check "blocker_category: auth" $?
189225
blocker_category "CLAUDE_PLUGIN_ROOT was empty" | grep -qx root; check "blocker_category: root" $?
190226
blocker_category "workflow name already exists" | grep -qx dupname; check "blocker_category: dupname" $?
227+
blocker_category "import_workflows.py Internal Server Error: Please provide trace-id='abc' to support" | grep -qx api500; check "blocker_category: api500" $?
191228

192229
is_none "NONE"; check "is_none treats NONE as nothing" $?
193230
is_none ""; check "is_none treats empty as nothing" $?

0 commit comments

Comments
 (0)