cd /news/developer-tools/smash-story-the-demo-script-that-out… · home topics developer-tools article
[ARTICLE · art-61271] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Smash Story: The Demo Script That Out-Debugged My Test Suite

A developer discovered a production bug in their MCP server for Google's gemini-3.1-flash-lite-image model when a demo script revealed that the server's validation layer accepted thinking levels 'minimal' and 'medium' that the live API rejects. The unit tests, which mocked the Gemini client, passed because the mock returned success for any input, masking the bug. The fix involved updating the allowed values to only 'low' and 'high' and adding a regression test.

read4 min views1 publishedJul 15, 2026

This is a Smash Stories submission for the DEV Summer Bug Smash: a debugging story about the gap between "all tests pass" and "it actually works" — and the unlikely hero that closed it.

The project is a small MCP (Model Context Protocol) server that wraps Google's gemini-3.1-flash-lite-image

model. It exposes image generation and stateful image editing as four tools that any MCP-speaking agent can call — Claude Code, a Google ADK agent, and a Rust CLI all consume the same ~300-line Python server. (Full architecture write-up here.)

By every signal a developer normally trusts, it was healthy:

Then I wrote a demo script. It found a production bug in under a minute of runtime.

demo.sh

walks the stack live: discover the tools, generate an image, then do a stateful edit. To keep the demo cheap, step 2 requested the lowest quality tier the server documents:

cargo run --quiet -- generate "a tiny robot chef cooking ramen" 16:9 minimal

First run:

🔴 Image generation failed: Error code: 400 - {'error': {'message':
"'minimal' is not a supported thinking level for this model.
Allowed values are: low, high.", 'code': 'invalid_request'}}

Wait. The server's own validation had approved minimal

before sending it. Here's that validation:

SUPPORTED_THINKING_LEVELS = {"minimal", "low", "medium", "high"}

@mcp.tool()
def generate_image(
    prompt: str, aspect_ratio: str = "1:1", thinking_level: str = "medium"
) -> str:
    ...

Four allowed values. The live API accepts two: low

and high

. And look at the default — medium

. That's the real smash-worthy find:

Every live call that didn't explicitly overrideThe validation layer wasn't validating the API's contract — it was validating a stale memory of it.thinking_level

was a guaranteed HTTP 400.

The suite mocks the Gemini client, as unit tests should:

@patch("server._get_client")
def test_generate_image_success(self, mock_get_client):
    mock_client.interactions.create.return_value = mock_interaction
    result = generate_image(prompt="test", thinking_level="medium")
    self.assertIn("🟢 Image successfully saved!", result)

The mock returns success for any input — including inputs the real API rejects. The tests correctly proved "the server forwards medium

faithfully." Faithfully forwarding an invalid value is still a bug; it's just invisible from inside the mock boundary.

Two conditions had to align for this to ship:

SUPPORTED_THINKING_LEVELS

was a cached copy of a fact only the API owns. Cached copies drift.high

for quality — so the broken default and the two phantom values were never exercised. f(x)

being called a hundred times tells you nothing about f()

.Two lines of production code, plus the part that actually takes discipline — locking the discovery in so it can't regress:

-SUPPORTED_THINKING_LEVELS = {"minimal", "low", "medium", "high"}
+SUPPORTED_THINKING_LEVELS = {"low", "high"}

-    prompt: str, aspect_ratio: str = "1:1", thinking_level: str = "medium"
+    prompt: str, aspect_ratio: str = "1:1", thinking_level: str = "low"
result = generate_image(prompt="test", thinking_level="medium")
self.assertIn("Unsupported thinking level 'medium'", result)

Then the sweep (three tool signatures, docstrings, the server's self-describing get_help

, every doc that repeated the wrong values) and a rebuild + push of the published Docker image, which had been shipping the bug to anyone who pulled it.

Before After
Live call with default params HTTP 400, every time 🟢 image saved
thinking_level="minimal" / "medium"
Approved locally, rejected remotely Rejected locally with the allowed values named
Test suite 10/10 green (bug invisible) 11 assertions incl. contract regression test
Published image xbill9/nb2lite-mcp
Shipped the broken default Rebuilt, pushed, verified live

Elapsed time from first failure to fixed-image-on-Docker-Hub: about ten minutes — because the failing tool call came back as readable text (Allowed values are: low, high

) instead of a stack trace. Error messages that name the fix are half the debugging.

DEMO_FAST=1 ./demo.sh

).The whole project is built on Google AI, end to end:

gemini-3.1-flash-lite-image

store=True

  • previous_interaction_id

) are what make multi-turn image editing work: the demo's edit step adds a neon RAMEN sign to LlmAgent

on gemini-2.5-flash

MCPToolset

— Gemini calling Gemini, with the bug fix sitting in between.Allowed values are: low, high

) is what made this a ten-minute smash.xbill9/nb2lite-mcp

── more in #developer-tools 4 stories · sorted by recency
── more on @google 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/smash-story-the-demo…] indexed:0 read:4min 2026-07-15 ·