State what an engine has to do, once, for every engine - #62
Merged
Conversation
The exit criterion for the gRPC engine is that it passes the same client test suite as the REST engine. That suite did not exist. What existed was a set of transport tests asserting HTTP bodies through Ktor's MockEngine, which is the right way to pin a wire format and the wrong way to compare two of them: a gRPC engine cannot satisfy an assertion about a JSON body, by construction. The one protocol-agnostic file, CollectionsIntegrationTest, built its client with the REST factory in its second line. So the suite is extracted into kdrant-testkit and never names a protocol. It takes a QdrantClient from its subclass, talks to a real Qdrant, and asserts on what comes back. Thirty tests where there were five: collection lifecycle and named vectors, points by list and by flow, search with filters and selectors, batch and grouped search, paged, filtered and ordered scroll, the four payload operations, named vector updates and deletes, payload indexes, ordered batch updates, aliases, facets, both distance matrix forms, health, cluster info, snapshots, and every matcher the filter DSL offers put to a server that has to understand it. Scope is what both engines can do. Qdrant's gRPC API has no telemetry, no metrics, no issues endpoint, no snapshot transfer and no shard-scope snapshots, so those stay in the REST module's own tests rather than being weakened here into something both can pass. Naming that boundary is the point: a shared suite that quietly drops what one side cannot do proves less than one that says so. The testkit is not published. Its only consumers are the two transport modules' test source sets, so it carries no publishing block and is out of public-API tracking. It lives in a main source set because a test source set cannot be depended on across modules, which is why detekt's function naming rule gets the same exclusion there that it already gives src/test.
Three tests in the new contract failed on a real server for two reasons, both of them the tests being wrong rather than the engine. A cosine collection stores the unit vector: Qdrant normalizes on write, because cosine distance only depends on direction. Two round-trip assertions were reading that back and comparing it to what went in. They use dot distance now, which stores the vector as written, and the normalization gets a test of its own — it is the server's behaviour, it surprises people, and it is worth pinning where someone will find it. Phrase matching is a gap rather than a mismatch. Qdrant matches a phrase only against a text index created with phrase_matching: true, and createPayloadIndex cannot ask for that, so the filter goes out, is accepted, and matches nothing. The assertion is removed and the reason written where it was: asserting zero would pin the gap as if it were the behaviour.
tonytonycoder11
force-pushed
the
feat/m31-client-contract-suite
branch
from
July 31, 2026 13:52
9310d5d to
b621649
Compare
…act-suite # Conflicts: # build.gradle.kts
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.
The exit criterion for #52 is that the gRPC engine "passes the same client test suite as the REST
engine". That suite did not exist.
What existed were transport tests asserting HTTP bodies through Ktor's
MockEngine— the right way topin a wire format, and the wrong way to compare two of them, since a gRPC engine cannot satisfy an
assertion about a JSON body by construction. The one protocol-agnostic file,
CollectionsIntegrationTest, built its client with the RESTKdrant(host, port)factory in its secondline.
What this adds.
kdrant-testkit, a module that is not published, holdingQdrantClientContract:an abstract suite that takes a
QdrantClientfrom its subclass, talks to a real Qdrant in Docker, andasserts on what comes back. It never names a protocol.
RestClientContractIntegrationTestis the wholeof the REST side.
Thirty tests where there were five:
ensureCollection, updatebatchUpdate, where a later operation sees an earlier oneCollectionNotFoundScope is what both engines can do. Qdrant's gRPC API has no telemetry, no metrics, no issues
endpoint, no snapshot transfer and no shard-scope snapshots. Those stay in the REST module's own tests
rather than being weakened here into something both sides can pass — a shared suite that quietly drops
what one side cannot do proves less than one that says where the line is.
Notes on the module. It carries no
mavenPublishingblock and is excluded from public-APItracking; its only consumers are the two transport modules' test source sets. It lives in a
mainsource set because a test source set cannot be depended on across modules, so detekt's function-naming
rule gets the same exclusion there it already gives
src/test.The suite is skipped, not failed, without Docker, so it is CI that proves it: the
qdrant-compatjobalready runs
--tests "*IntegrationTest*"against both Qdrant images.Part of #52. Merge after #61.