Testing & review
Every contribution lands with a failing test first (red), then the implementation (green), then a PR that keeps the gates green on bothg++ and clang++.
The four test layers
- Unit: capabilities against a fake
PluginServices; assert what you contribute and that removal is total. - Hermetic integration: register your plugin in a
PluginRuntime, drive a turn with the fake LLM client (tests/fake_llm.h), assert observable effects. No network, ever. - Dialect tests (providers), pure body/parse/stream fixtures, mirroring
tests/dialect_gemini_test.cpp. - Regression:
make test; your contributions must not leak into other tests. The ledger test exists to catch exactly this.
flowchart TB
unit["1 · Unit<br/>a capability against a fake PluginServices"] --> integ
integ["2 · Hermetic integration<br/>PluginRuntime + fake LLM client<br/>no network, ever"] --> dialect
dialect["3 · Dialect, for providers<br/>pure request / parse / stream fixtures"] --> reg
reg["4 · Regression: make test<br/>contributions must not leak into other tests"]
guard["any test that activates a plugin redirects<br/>XDG_CONFIG_HOME to a scratch directory"] -.-> integRedirection and hermeticity
Plugin state is persisted under ~/.config/amber/plugins/<id>/. A test that activates a plugin must redirect XDG_CONFIG_HOME to a scratch directory so it never touches the developer's real config. The reference test does this with a small RAII guard.
The reference test
// tests/example_plugin_test.cpp, the reference test for the hello plugin.
TEST(example_hello_plugin_contributes_and_unwinds_a_tool) {
ScratchConfig scratch("hello"); // redirects XDG_CONFIG_HOME
Fixture f; // ToolRegistry + Config + Workspace
PluginRuntime runtime(f.tools, f.cfg, f.ws);
runtime.add(std::make_shared<examples::HelloPlugin>());
ASSERT_FALSE((bool)f.tools.find("greet")); // registered, not started
runtime.start();
ASSERT_TRUE(runtime.status("hello").enabled);
auto tool = f.tools.find("greet");
ASSERT_TRUE((bool)tool);
ASSERT_EQ(tool->execute(json{{"name", "amber"}}).output,
std::string("Hello, amber!"));
ASSERT_TRUE(runtime.set_state("hello", false)); // disable
ASSERT_FALSE((bool)f.tools.find("greet")); // ledger unwound
}This is the shape to copy: register, assert nothing is installed, start, assert the contribution is live, disable, assert it is gone. It is compiled from plugins/_template/ by make test, so it cannot rot.
Doc-as-test
tests/plugin_docs_test.cpp asserts that everyCapabilityKind, every event payload struct, and every bundled plugin id is documented in the developer guide. Add a capability or an event without updating the guide and the suite fails, documentation drift is caught in CI, not in review.
The gates
make clean && make && make test && make lint && make analyze && make check
make test: the unit suite (hermetic).make check: build-hygiene invariants.make lint: clang-tidy;make analyze: cppcheck. Both must be clean.
Review checklist
- [ ] Plugin depends only on the public plugin API, no tui/ includes.
- [ ] Every contribution goes through a capability; nothing registered
in initialize outside the ledger.
- [ ] Render callables are pure; no I/O, no blocking, no mutation.
- [ ] No blocking in event handlers.
- [ ] Tests: contribution, removal, and one hermetic end-to-end path.
- [ ] The availability table is updated if a capability's status changed.
- [ ] Zero new clang-tidy/cppcheck findings; class <= 200 lines,
method <= 10 lines.Prove the seam held
A provider or tool plugin's diff must not touchlib/http_transport.cpp, the agent loop, or the TUI. If it does, the framework, not your plugin, is missing an extension point. Say so in the PR instead of working around it.