Skip to content

MockServer 7.5.0

Latest

Choose a tag to compare

@jamesdbloom jamesdbloom released this 29 Jul 18:21

[7.5.0] - 2026-07-29

Security

  • BREAKING: response templates can no longer reach arbitrary Java classes by default, closing the
    template remote-code-execution path reported as
    GHSA-7pwj-xvc2-hfpc.

    A caller who can reach the management API can register an expectation, and a response template was able
    to load java.lang.Runtime and execute OS commands in the MockServer process. Both engines that could
    do this are now sandboxed out of the box:
    • velocityDisallowClassLoading now defaults to true (was false), installing Velocity's
      SecureUberspector so a template cannot reach classes through $request.class.classLoader.loadClass(...).
      This is the more exposed half of the issue, and the half the report did not cover: Velocity ships in
      the DEFAULT distribution, whereas the JavaScript engine does not.
    • JavaScript templates now resolve no Java classes unless an operator grants them. Previously an
      empty javascriptAllowedClasses and empty javascriptDisallowedClasses meant unrestricted
      Java.type(...) access; that combination — the out-of-the-box state — now denies every class.
    • The GraalJS guest context no longer grants access to the members of java.lang.Class or
      java.lang.ClassLoader. Denying classes at Java.type(...) alone was not sufficient: real host
      objects are bound into the context (faker and the other built-in helpers), and under the previous
      HostAccess.ALL a template could walk from one of them to a classloader —
      faker.getClass().getClassLoader().loadClass('java.lang.Runtime') — reaching Runtime without the
      class filter ever being consulted. That walk is now closed, so host-class lookup is the single complete
      gate; a regression test drives four such walks (including through request) and fails if any resolves.
      Velocity's SecureUberspector already blocked the equivalent walk through its own bound helpers, which
      is now covered by a test too.
      Both flips are fully reversible with one property and remove no functionality: set
      mockserver.velocityDisallowClassLoading=false, or list the classes your templates need in
      mockserver.javascriptAllowedClasses (the single entry * lets any class resolve again). Templates that
      do not touch Java classes are unaffected, which is the overwhelming majority — JavaScript templates have
      the full ES2023 standard library available regardless of this setting. A refused class is logged once at
      WARN naming the class and the property to set, because GraalJS otherwise surfaces a refusal only as the
      class being undefined ("... is not a function"); the log is bounded and de-duplicated so a hostile
      template cannot flood it. mockserver.javascriptAllowedClasses is now also settable through the Spring
      test listener's @MockServerTest properties, which it was not before — it was a nice-to-have while the
      default was unrestricted, and is the only way to grant a class now that it is not. The insecure-mode WARN
      now fires when an operator has explicitly opened the
      sandbox rather than when it is closed. Proven end-to-end by a Netty integration test that registers the
      reported payload through the real management API and asserts the OS command creates no marker file, with
      a negative control on a deliberately unsandboxed server that DOES create it — so a regression cannot pass
      as an inert payload. This lands DEF-2 and DEF-3 of docs/plans/later/security-defaults.md ahead of the
      other default flips listed there; JavaScript went further than that plan proposed (deny everything, not a
      built-in "safe types" allow-list) because deny-by-default is the only form that stays safe as the JDK
      grows new reachable classes.

Fixed

  • A property file that cannot be read is now reported instead of ignored in silence
    (#2358).
    When a
    mockserver.propertyFile an operator had explicitly configured could not be read, MockServer applied
    none of its properties and said nothing about it — at any log level. The only symptom was that every
    property in the file appeared to be at its default, which surfaces far downstream as unexplained
    behaviour: in the reported case an unreadable (but present) mounted file meant initializationJsonPath
    was never set, so no expectations loaded, no loading JSON initialization file: line appeared, and no
    error was logged either. The message existed but was unreachable in practice — gated at DEBUG and
    emitted during static initialisation, before any log level has been applied, so neither
    -Dmockserver.logLevel=DEBUG nor a -logLevel argument could surface it. Such a file is now logged at
    WARN, naming the path and the underlying reason verbatim; because FileNotFoundException covers "not
    there" and "not allowed to read it" alike, that reason is usually the whole answer (Permission denied
    in the reported case, typically SELinux labelling or a rootless/user-namespace UID mismatch). A property
    file that is merely absent at its default location stays quiet, as does the Docker image's built-in
    -Dmockserver.propertyFile=/config/mockserver.properties, which the entrypoint always passes and which
    therefore expresses no intent — otherwise every container started without a mounted config would warn.
    Inside the image, only MOCKSERVER_PROPERTY_FILE can express that intent, and it does.
  • The mockserver-node launcher suite no longer fails intermittently on a TLS handshake reset. The
    two tests that exercise jvmOptions did so over HTTPS against a server started with
    dynamicallyCreateCertificateAuthorityCertificate=true, and issued that HTTPS request as soon as
    start_mockserver resolved. start_mockserver only proves the HTTP control plane is answering — it
    polls PUT /mockserver/retrieve over plain HTTP — but with a dynamically created certificate
    authority the server still has to generate a CA key pair and a leaf certificate before it can serve
    TLS on that same (port-unified) port. A handshake arriving in that window was closed mid-negotiation
    and surfaced as ECONNRESET "Client network socket disconnected before secure TLS connection was
    established", failing whichever of the two tests lost the race. This accounted for every
    mockserver-node failure on master over the preceding 40 builds (5 of 40, ~12%), so it was the sole
    cause of the pipeline's intermittent red. Both tests now wait for an actual TLS handshake to complete
    before asserting, which gates them on the condition they really depend on rather than retrying the
    assertions. The new waitForTlsReady helper is verified to reject — not resolve — both when nothing
    is listening and when a listener accepts the TCP connection then destroys it mid-handshake, which is
    exactly the failure signature it exists to absorb. The readiness budget is deliberately generous
    (120s): waiting costs nothing when the server is healthy, since a ready server completes the
    handshake on the first attempt in milliseconds, so the limit only decides how much CI contention is
    tolerated before a slow start is misreported as a fault. An earlier 30s budget went green five builds
    running and then expired on a loaded agent — the same flake wearing a clearer error message. A start
    that takes over 5s is now reported even when it passes, because readiness creeping towards the limit
    is the signal that the next run will not make it.
  • archiver.glob() works again in @mockserver/testcontainers (Node), and CVE-2026-14257 stays
    closed.
    The previous remedy for the brace-expansion denial of service (GHSA-mh99-v99m-4gvg,
    patched only in 5.0.8) was a blanket "brace-expansion": "^5.0.8" override. That resolved the whole
    tree to a single hoisted 5.0.8 and npm audit reported zero vulnerabilities — but 5.x changed the
    CommonJS export from a callable function to an object ({ expand, EXPANSION_MAX, ... }), while the
    minimatch copies actually installed (3.1.5, 5.1.9, 9.0.9) all call it as expand(pattern). Every
    glob containing a brace therefore threw TypeError: expand is not a function, crashing
    archiver.glob(). The blast radius is narrower than it first looks — testcontainers copies files
    with archiver.directory()/.append(), which pass no brace pattern and still work — so what broke
    is brace globbing for anything in this module's runtime tree that does use it. The failure was
    invisible because minimatch short-circuits patterns with no {, so plain globs kept working and the
    unit suite stayed green. The override is now targeted: readdir-glob and archiver-utils' glob
    take minimatch@^10.2.5, which depends on brace-expansion@^5.0.5 and is written against the new
    API, so both runtime copies land on the patched 5.0.8 with a matching minimatch. jest keeps its own
    minimatch@3.1.5 + brace-expansion@1.1.16 pairing and is untouched. npm audit --omit=dev still
    reports 0 vulnerabilities, and a new dependency-integrity unit test drives a brace pattern through
    both runtime minimatch copies and through a real archiver.glob() tar, plus asserts expansion stays
    bounded — it fails against the blanket override, so the silent half of this cannot return.
  • A forward responseOverride that replaces the body no longer inherits the upstream response's
    Content-Length, which truncated the response on the wire.
    The override swapped the body but left the
    upstream header in place, so the client read only as many bytes as the body it replaced — a 34-byte
    override behind an upstream Content-Length: 13 arrived as 13 bytes — or hung waiting for bytes that
    never came. The stale header is now dropped so the encoder recomputes it from what is actually written;
    a Content-Length set by the override itself, and connectionOptions.contentLengthHeaderOverride, are
    still honoured, and a header-only override (one that sets no body) is untouched. This affects every body
    override, and it was the remaining reason a FILE response body returned from a responseOverride
    still reached the client wrong after
    #2450: the file was materialised
    correctly and then cut short by the stale length. Covered by a Netty integration test that drives a real
    forward-with-override through a real upstream and asserts the bytes the client receives.
  • The JetBrains plugin's LLM tool window now sends a valid expectation
    (#2455).
    "Load into Server" was
    rejected with 400 incorrect expectation json format because the builder emitted a shape that never
    existed on the server: a flat completion string, a top-level finishReason, stream, and usage,
    and a provider of OPEN_AI. The completion text, streaming flag, stop reason, and token usage
    belong INSIDE the completion object (text, streaming, stopReason, usage.inputTokens /
    usage.outputTokens), and providers are the Provider enum names (OPENAI, AZURE_OPENAI, …). The
    provider and field catalogues shared with the VS Code extension are corrected the same way — they
    offered OPEN_AI, VERTEX_AI, messages, stream, finishReason and a top-level usage, none of
    which the server accepts — and completion inside a completion object now offers the nested fields.
    The plugin has always bundled the correct schema; it simply never validated its own output against it,
    and the previous tests asserted the builder matched the same invented shape it produced. Both editors
    now validate against the bundled schema in their test suites.
  • httpLlmResponse.provider now accepts every provider MockServer implements. The JSON Schema enum
    listed 9 of the 14 org.mockserver.model.Provider constants, so MISTRAL, XAI, DEEPSEEK, GROQ,
    and OPENROUTER were rejected with 400 incorrect expectation json format even though each has a
    fully registered response codec. The five missing values are added to the core schema, the generated
    VS Code and JetBrains schemas, and both copies of the OpenAPI specification, and a new parity test
    fails if the enum and Provider ever diverge again in either direction. The provider list on the
    LLM response mocking documentation and in the Rust client's field docs is updated to match.
  • The cloud blob-store, async-broker and transparent-proxy CI steps no longer OOM-kill their own build
    before any test runs.
    Each ran its Docker container with --memory=4g, but mockserver/.mvn/jvm.config
    pins the Maven JVM to -Xmx6144m and the wrapper prepends it to MAVEN_OPTS, so the -am dependency
    build was permitted a 6 GB heap inside a 4 GB cgroup and the kernel intermittently killed it with exit 137
    — losing the very coverage those fail-closed steps exist to guarantee. Raised each to --memory=7g, the
    value every other ./mvnw step already uses and which fits the single-agent c5.2xlarge/m5.2xlarge
    default-queue instances with margin.

Added

  • A cassette is now auto-registered when a fixture is loaded or recorded via the MCP tools, so it
    appears under GET /mockserver/cassettes without a separate PUT /mockserver/cassettes call.

    Previously the server-side cassette registry was populated only by an explicit
    PUT /mockserver/cassettes, so a fixture loaded with the load_expectations_from_file MCP tool, or
    written with record_llm_fixtures, never showed up in the dashboard's Cassettes tab unless the
    caller also registered it by hand. Both MCP tool handlers now register the fixture in
    CassetteRegistry at the point the file is loaded/written — the file path as the key, the loaded/
    written expectation count, and an origin of loaded or recorded respectively — so
    GET /mockserver/cassettes (which serialises that registry) lists it automatically. Re-loading or
    re-recording the same path updates the existing entry in place rather than duplicating it.

  • Clustered (Infinispan) expectation reload-on-startup is now proven end-to-end. A new test
    (ClusteredExpectationPersistenceReloadTest in mockserver-state-infinispan) forms an in-JVM
    JGroups cluster consisting of a bare "fleet keeper" InfinispanStateBackend that stays up for the
    whole test plus a full MockServer node started with stateBackend=infinispan,
    clusterEnabled=true and persistExpectations=true. An expectation is created on that node over
    the wire, the persisted document is polled for through the keeper's backend (proving it really
    replicated across the REPL_SYNC blob cache), the node is then stopped completely, and a fresh node
    is started against the same cluster and the same persistedExpectationsPath — which must restore
    the expectation and MATCH a real HTTP request with it. The local persisted file is asserted to be
    empty first, so the restore cannot be coming from the filesystem-initializer route. The reload path
    in ExpectationFileSystemPersistence was already covered at unit level in mockserver-core
    (ExpectationBlobStoreRestoreTest, against an InMemoryBlobStore, with no server and no cluster)
    and end-to-end only against S3/MinIO behind a Docker gate; what no test proved is that a clustered
    node's InfinispanBlobStore is the store HttpState wires into that restore, nor that a real
    restarted member of a live cluster recovers the fleet's shared expectations. A second test sets
    blobStoreRestoreTimeoutSeconds=0 (the documented way to skip the restore) and asserts the fresh
    node does NOT serve the expectation, which permanently pins the fact that no other mechanism —
    JGroups state transfer of the expectations cache, a stray invalidation event, or the local file —
    restores expectations when a node starts. Verified by a positive control: disabling the reload
    path in production makes the restarted node answer with an empty body and turns the test red.

  • The response-aware arm of the eviction false-green guard is now proven end-to-end over HTTP. A new
    Netty integration test (EvictedResponseVerificationIntegrationTest) boots a real server with
    maxLogEntries=2 and failVerificationOnEvictedLog=true, registers an expectation so a GET /was-responded exchange is recorded as a real EXPECTATION_RESPONSE request-response pair, then floods
    the bounded event log with further unmatched traffic so that pair is evicted. A subsequent
    verify(request("/was-responded"), response().withStatusCode(418), never()) through the Java client must
    throw an AssertionError saying the response "could not be verified" because entries were discarded
    after reaching maxLogEntries. MockServerEventLog implements this guard twice — once in verifyRequest
    and once, through a completely separate counting path over recorded pairs, in verifyResponse — and only
    the request arm had an *IntegrationTest; the response arm was covered solely by an engine-level test
    against an in-process event log. The test uses never() because it is the simplest shape that reaches
    the guard: the guard sits on the PASS branch behind any asserted upper bound (getAtMost() != -1 — so
    atMost(n), between(0,n) and exactly(0) reach it too), whereas an atLeast(1)/once() verification
    of an evicted pair fails earlier with an ordinary "Response not found" message and proves nothing.
    never() is exactly the case a guard-less server would answer with a silent false green. The assertion pins the message to Response could not be verified so it cannot be
    satisfied by the request-side arm. Verified by a positive control (disabling only the response-side guard
    in production makes the verification pass silently and turns the test red).

  • The eviction false-green guard is now proven end-to-end over HTTP. A new Netty integration test
    (EvictedLogVerificationIntegrationTest) boots a real server with maxLogEntries=2 and
    failVerificationOnEvictedLog=true, records a GET /was-called request, then floods the bounded
    request-log ring with further traffic so the /was-called entry is evicted. A subsequent
    verify(request("/was-called"), never()) through the Java client must throw an AssertionError whose
    message says the log "could not be verified" because entries were discarded after reaching
    maxLogEntries — proving the guard refuses to certify absence it can no longer see, rather than
    silently passing. Previously the guard was only covered by an engine-level test against an in-process
    MockServerEventLog and no *IntegrationTest exercised it across the wire. Verified by a positive
    control (disabling the guard in production makes verify(never()) pass silently and turns the test
    red).

  • Custom gRPC response metadata and trailing metadata are now proven against a real grpc-java
    client.
    Two new tests in GrpcUnaryClientIntegrationTest register an expectation whose gRPC
    response carries both custom response metadata authored with withHeader(...) and custom trailing
    metadata authored with withTrailer(...), drive it with a live grpc-java client, and read the
    values back off the real io.grpc.Metadata objects the client receives (via a capturing
    ClientInterceptor, and via StatusRuntimeException.getTrailers() on the error path). The
    assertions are deliberately discriminating: the response metadata must arrive in the initial
    headers
    and not in the trailers, the trailing metadata must arrive in the trailers and not be
    folded into the initial headers, and both values must round-trip byte-for-byte including a value
    carrying =, ;, , and spaces. Previously this behaviour was exercised only structurally
    (EmbeddedChannel / model-level assertions, which cannot tell a trailer emitted as a trailer from
    one folded into the headers) and by the existing -bin metadata tests, which deliberately accept
    the value from either side because a body-less unary response may legitimately collapse to
    Trailers-Only. Verified by positive controls: dropping the user-authored trailers turns both tests
    red, and dropping the user-authored response headers turns the header assertion red.

  • The maxResponseBodySize limit is now proven behaviourally against a real upstream. A new
    integration test (MaxResponseBodySizeIntegrationTest) boots a forwarding MockServer configured with a
    4KB maxResponseBodySize, points it at a raw upstream socket that returns a 64KB body, and drives it
    over a plain client socket: the oversized body fails the forward and the client receives 502 Bad
    Gateway
    with none of the payload relayed, while a control request whose body sits under the limit is
    forwarded intact. A third case repeats the oversized body with Transfer-Encoding: chunked and no
    Content-Length, proving the cap is enforced against the bytes actually accumulated by the forward
    client's aggregator rather than merely against a declared header. Previously this documented,
    memory-protecting bound — read whenever a forward-client pipeline is built — had no behavioural
    coverage at all, so a regression that dropped the wiring (or passed an unbounded value) would have
    removed the limit silently; only the inbound analogue maxRequestBodySize was verified. The new test
    covers the HTTP/1.1 forward aggregator; the HTTP/2 forward path reads the same property (for the
    per-stream aggregator and to derive the client's maxFrameSize) and remains uncovered.
    maxResponseBodySize accordingly moves from ENFORCEMENT_EXEMPT to ENFORCEMENT_VERIFIED in
    ConfigurationEnforcementClassificationTest. Verified by a positive control (restoring an unbounded
    aggregator lets the oversized body through with a 200 and turns both over-limit assertions red).

  • The Ruby client now proves live SSE stream consumption over the wire. New integration examples
    (spec/integration_spec.rbSSE streaming) register an httpSseResponse expectation via the Ruby
    client against a running MockServer, then open a real streaming HTTP consumer and assert every data:
    frame arrives in order, that the reconstructed multi-delta message matches, and that a multi-line
    data: payload survives the framing intact (Content-Type: text/event-stream). Previously the Ruby
    suite only asserted the JSON keys of a built streaming expectation (a2a_spec) and never consumed a
    live SSE stream, so a silent server-emission or client-parsing drop would have gone uncaught. Verified
    by a positive control (dropping events from the emitted stream turns the received-frames assertion red).

  • The assumeAllRequestsAreHttp protocol-detection fallback now has direct unit coverage. Two
    paired EmbeddedChannel tests in DirectProxyUnificationHandlerTest drive
    PortUnificationHandler.decode() with an HTTP request using a non-standard method (PURGE, which is
    not one of GET/POST/PUT/HEAD/OPTIONS/PATCH/DELETE/TRACE/CONNECT): with
    assumeAllRequestsAreHttp=true the full HTTP pipeline is added (rather than falling to binary request
    proxying), and with the flag disabled the HTTP codec is not added — proving the flag is the only
    difference. Previously the fallback branch was exercised only by a live-socket integration test and
    the config getter's own unit test, so the EmbeddedChannel protocol-detection path for the flag was
    unexercised.

  • HTTP/3 streaming response bodies are now proven end-to-end through the action pipeline with a real
    QUIC client.
    A new integration test (Http3StreamingForwardIntegrationTest) registers a forward
    expectation on the HTTP/3 port (with streamingResponsesEnabled) pointing at an upstream Server-Sent
    Events stream that serves an early event immediately and withholds the late event for 1.5s, then drives
    it with a live Netty QUIC client and asserts both events arrive as SEPARATE DATA frames spread across
    that delay — proving the streaming relay funnels through HttpActionHandler ->
    ResponseWriter.writeResponse -> Http3ResponseWriter.writeStreamingResponse and emits chunks
    incrementally. Previously Http3StreamingIntegrationTest drove Http3ResponseWriter directly from a
    hand-built QUIC server (bypassing expectation matching), and Http3MockingMatrixIntegrationTest
    exercised the real pipeline over QUIC but only with non-streaming actions, so incremental delivery of a
    streamed body through the full pipeline was untested. QUIC-gated like the sibling HTTP/3 tests so it
    skips cleanly where the native transport is unavailable.

  • The dashboard's Monaco code editor is now proven in a real browser end-to-end. A new Playwright
    e2e test (mockserver-ui/e2e/dashboard.spec.ts) drives the actual bundled Monaco editor in the
    served dashboard's composer against a live MockServer: it asserts Monaco's own DOM
    (.monaco-editor / .view-lines) renders, authors a JSON response body via real editor input,
    raises and clears a live validation marker from Monaco's JSON language web worker, then registers
    the mock and confirms the Monaco-authored body round-trips to the server (present in
    PUT /mockserver/retrieve and served verbatim on the matching request). Previously the 178
    jsdom/vitest specs globally replaced Monaco with a bare <textarea>, so nothing exercised the real
    editor's tokenisation, web-worker validation, or DOM.

  • SOCKS4 CONNECT tunnelling is now proven end-to-end over a real socket. A new socket-level
    integration test (NettyHttpProxySOCKS4IntegrationTest) performs a raw SOCKS4 CONNECT handshake
    against a bound MockServer targeting a loopback EchoServer, then sends an HTTP GET through the
    granted tunnel and asserts the EchoServer received the request and returned 200 (bytes relayed by
    Socks4ConnectHandler). Previously SOCKS4 was only exercised by an EmbeddedChannel unit test
    (Socks4ProxyHandlerTest, which asserts handler removal) while every real-socket proxy integration
    test used SOCKS5, so nothing drove the SOCKS4 relay path over the wire.

  • Per-host forward-proxy client-certificate selection is now proven at a real TLS handshake. A new
    integration test (ForwardWithCustomClientCertificateByHostIntegrationTest) configures
    forwardProxyClientCertificatesByHost to present two independent client certificates (each backed by
    its own CA) keyed by host, then forwards through MockServer to two secure upstream EchoServers that
    each REQUIRE client auth and trust only ONE of the two client CAs. Because the host string is the
    cert-mapping key while the connection target is fixed independently by the forward port, the same
    upstream is reached under both host keys: the mapped cert is accepted (200) and the mismatched cert is
    rejected at the handshake (502). This makes the presented client certificate a load-bearing assertion,
    verified by a positive control (degrading the mapping to always present cert A flips host B's accepted
    case to 502). Previously NettySslContextFactoryTest asserted only SslContext identity/distinctness
    and the pure resolver -- nothing drove the per-host cert to an actual mTLS handshake.

  • LLM streaming physics for Gemini, Ollama, and Bedrock are now proven over a real socket. Streaming
    physics over the wire was previously e2e-tested only for Anthropic and OpenAI (both SSE); Gemini, Ollama,
    and Bedrock rested on self-derived golden JSONL plus codec unit tests, with no socket streaming e2e. Three
    new tests in LlmAgentLoopE2eTest serve a streaming httpLlmResponse for each provider, connect a real
    socket client, and assert both that the wire Content-Type is the provider's streaming media type
    (text/event-stream for Gemini SSE, application/x-ndjson for Ollama NDJSON,
    application/vnd.amazon.eventstream for Bedrock AWS event-stream binary framing) and that the text
    reconstructed by concatenating the streamed deltas — Gemini candidates[].content.parts[].text, Ollama
    message.content, and Bedrock's base64-wrapped Anthropic text_delta fragments decoded from the
    CRC32-validated binary event-stream messages — equals the completion text exactly. The Bedrock case
    de-chunks the HTTP/1.1 chunked body and decodes the binary framing end to end.

  • The PHP client's fidelity harness now gates the TYPED model, not just raw replay. The existing
    RoundTripFidelityTest deserialises each shared fixture with Expectation::fromArray(), which stores
    the decoded array verbatim in rawData and replays it unchanged -- so it records zero gaps for every
    fixture BY CONSTRUCTION and can never detect a field the typed builders (HttpResponse, HttpForward,
    HttpError, HttpRequest) fail to model. A new TypedRoundTripFidelityTest closes that tautology:
    for every shared fixture it reconstructs each action/matcher block THROUGH the typed model (reflection
    copies across only the properties each class declares, recursing into declared nested typed objects,
    then serialises via the class's own toArray()) and diffs the rebuilt structure back against the
    fixture -- the server-schema side of the contract -- so any server field the typed model drops surfaces
    as a concrete diff path derived from the corpus, never from the client's own key list. This immediately
    documented five real request-matcher gaps the raw harness hid (dnsClass/dnsName/dnsType,
    pathParameters, protocol, plus NottableString method/path), each pinned in a per-field gap
    ledger with a stale-entry ratchet, while the httpResponse/httpForward/httpError models are proven
    to cover their entire fixture surface. A positive-control test proves the gate fires when a typed
    builder drops a field it is supposed to carry (removing statusCode from HttpResponse::toArray()
    turns the suite red for every fixture carrying it, and green again on restore) -- the exact regression
    the raw-replay harness cannot catch. Shared comparator logic is extracted to FidelityComparator.

  • The Go client's FORWARD and ERROR response actions are now proven over the wire. New integration
    tests (response_action_integration_test.go) register a httpForward and a httpError
    (dropConnection) action via the Go client and drive real requests that assert the SERVER actually
    performs them: the forwarded request loops back through a match-once, higher-priority self-forward to
    a distinct upstream response body (needing no externally-reachable upstream, so it runs identically in
    the CI sibling-container harness and against a locally port-mapped server), and the error endpoint
    drops the connection at the transport level (no HTTP response) while a control endpoint on the same
    server still answers cleanly. Previously the Go client's response-action coverage was builder/JSON
    only -- no test drove a forward or error action to completion over a socket.

  • Rust client wire tests proving the server actually enforces negation matchers and performs
    response actions.
    The Rust integration suite previously only asserted control-plane serialization
    (matcher_value_tests.rs) and registered a forward expectation it never drove (test_forward_expectation
    "won't actually forward"), so nothing proved a running MockServer acted on either. Three new
    #[ignore]d integration tests drive a live server over the data plane via a dependency-free raw
    socket: test_negation_matcher_enforced_over_wire registers a NottableString negation (bare
    "!foo", explicit MatcherValue::not_literal, and an escaped literal "!foo") and asserts the
    server matches a non-foo value (200) while excluding foo (404) — and that an escaped "!foo"
    matches literally rather than as a negation; test_forward_action_actually_forwards registers a
    higher-priority run-once FORWARD that loops back to the server's own port plus a lower-priority
    fall-through RESPOND, and asserts the distinctive fall-through body is returned only if the forward
    genuinely executed (topology-independent — no external upstream); and
    test_error_action_actually_returns_raw_bytes registers an ERROR action and asserts the server
    writes the configured raw bytes back. Run in CI by the existing rust-integration-test step.

  • Wire-level .NET client test coverage for NottableString header negation and for
    forward/error response actions.
    The .NET integration suite asserted expectation creation but
    never proved the server honours what the client sends: there was no test that a !foo header
    matcher (MatcherValue.NotLiteral) is transmitted and enforced over the wire, nor that a
    registered forward or error action is actually performed. A new WireBehaviorTests drives real
    requests through a running MockServer (reached via the existing MOCKSERVER_URL harness) to prove:
    a "not foo" header matcher matches a non-foo request (200) and rejects a foo request (404); the
    escaped literal MatcherValue.Literal("!foo") matches only a header whose value really is !foo;
    a forward action is genuinely forwarded (the path is received twice — original plus the re-entered
    forward — not merely served directly); and an error action actually drops the connection (a
    transport failure, not an HTTP status). Each assertion was confirmed to redden when the
    corresponding client behaviour is degraded.

  • Live-broker test coverage proving Kafka SASL credentials reach and are enforced by a real
    broker.
    Kafka SASL/SSL security was only asserted at the property-map level
    (KafkaMessagePublisherSecurityTest) and every live-broker Kafka integration test used a plaintext
    bootstrap, so nothing proved the configured credentials actually authenticate against a broker. A
    new KafkaSecurityLiveBrokerIntegrationTest starts a Testcontainers Kafka whose external listener
    is SASL_PLAINTEXT/PLAIN with a broker-side JAAS config that knows a single credential, then
    drives MockServer's KafkaMessagePublisher with a KafkaSecurity: a correctly-credentialed
    publisher publishes successfully and the message is read back by a matching credentialed consumer,
    while a wrong-password publisher is rejected with an authentication exception (proving enforcement,
    not merely that plaintext works). Docker-gated so it SKIPS cleanly when Docker is unavailable.

  • Credential-enforcement test coverage for MQTT security against a secured live broker. MQTT
    MqttSecurity credentials were only asserted at the options-carrier unit level
    (MqttSecurityOptionsTest), while the sole live Mosquitto integration test ran an
    allow_anonymous plaintext broker — so nothing proved credentials are actually applied and
    enforced on the wire. A new MqttTlsLiveBrokerIntegrationTest drives MockServer's MQTT publisher
    against a Testcontainers Mosquitto broker configured with a password_file and
    allow_anonymous false: it asserts that a publisher wired with the correct MqttSecurity
    username/password authenticates and delivers a message to an authenticated subscriber, and that a
    publisher with the wrong password (and one with no credentials) is rejected by the broker at
    CONNECT. Docker-gated so it skips cleanly when Docker is unavailable.

  • Live-broker test coverage for the AsyncAPI control-plane load() → publish-on-load path. The
    control-plane's broker-connecting path (createBrokerConnections / publishOnLoad) was untested
    against a real broker — AsyncApiControlPlaneImplTest loads without a reachable broker (asserting
    publishers=0), and the endpoint IT covers only the broker-less endpoints. A new Docker-gated
    AsyncApiControlPlaneLiveBrokerIntegrationTest (Testcontainers Kafka) drives load() with a real
    brokerConfig, publishOnLoad:true and consume:true, then proves the control-plane genuinely
    connected and published by consuming the on-load message with a plain third-party Kafka client and
    asserting status() reports publishers>0, a subscriber, and the recorded round-tripped message.

  • Over-the-wire test coverage for an LLM refusal preset served with a rate-limit quota. The LLM
    refusal presets, provider-specific rate-limit headers, and stateful request-count quota were only
    asserted at the body-builder / handler-unit level; nothing drove them through a running server. A
    new LlmRefusalQuotaRateLimitIntegrationTest serves an httpLlmResponse configured with an
    Anthropic refusal preset and a 2-request quota, then asserts on the raw socket response that the
    first two requests return a 200 refusal envelope (stop_reason:"refusal") carrying the
    anthropic-ratelimit-requests-* headers, and that the third (over-quota) request flips to a 429
    rate_limit_error envelope with the exhausted rate-limit headers and Retry-After.

Fixed

  • The testcontainers-mockserver (Python) port assertions no longer break against testcontainers
    4.15.0, which keys DockerContainer.ports by str(port) rather than int.
    with_exposed_ports
    now stores self.ports[str(port)] = None (4.15.0 types the attribute as
    dict[str, Optional[int]]), so the suite's assert 1080 in container.ports started failing with
    assert 1080 in {'1080': None} and took five tests — and the whole MockServer Python pipeline,
    and with it the umbrella MockServer build — red on master. The tests now read the exposed ports
    through a normaliser that parses each key to an int (tolerating a "1080/tcp" protocol suffix),
    so they assert the same thing under either key style. This also closes a latent false green: the
    assert MOCKSERVER_PORT not in container.ports check in test_replaces_default_port passed
    trivially once the keys became strings, and so would no longer have caught with_server_port
    failing to drop the previously exposed port. Only the tests changed — MockServerContainer itself
    was already correct, as get_exposed_port takes an int and normalises internally.
    <blobStoreKeyPrefix>/<file name> instead of under the writing machine's absolute local path, and a
    blobStoreKeyPrefix that does not end in a separator is now treated as a folder-style prefix instead
    of being glued straight onto the key (mockserver + x.json was mockserverx.json and is now
    mockserver/x.json). Anything persisted by an earlier version is stored under the OLD name and will
    NOT be restored after upgrading — the one-line migration is below.** The blob key was the ABSOLUTE
    local persistedExpectationsPath (for example /var/folders/.../persistedExpectations.json) and the
    configured blobStoreKeyPrefix was concatenated onto it with plain string addition. With the prefix
    shape the documentation recommends — blobStoreKeyPrefix="mockserver/", with a trailing separator —
    that composed mockserver//var/folders/.../persistedExpectations.json, and MinIO rejects the doubled
    // outright with HTTP 400, "Object name contains unsupported characters", so under that one
    configuration nothing was ever persisted and consequently nothing could be restored on restart. Under
    every other prefix shape the write SUCCEEDED and restore worked — a leading / is a legal byte in an
    S3 object name, and the read composed exactly the same name back — but the object was then named after
    the writing container's local filesystem layout, so a second instance that resolved
    persistedExpectationsPath to a different absolute path (started from a different working directory,
    or in a different container) looked under a different name and silently restored nothing. The key is
    now derived by the new shared org.mockserver.state.BlobKeys helper in mockserver-core: for every
    store other than FilesystemBlobStore the key is the FILE NAME of persistedExpectationsPath alone,
    and prefix and key are joined with exactly one separator, with any leading separator dropped and any
    repeated separators collapsed, so every prefix shape a user can configure — unset, mockserver,
    mockserver/, /mockserver/ — now produces the same valid object name
    (mockserver/persistedExpectations.json). That normalisation is applied for all
    put/get/list/delete operations wherever blobStoreKeyPrefix is applied, so it renames EVERY
    blob key, not only the persisted-expectations document. FilesystemBlobStore is unaffected: it
    interprets the key as a file path, so it keeps the absolute path and writes exactly the file it always
    did. Upgrading: because the old key always embedded the absolute local path and the new key is the
    bare file name, the object name changes for every non-filesystem persistence user, under every prefix
    shape and on every platform — there is no configuration in which the old name is preserved. On the
    first start after upgrading, the restore looks under the new name, misses (logged at INFO with the
    name it looked for), and the instance starts with no restored expectations; the next expectation change
    then writes a fresh object under the new name and leaves the old one behind. To carry existing state
    across the upgrade, copy or rename the object once before starting the new version, for example
    aws s3 mv s3://<bucket>/<prefix>/<old absolute path> s3://<bucket>/<prefix>/<file name> — otherwise
    accept the miss and let the first expectation change re-create it. One long-standing footgun goes away
    with the change: restoring after a restart no longer requires the two instances to resolve
    persistedExpectationsPath to the same absolute path, only to the same file name. Deployments that
    must NOT share state within one bucket should give each its own blobStoreKeyPrefix (or its own file
    name). Proven by a Docker-gated MinIO round trip that writes and then reads back an expectation with a
    trailing-slash blobStoreKeyPrefix (the exact configuration that returned HTTP 400 before), a MinIO
    put/get/list/delete round trip across all four prefix shapes, and Docker-free unit coverage of the key
    composition and of the key the persistence layer derives.
  • The configuration enforcement-evidence guard no longer certifies evidence it cannot see.
    ConfigurationEnforcementClassificationTest records, for every risky configuration property, the
    Class#method test that proves an instance-set value changes observable behaviour. It validated those
    pointers by loading the class — but it runs in mockserver-core, so any pointer naming a test in a
    sibling module was silently skipped on ClassNotFoundException. That exempted precisely the most
    valuable evidence, the end-to-end Layer C pointers: renaming, moving or deleting the referenced test
    left a dangling pointer and the guard still passed green, for maxRequestBodySize,
    maxResponseBodySize, wasmEnabled, redactSecretsInLog, clusterEnabled, dnsEnabled,
    grpcBidiStreamingEnabled, http3ConnectUdpEnabled and transparentProxyEnabled. A class that
    cannot be loaded is now resolved
    by locating its .java source under any module's src/test/java and asserting the file declares both
    the class and the referenced method, so cross-module pointers are checked in a full reactor build and
    when only some modules are built. The guard fails closed: a pointer resolvable by neither route is now
    a failure naming the dangling pointer, never a silent skip. Anti-vacuity assertions in the spirit of
    the sibling ConfigurationCallSiteGuardTest keep the scan honest — the set of pointers resolved by
    source scan must match the declared cross-module ratchet exactly, mockserver-netty and
    mockserver-state-infinispan must both have contributed, and classpath resolution must still cover
    the bulk of the pointers — so a scan that resolves nothing cannot pass. Verified by degrading a real
    mockserver-netty test method name: the guard now fails with a message naming the dangling pointer,
    where the previous version passed green with the identical defect in place.
  • gRPC trailing metadata is no longer silently dropped over HTTP/3. Every trailer an expectation
    authored — response().withTrailer("x-request-cost", "42"), and the gRPC chaos profile's
    customTrailers — reached an HTTP/1.1 or HTTP/2 client but never reached an HTTP/3 client at
    all
    , in every branch of the HTTP/3 gRPC response path (with a body and body-less, and both with
    and without proto descriptors loaded). The same expectation therefore produced different trailing
    metadata depending only on which transport the client happened to use, and there was no error or
    warning anywhere to indicate the loss — a test asserting on trailing metadata over HTTP/3 simply
    saw nothing. The cause was that Http3GrpcResponseWriter builds its HTTP/3 frames by hand rather
    than through MockServerHttpResponseToFullHttpResponse.mapResponseWithTrailers (which is what
    carries trailers on the other transports), and GrpcHttp3Adapter.buildTrailingHeadersFrame /
    buildTrailersOnlyFrame populated only grpc-status and grpc-message; the response's own
    trailers were never read. They are now emitted on the terminal frame: on the trailing HEADERS
    frame when the response has a body, and folded into the Trailers-Only frame when it does not,
    which is the shape gRPC defines for that form (HTTP-Status Content-Type Trailers, where
    Trailers includes custom metadata) and leaves the framing unchanged — there is still exactly one
    terminal frame, written with SHUTDOWN_OUTPUT, so an added trailer cannot cost the response its
    end-of-stream marker the way it did on HTTP/2 before the fix above. A user-authored trailer cannot
    override or spoof the transport's own status: the new shared
    GrpcResponseStatusResolver.passThroughTrailers (the trailer twin of passThroughHeaders)
    excludes grpc-status, grpc-message and grpc-status-name, mirroring the exclusion the HTTP/2
    path makes in remainingTrailers, and also excludes the connection-specific fields,
    content-length/content-type and pseudo-header names that RFC 9114 forbids in a trailer section.
    Trailer field names are lower-cased and CR/LF stripped from values before reaching the frame,
    because HTTP/3 field names must be lower-case and Netty's DefaultHttp3Headers rejects an
    upper-case one by throwing — so an expectation authoring withTrailer("X-Request-Cost", …) would
    otherwise have taken the client's entire response down rather than dropping a single field.
    Verified over the wire by two new tests in Http3GrpcIntegrationTest that drive a live in-JVM
    Netty QUIC client and read the metadata off the HEADERS frames it actually received, asserting
    which side each value arrived on (a trailer must not be folded into the initial headers, a
    response header must not be repeated as a trailer) and asserting the HEADERS-frame count so the
    metadata cannot arrive at the cost of correct framing, plus adapter-level coverage in
    GrpcHttp3AdapterTest. Positive control: neutering the trailer pass-through in production turns
    all seven new assertions red with the trailer absent.
  • A FILE response body is now served verbatim (and templated) on every response path, and a FILE
    request body is now actually matched (#2450).
    A response whose body is a FILE (a filePath with
    no template engine) was previously read on the static response action only; the same FILE body
    returned from an object callback, a class callback, a response template, or a forward
    responseOverride reached the wire unread, emitting the file path string instead of the file
    contents. Materialisation now lives in a single shared FileBodyMaterialiser invoked from the two
    response-write funnels (writeResponseActionResponse, covering the static, object-callback,
    class-callback, response-template and SSE paths, and writeForwardActionResponse, covering the
    forward responseOverride), so all five producers — and the shared WAR/servlet path — serve the file
    contents. Templated FILE bodies (a FileBody carrying a Velocity/Mustache templateType) are rendered
    against the request on these paths too, not only verbatim ones; a text content type yields the decoded
    string and a binary or absent content type yields the raw bytes intact. A missing or unreadable file
    now produces a clean, logged 500 whose body does not leak the path, instead of a broken connection or
    the path string. Separately, a FILE body used for request matching had no case in
    BodyMatcherBuilder, so the body constraint was silently ignored (it matched any body); it now matches
    the request body against the exact file contents (string or binary). This resolves the earlier
    "static response only" caveat.
  • A forward responseOverride that replaces the body is no longer truncated to the upstream
    Content-Length.
    When a forwarded request's response is overridden with a new body,
    HttpResponse.update() replaced the body but kept the Content-Length inherited from the upstream
    response. A replacement body longer than the upstream body was therefore truncated on the wire (and a
    shorter one could over-run) — visible only to a real client, since every layer above the encoder held
    the full, correct response. update() now drops the inherited Content-Length whenever the override
    supplies a new body (or a generateFromSchema), unless the override itself sets an explicit
    Content-Length (which is still honoured verbatim), so the encoder recomputes the length from the
    actual body. This closes the residual on the forward-override path left by the FILE-body fix above.
  • A gRPC error response carrying custom trailing metadata no longer loses its status. On HTTP/2 a
    body-less gRPC response is collapsed into the gRPC Trailers-Only form, which moves grpc-status
    into the initial HEADERS frame and relies on that frame being end-of-stream. When the expectation
    also authored a custom trailer with withTrailer(...), that trailer kept a separate trailing
    HEADERS frame alive, so the initial frame was no longer end-of-stream: a real client read it as
    ordinary headers (where grpc-status is ignored) and then found no status at all in the terminal
    frame, failing the call with UNKNOWN: missing GRPC status. In other words, adding a single
    trailer to an error response destroyed the error — the caller lost both the status code and the
    message. GrpcToHttpResponseHandler.asTrailersOnlyIfHttp2 now skips the Trailers-Only collapse
    whenever any user-authored trailer remains, keeping grpc-status/grpc-message in the trailing
    HEADERS frame alongside the custom metadata, which is the correct shape in that case. The same fix
    covers the gRPC chaos fault path, which produced the byte-identical broken shape: a fault response
    configured with customTrailers emits them as real trailers alongside grpc-status/grpc-message
    on a body-less response, so a chaos-injected error over HTTP/2 also reached the client as
    UNKNOWN: missing GRPC status instead of the configured status. Found by the new real-client
    trailing-metadata coverage described under Added.
  • gRPC-Web now re-frames matched-expectation responses correctly over a real HTTP/1.1 socket, and
    is covered by an over-the-wire integration test.
    Every previous gRPC-Web test drove the handler
    through an EmbeddedChannel and set x-grpc-web-content-type directly on the response, so none
    exercised the actual mock-matching path: there the marker lives on the request only and was lost,
    and a matched expectation went back to a browser client as application/grpc with grpc-status in
    HTTP trailers a gRPC-Web client cannot read. The original request content-type is now retained in
    the per-stream GrpcPendingRequests record alongside the resolved service/method, so
    GrpcToHttpResponseHandler re-frames the response as gRPC-Web (length-prefixed message frame + a
    0x80 trailer frame carrying grpc-status in the body, base64-encoded for the -text variant).
    A new GrpcWebOverTheWireIntegrationTest posts a real application/grpc-web and
    application/grpc-web-text framed request to a running server over a raw socket and asserts on the
    exact bytes a browser client would receive.
  • The AsyncAPI control-plane HTTP endpoints now have an over-the-wire integration test.
    PUT /mockserver/asyncapi, GET /mockserver/asyncapi and PUT /mockserver/asyncapi/verify were
    only exercised at the orchestrator/control-plane level, so a regression in the Netty →
    HttpStateAsyncApiControlPlaneRegistry routing or response wiring would not have been caught.
    A new AsyncApiControlPlaneIntegrationTest boots a real MockServer and drives all three endpoints
    over a raw socket without any live broker: it asserts the load response (201, loaded:true,
    channel count, zero publishers/subscribers), the status response (200, channels, counts, and the
    empty/unloaded case), and the broker-less verify verdict (406 with the "at least 1 … found 0"
    failure detail) plus the blank-body 400.
  • A StreamingBody response delivered to a real HTTP/2 inbound client is now covered end-to-end.
    NettyResponseWriter.writeStreamingResponse re-stamps the request's HTTP/2 stream id onto the
    streaming response head (the field is not part of the copied header multimap) and flushes each chunk
    as it arrives, but no test drove that path with a real HTTP/2 client — Http2SseStreamingIntegrationTest
    covered only the SSE sibling and explicitly noted the StreamingBody case was untested, while the
    existing streaming-relay tests drive an HTTP/1.1 inbound socket. A new Http2StreamingBodyIntegrationTest
    drives a real prior-knowledge h2c multiplex client through a streamingResponsesEnabled forward
    MockServer to an SSE upstream and asserts on the frames the client receives on its OWN request stream:
    both events arrive (proving the stream-id stamp — the #2419 class), and the early event's DATA frame
    arrives promptly as one of at least two separate, in-order DATA frames rather than being buffered into
    one. Degrading the write path to buffer chunks until stream completion was verified to make the
    incremental-delivery assertion go red.
  • PROXY-protocol destination resolution is now proven to drive transparent-proxy forwarding over a real
    socket.
    ProxyProtocolOriginalDestinationHandler was only exercised via EmbeddedChannel, which asserts
    the handler sets the REMOTE_SOCKET channel attribute but never that this attribute actually chooses the
    forward target end-to-end. A new non-privileged loopback ProxyProtocolForwardingIntegrationTest runs
    MockServer with transparentProxyEnabled=true and no fixed remote, opens a raw socket, writes a valid
    PROXY v1 TCP4 header naming a loopback EchoServer as the destination followed by a plain GET whose
    Host header points at an unrelated (closed) decoy port, and asserts the EchoServer reflects the request
    back — proving the PROXY-protocol REMOTE_SOCKET, and not the Host header, drives forwarding. The test
    needs no NET_ADMIN/privileged capability because the PROXY-protocol header is an application-level byte
    prefix. Verified as a genuine regression guard by a positive control: ignoring the PROXY-header
    destination turns the forwarding assertions RED, restoring it returns them GREEN.
  • A FILE response body with no template engine now serves the file contents, not the file path (#2450).
    A static response with a body of type FILE and a filePath but no templateType previously returned
    the literal file-path string as the response body instead of the file's contents; only adding a
    templateType (e.g. MUSTACHE) caused the file to actually be read. HttpResponseActionHandler now
    reads any FILE body that is not template-rendered — no templateType, or an unsupported one such as
    JavaScript — and serves its contents verbatim, preserving the declared content type. (A FILE body
    returned from an object/class callback, from a response template, or as a forward responseOverride
    bypasses this handler and is addressed separately.) Binary files (images, PDFs,
    archives, identified via the content type) are served as raw bytes so they are not corrupted by
    charset decoding; text files are served as-is with no template processing. A missing file fails the
    same way as the templated path.
  • The VS Code extension (mockserver-vscode) now compiles under TypeScript 7. TypeScript 7 no
    longer auto-includes every installed @types/* package, so @types/node's ambient declarations
    (the path/fs/crypto/child_process module globals, the NodeJS namespace, Buffer,
    process, console) were dropped and tsc -p ./ failed with 97 errors. The extension's
    tsconfig.json now explicitly opts @types/node back in via "types": ["node"] — the fix the
    compiler itself suggests — with no change to the extension's source or its published output. This
    is build tooling only and is not shipped to extension users.
  • The drift responseTimeThresholdMs performance-flag gate is now covered by behavioural tests.
    DriftAnalyzer.checkPerformanceDrift raises a PERFORMANCE drift record only when an expectation's
    observed p95 latency exceeds the instance-set responseTimeThresholdMs, but no test drove responses
    straddling that threshold, so a regression that flagged everything (or nothing) would not have been
    caught. A new DriftPerformanceThresholdTest feeds the real PercentileTracker latencies under and
    over the threshold and asserts on the production DriftStore outcome: the over-threshold case flags
    exactly one PERFORMANCE record (with expectedValue=<=threshold and the actual p95), the
    under-threshold and disabled (0) cases flag nothing, a slow-tail distribution whose p95 crosses the
    threshold flips, and the DriftAlertNotifier webhook fires only when the flag is raised and its
    severity meets the notifier threshold.
  • LLM provider codecs now have their emitted token-usage counts asserted, not normalized away. The
    LlmCodecGoldenFileTest golden drift harness deliberately zeroes usage blocks before comparing (usage
    counts are structural, not stable values), which meant the golden files alone could not prove a codec
    emits the correct token counts — a codec that silently regressed usage to 0, or swapped
    input/output, would still have matched its golden. A new
    LlmCodecGoldenFileTest.shouldEncodeCanonicalTokenUsageCounts closes that blind spot: for all seven
    chat/completion providers (OpenAI, OpenAI-Responses, Anthropic, Gemini, Bedrock, Azure-OpenAI, Ollama)
    it encodes the canonical text and tool-call completions and asserts the actual encoded token-count
    fields — named per each provider's published usage schema (prompt_tokens/completion_tokens/
    total_tokens, input_tokens/output_tokens, usageMetadata.promptTokenCount/candidatesTokenCount/
    totalTokenCount, Ollama's top-level prompt_eval_count/eval_count) — equal the hand-authored
    canonical Usage values (input 12 / output 8 for text, 25 / 15 for tool-call), using asInt(-1) so a
    dropped or missing field fails the equality rather than silently defaulting to 0.
  • GenAI span emission on the LLM SERVE path is now covered end-to-end. When MockServer serves a
    locally-mocked httpLlmResponse completion, HttpLlmResponseActionHandler emits an OpenTelemetry
    GenAI (gen_ai.*) span via GenAiSpans.recordCompletion(...) — a distinct code path from the
    forward/proxy-path emission already guarded by ForwardPathGenAiSpanEmissionTest, and previously
    untested end-to-end. A new ServePathGenAiSpanEmissionTest installs an InMemorySpanExporter
    through the GenAiSpanExporter.startWithProcessor(...) seam, drives a real MockServer serving an
    OpenAI-shaped completion, and asserts exactly one GenAI span carrying gen_ai.request.model,
    gen_ai.system, and the input/output usage-token attributes is produced by the production serve
    path (read back from the exporter, not reconstructed).
  • The HTTP parser limit maxHeaderSize is now covered by a behavioural test. The three parser
    limits (maxInitialLineLength, maxHeaderSize, maxChunkSize) are wired into the Netty
    HttpServerCodec in the HTTP/1.1 request pipeline (PortUnificationHandler.switchToHttp), but no
    test drove an over-limit request, so a regression that dropped the configured value and fell back to
    Netty's 8192-byte default — or removed the wiring entirely — would have gone unnoticed. A new
    HttpParserLimitsIntegrationTest starts a server configured with maxHeaderSize=1024 and drives raw
    HTTP/1.1 requests over a plain Socket against a header-conditional expectation. The
    client-observable effect of the limit is header truncation: Netty's decoder stops parsing at the
    byte that crosses the limit and drops every header after it (MockServer logs the decode failure but
    still serves the request from the headers it did parse). A control request whose marker header sits
    within the 1024-byte limit is parsed, matches, and returns the mocked 200; an otherwise-identical
    request with a ~2KB filler header inserted ahead of the marker pushes the marker past the boundary,
    so it is dropped, the request no longer matches, and MockServer returns 404. The filler size sits
    strictly between the configured limit and Netty's 8192-byte default, so the test is a genuine
    positive control: reverting the wiring to ignore the configured maxHeaderSize (using the default)
    lets the whole header block through, the marker survives, and the over-limit request matches and
    returns 200 — turning the test red (confirmed).
  • OpenAI Responses API previous_response_id chaining and GET /v1/responses/{id} retrieval are now
    covered end-to-end over a real socket.
    These stateful behaviours were previously exercised only at the
    handler+store level (OpenAiResponsesStateTest), so a regression in the wire path — the automatic
    storing of an issued response, the codec's reconstruction of a prior turn from previous_response_id,
    or the GET-based retrieval — would not have been caught. A new OpenAiResponsesStateEndToEndTest boots
    a real MockServer, POSTs a first /v1/responses turn (default store:true) and captures its response
    id, POSTs a second turn carrying only the new input plus previous_response_id, and asserts over the
    wire that the chained turn matches (proved via a whenTurnIndex(1) predicate that can only match once
    the prior assistant turn has been reconstructed) and that GET /v1/responses/{id} returns the stored
    response body.
  • The outputMemoryUsageCsv memory-usage CSV export is now covered by tests. MemoryMonitoring
    builds a CSV header from the buildStatistics() keys on construction and appends a data row on each
    logMemoryMetrics() call, but this export path had no test anywhere. A new MemoryMonitoringTest
    enables CSV output to a JUnit TemporaryFolder and asserts the file is created, its header row
    exactly matches the buildStatistics() column keys, a triggered data row has a matching column count
    with a positive numeric heapUsed value, and that NO file is written when outputMemoryUsageCsv is
    disabled.
  • TOKEN_BUCKET rate-limit enforcement is now covered end-to-end through the handler/wire path.
    Every 429-rendering test (HttpActionHandlerRateLimitTest, RateLimitIntegrationTest) previously used
    only FIXED_WINDOW; TOKEN_BUCKET was exercised solely at the registry level, so a regression that
    failed to render the synthetic 429 for a token-bucket limit would not have been caught. A new
    HttpActionHandlerRateLimitTest.tokenBucketBurstOfOneAllowsBurstThenReturns429 drives two immediate
    requests against a TOKEN_BUCKET limit with burst=1 and a negligible refill through the real
    HttpActionHandler, asserting the algorithm-specific behaviour: the burst of one is allowed (normal
    response), the second request exhausts the bucket and returns a 429 carrying
    X-RateLimit-Limit: 1 (the bucket burst), X-RateLimit-Remaining: 0, and the Retry-After/reset headers.
  • WASM custom-rule host-isolation is now pinned by a test. A WASM rule module can only reach the
    filesystem or any host/WASI capability through functions the host explicitly imports into the instance,
    and WasmRuntime deliberately wires NONE — it instantiates every module with a bare
    Instance.builder(module).build(), never withImportValues(...). No test asserted this, so a
    regression that started wiring host imports would have gone unnoticed. A new
    WasmRuntimeHostIsolationTest hand-assembles a minimal-but-valid module whose import section declares
    wasi_snapshot_preview1.fd_write and whose exported match actually calls it, then asserts chicory
    refuses to instantiate it (UnlinkableException, mirroring the runtime's own build call), that
    WasmRuntime.callMatch therefore fails closed to false, and — as a positive control — that supplying
    a stub fd_write host import (the wiring MockServer omits) makes the identical module instantiate. The
    refusal assertion was confirmed to go RED when the host import is wired and GREEN when it is not.
  • Request-side OpenAPI violations in trafficValidate are now covered by an integration test. Both
    existing TrafficValidateIntegrationTest cases only exercised response-schema violations, leaving the
    request-validation half of the traffic-validation path (OpenApiTrafficValidator
    OpenAPIRequestValidator) unverified end-to-end. A new
    shouldReportFailureWhenRecordedRequestViolatesSpec records a POST /pets whose body omits the
    required id/name fields (with a 201 response that conforms to the spec, isolating the failure to
    the request side) and asserts the resulting ContractReport surfaces a failing result carrying
    REQUEST validation errors.
  • The driftDetectionEnabled master switch is now covered by a behavioural enforcement test. The
    existing DriftDetectionConfigTest only re-implemented the gate boolean inline and never exercised
    the production code path, so a regression that ignored the flag would not have been caught. A new
    HttpActionHandlerDriftDetectionTest drives the real forward request path through HttpActionHandler
    — forwarding a request whose upstream response drifts (500) from a matching response-type stub (200) —
    and asserts that a STATUS DriftRecord IS recorded into the shared DriftStore when
    driftDetectionEnabled(true), and that NONE is recorded when driftDetectionEnabled(false) or the
    sample rate is zero. This asserts on the real drift-recording outcome rather than a hand-mirrored
    copy of the gate.
  • The transparent-proxy original-destination end-to-end suites are now collectable by CI. The three
    privileged interception suites — SoOriginalDstEndToEndIntegrationTest (iptables REDIRECT +
    SO_ORIGINAL_DST), TproxyEndToEndIntegrationTest (iptables TPROXY / IP_TRANSPARENT) and
    EbpfOriginalDestinationEndToEndIntegrationTest (pinned BPF map read path) — were previously named
    *EndToEndIT, a suffix that matches NEITHER Surefire's **/*Test.java include NOR Failsafe's
    **/*IntegrationTest.java include, so they were never compiled into a run set and never executed on
    any build. They are renamed to *EndToEndIntegrationTest so Failsafe collects them, and each now
    additionally SKIPS cleanly (rather than erroring) when the Docker daemon refuses to start the required
    NET_ADMIN / --privileged sibling container (e.g. a user-namespace-remapped daemon), via
    DockerCliTestSupport.containerStartRejected(...). A new opt-in CI step
    (.buildkite/scripts/steps/java-transparent-proxy-test.sh, RUN_TRANSPARENT_PROXY_E2E=true) runs
    them under the Docker socket and asserts via assert-suite-ran.sh that they actually executed; by
    default it prints a loud, visible notice that they were not run, because the standard build agents
    lack the docker CLI and reject --privileged containers.

Added

  • The mock-drift detection pipeline now has an end-to-end assembly test spanning the live forward
    through to the GET /mockserver/drift retrieval endpoint.
    The individual pieces (DriftAnalyzer,
    DriftStore, and the driftDetectionEnabled gate in HttpActionHandler) were unit-tested, but no
    test drove the assembled path the Drift dashboard actually depends on: a live forward whose upstream
    response differs from a co-registered response stub → asynchronous drift analysis → the process-wide
    DriftStore → the real control-plane GET /mockserver/drift handler that reads it back. A new
    DriftEndToEndAssemblyTest forwards a request through the real HttpActionHandler (upstream 500 vs a
    stub's 200, drift analysis forced to run synchronously), then serves GET /mockserver/drift through a
    real HttpState and asserts the returned JSON contains the recorded STATUS drift (both unfiltered
    and via the expectationId query filter the dashboard uses); a companion case proves a non-drifting
    forward leaves the endpoint empty. Registered in the sequential Surefire phase because it mutates the
    singleton DriftStore and PercentileTracker.
  • The WAR servlet decoder's RFC 6265 cookie surrounding-quote stripping now has direct coverage.
    HttpServletRequestToMockServerHttpRequestDecoderTest gains a test that feeds a
    jakarta.servlet.http.Cookie whose value carries surrounding double quotes ("quotedValue", as
    Servlet 6 / Tomcat 11+ preserves) alongside an already-unquoted value, and asserts the mapped
    HttpRequest cookies are quotedValue (quotes stripped) and plainValue (unchanged) — pinning the
    stripSurroundingQuotes(...) behaviour that every prior fixture left unexercised because it only used
    plain ASCII values a container never quotes.
  • The dashboard static-asset handler's default MIME-type fallback is now covered, extending the
    #2358 null-Content-Type NPE guard to unmapped file extensions.
    Every existing DashboardHandlerTest
    serves a mapped extension (.js, .svg), so MIME_MAP.getOrDefault(extension, DEFAULT_MIME_TYPE)
    never exercised its fallback arm — the exact branch that turns an unmapped extension into a valid,
    non-null application/octet-stream header instead of the null value that crashes Netty's header
    encoder. A new test serves a synthetic unmapped-fixture.webp (an extension deliberately absent from
    both MIME_MAP and the string-content list) and asserts the served response is found (not the 404
    not-found response) and carries Content-Type: application/octet-stream.
  • The BCKeyAndCertificateFactory IPv6 Subject-Alternative-Name branch is now covered, closing the gap
    where only IPv4 SAN IPs were exercised.
    BCKeyAndCertificateFactoryBehaviourTest gains
    shouldIncludeIPv6AddressesInSAN, which configures sslSubjectAlternativeNameIps("127.0.0.1", "::1", "2001:db8::1"), generates the leaf certificate, and asserts the generated cert's iPAddress SAN entries
    (GeneralName type 7) contain both IPv6 addresses AND the IPv4 address in the same certificate. Assertions
    compare via InetAddress so they are independent of the JDK's canonical string form for IPv6 (e.g.
    ::1 -> 0:0:0:0:0:0:0:1). This pins the IPAddress.isValidIPv6/isValidIPv6WithNetmask branch of the
    SAN-IP handling, previously reachable only through IPv4 literals.
  • Times exhaustion now has a direct passive-removal assertion, mirroring the existing time-to-live
    test.
    AbstractControlPlaneIntegrationTest gains shouldRemoveExhaustedTimesFromActiveExpectations
    next to shouldRemoveExpiredTimeToLiveFromActiveExpectations: it registers an expectation with
    Times.exactly(1), asserts retrieveActiveExpectations(null) reports one active expectation, makes the
    single matching request that exhausts the Times, then asserts the active list is now empty — WITHOUT a
    second request. Previously exhausted-Times removal from the active list was only observed indirectly
    via the wire 404 (a second request no longer matching); this pins that an exhausted Times expectation
    is dropped from the active list itself.
  • The OpenAPI forward-validate action's LOG_ONLY mode now has behavioural passthrough coverage,
    closing the gap where only the getter was asserted.
    HttpForwardValidateActionHandlerTest gains two
    tests that drive handle(...) with validationMode = LOG_ONLY: one sends a schema-violating request
    and proves the bad request is still forwarded upstream (verify(mockHttpClient).sendRequest(...)) and
    the upstream 200 flows back unchanged (not a 400); the other stubs a schema-violating upstream response
    and proves it is returned unmodified (not a 502). These pin the "validate and log, but forward
    unmodified" behaviour that distinguishes LOG_ONLY from the already-covered STRICT reject branches.
  • The Http2StreamIdAuditHandler safety-net is now covered by a unit test, so the guard against the
    "HTTP/2 response head written without an x-http2-stream-id" defect class (GitHub issue #2419 and its
    SSE / streaming-body / metrics / MCP siblings) can no longer silently stop warning.
    The handler is
    the only thing that turns a mis-routed, silently-dropped HTTP/2 response into a loud WARN, yet it had
    no test anywhere. A new Http2StreamIdAuditHandlerTest drives the handler on an EmbeddedChannel with
    a capturing logger and asserts the observable behaviour across three cases: an unstamped response head
    logs exactly one WARN naming the missing header, a correctly-stamped head logs nothing, and a second
    unstamped head on the same connection does NOT warn again (the per-connection dedup that stops a
    genuinely-broken write site from flooding the log). Suppressing the warn reddens the first and third
    assertions.
  • The Velocity velocityDisallowClassLoading sandbox now has coverage for taking effect when toggled
    on an ALREADY-CONSTRUCTED engine, not just when a fresh engine is built.
    The existing test flipped
    the setting and then built a brand-new VelocityTemplateEngine, so the runtime rebuild-on-live-engine
    path (currentEngineHolder() rebuilding the underlying VelocityEngine with the SecureUberspector
    when the configured flag differs from the flag the current engine was built with) was never exercised
    — meaning a regression that made the setter/system-property/PUT /mockserver/configuration toggle
    inert on a cached engine would have reddened nothing. A new test builds ONE engine with class loading
    allowed, renders a class-loading template and asserts it genuinely EXECUTES the class-loading line
    (reaching Runtime.exec), then flips velocityDisallowClassLoading(true) on the SAME configuration
    and re-renders through the SAME engine, asserting the class-loading line is now BLOCKED (inert, empty
    body, no exception) — proving the live rebuild applies the new restriction.
  • The metrics endpoint's ENABLED path is now proven end-to-end over the handler, not just its
    disabled 404 and CORS behaviour.
    Previously the only enabled-path coverage was a mock-ctx unit
    test (MetricsHandlerTest) that asserted the content-type header was non-null but never that
    GET /mockserver/metrics returns 200 with a real Prometheus exposition body. Two new tests in
    HttpRequestHandlerTest drive the request through the real HttpRequestHandler routing and
    MetricsHandler, with metrics enabled and the mock_server_requests_received counter incremented:
    they assert the response is 200 (mapping it through the same wire encoder the server uses, since the
    handler writes a status-less response the encoder resolves to 200 OK) and that the body carries the
    mock_server_requests_received_total series. A second case sends an OpenMetrics Accept header and
    asserts the negotiated OpenMetrics content-type, complementing the existing
    shouldReserveMetricsPathWithCORSWhenMetricsDisabled negative (disabled -> 404) so the enabled path
    is provably the difference.
  • The reflective cloud-blob-store auto-discovery path in StateBackendFactory now has direct test
    coverage.
    Previously StateBackendFactoryTest only instanceof-checked the filesystem/memory blob
    stores, so discoverBlobStoreBackend(...) and the BLOB_STORE_REGISTRARS map — the reflective
    blobStoreType=s3Class.forName(...S3BlobStoreRegistrar)register() → factory create()
    chain — were exercised by no test, and a broken registrar-class name or map wiring would have redded
    nothing. Two layers now cover it: a new S3BlobStoreDiscoveryTest (in mockserver-blob-s3, which has
    the S3 module on its classpath) configures blobStoreType=s3 and calls StateBackendFactory.create(...)
    with NO manual register(), asserting the resulting backend's blobs() is an S3BlobStore — provable
    only if discovery loaded the module reflectively (no network/Docker; the S3 client is built lazily); and
    StateBackendFactoryTest gains a core-only assertion that blobStoreType=s3 with the module ABSENT
    fails hard with the documented IllegalStateException ("add the mockserver-blob-s3 dependency") plus a
    case proving an unrecognised type is rejected with the supported-types guidance.
  • The dashboard WebSocket frame now has a cross-boundary STRUCTURAL contract test, closing the gap
    where the server and the UI were tested against separately-authored payloads and could silently
    drift apart.
    A single checked-in contract file (mockserver-ui/src/__fixtures__/dashboardFrameContract.json)
    lists, for every one of the four dashboard panels (log messages, active expectations, received and
    proxied requests), the fields the UI store/panels actually read and their JSON types. The server side
    (DashboardWebSocketFrameContractTest) drives the REAL DashboardWebSocketHandler across all four
    panels, captures the frame it emits, and asserts every required field is present with the correct type
    and that the server-assigned key correlations hold (a received-request row and its originating log
    entry share the same server log id). The UI side (dashboardFrameContract.test.ts) feeds the same
    file's representative frame through the real store applyMessage and asserts the resulting items
    expose those same fields. Because both read the one file, renaming or removing a field name reddens
    both tests; a server-side field rename reddens the Java test. Unlike the previous byte-equal captured
    golden (which passed locally but drifted in CI), the check is a per-field SUBSET assertion — immune to
    non-deterministic emission ordering, to timestamp/UUID/port/hostname values, and to
    environment-dependent extra fields — and a companion assertion captures the frame twice and proves a
    value-blind, order-independent structural fingerprint is identical across the two captures.
  • The REAL built dashboard bundle is now proven to be packaged and served, not just synthetic
    fixtures.
    A new integration test starts a live MockServer, GETs /mockserver/dashboard, and
    asserts the response is the genuine React application shell (the id="root" mount point and the
    MockServer Dashboard title) that references a hashed JS entry chunk (assets/index-<hash>.js),
    then GETs that referenced asset and asserts it is served (200) with a JavaScript content-type.
    Previously the only dashboard-serving coverage used synthetic test fixtures placed at the same
    classpath path the build-ui Maven profile copies the real Vite output into, so a broken or missing
    real bundle (for example a Monaco-worker regression) reddened nothing. The test fails closed when the
    built bundle is present but broken (missing hashed reference, or the referenced asset is not served),
    and skips with a clear message only when the build-ui profile did not run and no real bundle is on
    the classpath.
  • The served dashboard now has real-browser end-to-end coverage against a live MockServer. A new
    Playwright suite (mockserver-ui/e2e/) boots the runnable netty JAR — which serves the dashboard,
    the /mockserver/* control plane, and the _mockserver_ui_websocket feed on one origin — loads the
    dashboard in headless Chromium, and asserts real end-to-end behaviour: (1) an expectation authored in
    the composer UI matches a request fired over the wire, which then streams into the log panel live over
    the real WebSocket; and (2) expectation create, update, and clear driven through the dashboard change
    the server's own active-expectation list, verified over real REST (PUT /mockserver/retrieve).
    Previously the dashboard had no browser-level coverage at all — all 3,000+ UI tests run in jsdom with
    a mocked fetch and a hand-written WebSocket, so expectation CRUD and the live log stream were never
    exercised against the actual endpoints. Wired into the UI pipeline (.buildkite/pipeline-ui.yml) as a
    fail-closed CI gate that builds the current JAR, boots it, and runs the suite in the Playwright image.
  • Enabling tlsMutualAuthenticationRequired at runtime is now proven to be enforced over a real TLS
    socket.
    A new integration test starts a live MockServer with mutual TLS OFF, confirms a
    certificateless client completes the handshake, then requires mutual authentication at runtime on the
    already-listening instance and asserts that a new certificateless connection is refused at the
    handshake (fatal alert), while a client presenting a certificate trusted by MockServer's CA still
    connects — proving the runtime change applies ClientAuth.REQUIRE selectively rather than being
    silently ignored or breaking TLS altogether. Previously the runtime-reconfiguration path was covered
    only by a unit test asserting the cached SslContext instance was replaced (which cannot assert the
    resulting ClientAuth), while the wire-level client-authentication tests all enabled mutual TLS at
    startup, so the enforcement outcome of a runtime enable was never asserted over the wire.
  • The OpenAPI validation-proxy enforce path is now proven end-to-end over a real socket. A new
    integration test stands up a validation proxy (validateProxyOpenAPISpec + validateProxyEnforce)
    that forwards unmatched traffic to a second (upstream) MockServer, then drives real requests through
    it and asserts on the bytes the client receives: a schema-invalid POST /pets is rejected with 400
    ("OpenAPI request validation failed") and never reaches the upstream, a conformant request is
    forwarded and served normally, and a non-conformant upstream response is rejected with 502
    ("OpenAPI response validation failed"). Previously the enforce branch was only re-implemented inline
    in a unit test, so the production short-circuit in HttpActionHandler.validateProxyRequest /
    validateProxyResponse was never exercised over the wire.
  • The core mocking-action matrix is now proven over cleartext HTTP/2 (h2c) with a real client. A new
    integration test drives a real prior-knowledge Netty HTTP/2 multiplex client over a socket against the
    insecure port and asserts on the bytes the client receives on its own stream for each action: a
    respond action delivers its status and body, a class callback delivers its produced body, a
    forward action relays the upstream body back, and an error action resets the stream with the
    configured HTTP/2 error code. Previously the full action matrix ran only over HTTP/1.1 and
    h2-over-TLS; h2c was exercised only by an EmbeddedChannel pipeline-shape test and gRPC-unary, so no
    test proved a real cleartext-HTTP/2 client actually received the response body for these actions (the
    streaming / stream-id sibling of gRPC issue #2419). The shared integration harness cannot cover this
    because its client has no h2c prior-knowledge path — an insecure request tagged HTTP/2 silently falls
    back to HTTP/1.1.
  • The core mocking-action matrix is now proven over HTTP/3 (QUIC) with a real client. A new
    integration test drives a live Netty HTTP/3 client over QUIC against an HTTP/3-enabled MockServer and
    asserts on the bytes the client receives on its own request stream for each action: a respond action
    delivers its status and body, a class callback delivers its produced body, a forward action relays
    the upstream body back, a forwardOverride (overridden-forwarded-request) action rewrites the request
    and relays the overridden body back, and an error action resets the QUIC request stream (RFC 9114
    RESET_STREAM) instead of returning a response. Previously the HTTP/3 tests covered trace-context,
    mTLS capture, gRPC, streaming, MCP and lifecycle but none drove the forward / forwardOverride /
    callback / error matrix over QUIC or proved the forwarded/callback body reached the client over
    HTTP/3. The shared integration harness cannot cover this because its client has no HTTP/3 request path.
    Skips cleanly where the native QUIC transport (BoringSSL) is unavailable.
  • Interactive breakpoints are now proven end-to-end over a live transport. A new integration test
    starts a running server, opens the real breakpoint callback WebSocket via MockServerClient.addBreakpoint,
    and drives a real JDK HTTP client through the full pause -> dispatch -> resolve loop: a RESPONSE-phase
    breakpoint whose client handler rewrites the matched mock response has the originating caller receive the
    modified status and body (not the original), and a REQUEST-phase breakpoint whose client handler returns a
    response ABORTs before the mock is generated so the caller receives the abort response instead. Assertions
    are made only on what the originating HTTP client reads back from the running server, so a pass proves the
    pause/resume/modify actually happened server-side. Previously breakpoints were exercised only by client
    unit tests (mocked HTTP client) and registry/handler tests over EmbeddedChannel; no test connected the
    real callback WebSocket client to a running server and drove a live request through a pause-resolve cycle.
  • VCR cassette replay is now covered end-to-end at the data plane. A new integration test loads a
    cassette (recorded request -> response pairs) through the load_expectations_from_file tool into
    a running server, then drives real requests over a socket and asserts on the bytes the client
    receives: a matching request is served the recorded response body, a request matching no recorded
    entry falls through to 404 rather than borrowing another entry's response, and when a volatile
    request-body field (e.g. request_id) is normalised away a live request carrying a different
    volatile value still matches and is served the recorded body. Previously the cassette tests loaded a
    fixture and asserted only the control-plane ACTIVE_EXPECTATIONS echo, never proving a recorded
    response was actually served.
  • SNI-driven per-host server-certificate selection is now proven end-to-end over a real TLS
    handshake.
    A new integration test opens an actual TLS connection presenting a chosen, non-default
    SNIHostName, then reads the served peer certificate and asserts its Subject Alternative Names
    contain that host — and repeats with a second distinct SNI host on the same running server to prove
    the certificate is regenerated per host. Previously this path was exercised only through
    SniHandlerTest (an EmbeddedChannel asserting the hostname was added to the SAN configuration
    set); no handshake test connected with a chosen SNI host and inspected the certificate the server
    actually served.
  • The forward/proxy-path GenAI span emission is now covered end-to-end through a running server.
    A new test boots a real forwarding MockServer that proxies a chat-completions POST to an upstream
    MockServer stubbed as an OpenAI endpoint, and reads the emitted span back out of an in-process
    InMemorySpanExporter wired into the process-wide tracer — so the assertion exercises production
    HttpActionHandler.emitForwardGenAiSpan (provider sniffing, response parsing, span recording) rather
    than reconstructing it. Previously the forward path's span emission was only guarded by a core test
    that hand-mirrored the production logic and never drove the running server.
  • gRPC client-streaming and bidirectional-streaming are now covered by a real grpc-java client
    end-to-end.
    A new integration test drives an actual io.grpc channel over h2c and asserts on the
    bytes the client deframes — a single collected response for client-streaming, and two interleaved
    replies plus the terminal grpc-status trailer for bidi. Previously these two RPC shapes were
    exercised only through EmbeddedChannel, the same mocked seam that let issue #2419 ship for the
    unary and server-streaming paths.
  • The SPY and CAPTURE operating modes are now covered end-to-end at the data plane. A new
    integration test drives an unmatched request through each mode and asserts the documented
    behaviour: the request is proxied to the real upstream (the client receives the upstream body) and
    the exchange is recorded so it can be retrieved as an expectation. The test proves the operating
    mode is the decisive factor — the same request returns 404 in SIMULATE mode and is only proxied
    and recorded once the mode is switched to SPY or CAPTURE.
  • The breakpoint and verification forms accept the same search syntax as the Traffic view. A quick
    scope box on both forms takes method:, path: and host: terms and fills the matcher fields from
    them, so the operator vocabulary learned in the Traffic search works when writing a breakpoint
    condition or a verification. It only ever fills the form — every existing field, including full
    regex paths and the header, query-parameter and cookie matchers, still works exactly as before, and
    a term using an operator the form cannot express (such as status:) applies nothing rather than
    half of itself. Path globs are translated to the regex form MockServer matches paths with, so
    path:/api/* selects the same requests in the form as it does in the search box.
  • Chaos host targeting now rejects targets that could never fire. MockServer matches a chaos host
    exactly (case-insensitively, ignoring the port), so a wildcard, a pasted host: search operator, a
    URL scheme or a path silently produced a registration that appeared active and never faulted a
    request. All four places a chaos host can be entered — the HTTP and TCP register forms, the Quick
    Chaos strip and each stage of a chaos experiment — now refuse those with an explanation. The
    experiment case mattered most: a dead wildcard there produced a completed experiment reporting a
    clean resilience verdict having injected no faults at all.
  • The dashboard supports multiple workspaces in one browser tab. Investigating two things at once
    meant losing your filters every time you switched between them, because the whole window shared one
    view and one set of search terms. A workspace now bundles the current view and the five panel search
    terms, so you can keep a filtered Traffic investigation in one and a Log Messages search in another
    and switch between them without either losing state. Workspaces can be named, and are restored on
    reload. The switcher row appears only once a second workspace exists, so a single-workspace user
    sees no change beyond one new app-bar icon, and existing persisted view and search settings carry
    over into the first workspace on upgrade. Captured data, the connection target, the request filter
    and the theme stay shared — a workspace is a lens over one server's data, not a second connection,
    and targeting a different MockServer instance per workspace is not yet supported.
  • The dashboard recognises GraphQL operations in captured traffic. Every GraphQL request is a
    POST /graphql, so the Traffic and Log views showed a wall of identical rows and the operation name
    — the only thing distinguishing them — was buried in the body. Rows carrying a GraphQL request now
    show the operation type and name as a chip, and the shared operation: search operator filters by
    name (globs supported), so operation:Get* narrows to the queries you care about. The name is read
    from the operationName member when present and otherwise parsed out of the query document itself,
    which is where it usually lives. Detection is deliberately strict — an ordinary JSON body that
    happens to carry a query key is not treated as GraphQL — and parsing is bounded and never throws,
    so a large, binary or malformed body degrades to no chip rather than an error.
  • The dashboard Traffic view can focus on a single upstream host. In proxy mode a session can
    capture traffic from dozens of hosts. A collapsible host list at the top of the traffic list shows
    each distinct host with its request count, busiest first; clicking one pins host:<value> into the
    search box and narrows the list, and clicking it again unpins. Pinning composes with whatever else
    is in the search box rather than replacing it, and because the pin is just a search term it persists
    across a view switch and a reload like any other search. The list appears only when captured traffic
    spans more than one host, so mock-only sessions — where everything targets localhost — are
    unaffected. Hosts are grouped by the same value the row displays and the host: operator matches.
  • The dashboard expectation composer can fire a real request against the draft matcher and show the
    live response.
    A "Try It" button beside "Test Matcher" opens an inline panel that derives an
    editable HTTP request from the expectation being authored, sends it to MockServer, and renders the
    status, headers, body and round-trip time. Because a matcher is a pattern rather than a request,
    only exact non-negated values are pre-filled: regex, glob, schema, JSON-path, XPath and negated
    matcher forms — and the numeric-comparison and content-negotiation forms used by header and query
    matchers — are left blank and listed as underivable, so a pattern is never fired verbatim as though
    it were a literal. Headers the browser forbids a page from setting (Cookie, Host,
    Content-Length and the rest of the Fetch forbidden list) are named as unexercisable from the
    dashboard rather than silently stripped by fetch. The dashboard is served by the same listener
    that serves mock traffic, so the default target is same-origin; selecting one of the server's other
    bound ports raises a CORS warning up front and distinguishes a CORS block from an unreachable port
    when a send fails.
  • The dashboard TCP chaos form offers named network-condition presets. Seven one-click presets —
    dial-up, Slow/Fast 3G (throughput and latency variants), satellite and a fragmented link — fill the
    TCP chaos latency, bandwidth or fragmentation field for the host being registered. Throughput and
    latency figures are anchored to Chrome DevTools' throttling profiles and every preset shows its
    concrete numbers in the picker, since names like "3G" carry era-dependent implicit values. Because
    MockServer's TCP chaos engine applies only the highest-priority configured fault
    (down > reset_peer > limit_data > slicer > bandwidth > latency) rather than composing them, each
    preset sets exactly one fault, so the panel never advertises a number the engine would discard;
    throughput presets also show the read size below which the bandwidth ceiling has no effect. The
    panel notes that TCP faults shape inbound request bytes only, not the response, and that latency is
    charged per read rather than per round trip.
  • The dashboard search operators are now a shared, extensible filter DSL, and a search box no longer
    offers an operator it cannot honour.
    The status:/method:/path: vocabulary was hard-coded into
    the traffic/expectation/request search matcher; it is now a field registry (lib/filterDSL.ts) where
    each field declares how to resolve its value and whether it supports numeric comparison or glob
    matching. The three existing operators behave exactly as before. Two new fields ship with it —
    host: (glob, from the request Host header, resolved identically to the Traffic view's own host
    column) and operation: (glob, from a request body operationName). A call site can now declare
    which subset of operators it supports: the Log Messages panel declares none, so its placeholder
    advertises only /regex/, and typing status:>=400 error there marks the field invalid and explains
    that no field operators apply, instead of silently returning an empty list.
  • The declarative rateLimit expectation clause is now enforced on streaming response actions.
    Previously the general-purpose rateLimit clause was applied only to buffered RESPONSE/FORWARD
    actions, so a matched SSE_RESPONSE, GRPC_STREAM_RESPONSE or WEBSOCKET_RESPONSE was never
    throttled. The same rateLimitResponseOrNull check now runs once per matched request at the top of
    each of those three stream cases, so an over-limit request receives the deterministic 429 (with
    Retry-After and X-RateLimit-* headers) instead of opening the stream; within the limit the stream
    proceeds unchanged. Reuses the existing RateLimitRegistry (no second implementation). The
    LLM_RESPONSE action keeps its own token-based TPM/TPD limiter and is unaffected.
  • The generic CRUD simulation GET-list endpoint now supports pagination, sorting and field filtering.
    The list path accepts optional query parameters — filterField+filterValue (case-insensitive
    equality on a dot-separated attribute path), sortBy+sortOrder (asc/desc, missing values sort
    last, stable), and page+size (0-based page, size≤0 means no limit) — applied in the order
    filter → sort → paginate. Malformed parameters return a 400. When any list parameter is active the
    response adds X-Total-Count, X-Page and X-Page-Size headers; a plain list request with no
    parameters returns the exact legacy response (unchanged body, no extra headers). This is the generic
    CRUD store's own query surface and is independent of the SCIM list callback's sorting/filtering.
  • Interactive breakpoints support an optional maxHits one-shot / bounded budget. A breakpoint
    registered with "maxHits": 1 pauses once and then auto-deregisters, so the next matching request
    is no longer intercepted; "maxHits": 3 fires three times then removes itself. Only real pauses
    count against the budget, so maxHits composes with skipCount (hits skipped by a skipCount
    window do not consume the budget). Absent (or 0/negative) keeps the legacy behaviour of never
    auto-deregistering. maxHits is validated as a positive integer (400 otherwise) and is echoed by
    PUT /mockserver/breakpoint/matcher and listed by GET /mockserver/breakpoint/matchers.
  • Sixteen implemented control-plane endpoints are now described by the OpenAPI specification, and
    therefore by the published Postman and Bruno collections, which are generated from it:
    GET /mockserver/ready, GET /mockserver/config, GET /mockserver/proxyConfiguration,
    GET /mockserver/http3status, GET /mockserver/metrics, GET /mockserver/cluster,
    GET /mockserver/chaosExperiment/history, PUT /mockserver/recordings/promote,
    PUT /mockserver/pact/import, PUT /mockserver/baseline/compare, PUT /mockserver/trafficValidate,
    GET /mockserver/llm/optimisationReport, PUT /mockserver/llm/diffRuns, POST /mockserver/wasm/test,
    DELETE /mockserver/wasm/modules and the MCP endpoint /mockserver/mcp. All of these were
    implemented and reachable but documented nowhere, so they were absent from the collections and from
    any client generated from the spec. GET /mockserver/http3status had no documentation at all
    anywhere. Each signature was verified against its handler rather than transcribed, which corrected
    four things a plausible reading would have got wrong: GET /mockserver/metrics deliberately has
    no bare /metrics alias (unlike its siblings, because /metrics is a plausible path for a
    user's own mocked API and reserving it would shadow their expectation); PUT /mockserver/trafficValidate
    accepts specUrlOrPayload as an alias for spec and can answer 403 and 503, not just 200/400;
    PUT /mockserver/llm/diffRuns treats an empty body as an empty filter rather than rejecting it; and
    the MCP endpoint reports a missing or invalid session as a JSON-RPC error inside a 200, with
    GET /mockserver/mcp a hard 405 rather than an SSE stream.
  • The Expectation schema now declares all eleven properties it was missinghttpLlmResponse,
    grpcStreamResponse, grpcBidiResponse, binaryResponse, dnsResponse,
    httpForwardValidateAction, httpForwardWithFallback, beforeActions, afterActions, steps and
    capture — together with the supporting component schemas. The published specification described a
    substantially smaller API than MockServer implements, so the LLM, gRPC streaming, gRPC bidi, binary
    and DNS actions could not be expressed by any client generated from it. Note this was a
    documentation gap only: the OpenAPI document is served verbatim and never parsed at runtime, and
    incoming expectation JSON is validated against org/mockserver/model/schema/expectation.json, which
    already declared all eleven — so these expectations were always accepted on the wire.
  • New OpenApiSpecExpectationSchemaTest guards the specification against the Java model. The
    existing OpenApiSpecSyncTest asserts the two copies of the spec are byte-identical, which makes
    them one document but is blind to both copies being wrong together — which is exactly how the eleven
    properties above went missing. The new test drives the comparison from ExpectationDTO, using the
    same Jackson ObjectMapper that serialises expectations at runtime, so it fails when the server
    gains a property the spec does not declare. It deliberately does not enumerate the schema and look
    for matching Java fields: a test whose cases come from the artefact it polices cannot detect an
    omission in that artefact. The reverse direction is asserted too, which is the shape that would have
    caught HttpChaosProfile.connectionDrop — documented, implemented nowhere, and propagated into the
    Go client where users set a property the server silently ignored.
  • New OpenApiSpecEndpointCoverageTest asserts every control-plane route the server dispatches is
    described by the specification.
    This is the guard whose absence let the sixteen endpoints above go
    undocumented: nothing compared the routes to the document. It extracts the route literals from the
    canonical request.matches("METHOD", PATH_PREFIX + "/path", "/path") dispatch shape in
    HttpState and HttpRequestHandler and checks each against the spec's paths. Because the control
    plane is dispatched by an if / else if chain rather than a route registry, there is no structured
    object to enumerate and the extraction has to read source text — so the test also asserts a floor on
    the number of routes it finds. That floor is the point: a refactor that changes the call shape then
    fails loudly, instead of silently extracting zero routes and passing while guarding nothing. Three
    dispatch mechanisms are deliberately out of scope rather than approximated (/mockserver/metrics,
    matched by regex; /mockserver/mcp, matched by prefix; and the four templated {name} routes); all
    are documented, just not machine-checked. Covering them cleanly needs a route registry the
    dispatcher and the test can both read, which is the durable fix.
  • New end-to-end tests for PUT /mockserver/crud and PUT /mockserver/debugMismatch, the two
    least-defended endpoints in the control plane, both of which previously had no test reaching the
    server's HTTP dispatch at all. /crud is covered behaviourally rather than by status code: the test
    registers a resource and then drives POST/GET/PUT/PATCH/DELETE against the registered base path,
    asserting auto-increment continues past seeded ids, PATCH merges without clobbering, insertion order
    holds, deletes 404 afterwards, and the UUID strategy yields non-numeric ids under a custom idField.
    Both endpoints' bare aliases (/crud, /debugMismatch) are covered, as are their error paths.
  • CI now fires every generated API-collection example at a live MockServer. The existing
    collections gate regenerates the Postman and Bruno collections and diffs them against the committed
    copies, which proves the generator is deterministic and the artifacts are current — but proves
    nothing about whether the documented examples actually work. An endpoint whose requestBody is
    required: true with no example generates a bodyless request; the committed collection contains
    it, regeneration reproduces it exactly, and the gate is green while every user who imports the
    collection gets a 400. That had happened to /mockserver/baseline/compare and
    /mockserver/pact/import, both now fixed with examples. scripts/collections/test_collections.py
    already existed and was wired into no pipeline; it now runs as its own step. The step starts
    MockServer on the agent and runs the checker over --network host rather than mounting the Docker
    socket, because run-in-docker.sh always withholds the socket from PR builds — a socket-based
    wiring would have silently degraded to "cannot start a server" on exactly the builds that most need
    checking, which is the same defect as the cloud-storage contract suites that skipped on 100% of CI
    builds while reporting green. Examples that are known to be rejected today are listed in
    KNOWN_FAILING as a ratchet rather than an exemption list: each entry carries a reason, and an entry
    that stops failing fails the run, so the list can only shrink.
  • New javascriptAllowedClasses — an ALLOW-list for the classes JavaScript templates may resolve via
    Java.type(...).
    When set it takes precedence over javascriptDisallowedClasses and nothing outside the
    list can be resolved. Entries match a class name exactly or, when they end in .*, as a package prefix
    (e.g. java.util.*). An allow-list is the only form that is safe by construction: the existing deny-list
    matched class names by exact string equality, so denying java.lang.Runtime still left
    java.lang.ProcessBuilder — and Class.forName reach-through — available. Both lists now also support
    package prefixes. The default is unchanged (no restrictions) so existing templates keep working; setting
    javascriptAllowedClasses is the recommended hardening step for any instance that renders templates from
    a source you do not fully control. Behavioural tests cover all three semantics: only listed classes
    resolve (java.lang.Runtime, java.lang.ProcessBuilder, java.lang.Class.forName(...) and the explicit
    Java.type('java.lang.Runtime') form are all refused at render time), the allow-list wins when a class is
    on both lists, and a .*/. package prefix matches the package it names without leaking into a sibling
    package that merely shares its leading characters.
  • New wasmExecutionTimeoutMillis (default 5000) — a wall-clock execution budget for WASM custom rules.
    WASM modules ran with no fuel, timeout or interrupt, so a module containing an unbounded loop pinned the
    calling thread permanently; because WASM rules are evaluated during request matching this could wedge
    matcher threads. An invocation exceeding the budget is now aborted and fails closed (treated as a
    non-match). Set to 0 to restore the previous unbounded behaviour. Both this and the existing
    wasmMaxMemoryPages are now read from the live configuration at the point of use, so setting either on a
    Configuration instance or via PUT /mockserver/configuration takes effect — previously both were read
    from the static property store, so only the system-property route worked while the others were accepted
    and ignored. wasmEnabled is read the same way for the same reason.
  • CI now guards that every client library pins the same MockServer binary version. Each client
    decides for itself which server binary its launcher downloads, through seven different mechanisms,
    and three of them had no release-time bump at all: the Python and PHP launchers sat at 7.1.0 and
    the Rust crate at 7.3.0 while the project released 7.4.0, so those clients silently downloaded
    a three-minor-old server and the shared binary cache the documentation promises was never shared.
    All three are now corrected to 7.4.0, scripts/release/prepare.sh bumps them (hard-failing if a
    pattern no longer matches), and .buildkite/scripts/steps/clients-version-consistency.sh asserts
    agreement so drift is caught between releases rather than at the next one. The check is emitted
    unconditionally by generate-pipeline.sh rather than behind a changed-path filter: the pins it
    guards live in per-client directories, so a commit that drifts one routes only to that client's own
    pipeline and would never reach a path-filtered gate — the drift vector and the guard would never
    meet. The expected version is read from the topmost released changelog.md heading, so it also
    works on shallow, tagless CI checkouts.
  • Event-log eviction is now observable. MockServerEventLog.getEvictedLogEntryCount() reports how many
    entries have been discarded because the log reached maxLogEntries (or maxEventLogSizeInBytes), a WARN is
    logged once on the first eviction (naming the current maxLogEntries and the fact that verifications are
    affected), and the count is mirrored to the mock_server_evicted_log_entries Prometheus counter when
    metrics are enabled. Previously eviction was completely silent — no counter, no log line, no metric.
    The count includes only true evictions: an explicit reset()/clear() resets it to zero.
  • The cassette-registry control-plane endpoints now have end-to-end test coverage. A new
    over-the-wire integration test drives GET/PUT/DELETE /mockserver/cassettes against a running
    server and pins the documented contract: PUT registers a cassette and returns 201 with the stored
    entry, GET lists cassettes most-recently-used first (and re-registering an existing cassette moves it
    to the front without duplicating it), DELETE (by query parameter or JSON body) removes a cassette so a
    later GET no longer lists it, a server reset empties the registry, and — when control-plane
    authentication is required — every verb is rejected with 401. No production behaviour changed.
    authentication is required — every verb is rejected with 401. The bare /cassettes aliases are
    exercised alongside the /mockserver-prefixed paths, each rejected-input branch (PUT with no body,
    PUT with no path, DELETE with neither a path query parameter nor a body path) is pinned to its
    400 and its message, and the CORS headers that let the dashboard call these endpoints cross-origin are
    asserted. No production behaviour changed.

Changed

  • Editor and dashboard package lockfiles refreshed to clear three open advisories. dompurify
    (3.4.113.4.12) in mockserver-ui, where the fix matters most: the dashboard renders
    captured request and response bodies it did not author, so a sanitiser bypass through
    CUSTOM_ELEMENT_HANDLING is a cross-site-scripting vector rather than the low-severity issue its
    rating suggests. monaco-editor pins dompurify to an exact version, so the existing overrides
    floor was raised to ^3.4.12 rather than downgrading the editor. Also fast-uri
    (3.1.23.1.4, host confusion from a literal backslash and failed international-domain
    canonicalisation) and linkify-it (5.0.15.0.2, quadratic-time mailto: validation) in
    mockserver-vscode, both transitive build/packaging tooling that is not shipped to extension users,
    and both reachable by a lockfile refresh with no manifest change.
  • Dependabot now watches the VS Code extension's npm dependencies. mockserver-vscode has a
    package-lock.json but was missing from the npm directories list, so unlike every other Node
    project it never received routine minor and patch update pull requests and drifted until its
    dependencies raised security alerts.
  • The S3 blob-store config-to-client wiring is now covered by a behavioural unit test. A new
    network-free test exercises S3BlobStoreRegistrar.createS3BlobStore(...) directly and asserts the
    resulting client/store reflects the configuration: a missing bucket throws, the region defaults to
    us-east-1 when unset (and honours an explicit region), an explicit endpoint override is applied
    (and left unset otherwise), static credentials are used when supplied (falling back to the default
    AWS credential chain when not), and the bucket and key prefix are passed through. Previously only
    registration idempotency and a Docker-gated MinIO contract test (which hand-built its own client)
    were covered, so a mis-wired property could pass unnoticed.
  • Node package lockfiles refreshed to clear six open denial-of-service advisories. brace-expansion
    (1.1.151.1.16, 2.1.12.1.2) in mockserver-client-node, mockserver-node and
    mockserver-testcontainers/node, plus js-yaml (4.2.04.3.0) and protobufjs
    (7.6.47.6.5) in mockserver-testcontainers/node. All are transitive dev/test-tooling
    dependencies, and every fixed version was already inside the existing declared ranges, so this is a
    lockfile refresh only — no package.json dependency bump and no new overrides entry was required.
  • Chaos testing doc navigation refreshed for the multi-stage experiment features. The "On this page"
    feature map on chaos_testing.html now surfaces the scheduled-experiment sub-capabilities that were
    documented in the body but not linked from the top of the page: recurring/scheduled (cron and delayed)
    starts, the steady-state baseline pre-check, and experiment history. Two missing section anchors were
    added so the new links resolve, and a pre-existing broken in-page link (#tcp_chaos
    #tcp_layer_chaos) was fixed.
  • The control-plane trust anchor is now mutable at runtime rather than frozen at startup. The
    control-plane authentication handler (mTLS CA chain, JWT JWK source, OIDC issuer/audience/JWKS) is derived
    from the LIVE configuration on every request instead of being built once during server bootstrap. This is
    what makes enabling, disabling or re-pointing control-plane authentication on an already-running instance
    actually take effect, instead of returning success and being silently ignored — but it is a genuine
    widening versus immutable-after-bootstrap and is worth understanding. Any configuration route can move
    the trust anchor of a running server
    : a system property, a Configuration setter, or
    PUT /mockserver/configuration. Critically, a Configuration instance reads through to the process-global
    static ConfigurationProperties store for any field it has not set itself, so a server whose CA chain was
    never pinned on its own instance will follow later mutations of the global store — including mutations made
    by unrelated code sharing the JVM. To pin a trust anchor that unrelated code cannot move, set it on the
    Configuration instance you start the server with
    (an explicitly-set instance field wins over the static
    store); embedded and test usage should not treat the global store as a client-configuration vehicle. If the
    control plane is reachable by parties who should not be able to change its own trust anchor, keep
    control-plane authentication enabled — PUT /mockserver/configuration routes through the same gate. See
    tls-and-security.md.
  • WIRE FORMAT: a matcher value whose first character is ! or ? is now sent as an object rather
    than a bare string.
    In the plain-string form a leading ! means "not" and a leading ? means
    "optional", and the receiver strips those markers unconditionally — so asking for "path is
    !foo" was transmitted as "!foo" and read back as "path is NOT foo", the exact opposite of
    what was requested, with no way to escape it. Such values are now serialised as
    {"not": false, "value": "!foo"}, which the server already read verbatim. This only affects
    values that were previously impossible to express correctly
    ; every value that round-tripped
    before is byte-for-byte unchanged on the wire, so existing expectations, recordings and fixtures
    are unaffected. The object form is already permitted by the published JSON schema
    (stringOrJsonSchema), and httpWebSocketResponse.matchers[].textMatcher and
    grpcBidiResponse.rules[].matchJson have been updated to reference it. The negated direction was,
    and remains, expressible as a string: !!foo still means "not !foo". Generated Java code is
    fixed the same way, emitting string("!foo", false) instead of a bare literal that would be
    re-parsed as a negation when the generated code runs.
    Scope: matcher values only, not header/parameter/cookie names. A name is a JSON field name,
    which cannot carry the object form, so header(string("!X-Foo", false), "bar") still inverts. That
    is pre-existing rather than a regression, needs a schema change to fix, and is recorded with the
    reasoning in test-fixtures/expectations/known-gaps.json.
  • A DNS record that cannot be encoded on the wire now returns SERVFAIL instead of being silently dropped
    or emitted as corrupt bytes.
    Previously an unparseable IP address dropped that one record and still
    returned NOERROR (so the client saw a successful, empty answer), and an over-long label or mismatched
    address width was written to the wire unchecked. Configuration that cannot produce a conformant response is
    now reported as a server failure, with the reason logged at ERROR. If a suite depended on a malformed
    record being quietly skipped, it will now see SERVFAIL — the record needs correcting.
  • DNS TXT values longer than 255 octets are now split across multiple character-strings rather than
    truncated.
    Resolvers concatenate them, so the value a client reads is now the full configured value. A
    test that asserted on the truncated 255-octet prefix will need updating — it was asserting on corruption.
  • BREAKING BEHAVIOUR: verify(never()) and other upper-bound verifications now FAIL instead of passing once
    the event log has evicted entries. Suites that are green today may legitimately go red — that is the point.

    Previously, when the event log rolled over, the entries proving a request had happened were silently

These release notes are truncated. This release's changelog entry exceeds GitHub's 125,000 character
limit for a release body. Read the complete entry in
changelog.md.