Skip to content

Assert what the served endpoints return, not just a 200 - #420

Open
nicknikolakakis wants to merge 2 commits into
modelplaneai:mainfrom
nicknikolakakis:e2e-behavioral-verify
Open

Assert what the served endpoints return, not just a 200#420
nicknikolakakis wants to merge 2 commits into
modelplaneai:mainfrom
nicknikolakakis:e2e-behavioral-verify

Conversation

@nicknikolakakis

Copy link
Copy Markdown

Fixes #368.

The local e2e brings the whole path up and then checks one thing about it: that /v1/chat/completions and /v1/messages return 200. A regression that answers with the wrong shape, loses the request body between the gateway and the engine, or buffers a stream into a single blob passes that check.

The mock engine is what limited it: one canned response to any POST, body ignored, so there was nothing else to assert. It now echoes the last user turn and the requested model, streams chat.completion.chunk frames terminated by [DONE] when a request sets stream, and returns 400 for an unparseable body or one with no messages and 404 for an unknown path. On top of that, the two curl pods become a stdlib unittest suite in e2e/verify, run from a pod on the control plane because the address is on the kind Docker subnet. It ships as a ConfigMap and runs under the same python image the mock uses, so nothing is built or installed.

The suite fails 5 of its 8 cases against the pre-change mock, which is what says it tests behaviour rather than reachability. I verified the pod wiring on a throwaway kind cluster: green against the new mock with the pod reaching Succeeded, and a deliberately wrong MODELPLANE_MODEL putting it in Failed, which is what makes run.sh exit non-zero. I have not run the full two-cluster e2e, so this needs the test-e2e label to be proven end to end.

The mock is about twice as long and still inline in args, since the ModelDeployment container template is a curated subset with no volume to load a file from. env.valueFrom from a ConfigMap is the way out if it grows further.

The mock engine returned one canned response to any POST and ignored the
request body. That caps what the e2e can check at a status code: a reply
that never reached the engine, or one a proxy invented, looks the same as
a real one, and there is nothing to assert about streaming or bad input
because the mock has no such behaviour.

It now echoes the last user turn and the requested model, so a caller can
tell its body arrived intact; serves an SSE stream of chat.completion.chunk
frames terminated by [DONE] when a request sets stream; and rejects an
unparseable body or one with no messages with a 400, and an unknown path
with a 404, instead of answering 200 either way. It serves HTTP/1.1 so the
stream is chunked the way an engine's is, which means keep-alive, so it
also moves to ThreadingHTTPServer: a single-threaded server would hold the
probes and the endpoint-picker behind one client.

The cost is size. The ModelDeployment container template is a curated
subset of name, image, command, args and env, so there is no volume to
load a file from and the server stays inline in args, now about twice as
long. Loading it from a ConfigMap through env.valueFrom is possible if it
grows further.

Towards modelplaneai#368.

Signed-off-by: Nick Nikolakakis <nonicked@protonmail.com>
The local e2e curls /v1/chat/completions and /v1/messages and checks the
status code. That proves the path is wired, and nothing else: a regression
that answers 200 with a wrong shape, drops the request body on the way to
the engine, or buffers a stream into one blob passes unnoticed.

This replaces the two curl pods with a stdlib unittest suite under
e2e/verify, pointed at the ModelService through MODELPLANE_ADDRESS. It
asserts the OpenAI and Anthropic response shapes, that a marker in the
last user turn comes back from the engine, that a streamed request returns
event-stream frames whose content deltas reassemble into the message and
end in [DONE], that a malformed body and a body with no messages are
refused, and that /v1/models lists the served model. It waits for the
route to serve before the first assertion, since the address publishes
before the path carries traffic.

The address is on the kind Docker subnet, so the suite runs from a pod on
the control plane like the curls did. It goes in as a ConfigMap and runs
under the same python image the mock engine uses: urllib and unittest
only, so there is nothing to build and no install step. run.sh polls the
pod's phase rather than using kubectl wait, which has no single condition
for "ran, either way", prints the logs so a red run says which assertion
failed, and exits non-zero unless the pod succeeded.

The ruff and license checks grow a path to cover it, since both were
scoped to functions/ and the docs validator and would otherwise leave new
Python unchecked.

Fixes modelplaneai#368.

Signed-off-by: Nick Nikolakakis <nonicked@protonmail.com>
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.

Local e2e asserts only a 200; add behavioral test-suites against the running control plane

1 participant