VCR Test Harness

The VCR (Video Cassette Recorder) modules provide record/replay infrastructure for AI model API calls. Record responses from embedding and chat models during development, then replay them in CI without network access or API keys.

Concepts

  • Cassette — a stored model response, identified by call type, test class, test method, and call index.

  • Mode — controls VCR behavior:

    • PLAYBACK — serve only a cassette whose request signature matches; fail if missing or stale.

    • RECORD — always call the real model and store the response.

    • RECORD_NEW — record only if no cassette exists; replay otherwise.

    • RECORD_FAILED — re-record only for tests that previously failed.

    • PLAYBACK_OR_RECORD — replay a matching cassette; record when it is missing or its request signature is stale.

    • OFF — bypass VCR entirely; all calls go to the real model.

  • Registry — tracks per-test status (RECORDED, FAILED, MISSING) across test runs.

  • Serializer — encodes cassettes to JSON. Two implementations: Avaje Jsonb (default) and Jackson.

JUnit 5 Setup

Add the dependency:

dependencies {
    testImplementation("com.integrallis:vectors-vcr-junit5:0.1.20")
    testImplementation("com.integrallis:vectors-vcr-serde-avaje:0.1.20")
    // or: vectors-vcr-serde-jackson

    // Framework-specific wrappers (pick one or both):
    testImplementation("com.integrallis:vectors-vcr-spring-ai:0.1.20")
    testImplementation("com.integrallis:vectors-vcr-langchain4j:0.1.20")
}

Annotate your test class:

@VCRTest(mode = VCRMode.PLAYBACK_OR_RECORD,
         dataDir = "src/test/resources/vcr-data")  // this is the default; omit to inherit it
class EmbeddingTest {

    @VCRModel
    EmbeddingModel model = new OpenAiEmbeddingModel(apiKey);

    @Test
    void testEmbed() {
        float[] embedding = model.embed("hello world");
        assertThat(embedding).hasSize(1536);
    }

    @VCRRecord   // force re-record for this test
    @Test
    void testReRecord() {
        // ...
    }

    @VCRDisabled  // bypass VCR entirely
    @Test
    void testLive() {
        // ...
    }
}

On the first run, the real model is called and responses are saved to cassette files under dataDir. On subsequent runs, responses are replayed from cassettes — no API key or network needed.

Each framework wrapper signs the complete canonical request. Chat signatures include the messages, the configured model label, request options such as temperature and token limits, provider request parameters, tool specifications, and model defaults exposed by the framework. Embedding signatures include the input text or batch and model label. The ordinal key locates the cassette; the signature then proves that the current call is the interaction that was recorded there.

PLAYBACK_OR_RECORD is therefore the automatic maintenance mode: an unchanged request replays, while a changed prompt, model setting, tool definition, or embedding input invokes the delegate and replaces the stale cassette. Strict PLAYBACK never refreshes fixtures and throws VCRCassetteStaleException on a mismatch. Legacy cassettes without signatures are refreshed by PLAYBACK_OR_RECORD and rejected by strict PLAYBACK.

If a recording test fails after one or more model calls, the JUnit 5 extension deletes every cassette written by that test and marks the test failed in the registry. A later RECORD_FAILED run can then create a complete replacement without replaying partial fixtures.

TestNG Setup

dependencies {
    testImplementation("com.integrallis:vectors-vcr-testng:0.1.20")
    testImplementation("com.integrallis:vectors-vcr-serde-avaje:0.1.20")
}
@VCRTestNG(mode = VCRMode.PLAYBACK_OR_RECORD,
           dataDir = "src/test/resources/vcr-data")
public class EmbeddingTest {

    @VCRModel
    EmbeddingModel model = new OpenAiEmbeddingModel(apiKey);

    @Test
    public void testEmbed() {
        // ...
    }
}

The TestNG listener is discovered through the service provider shipped by the module. If a recording test fails, it marks the registry entry failed and deletes every cassette successfully written by that test.

Spring AI Integration

Wrap Spring AI models with VCR-aware decorators:

@VCRModel
EmbeddingModel embeddingModel = new OpenAiEmbeddingModel(apiKey);

@VCRModel
ChatModel chatModel = new OpenAiChatModel(apiKey);

The vectors-vcr-spring-ai module provides VCRSpringAIEmbeddingModel, VCRSpringAIChatModel, and VCRSpringAIStreamingChatModel wrappers. Blocking chat replay preserves assistant messages, tool calls, all generations, generation metadata, response attributes, token usage, rate limits, and prompt-filter metadata. Streaming replay preserves the ordered ChatResponse chunks. Field wrapping happens automatically when @VCRModel is used with the JUnit 5 or TestNG extension.

LangChain4j Integration

@VCRModel
dev.langchain4j.model.embedding.EmbeddingModel model =
    new AllMiniLmL6V2EmbeddingModel();

@VCRModel
dev.langchain4j.model.chat.ChatModel chatModel = OpenAiChatModel.builder()
    .apiKey(apiKey).build();

The vectors-vcr-langchain4j module provides VCREmbeddingModel, VCRChatModel, and VCRStreamingChatModel wrappers, which decorate the LangChain4j embedding, blocking chat, and streaming chat interfaces. Streaming cassettes preserve partial text, thinking, partial and complete tool calls, and the final structured response.

Cassette Storage Options

File-Based (Default)

Cassettes are stored under the configured dataDir using the Vectors local-file backend. Avaje and Jackson serialize the cassette payload as JSON inside the backend’s file framing.

Semantic Store

vectors-vcr-semantic-db provides a lower-level SemanticCassetteStore for reusing nearby recorded embeddings in a manually configured LangChain4j embedding wrapper:

dependencies {
    testImplementation("com.integrallis:vectors-vcr-semantic-db:0.1.20")
}

Adding the dependency does not replace the JUnit cassette-store factory automatically. In PLAYBACK_OR_RECORD, the wrapper computes the current embedding with the live delegate before querying the semantic store. A match above the configured cosine threshold (default: 0.95) can reuse an existing embedding cassette instead of writing another one. This is not offline playback, and it does not apply to chat calls or the Spring AI adapter.

Serializer Choice

Two cassette serializers are available:

Serializer Dependency Notes

Avaje Jsonb

vectors-vcr-serde-avaje

Default. Zero-annotation tree API. No annotation processor needed.

Jackson

vectors-vcr-serde-jackson

Streaming API (JsonGenerator/JsonParser). Lower memory for large cassettes.

Both delegate the cassette shape to the core CassetteTreeCodec, so cassettes are interchangeable between serializers and new response fields cannot drift between the two implementations.