lemon_platform_test

Contract-test kit for Lemon's extension behaviours.

Three ExUnit case templates run the platform's compliance suites against your implementation of a Lemon behaviour:

Behaviour Case template
LemonCore.Store.Backend LemonPlatformTest.BackendCase
LemonChannels.Plugin LemonPlatformTest.PluginCase
LemonMemory.Provider LemonPlatformTest.ProviderCase

PluginCase targets lemon_channels, which is not published to Hex; outside this repository it needs a path dependency on a lemon checkout.

defmodule MyApp.RedisBackendComplianceTest do
  use LemonPlatformTest.BackendCase,
    async: true,
    backend: MyApp.RedisBackend,
    backend_opts: [url: "redis://localhost:6379/15"]
end

Each case template's moduledoc is the reference guide for its behaviour: the contract in prose, a worked minimal implementation, every option the suite takes, and the places the contract is still underspecified. Start with LemonPlatformTest for the overview.

The suites test a contract, never an implementation's internals, and are safe by default: nothing makes a network call, delivers a message, or starts a real task unless you pass an explicit probe.

Gateway executor cutover

Gateway no longer accepts extension-provided engines. Its engine behaviour, registry, Echo reference implementation, and EngineCase were intentionally removed when Gateway became the runtime for its configured singleton executor. This kit therefore does not provide a Gateway executor compliance case.

Use the boundary that matches the desired integration:

Need Use
Integrate a model/API a LemonAi.Provider
Add in-process agent capability a CodingAgent tool
Delegate bounded work to a subagent the native task/agent tools (in-process CodingAgent.Session)

Delegated tasks run as native in-process subagents; the vendor CLI task runners (Claude Code, Codex, Kimi, OpenCode, and Pi) were removed. LemonCore.ResumeToken retains native and delegated-task resume support. Top-level engine: "lemon" is fixed compatibility provenance, while a delegated task retains its own task identity; neither is a Gateway routing choice.

Optional dependencies

Each case needs exactly one platform app, and they are all optional: true, so you compile only the one you use — a BackendCase does not drag in SQLite and the agent runtime. Add the app your case targets alongside the kit:

Case / tool Add
BackendCase, EventsCase {:lemon_core, "~> 0.2", only: :test}
ProviderCase {:lemon_memory, "~> 0.2", only: :test}
FakeLLM {:lemon_ai, "~> 0.2", only: :test} and {:lemon_agent, "~> 0.2", only: :test}
PluginCase lemon_channels, which is not published to Hex: a path dependency on apps/lemon_channels in a lemon checkout

Forget one and the case raises at compile time naming the package to add.

FakeLLM: drive an agent loop without a provider

LemonPlatformTest.FakeLLM is a scripted stand-in for an LLM. It turns a list of turns into the stream function LemonAgent expects, emitting the same event protocol a real provider adapter does — so you can test what your agent does with a tool call or a refusal, with no network and no API key:

stream_fn =
  LemonPlatformTest.FakeLLM.script([
    {:tool_call, "get_weather", %{"city" => "Paris"}},
    {:text, "It is sunny in Paris."}
  ])

config = %LemonAgent.Types.AgentLoopConfig{stream_fn: stream_fn, ...}

Steps cover a plain answer, one or many tool calls, a model refusal, and a transport error. See the LemonPlatformTest.FakeLLM moduledoc for the full worked example.

In this repo

test/compliance/ runs the kit against the platform's own implementations — three store backends, two channel adapters, and the local memory provider. Additional compliance suites should live with their implementations where dependency direction requires it.

mix test apps/lemon_platform_test