Testing Overview
Testing strategies for Cogitator tools, agents, workflows and swarms without calling a real LLM.
What to Test
Model output is non-deterministic, but almost everything around it is not. Test the deterministic parts directly and script the model:
- Tools — call
execute()with parameters and aToolContext - Agent behaviour — script the LLM with a mock backend and assert on tool calls, output and the requests sent
- Workflow logic — run DAGs of function nodes; no LLM needed
- Error handling — make the mock throw and check how the run fails or recovers
- Real providers — a smaller end-to-end suite against Google, OpenAI or Ollama
Unit Testing Tools
tool() returns a plain object, so its execute is directly callable. It takes the validated parameters and a ToolContext:
import { tool } from '@cogitator-ai/core';
import type { ToolContext } from '@cogitator-ai/types';
import { z } from 'zod';
import { describe, it, expect } from 'vitest';
const add = tool({
name: 'add',
description: 'Add two numbers',
parameters: z.object({ a: z.number(), b: z.number() }),
execute: async ({ a, b }) => ({ sum: a + b }),
});
const context: ToolContext = {
agentId: 'agent-test',
runId: 'run-test',
signal: new AbortController().signal,
};
describe('add', () => {
it('adds', async () => {
await expect(add.execute({ a: 2, b: 2 }, context)).resolves.toEqual({ sum: 4 });
});
it('rejects bad arguments the way the runtime does', () => {
expect(add.parameters.safeParse({ a: '2' }).success).toBe(false);
});
});execute does not validate on its own — the runtime parses the model's arguments with parameters first and answers the model with Invalid arguments: … when they do not match. The built-in tools work the same way (calculator.execute({ expression: '2 + 2' }, context)).
Agent Tests with a Mock Backend
Give the Cogitator a scripted backend in llm.backends and point the agent's model at it. @cogitator-ai/test-utils ships one:
import { Cogitator, Agent } from '@cogitator-ai/core';
import { MockLLMBackend, createToolCall } from '@cogitator-ai/test-utils';
const mock = new MockLLMBackend().setResponses([
{ toolCalls: [createToolCall('add', { a: 2, b: 3 })] },
{ content: 'The answer is 5.' },
]);
const cog = new Cogitator({ llm: { backends: { mock } } });
const agent = new Agent({
name: 'math',
model: 'mock/test-model',
instructions: 'Use the add tool for arithmetic.',
tools: [add],
});
const result = await cog.run(agent, { input: 'What is 2 + 3?' });
expect(result.output).toBe('The answer is 5.');
expect(result.toolCalls[0]).toMatchObject({ name: 'add', arguments: { a: 2, b: 3 } });
expect(mock.getCallCount()).toBe(2);
await cog.close();The tool executes for real between the two scripted responses. For responses that depend on the request, implement LLMBackend yourself — see Your Own Scripted Backend.
A few things that matter in tests:
- The runtime retries failed LLM calls (2 retries by default). Set
llm: { backends, retry: false }when you script errors. - Streaming (
chatStream) is used only when the run passes bothstream: trueandonToken. - Pass
threadIdto reuse a conversation; give theCogitatora memory adapter (cog.memory = new MockMemoryAdapter()) to inspect what is saved. - Call
cog.close()after each test to release memory adapters and sandboxes.
Workflow Tests
Workflows built only from function nodes never touch the Cogitator, so the workflows package's own tests pass a stub ({} as Cogitator). Use a mock backend as above when the workflow contains agentNodes.
import { WorkflowBuilder, WorkflowExecutor, functionNode } from '@cogitator-ai/workflows';
import type { Cogitator } from '@cogitator-ai/core';
type PipelineState = { data?: string; processed?: boolean };
const workflow = new WorkflowBuilder<PipelineState>('test-pipeline')
.addNode(
'fetch',
functionNode<PipelineState>('fetch', async () => 'test', {
stateMapper: (data) => ({ data: data as string }),
})
)
.addNode(
'process',
functionNode<PipelineState>('process', async () => true, {
stateMapper: (processed) => ({ processed: processed as boolean }),
}),
{ after: ['fetch'] }
)
.build();
const executor = new WorkflowExecutor({} as Cogitator);
const result = await executor.execute(workflow);
expect(result.error).toBeUndefined();
expect(result.state).toMatchObject({ data: 'test', processed: true });A failing node does not reject execute(): the result carries it in error.
End-to-End Tests
Unit tests live next to each package (packages/<pkg>/src/__tests__/*.test.ts); tests against real providers live in packages/e2e/src/__tests__/<pkg>/*.e2e.ts and need GOOGLE_API_KEY, OPENAI_API_KEY and/or a running Ollama. When a test uses Google, pass the key through providers:
const cog = new Cogitator({
llm: {
defaultModel: 'google/gemini-3.5-flash-lite',
providers: { google: { apiKey: process.env.GOOGLE_API_KEY! } },
},
});Running Tests
pnpm test # unit tests of every package
pnpm --filter @cogitator-ai/core test # one package
pnpm --filter @cogitator-ai/e2e test # all e2e tests
pnpm --filter @cogitator-ai/e2e test -- --run src/__tests__/core # e2e tests of one packagePackages use Vitest; pnpm --filter <pkg> test:watch (where defined) or npx vitest inside a package starts watch mode.