Skip to content

Commit 6e13a99

Browse files
test(starter): pin the promise that reading the catalog creates nothing
"Discovery inspects bean types, never bean instances" is the starter's central safety claim — it is in the README, it is a design rule in CONTRIBUTING, and it is why a monitoring system may poll the endpoint at all. Nothing enforced it: readAgents() was only ever exercised against a context with no platform in it, so platform() was never reached and swapping its getSingleton for getBean left the whole suite green while every poll began initialising the platform, and through it every agent it wires. Both halves are covered now — a platform that already exists is read, one that is merely defined reads as absent and is still uninstantiated afterwards. Verified by making that swap: only the new test fails. The fixture is a proxy resolved by class name rather than a hand-written implementation, because AgentPlatform inherits eight interfaces and a stub would need updating on Embabel releases that change nothing this reads.
1 parent 592bcbc commit 6e13a99

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

embabel-workflow-visualizer-starter/src/test/java/com/patbaumgartner/embabel/workflow/visualizer/AgentPlatformReaderTests.java

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
import com.patbaumgartner.embabel.workflow.visualizer.AgentPlatformReader.RuntimeAgent;
44
import com.patbaumgartner.embabel.workflow.visualizer.AgentPlatformReader.RuntimeStep;
55
import org.junit.jupiter.api.Test;
6+
import org.springframework.beans.factory.support.RootBeanDefinition;
67
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
8+
import org.springframework.util.ClassUtils;
79

10+
import java.lang.reflect.Proxy;
811
import java.util.List;
912
import java.util.Map;
1013
import java.util.Set;
14+
import java.util.concurrent.atomic.AtomicBoolean;
1115

1216
import static org.assertj.core.api.Assertions.assertThat;
1317

@@ -108,6 +112,75 @@ void reportsNothingWithoutAnAgentPlatformBean() {
108112
}
109113
}
110114

115+
/**
116+
* A platform that has already been created is read, and read through the same by-name
117+
* reflection the rest of the reader uses.
118+
*/
119+
@Test
120+
void readsAPlatformBeanThatHasAlreadyBeenCreated() {
121+
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
122+
ctx.refresh();
123+
ctx.getBeanFactory()
124+
.registerSingleton("agentPlatform", platformReturning(
125+
List.of(FakeAgentPlatform.Agent.named("com.example.ReviewAgent", List.of(), Set.of()))));
126+
127+
assertThat(new AgentPlatformReader(ctx).readAgents())
128+
.hasValueSatisfying(agents -> assertThat(agents).extracting(RuntimeAgent::name)
129+
.containsExactly("com.example.ReviewAgent"));
130+
}
131+
}
132+
133+
/**
134+
* Reading the catalog must never be the thing that brings a bean to life. A platform
135+
* that is only <em>defined</em> therefore has to read as absent: instantiating it
136+
* here would initialise the platform — and through it every agent it wires — in an
137+
* application that had not asked for one yet, on every poll of the endpoint.
138+
*
139+
* <p>
140+
* The definition is left uninstantiated afterwards, which is the half that a change
141+
* from {@code getSingleton} to {@code getBean} would break while every other test
142+
* still passed.
143+
*/
144+
@Test
145+
void neverInstantiatesAPlatformBeanThatIsOnlyDefined() {
146+
AtomicBoolean instantiated = new AtomicBoolean();
147+
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
148+
RootBeanDefinition definition = new RootBeanDefinition();
149+
definition.setTargetType(agentPlatformType());
150+
definition.setLazyInit(true);
151+
definition.setInstanceSupplier(() -> {
152+
instantiated.set(true);
153+
return platformReturning(List.of());
154+
});
155+
ctx.registerBeanDefinition("agentPlatform", definition);
156+
ctx.refresh();
157+
158+
assertThat(new AgentPlatformReader(ctx).readAgents()).isEmpty();
159+
assertThat(instantiated).isFalse();
160+
}
161+
}
162+
163+
private static Class<?> agentPlatformType() {
164+
return ClassUtils.resolveClassName("com.embabel.agent.core.AgentPlatform", null);
165+
}
166+
167+
/**
168+
* A platform-shaped proxy. The reader locates the platform by type and reads it by
169+
* method name, so a proxy answers every method the interface declares without this
170+
* test having to track the rest of Embabel's shape.
171+
*/
172+
private static Object platformReturning(List<?> agents) {
173+
Class<?> type = agentPlatformType();
174+
return Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[] { type },
175+
(proxy, method, args) -> switch (method.getName()) {
176+
case "agents" -> agents;
177+
case "hashCode" -> System.identityHashCode(proxy);
178+
case "equals" -> proxy == args[0];
179+
case "toString" -> "platform";
180+
default -> null;
181+
});
182+
}
183+
111184
@Test
112185
void stripsEmbabelsQualifierFromRuntimeNames() {
113186
assertThat(AgentPlatformReader.simpleName("com.example.ReviewAgent.screen")).isEqualTo("screen");

0 commit comments

Comments
 (0)