My test report printed "0/96, 0% pass rate". The truth was my account was out of credit A developer discovered that their test report showing a 0% pass rate was actually caused by an empty API credit balance, not broken code. The engineer's reconciliation script and behavioral tests revealed that all 96 test cases failed due to insufficient Anthropic API credits, highlighting how infrastructure failures can be misreported as test failures. The developer also found that a previously believed missing question bank existed all along, underscoring the importance of chasing state changes in audit reports. Open any script you have that prints a score — an eval, a CI check, an audit tool, a health check — and ask it one question: If the infrastructure fails out of credit, expired key, rate limited, no network , what does this script print? If the answer is a score, a ratio, or a pass count , you own a false-red-light generator. Not "might go wrong." Structurally guaranteed — because it put "couldn't measure" and "didn't pass" in the same box. It took me a full day to see this, and I had to see it twice, on two different scripts. I develop with Claude Code and had accumulated 89 custom skills think "specialised tools for the AI" — each has a name and a self-description . Those descriptions get packed into the model's opening memory, and that memory has a character limit . Go over it and descriptions get dropped, names kept — and that tool never surfaces on its own again. I squeezed 24 of those descriptions shorter, saving 2,754 characters, retiring nothing. The full write-up of that, with the A/B test: I cut 41 AI tools' self-descriptions in half https://dev.to/content/slimming-skill-descriptions-ab-test-en . The scary part of trimming is cutting a trigger word : that description is the basis on which the model decides whether to invoke the skill, so cutting the wrong phrase makes it silently stop firing, with no error. So I wrote a reconciliation script that lists every token present in the old version and absent in the new, forcing me to judge them one by one. Eight had genuinely lost trigger words. All restored. Then I wanted harder evidence: behavioral tests . I already had a question bank — 4 cases per skill should-trigger, strict, should-not-trigger, boundary , 96 total. I had believed that bank didn't exist. I'd even written in a handoff doc: "most of these 24 have no test cases, so 'will it still trigger?' can only be answered by token reconciliation." That sentence was wrong. All 24 had cases. What caught the wrong sentence wasn't me re-reading it. It was running a full audit before pushing, and noticing that one check had gone from "pass" to "blind", and had taken 100 seconds . It had been triggered into actually running by my 24 changed skills. I chased that state change, and found the question bank had been there all along. A state change in an audit item is itself a signal — often more informative than its green light. pass→blind, fast→slow, warning count 9→10 — every one of those deserves a "why did that change?" If I hadn't chased it, I'd have kept a false sentence "there's no bank to test against" while the bank sat right there. So I ran the 96 tests. ┌─────────────────────────────────────────────────────────┐ │ Skill Eval Report advisory │ ├─────────────────────────────────────────────┬───────────┤ │ ❌ action-gating-surface-disclosure │ 0/4 0% │ │ ❌ architecture-completeness-guardian │ 0/4 0% │ │ ❌ audit-cross-repo │ 0/4 0% │ ⋮ all 24 like this ├─────────────────────────────────────────────┼───────────┤ │ TOTAL │ 0/96 0% │ └─────────────────────────────────────────────┴───────────┘ ⚠️ The following skills scored < 50% — consider reviewing: all 24 listed If I only read that table, the conclusion is unambiguous: I just broke all 24 tools and should roll back immediately. Scroll down a few hundred lines and every single case's raw output is the same sentence: ERROR: Anthropic API error 400: {"type":"error","error":{"type":"invalid request error", "message":"Your credit balance is too low to access the Anthropic API..."}} Zero successful API calls. Total spend: US$0.0000. An empty wallet, rendered as "none of these 24 tools trigger any more." The root cause is one line. The per-case runner looked like this: js try { const r = await evalCase c, systemPrompt, skillName runs.push r } catch err { runs.push { pass: false, raw: ERROR: ${err.message} } // ← here } Any exception is recorded as pass: false . Network down, expired key, empty account, a bug in my own code — all of it becomes "this skill did not pass." And the harder half to notice: that pass=0/96 line was already printing in the round before my changes. The false red light had been there a long time. Nobody read it — it's advisory doesn't block the push , so every audit round printed one line and every round skipped past it. My system already had a convention: a check result is three states, not two — Blind means " this round's green light doesn't count ", not "there's a problem." I'd written that convention into six sentinel scripts. This eval didn't implement it. Three changes: ✗ passed 0/3 detail doesn't print for blind cases either — that display Both directions have to be blocked; blocking one just swaps a false red for a false green. I added reverse tests: "the model answered but didn't trigger" and "my own JSON parsing broke" must not be classified as blind — otherwise a real failure gets laundered into "we didn't measure it," which is worse than a false red. Eleven self-tests pass, three of them "prove it starts red" using the day's real error text as fixtures, not strings I made up — strings I make up get contaminated by my own imagination . End to end, verified against the day's real failure condition: before the fix, 0/96 0% plus 24 ❌; after, a blind banner and the actual reason — and those two lines of fake numbers vanished from the audit output. After topping up the account, I ran it again. This time it worked: │ TOTAL │ 90/96 94% │ All 24 passed six at 3/4, the rest 4/4 . That is the evidence I wanted. Then the same script printed, below the table: ⚠️ Blind: the eval runner produced output but no n/m results could be parsed — not counted as a pass. It printed a beautiful table, and then said it couldn't read the results. Here's why. The outer script — the one that decides pass/fail — called the inner scorer with execFileSync , then used a regex to scrape lines like Testing