Skip to content

State what an engine has to do, once, for every engine - #62

Merged
tonytonycoder11 merged 3 commits into
mainfrom
feat/m31-client-contract-suite
Jul 31, 2026
Merged

State what an engine has to do, once, for every engine#62
tonytonycoder11 merged 3 commits into
mainfrom
feat/m31-client-contract-suite

Conversation

@tonytonycoder11

Copy link
Copy Markdown
Contributor

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 to
pin 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 REST Kdrant(host, port) factory in its second
line.

What this adds. kdrant-testkit, a module that is not published, holding QdrantClientContract:
an abstract suite that takes a QdrantClient from its subclass, talks to a real Qdrant in Docker, and
asserts on what comes back. It never names a protocol. RestClientContractIntegrationTest is the whole
of the REST side.

Thirty tests where there were five:

Area Covered
Collections create, exists, describe, list, delete, named vectors, ensureCollection, update
Points upsert from a list and from a flow, retrieve, count with and without a filter, delete by id and by filter, numeric and uuid ids
Search nearest-first ordering, filters, payload and vector selectors, batch, groups
Scroll paging without repeats, filtered, ordered
Payload set, overwrite, delete keys, clear, indexes
Vectors update and delete one named vector at a time
Batch ordered batchUpdate, where a later operation sees an earlier one
Filters the four clauses, and every matcher the DSL offers put to a server that has to understand it
Service aliases, facets, both distance-matrix forms, health probes, cluster info, collection and storage snapshots
Failures a missing collection reported as CollectionNotFound

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. 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 mavenPublishing block and is excluded from public-API
tracking; its only consumers are the two transport modules' test source sets. It lives in a main
source 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-compat job
already runs --tests "*IntegrationTest*" against both Qdrant images.

Part of #52. Merge after #61.

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
tonytonycoder11 merged commit 80cfa4e into main Jul 31, 2026
6 checks passed
@tonytonycoder11
tonytonycoder11 deleted the feat/m31-client-contract-suite branch July 31, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant