Skip to content

test: end-to-end proxy test for chunked save → chain serve - #90

Merged
elevran merged 3 commits into
mainfrom
feat/chunked-roundtrip-test
Jul 12, 2026
Merged

test: end-to-end proxy test for chunked save → chain serve#90
elevran merged 3 commits into
mainfrom
feat/chunked-roundtrip-test

Conversation

@elevran

@elevran elevran commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Closes a test gap surfaced after PR #88 merged: nothing proved that a response saved via the chunked staging protocol (POST /staging → PUT /staging/{sid}/chunks/{k} → PUT /staging/{sid}/complete) was correctly reassembled into a chain entry that the proxy could serve back as part of a subsequent turn's flat context.

What this PR adds

cmd/proxy/inference/mock.goMockServer gains a RequestBodies() accessor that snapshots every POST /responses body in arrival order. Tests use this to inspect what the proxy actually forwarded to the inference backend.

cmd/proxy/chunked_roundtrip_test.goTestChunkedRoundtripReassemblesIntoChain saves an anchor turn with maxChunkBytes=64 (forces ≥2 AppendChunk), asserts the wire pattern (≥2 chunks, 1 complete, 0 abort), issues a continuation via previous_response_id, and verifies the continuation's inference call received the anchor's response output (msg_ok, role=assistant) in its input array. That is the strongest end-to-end proof that chunks → chain → served-context is intact.

Existing coverage this complements

Test Layer Validates
chunk_test.go unit chunkedResponseWriter chunk→chunked byte round-trip
internal/chainstore/streaming_test.go server AppendChunk/Complete/Retrieve reassembly
backend_routing_test.go proxy wire pattern (path counts) — no byte check
handler_test.go::TestStoreTrueContinuation proxy continuation returns 200 with different ID
This test proxy end-to-end chunked save → chain serve → inference context

make presubmit is green.

elevran added 3 commits July 10, 2026 11:42
Closes a real test gap surfaced after PR #88 merged: nothing proved that
a response saved via the chunked staging protocol (POST /staging →
PUT /staging/{sid}/chunks/{k} → PUT /staging/{sid}/complete) was
correctly reassembled into a chain entry that the proxy could serve back
as part of a subsequent turn's flat context.

Existing coverage:
- cmd/proxy/chunk_test.go: unit-level chunk→chunked byte round-trip via
  recordingBackend.
- internal/chainstore/streaming_test.go: server-side chunk reassembly via
  AppendChunk/Complete/Retrieve.
- cmd/proxy/backend_routing_test.go: wire-pattern pinning via
  routingRecorder (counts paths, doesn't inspect bytes).
- cmd/proxy/handler_test.go::TestStoreTrueContinuation: continuation
  succeeds but doesn't verify the inference call received the prior turn's
  output.

What this adds:
- cmd/proxy/inference/mock.go: MockServer gains a RequestBodies() accessor
  that snapshots every POST /responses body in arrival order. Tests use
  this to inspect what the proxy actually forwarded to the inference
  backend.
- cmd/proxy/chunked_roundtrip_test.go::TestChunkedRoundtripReassemblesIntoChain:
  saves an anchor turn with maxChunkBytes=64 (forces ≥2 AppendChunk),
  asserts the wire pattern (≥2 chunks, 1 complete, 0 abort), issues a
  continuation via previous_response_id, and verifies the continuation's
  inference call received the anchor's response output (msg_ok,
  role=assistant) in its input array — the strongest end-to-end proof
  that chunks → chain → served-context is intact.
Address the 3 Important findings from the PR #90 review:

1. recordBody call-order dependency — added a comment in handle() above
   recordAndReplaceBody explaining that it MUST run before any JSON decode
   of r.Body, and what would break if a future change decodes first.

2. Silent io.ReadAll error → r.Body left in unknown state. Reworked
   readAndRecord into recordAndReplaceBody that always closes the original
   r.Body and re-installs io.NopCloser(bytes.NewReader(body)) with what
   was read. On a read error the body may be partial or empty — but the
   downstream decoder now sees a deterministic payload rather than
   re-draining an unknown state.

3. RequestBodies() shallow-copied slice headers. Now deep-copies each
   body via append([]byte(nil), b...) so callers may mutate the returned
   slices without affecting each other or future snapshots.

Also renamed readAndRecord → recordAndReplaceBody to match the new
contract (it now always replaces r.Body, not just records).
Address the 4 Minor findings from the PR #90 review:

4. Wire-pattern assertions (steps 1+2) duplicated TestBufferedProxyMultipleChunks.
   Dropped. The wire pattern is pinned by that existing test — this test
   focuses on the unique chain-reassembly property (anchor's response output
   appears in the continuation's inference input).

5. With the wire-pattern assertions gone, no need for routingRecorder.
   Switched from newRoutingStack to newTestStack — drops the recorder
   import and aligns with how backend_routing_test.go and other proxy
   tests set up the inference-side stack.

6. decodeJSON closes the body, so calling it before require.Equal on
   StatusCode swallows the status assertion on non-200 responses.
   Swapped the order in both anchor and continuation turns — status
   check now runs first, with a self-diagnosing message.

7. Loose []map[string]interface{} replaced with a local inputItem struct
   that decodes only the fields the assertions check. No equivalent
   type exists in inference/types.go (Response / SSEEvent / UsageInfo
   are server-side, not input items), so the local definition is
   justified; typed access also documents the shape under test.
@elevran
elevran merged commit fabf419 into main Jul 12, 2026
6 checks passed
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