test(image_gen): pin the availability check to the dispatch chain - #126
Merged
Conversation
`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>
check_image_generation_requirements — it had none
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tests only. No source change. The behaviour is already correct on
main(fixed ine13104cfd). 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 whetherimage_generateis 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 unlessimage_gen.provideris explicitly set (deliberately — anOPENAI_API_KEYpresent 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_pluginalready called this function. What makes it worth naming is that it is vacuous: it stubscheck_fal_api_keyand_read_configured_image_providerbut never stubs_ensure_plugins_discoveredand never registers a ready provider. In a keyless environment all eight real providers reportis_available() == False, so the old any-provider body returnedFalsetoo.Verified by splicing in the genuine pre-
e13104cfdbody:test_image_generation_plugin_dispatch.pytest_image_gen_availability.py(this PR)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 linee13104cfdactually added survived a fully green suite. Addedtest_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_providerreturns None for any non-strname (agent/image_gen_registry.py:69-70), so an unset provider can never resolve and thenot configuredhalf 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
== "fal"get_provider(configured)→ any-provider scan.is_available()_load_fal_clientSDK probenot configuredOrder-dependence fixed
tests/agent/test_empty_tool_name_loop_dampening.py:136-141purgesagent.*/tools.*fromsys.moduleswithout 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 failedregister_provider'sisinstancecheck. 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.shspawns a subprocess per file — which is exactly why it would have sat there indefinitely.Coverage
image_gen.providerunsetFalse← the regressionprovider = "fal"+ a ready plugin namedfalFalse← the short-circuitTrueFalseFalseTrue← must not narrowfal-clientabsentFalse← falls through121 passed across the four image-related files. Verified live on the agent host earlier:
check_image_generation_requirements()returnsFalsethere withOPENROUTER_API_KEYset 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:77tells the model to referenceimage_generatein 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