A full test run surfaced three things that needed fixing. One was trivial, one took four attempts to understand, and one was a missed update from the previous session’s reactive migration.

The trivial one: McpServerIntegrationTest asserts exactly 60 MCP tools (8 Claudony + 52 Qhorus). A fresh SNAPSHOT of Qhorus had shed two tools since the assertion was written. Updated to 58.

The interesting one was MeshResourceInterjectionTest. Nine errors, all ToolCallException: IllegalStateException: No current Vertx context found. The @BeforeEach setup was calling tools.createChannel() from the test thread — fine before, broken now because ReactiveChannelService.create() gained a Panache.withTransaction() wrapper in a newer Qhorus version.

We tried vertx.executeBlocking(). The error changed: Can't get the context safety flag: the current context is not a duplicated context. Panache requires a duplicated context, not a root one. executeBlocking() gives you the latter.

Rather than chase the context problem further, we read the ReactiveChannelService source and found create() calls channelStore.put() inside the transaction — the in-memory store handles it, no actual database involvement. We bypassed the service entirely and wrote directly to InMemoryChannelStore.put(). Nine errors became six failures.

Still failing, but now 404s. We dug into how sendMessage() resolves channels and found it uses blockingChannelService.findByName() — the synchronous ChannelStore, not the reactive one. Our channel was in the right store. But the messages weren’t.

getChannelTimeline() doesn’t use MessageStore at all. It queries Message.find() directly — a Panache static hitting H2. Messages written through blockingMessageService land in InMemoryMessageStore (in-memory). Timeline reads from H2. Neither path sees the other’s data.

This is a Qhorus inconsistency: sendMessage() uses an abstracted store interface for writes, getChannelTimeline() bypasses the abstraction entirely for reads. In production both paths hit the same database and it doesn’t matter. In tests with in-memory alternatives it falls apart. We filed qhorus#173 — it was fixed and verified within the session.

The last failure was SessionLineageResourceTest returning 500. After the reactive SPI migration, CaseLineageQuery.findCompletedWorkers() now returns Uni<List<WorkerSummary>>. The endpoint still called Response.ok(lineageQuery.findCompletedWorkers(caseUuid)).build() — passing the Uni object itself as the response entity. RESTEasy can’t serialize a Uni, so 500. Fix: @Blocking on the endpoint, .await().indefinitely() on the call.

475 tests passing, 0 failures.


<
Previous Post
What the mock was hiding
>
Next Post
The wrong fix for the right problem