Guides
Signal Assertions
Assert on signal patterns in your tests
Signal Assertions
Use signal matchers to test agent behavior.
Available Matchers
toContainSignal
Check if a signal was emitted:
expect(result.signals).toContainSignal("agent:completed");
expect(result.signals).toContainSignal("analysis:complete");
expect(result.signals).toContainSignal("harness:end");toHaveSignalsInOrder
Check signals appear in a specific order:
expect(result.signals).toHaveSignalsInOrder([
"workflow:start",
"agent:activated",
"harness:start",
"harness:end",
"agent:completed",
"workflow:end",
]);The signals don't need to be consecutive—other signals can appear between them.
toHaveSignalWithPayload
Check a signal has specific payload properties:
expect(result.signals).toHaveSignalWithPayload("agent:completed", {
agent: "analyzer",
});
expect(result.signals).toHaveSignalWithPayload("harness:end", {
usage: expect.objectContaining({
inputTokens: expect.any(Number),
outputTokens: expect.any(Number),
}),
});Pattern Matching
Exact Match
expect(result.signals).toContainSignal("workflow:start");Namespace Prefix
Match all signals in a namespace:
// Check any agent signal exists
const hasAgentSignal = result.signals.some(s => s.name.startsWith("agent:"));
expect(hasAgentSignal).toBe(true);Custom Filter
For complex assertions, filter signals directly:
const agentActivations = result.signals.filter(
s => s.name === "agent:activated"
);
expect(agentActivations).toHaveLength(2);
expect(agentActivations[0].payload.agent).toBe("analyzer");
expect(agentActivations[1].payload.agent).toBe("reviewer");Testing Signal Flow
Multi-Agent Workflows
Test that agents activate in the right order:
it("reviewer activates after analyzer", async () => {
const result = await runReactive({
agents: { analyzer, reviewer },
state: initialState,
harness,
});
expect(result.signals).toHaveSignalsInOrder([
"agent:activated", // analyzer
"analysis:complete",
"agent:activated", // reviewer
"review:complete",
]);
});Conditional Activation
Test when guards:
it("reviewer skipped when score is low", async () => {
const result = await runReactive({
agents: { analyzer, reviewer },
state: { input: "bad input", score: 0.3, result: null },
harness,
});
// Reviewer should be skipped
expect(result.signals).toContainSignal("agent:skipped");
const skipped = result.signals.find(s => s.name === "agent:skipped");
expect(skipped?.payload.agent).toBe("reviewer");
});Error Handling
Test harness errors:
it("handles harness errors gracefully", async () => {
// Use a harness that will fail
const result = await runReactive({
agents: { analyzer },
state: initialState,
harness: new FailingHarness(),
});
expect(result.signals).toContainSignal("harness:error");
});Testing State
Final State
it("produces expected final state", async () => {
const result = await runReactive({
agents: { analyzer },
state: { input: "Hello", result: null },
harness,
endWhen: (s) => s.result !== null,
});
expect(result.state.result).toBeDefined();
expect(result.state.result).toContain("analysis");
});State Changes
Check state signals for intermediate changes:
it("state updates correctly", async () => {
const result = await runReactive({
agents: { analyzer },
state: { input: "Hello", result: null },
harness,
});
// Find state change signals
const stateChanges = result.signals.filter(
s => s.name === "state:result:changed"
);
expect(stateChanges).toHaveLength(1);
expect(stateChanges[0].payload.previousValue).toBeNull();
expect(stateChanges[0].payload.value).toBeDefined();
});Testing Metrics
Execution Time
it("completes within timeout", async () => {
const result = await runReactive({
agents: { analyzer },
state: initialState,
harness,
timeout: 5000,
});
expect(result.metrics.durationMs).toBeLessThan(5000);
});Activation Count
it("activates expected number of agents", async () => {
const result = await runReactive({
agents: { analyzer, reviewer, fixer },
state: initialState,
harness,
});
expect(result.metrics.activations).toBe(3);
});Token Usage
it("stays within token budget", async () => {
const result = await runReactive({
agents: { analyzer },
state: initialState,
harness,
});
const totalTokens = result.signals
.filter(s => s.name === "harness:end")
.reduce((sum, s) => {
const usage = s.payload.usage || { inputTokens: 0, outputTokens: 0 };
return sum + usage.inputTokens + usage.outputTokens;
}, 0);
expect(totalTokens).toBeLessThan(10000);
});Custom Matchers
Create your own matchers for domain-specific assertions:
import { expect } from "vitest";
import type { Signal } from "@open-harness/core";
expect.extend({
toHaveCompletedAgent(signals: Signal[], agentName: string) {
const completed = signals.find(
s => s.name === "agent:completed" && s.payload.agent === agentName
);
return {
pass: !!completed,
message: () =>
completed
? `Expected agent "${agentName}" not to have completed`
: `Expected agent "${agentName}" to have completed`,
};
},
});Usage:
expect(result.signals).toHaveCompletedAgent("analyzer");Debugging Failed Assertions
When a test fails, log the signal trace:
it("analyzer completes", async () => {
const result = await runReactive({ /* ... */ });
// Debug: log all signals
console.log("Signals:", result.signals.map(s => s.name));
expect(result.signals).toContainSignal("agent:completed");
});