Skip to content

[Platform] HTTP-boundary record/replay cassettes for bridge tests#2128

Draft
wachterjohannes wants to merge 4 commits into
symfony:mainfrom
wachterjohannes:feature/platform-fake-http-cassette
Draft

[Platform] HTTP-boundary record/replay cassettes for bridge tests#2128
wachterjohannes wants to merge 4 commits into
symfony:mainfrom
wachterjohannes:feature/platform-fake-http-cassette

Conversation

@wachterjohannes

@wachterjohannes wachterjohannes commented May 30, 2026

Copy link
Copy Markdown
Contributor
Q A
Bug fix? no
New feature? yes
Docs? yes
Issues -
License MIT

Stacked on #2123 (the Mock provider). Builds on / should land after it — until #2123 merges, this diff also contains the Mock provider commits.

Adds a cassette-backed HttpClientInterface for e2e-testing bridge internals offline. You pass Mock\Http\CassetteHttpClient to any real bridge Factory (they all accept ?HttpClientInterface):

By default the mode follows the cassette file (override with the explicit record: argument):

  • record (cassette missing, + a real key): performs the live request and writes a JSON cassette (status/headers/body), redacting Authorization/x-api-key/auth_bearer;
  • replay (cassette exists): serves the recorded bytes as a real MockResponse, so the real Contract + ModelClient + ResultConverter run end-to-end with no network. Delete the cassette to re-record.

Unlike Test\InMemoryPlatform and the Mock provider (which bypass the converter), this exercises the bridge pipeline — so a bridge test can shrink to "load cassette → invoke → assert on the real converter output", and the cassette doubles as the request fixture.

Mistral example (the payoff)

tests/Bridge/Mistral/ResultConverterCassetteTest.php drives the real Bridge\Mistral\Llm\ResultConverter from committed cassettes:

$platform = Factory::createPlatform($apiKey, new CassetteHttpClient(
    new HttpCassette(__DIR__./fixtures/mistral_text_response.json),
));
$platform->invoke(mistral-large-latest, new MessageBag(Message::ofUser(Hello)))->asText();
// "Hello from Mistral!" — produced by the REAL converter from recorded bytes

Covers both a stop-finish text response and a tool_calls-finish response (asToolCalls()).

Details

  • FIFO replay (same drop-in semantics as MockHttpClient with an array of responses); each interaction also stores a request signature + body for debugging/assertions.
  • Streamed Server-Sent Events round-trip through EventSourceHttpClient + SseStream.
  • Ships in platform core under Symfony\AI\Platform\Mock\Http; symfony/http-client stays a dev dependency (documented, like InMemoryPlatform).

Adds Mock\Http\CassetteHttpClient + HttpCassette: a cassette-backed
HttpClientInterface passed to a real bridge Factory. By default the mode
follows the cassette file - record (cassette missing + real key) stores
raw HTTP responses; replay serves a MockResponse so the real Contract,
ModelClient and ResultConverter run offline. Includes a Mistral example
test driving the real Llm\ResultConverter from a committed cassette, plus
secret redaction and SSE round-trip.
@wachterjohannes

Copy link
Copy Markdown
Contributor Author

@OskarStark symfony/symfony#63781 RecorderHttpClient is the same idea at the same layer. As this PR would only be an improvement to the current test-setup we could postpone that OR merge this as something internal, use it in the bridges of this Repo and replace as soon as the RecorderHttpClient is landed (and we bump to this version).

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