Open Harness
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:

test/matchers.ts
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");
});

Next Steps

On this page