resources/boost/skills/neuron-test-engineer/SKILL.md
Write tests for Neuron AI agents, RAG systems, workflows, and tools using the built-in testing utilities. Use this skill when the user mentions testing agents, writing unit tests, mocking AI providers, testing tool execution, verifying RAG retrieval, testing workflow behavior, or creating test cases for Neuron AI components. Also trigger for any task involving PHPUnit tests, fake providers, test assertions, or quality assurance in Neuron AI projects.
npx skillsauth add neuron-core/neuron-laravel neuron-test-engineerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill helps you write comprehensive tests for Neuron AI applications using the built-in testing utilities in NeuronAI\Testing.
Neuron AI provides fake implementations that:
The primary tool for testing agents without real AI API calls.
use NeuronAI\Testing\FakeAIProvider;
use NeuronAI\Chat\Messages\AssistantMessage;
use NeuronAI\Chat\Messages\UserMessage;
// Create with predetermined responses
$provider = new FakeAIProvider(
new AssistantMessage('Hello! How can I help you?'),
new AssistantMessage('The answer is 42.')
);
// Or use static constructor
$provider = FakeAIProvider::make(
new AssistantMessage('Response 1'),
new AssistantMessage('Response 2')
);
Key Features:
chat(), stream(), and structured() methodsFor testing RAG systems without real vector databases.
use NeuronAI\Testing\FakeVectorStore;
use NeuronAI\RAG\Document;
// Pre-populate with search results
$vectorStore = new FakeVectorStore([
new Document('France is a country in Europe. Its capital is Paris.'),
new Document('Germany is a country in Europe. Its capital is Berlin.'),
]);
// Or create empty and set results later
$vectorStore = FakeVectorStore::make();
$vectorStore->setSearchResults([
new Document('Relevant document content')
]);
For testing embeddings without real API calls.
use NeuronAI\Testing\FakeEmbeddingsProvider;
// Create with default dimensions (8)
$embeddings = new FakeEmbeddingsProvider();
// Or specify dimensions
$embeddings = new FakeEmbeddingsProvider(dimensions: 1536);
// Use static constructor
$embeddings = FakeEmbeddingsProvider::make();
For testing MCP (Model Context Protocol) integrations without a real MCP server.
use NeuronAI\Testing\FakeMcpTransport;
// Queue predetermined responses
$transport = new FakeMcpTransport(
['result' => ['tools' => [['name' => 'search', 'description' => 'Search the web']]]],
['result' => ['content' => [['type' => 'text', 'text' => 'Search results...']]]],
);
// Or add responses later
$transport->addResponses(['result' => ['content' => 'More data']]);
Key Features:
receive()assertInitialized, assertToolCalled, etc.)For testing workflow middleware behavior.
use NeuronAI\Testing\FakeMiddleware;
$middleware = FakeMiddleware::make();
// Configure custom handlers
$middleware->setBeforeHandler(function ($node, $event, $state): void {
$state->set('injected_data', 'value');
});
// Configure exceptions for testing error handling
$middleware->setThrowOnBefore(new \Exception('Test exception'));
use PHPUnit\Framework\TestCase;
use NeuronAI\Agent\Agent;
use NeuronAI\Chat\Messages\AssistantMessage;
use NeuronAI\Chat\Messages\UserMessage;
use NeuronAI\Testing\FakeAIProvider;
class MyAgentTest extends TestCase
{
public function test_agent_returns_expected_response(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('Expected response')
);
$agent = Agent::make();
$agent->setAiProvider($provider);
$message = $agent->chat(new UserMessage('Hello'))->getMessage();
$this->assertSame('Expected response', $message->getContent());
$provider->assertCallCount(1);
}
public function test_agent_uses_system_prompt(): void
{
$provider = new FakeAIProvider(new AssistantMessage('OK'));
$agent = Agent::make();
$agent->setAiProvider($provider);
$agent->setInstructions('Always respond in French.');
$agent->chat(new UserMessage('Hello'))->getMessage();
$provider->assertSystemPrompt('Always respond in French.');
}
}
use NeuronAI\Tools\Tool;
use NeuronAI\Tools\ToolProperty;
use NeuronAI\Tools\PropertyType;
use NeuronAI\Chat\Messages\ToolCallMessage;
public function test_agent_executes_tool_and_returns_result(): void
{
$searchTool = Tool::make('search', 'Search the web')
->addProperty(new ToolProperty('query', PropertyType::STRING, 'Search query', true))
->setCallable(fn (string $query): string => "Results for: {$query}");
// First response: model calls the tool
// Second response: model uses tool result to answer
$provider = new FakeAIProvider(
new ToolCallMessage(null, [
(clone $searchTool)->setCallId('call_1')->setInputs(['query' => 'PHP frameworks']),
]),
new AssistantMessage('Based on my search, here are the top PHP frameworks...')
);
$agent = Agent::make();
$agent->setAiProvider($provider);
$agent->addTool($searchTool);
$message = $agent->chat(new UserMessage('What are the best PHP frameworks?'))->getMessage();
$this->assertSame('Based on my search, here are the top PHP frameworks...', $message->getContent());
$provider->assertCallCount(2); // Tool call + final response
$provider->assertToolsConfigured(['search']);
}
use NeuronAI\Chat\Messages\Stream\Chunks\TextChunk;
public function test_agent_streams_response(): void
{
$provider = new FakeAIProvider(new AssistantMessage('Hello world'));
$provider->setStreamChunkSize(5); // Control chunk size for predictable tests
$agent = Agent::make();
$agent->setAiProvider($provider);
$handler = $agent->stream(new UserMessage('Hi'));
$chunks = [];
foreach ($handler->events() as $event) {
if ($event instanceof TextChunk) {
$chunks[] = $event->content;
}
}
$this->assertSame(['Hello', ' worl', 'd'], $chunks);
$state = $handler->run();
$this->assertSame('Hello world', $state->getMessage()->getContent());
}
use NeuronAI\Chat\Messages\AssistantMessage;
public function test_agent_extracts_structured_data(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('{"name": "Alice", "age": 30}')
);
$agent = Agent::make();
$agent->setAiProvider($provider);
class Person
{
#[SchemaProperty(description: 'The person name', required: true)]
public string $name;
#[SchemaProperty(description: 'The person age')]
public int $age;
}
$person = $agent->structured(
new UserMessage('My name is Alice and I am 30 years old'),
Person::class
);
$this->assertInstanceOf(Person::class, $person);
$this->assertSame('Alice', $person->name);
$this->assertSame(30, $person->age);
$provider->assertMethodCallCount('structured', 1);
}
use NeuronAI\RAG\RAG;
use NeuronAI\RAG\Document;
use NeuronAI\Testing\FakeAIProvider;
use NeuronAI\Testing\FakeEmbeddingsProvider;
use NeuronAI\Testing\FakeVectorStore;
class MyRAGTest extends TestCase
{
public function test_rag_retrieves_and_answers(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('Paris is the capital of France.')
);
$vectorStore = new FakeVectorStore([
new Document('France is a country in Europe. Its capital is Paris.'),
]);
$rag = RAG::make();
$rag->setAiProvider($provider);
$rag->setEmbeddingsProvider(new FakeEmbeddingsProvider());
$rag->setVectorStore($vectorStore);
$message = $rag->chat(new UserMessage('What is the capital of France?'))->getMessage();
$this->assertSame('Paris is the capital of France.', $message->getContent());
$provider->assertCallCount(1);
$vectorStore->assertSearchCount(1);
}
public function test_rag_adds_documents(): void
{
$embeddings = new FakeEmbeddingsProvider();
$vectorStore = new FakeVectorStore();
$rag = RAG::make();
$rag->setAiProvider(new FakeAIProvider());
$rag->setEmbeddingsProvider($embeddings);
$rag->setVectorStore($vectorStore);
$rag->addDocuments([
new Document('First document'),
new Document('Second document'),
]);
$embeddings->assertCallCount(2);
$vectorStore->assertDocumentCount(2);
$vectorStore->assertHasDocumentWithContent('First document');
$vectorStore->assertHasDocumentWithContent('Second document');
}
}
use NeuronAI\Workflow\Workflow;
use NeuronAI\Workflow\WorkflowState;
use NeuronAI\Workflow\Events\StartEvent;
use NeuronAI\Workflow\Events\StopEvent;
use NeuronAI\Workflow\Node;
class MyWorkflowTest extends TestCase
{
public function test_workflow_executes_nodes_in_sequence(): void
{
$workflow = Workflow::make()
->addNodes([
new FirstNode(),
new SecondNode(),
new ThirdNode(),
]);
$finalState = $workflow->init()->run();
$this->assertTrue($finalState->get('first_executed'));
$this->assertTrue($finalState->get('second_executed'));
$this->assertTrue($finalState->get('third_executed'));
}
public function test_workflow_with_initial_state(): void
{
$workflow = Workflow::make(
state: new WorkflowState(['input' => 'test_value'])
)->addNodes([
new ProcessNode(),
]);
$finalState = $workflow->init()->run();
$this->assertEquals('test_value', $finalState->get('original_input'));
}
}
use NeuronAI\Testing\FakeMiddleware;
class MyMiddlewareTest extends TestCase
{
public function test_middleware_runs_on_all_nodes(): void
{
$middleware = FakeMiddleware::make();
Workflow::make()
->addGlobalMiddleware($middleware)
->addNodes([new NodeOne(), new NodeTwo(), new NodeThree()])
->init()
->run();
// 3 nodes = 3 before + 3 after calls
$middleware->assertBeforeCalledTimes(3);
$middleware->assertAfterCalledTimes(3);
$middleware->assertCallCount(6);
}
public function test_middleware_only_runs_for_specific_node(): void
{
$middleware = FakeMiddleware::make();
Workflow::make()
->addMiddleware(NodeTwo::class, $middleware)
->addNodes([new NodeOne(), new NodeTwo(), new NodeThree()])
->init()
->run();
$middleware->assertBeforeCalledTimes(1);
$middleware->assertBeforeCalledForNode(NodeTwo::class);
}
public function test_middleware_can_modify_state(): void
{
$middleware = FakeMiddleware::make()
->setBeforeHandler(function ($node, $event, $state): void {
$state->set('injected_by_middleware', true);
});
$finalState = Workflow::make()
->addMiddleware(NodeOne::class, $middleware)
->addNodes([new NodeOne(), new NodeTwo()])
->init()
->run();
$this->assertTrue($finalState->get('injected_by_middleware'));
}
}
use NeuronAI\Workflow\Interrupt\WorkflowInterrupt;
use NeuronAI\Workflow\Persistence\InMemoryPersistence;
class MyInterruptTest extends TestCase
{
public function test_workflow_interrupts_and_resumes(): void
{
$workflow = Workflow::make(
persistence: new InMemoryPersistence(),
resumeToken: 'test-workflow'
)->addNodes([
new NodeOne(),
new InterruptableNode(),
new NodeThree(),
]);
// First run should interrupt
$interrupt = null;
try {
$workflow->init()->run();
$this->fail('Expected WorkflowInterrupt exception');
} catch (WorkflowInterrupt $e) {
$interrupt = $e;
}
$this->assertNotNull($interrupt);
$this->assertEquals('human input needed', $interrupt->getRequest()->getMessage());
// Resume with human feedback
$finalState = $workflow->init($interrupt->getRequest())->run();
$this->assertTrue($finalState->get('interruptable_node_executed'));
}
}
Use FakeMcpTransport to test code that interacts with MCP servers without running a real server.
use NeuronAI\Testing\FakeMcpTransport;
class McpIntegrationTest extends TestCase
{
public function test_mcp_initialization_handshake(): void
{
$transport = new FakeMcpTransport(
['result' => ['capabilities' => [], 'serverInfo' => ['name' => 'test-server']]],
['result' => []],
);
$transport->connect();
// Simulate initialization handshake
$transport->send(['method' => 'initialize', 'params' => ['capabilities' => []]]);
$transport->receive(); // consume capabilities response
$transport->send(['method' => 'notifications/initialized']);
$transport->receive(); // consume ack
$transport->assertInitialized();
$transport->assertConnected();
}
public function test_mcp_tool_call(): void
{
$transport = new FakeMcpTransport(
['result' => ['tools' => [['name' => 'search', 'description' => 'Search']]]],
['result' => ['content' => [['type' => 'text', 'text' => 'Found 3 results']]]],
);
$transport->connect();
$transport->send(['method' => 'tools/list', 'params' => []]);
$transport->receive();
$transport->send(['method' => 'tools/call', 'params' => ['name' => 'search', 'arguments' => ['query' => 'test']]]);
$transport->receive();
$transport->assertToolsListCalled();
$transport->assertToolCalled('search');
$transport->assertSendCount(2);
$transport->assertReceiveCount(2);
}
}
// Verify number of calls
$provider->assertCallCount(3);
// Verify specific method was called
$provider->assertMethodCallCount('chat', 2);
$provider->assertMethodCallCount('stream', 1);
$provider->assertMethodCallCount('structured', 1);
// Verify system prompt
$provider->assertSystemPrompt('You are a helpful assistant.');
// Verify tools were configured
$provider->assertToolsConfigured(['search', 'calculator']);
// Verify no calls were made
$provider->assertNothingSent();
// Custom assertion with callback
$provider->assertSent(function (RequestRecord $record): bool {
return $record->method === 'chat'
&& str_contains($record->messages[0]->getContent(), 'keyword');
});
// Verify search count
$vectorStore->assertSearchCount(2);
// Verify document count
$vectorStore->assertDocumentCount(5);
// Verify specific document exists
$vectorStore->assertHasDocumentWithContent('Expected content');
// Verify store is empty
$vectorStore->assertNothingStored();
// Verify embedding call count
$embeddings->assertCallCount(3);
// Verify specific text was embedded
$embeddings->assertEmbeddedText('Expected text to embed');
// Verify no embeddings were made
$embeddings->assertNothingEmbedded();
// Verify before() was called
$middleware->assertBeforeCalled();
$middleware->assertBeforeNotCalled();
$middleware->assertBeforeCalledTimes(3);
$middleware->assertBeforeCalledForNode(NodeOne::class);
// Verify after() was called
$middleware->assertAfterCalled();
$middleware->assertAfterNotCalled();
$middleware->assertAfterCalledTimes(3);
$middleware->assertAfterCalledForNode(NodeOne::class);
// Verify total call count
$middleware->assertCallCount(6);
$middleware->assertNotCalled();
// Verify connection state
$transport->assertConnected();
$transport->assertDisconnected();
// Verify send/receive counts
$transport->assertSendCount(3);
$transport->assertReceiveCount(3);
$transport->assertNothingSent();
$transport->assertNothingReceived();
// Verify specific MCP methods
$transport->assertMethodSent('initialize', 1);
$transport->assertMethodReceived('initialize', 1);
// Convenience assertions for common MCP patterns
$transport->assertInitialized(); // initialize + notifications/initialized
$transport->assertToolsListCalled(1); // tools/list sent N times
$transport->assertToolCalled('search', 2); // tools/call with specific tool name
// Custom assertion with callback
$transport->assertSent(function (array $data): bool {
return ($data['method'] ?? null) === 'tools/call'
&& ($data['params']['name'] ?? null) === 'search';
});
public function test_conversation_remembers_context(): void
{
$provider = new FakeAIProvider(
new AssistantMessage('Hi! I can help with that.'),
new AssistantMessage('The capital of France is Paris.'),
);
$agent = Agent::make();
$agent->setAiProvider($provider);
$first = $agent->chat(new UserMessage('Hello'))->getMessage();
$second = $agent->chat(new UserMessage('What is the capital of France?'))->getMessage();
$this->assertSame('Hi! I can help with that.', $first->getContent());
$this->assertSame('The capital of France is Paris.', $second->getContent());
$provider->assertCallCount(2);
}
foreach ($provider->getRecorded() as $record) {
$record->method; // 'chat', 'stream', or 'structured'
$record->messages; // Message[] passed to provider
$record->systemPrompt; // ?string system prompt
$record->tools; // ToolInterface[] configured tools
$record->structuredClass; // ?string output class (structured only)
$record->structuredSchema;// array schema (structured only)
}
foreach ($middleware->getRecorded() as $record) {
$record->method; // 'before' or 'after'
$record->node; // NodeInterface being executed
$record->event; // Event passed/returned
$record->state; // WorkflowState at call time
}
# Run all tests
composer test
# Run specific test file
vendor/bin/phpunit tests/Agent/AgentTest.php
# Run specific test method
vendor/bin/phpunit --filter test_chat_with_tools
# Run with verbose output
vendor/bin/phpunit --colors=always -v
assertSent() with callbacks for complex verification// WRONG: Generator not consumed, code never runs
$generator = $provider->stream(new UserMessage('Hi'));
// Body of generator hasn't executed yet!
// CORRECT: Consume the generator
$generator = $provider->stream(new UserMessage('Hi'));
foreach ($generator as $chunk) {
// Process chunks
}
$finalMessage = $generator->getReturn();
// WRONG: No responses queued
$provider = new FakeAIProvider();
$provider->chat(new UserMessage('Hi')); // Throws ProviderException!
// CORRECT: Queue responses before calling
$provider = new FakeAIProvider(new AssistantMessage('Response'));
$provider->chat(new UserMessage('Hi'));
// Hidden tools are executable but not sent to AI
$hiddenTool = Tool::make('secret', 'Secret tool')
->setCallable(fn ($input) => "Result: {$input}")
->visible(false);
// This will NOT include 'secret' in tools configured
$provider->assertToolsConfigured(['search']); // Only visible tools
development
Build custom Neuron AI workflows with nodes, events, middleware, and human-in-the-loop patterns. Use this skill whenever the user mentions workflows, orchestration, event-driven systems, custom agents, complex multi-step processes, human-in-the-loop patterns, or wants to build a custom agentic system from scratch. Also trigger for tasks involving node creation, event routing, workflow middleware, persistence, or interruption patterns.
tools
Create custom tools, toolkits, and MCP integrations for Neuron AI agents. Use this skill when the user mentions creating tools, building toolkits, extending Tool class, defining tool properties, implementing tool execution, MCP server integration, Model Context Protocol, connecting external tools, or tool guidelines. Also trigger for any task involving ToolProperty, ArrayProperty, ObjectProperty, AbstractToolkit, McpConnector, or StdioTransport/SseHttpTransport/StreamableHttpTransport.
data-ai
Design and implement structured output classes for Neuron AI agents using SchemaProperty attributes and validation rules. Use this skill when the user mentions structured output, JSON schema extraction, data validation, output classes, DTOs for AI responses, extracting structured data from LLM, or configuring property schemas. Also trigger for any task involving SchemaProperty attribute, validation rules like NotBlank/Email/Url, nested objects, arrays of objects, enums, polymorphic types with anyOf, or the Validator class.
development
Implement RAG (Retrieval-Augmented Generation) with Neuron AI including vector stores, embeddings providers, document loaders, and retrieval strategies. Use this skill whenever the user mentions RAG, retrieval, vector search, document retrieval, semantic search, knowledge bases, chat with documents, or wants to build AI systems that can query and understand external documents. Also trigger for tasks involving vector databases, embeddings, document chunking, or retrieval strategies.