Skip to content

test(image_gen): pin the availability check to the dispatch chain - #126

Merged
juniperbevensee merged 2 commits into
mainfrom
dev/juniperbevensee/imagegen-fail-closed
Jul 30, 2026
Merged

test(image_gen): pin the availability check to the dispatch chain#126
juniperbevensee merged 2 commits into
mainfrom
dev/juniperbevensee/imagegen-fail-closed

Conversation

@juniperbevensee

@juniperbevensee juniperbevensee commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Tests only. No source change. The behaviour is already correct on main (fixed in e13104cfd). Revised after an independent audit (9 agents) found two false statements in my own docstring and one real coverage gap.

Why this exists

check_image_generation_requirements() gates whether image_generate is emitted into the tool schema (tools/registry.py:551-557), while _dispatch_to_plugin_provider() decides where a call goes. If the check is more permissive than the dispatch, the agent advertises a capability that fails on every call — a false claim about its own abilities.

They disagreed before e13104cfd: the check returned True if any registered provider was available, while dispatch falls through to the in-tree FAL path unless image_gen.provider is explicitly set (deliberately — an OPENAI_API_KEY present for other features must not silently opt you into a paid image backend).

There was a test — and it passes against the buggy code

My original claim of "zero tests" was wrong. tests/tools/test_image_generation_plugin_dispatch.py::test_requirements_ignore_unselected_paid_plugin already called this function. What makes it worth naming is that it is vacuous: it stubs check_fal_api_key and _read_configured_image_provider but never stubs _ensure_plugins_discovered and never registers a ready provider. In a keyless environment all eight real providers report is_available() == False, so the old any-provider body returned False too.

Verified by splicing in the genuine pre-e13104cfd body:

file result against the buggy body
test_image_generation_plugin_dispatch.py 6 passed — cannot catch it
test_image_gen_availability.py (this PR) 4 failed — catches it

A test that cannot fail is not coverage. That's the stronger version of the point I was originally making.

The coverage gap the audit found

My docstring claimed these tests pinned the configured == "fal" short-circuit. They didn't. Three mutants on the one line e13104cfd actually added survived a fully green suite. Added test_configured_fal_short_circuits_even_with_a_ready_fal_provider, which kills two of them.

The third — narrowing the guard to if configured == "fal" alone — is not a gap: get_provider returns None for any non-str name (agent/image_gen_registry.py:69-70), so an unset provider can never resolve and the not configured half cannot change the return value. Redundant defence, indistinguishable by any test. Documented in the file so nobody "closes" it with an assertion that asserts nothing.

Mutation results — all six re-verified directly

mutation
delete the short-circuit KILLED
drop == "fal" KILLED
get_provider(configured) → any-provider scan KILLED
drop .is_available() KILLED
drop the _load_fal_client SDK probe KILLED
FAL branch unconditional KILLED
drop not configured survives by construction — see above

Order-dependence fixed

tests/agent/test_empty_tool_name_loop_dampening.py:136-141 purges agent.*/tools.* from sys.modules without restoring, so module-level imports here bound to a different module object than the one the function re-imports at call time — and a fake subclassing the stale ABC failed register_provider's isinstance check. Modules and the fake are now resolved together at call time.

Before: 1 failed when run after that file. After: 18 passed. CI never saw it, because scripts/run_tests.sh spawns a subprocess per file — which is exactly why it would have sat there indefinitely.

Coverage

scenario expected
keyed provider, image_gen.provider unset False ← the regression
provider = "fal" + a ready plugin named fal False ← the short-circuit
selected + available True
selected but unavailable / not registered False
a different provider ready False
FAL key alone, no plugin, no provider set True ← must not narrow
FAL key but fal-client absent False ← falls through

121 passed across the four image-related files. Verified live on the agent host earlier: check_image_generation_requirements() returns False there with OPENROUTER_API_KEY set and no FAL key — Matilde was not misadvertising the tool.

Not addressed here

  • tests/tools/test_image_generation_plugin_dispatch.py:117 — the vacuous neighbour still needs a discovery stub and a registered fake, or deletion.
  • agent/learn_prompt.py:77 tells the model to reference image_generate in generated SKILL.md with no availability gate.
  • tools/registry.py's last-good grace keeps a tool advertised briefly after its backend dies.

🤖 Generated with Claude Code

`check_image_generation_requirements` gates whether `image_generate` is emitted
into the tool schema, while `_dispatch_to_plugin_provider` decides where a call
actually goes. If those two disagree, the agent advertises a capability that fails
on every call — a false claim about what it can do.

They did disagree before e13104c: the check returned True if ANY registered
provider was available, while dispatch falls through to the in-tree FAL path unless
`image_gen.provider` is explicitly set. FAL unconfigured + provider unset + any
cloud key present meant the tool was advertised and then died on use.

That is already fixed on main — the function now short-circuits on unset (and on
"fal") and probes only the selected provider. But it had zero test coverage, so
nothing would catch a reintroduction, and an audit reading a working tree 23
commits behind main reported it as a live P1. Tests are what make that distinction
cheap.

Tests only. No source change.

  - keyed provider but image_gen.provider unset      -> False  (the regression)
  - selected + available                             -> True
  - selected but unavailable                         -> False
  - selected but never registered                    -> False
  - a DIFFERENT provider being ready does not count  -> False
  - FAL key alone, no plugin, no provider set        -> True   (must not narrow)
  - FAL key but fal-client absent (ImportError)      -> False  (falls through)

Note for anyone extending these: plugin discovery MUST be stubbed. It registers the
eight real providers and `register_provider` overwrites by name, so a fake named
"openai" is silently replaced by the real one — whose is_available() is False
without a key. Every assertion then passes for the wrong reason. The first draft of
this file did exactly that and looked green against known-buggy code.

Verified live on the audit host: `check_image_generation_requirements()` returns
False there with OPENROUTER_API_KEY set, no FAL key and no image_gen block —
consistent with the fixed behaviour, i.e. Matilde was not misadvertising the tool.

114 passed across test_image_gen_registry / test_image_routing / this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lse claims

Revised after an independent audit (9 agents) found two false statements in the
committed docstring and one genuine coverage gap.

1. "had no tests at all" was FALSE. tests/tools/test_image_generation_plugin_dispatch.py
   ::test_requirements_ignore_unselected_paid_plugin already called this function.
   The interesting part is that it PASSES against the buggy body: it stubs
   check_fal_api_key and _read_configured_image_provider but never stubs
   _ensure_plugins_discovered and never registers a ready provider, so in a keyless
   env all eight real providers report is_available() False and the old
   any-provider implementation returned False too. Verified with the genuine
   pre-e13104cfd body spliced in: that file 6 passed, this one 4 failed. A test
   that cannot fail is not coverage — which is the stronger point anyway.

2. The docstring claimed these tests pin the `configured == "fal"` short-circuit.
   They did not. Three mutants on the one line e13104c actually added survived a
   green suite: deleting the short-circuit, dropping `== "fal"`, dropping
   `not configured`. Adds
   test_configured_fal_short_circuits_even_with_a_ready_fal_provider, which kills
   the first two.

   The third is NOT a coverage gap and is now documented as such: get_provider
   returns None for any non-str name (agent/image_gen_registry.py:69-70), so an
   unset provider can never resolve and the `not configured` half cannot change
   the return value. Redundant defence; no test can distinguish it. Recorded so
   nobody closes the "gap" with an assertion that asserts nothing.

3. Fixed an order-dependence. tests/agent/test_empty_tool_name_loop_dampening.py
   purges agent.* / tools.* from sys.modules without restoring, so module-level
   imports here bound to a different module object than the one the function
   re-imports at call time, and a fake subclassing the stale ABC failed
   register_provider's isinstance check. Modules and the fake class are now
   resolved together at call time. Before: 1 failed after that file. After: 18
   passed. CI never saw it (scripts/run_tests.sh spawns a subprocess per file),
   which is precisely why it would have sat there.

Mutation results, all six re-verified directly rather than taken on the audit's
word:
  delete the short-circuit            KILLED
  drop == "fal"                       KILLED
  any-provider scan for get_provider  KILLED
  drop .is_available()                KILLED
  drop _load_fal_client SDK probe     KILLED
  FAL branch unconditional            KILLED
  drop `not configured` (redundant)   survives by construction, documented

Also corrects the PR body's cite: the availability gate is tools/registry.py:551-557,
not :544 (which is `# TTL clock.`). The cite was accurate at 2fece9f and drifted
nine lines in e13104c — the same commit credited with the fix.

Still tests only. No source change. 121 passed across the four image-related files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juniperbevensee juniperbevensee changed the title test(image_gen): cover check_image_generation_requirements — it had none test(image_gen): pin the availability check to the dispatch chain Jul 30, 2026
@juniperbevensee
juniperbevensee merged commit c44a3be into main Jul 30, 2026
21 checks passed
@juniperbevensee
juniperbevensee deleted the dev/juniperbevensee/imagegen-fail-closed branch July 30, 2026 00:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant