Back to News & Insights
Web Development September 22, 2026 · 9 min read

About 1,800 tests passed. Running the real app found 14 bugs

My last post about Filament Studio was about v1.2.0 and multilingual content, back in April. Since...

About 1,800 tests passed. Running the real app found 14 bugs

My last post about Filament Studio was about v1.2.0 and multilingual content, back in April.

Since then I have shipped two larger things: an MCP server, so AI agents can manage collections and records, and Flows, an automation engine inside the plugin. Flows has a visual designer, four trigger types (manual, webhook, collection event, cron), a set of operations (create/update records, HTTP requests, email, conditions, calling another flow), draft/publish versioning, and a step-through debugger.

By the end of August the test suite was at about 1,800 tests. I run mutation testing on top of that, with an 80% MSI target per module. By the numbers I had, Flows was in good shape.

In September I stopped trusting those numbers and did something I should have done earlier. I installed the package into a separate Laravel app and used it the way a user would: real HTTPS webhook deliveries a real queue worker MySQL instead of in-memory SQLite the admin panel, clicked through in a browser

That found fourteen defects in one release (v1.8.0), plus one more the day before (v1.7.1). The existing suite covered none of them.

This post goes through the interesting ones. What I took from it: almost every bug had the same shape. The system reported success while doing nothing, or doing the wrong thing. A run that completed in 0 ms

I opened a seeded "Welcome Email" flow, triggered it, and the run page said Completed. Duration: 0 ms. Steps: none.

The engine walks the flow as a graph. After each node it asks for the successors on the success branch (or failure). The lookup matched the edge's sourceHandle exactly:

The visual designer always sets sourceHandle, because its nodes have named success and failure handles. But a graph written by hand, by a seeder, or through the REST API usually looks like this:

No handle. So the walk stopped at the trigger, found nothing to do, and finished. "Finished with nothing to do" was recorded as success.

Every graph in my tests had the shape the designer produces. The engine had only ever been tested on input from its own UI. A webhook that looked signed but accepted anyone

Flows can be triggered by a webhook, and the default auth mode is HMAC: the sender signs "{timestamp}.{body}" with a shared secret, and the endpoint checks the signature.

A few things combined here: The flow table has a webhookauthmode column that defaults to hmac. The secret was generated only when the trigger node's config contained authmode: hmac. The trigger's config schema never declared an authmode field, so the designer never showed it and nobody could set it. So on the normal path, webhooksecret stayed NULL. The verifier took the secret as a string. (string) null is ''.

The endpoint said it required a signature, but anyone who could compute hashhmac('sha256', "{ts}.{body}", '') could pass the check. That is anyone.

The fix has two parts. First, the verifier refuses to verify against an empty secret:

Second, the trigger now reads the webhookauthmode column instead of node config. The webhook trigger node has no config at all anymore. Auth mode, secret, API-key allowlist and redaction paths all live on the flow record, so there is one source of truth.

The real bug was having two places to set one security setting. Each had its own tests, and nothing checked that they agreed.

If you run Flows with a reachable webhook: check for flows where webhookauthmode = 'hmac' and webhooksecret IS NULL. Those endpoints were effectively public. After upgrading, publish the flow again and a secret will be generated. Until then it returns 401.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation