Skip to content

Testing ​

The package ships a small testing surface: a fake LLM client and a trait for extracting signed envelopes from rendered views. Together they let you write end-to-end feature tests that exercise the full request path without hitting a real provider.

The fake LLM client ​

php
use Aanfarhan\Chatbot\Facades\Chatbot;

$fake = Chatbot::fake();
$fake->addReply('Your order is on its way.');
$fake->addReply('Shipping to 123 Example St.');

Chatbot::fake() binds LLMClient in the container to a FakeClient and returns it. Queued replies are consumed in order as your test triggers chats.

The fake client also lets you assert what was sent:

php
$fake->assertCalledTimes(1);
$fake->assertLastPromptContains('order #1');

Extracting a signed envelope ​

Use the InteractsWithChatbot trait to pull the signed context token out of a rendered Blade response:

php
use Aanfarhan\Chatbot\Testing\InteractsWithChatbot;

class OrderChatTest extends TestCase
{
    use InteractsWithChatbot;

    public function test_chat_replies_about_order(): void
    {
        $fake = Chatbot::fake();
        $fake->addReply('Your order is on its way.');

        $page  = $this->get('/orders/1');
        $token = $this->extractSignedContext($page);

        $this->post('/chatbot/messages', [
            'signed_context' => $token,
            'message'        => 'Where is my order?',
        ])->assertOk();
    }
}

extractSignedContext() parses the <chatbot-widget signed-context="..."> attribute from the response HTML and returns the decoded token. It throws a RuntimeException if no widget is rendered — useful for catching mis-wired layouts.

Pest example ​

php
use Aanfarhan\Chatbot\Facades\Chatbot;
use Aanfarhan\Chatbot\Testing\InteractsWithChatbot;

uses(InteractsWithChatbot::class);

it('answers questions about the order', function () {
    Chatbot::fake()->addReply('It ships tomorrow.');

    $page = $this->get('/orders/1');

    $this->post('/chatbot/messages', [
        'signed_context' => $this->extractSignedContext($page),
        'message'        => 'When does my order ship?',
    ])->assertOk();
});

Testing tools without a real LLM ​

For tool-level testing, instantiate your tool class directly and pass a ToolInvocation:

php
use Aanfarhan\Chatbot\Tools\ToolInvocation;
use App\Chatbot\Tools\LookupOrderTool;

it('refuses guest callers', function () {
    $tool = new LookupOrderTool;

    $invocation = new ToolInvocation(
        args:    ['order_id' => 1],
        channel: 'default',
        context: [],
    );

    expect($tool->authorize(actor: null, invocation: $invocation))->toBeFalse();
});

For end-to-end tool tests, queue an assistant turn that emits a tool_calls payload on the fake client:

php
$fake = Chatbot::fake();
$fake->queueToolCall('lookup_order', ['order_id' => 1]);
$fake->addReply('Your order total is $42.00.');

The streaming pipeline will resolve the tool, run authorize() and handle() against your real implementation, append the result, and continue with the queued reply.

Inspecting the assembled prompt ​

bash
php artisan chatbot:inspect-prompt \
  --route=orders.show \
  --channel=default \
  --user=42 \
  --context-json=tests/fixtures/order.json

Useful when a test fails because the LLM "isn't seeing" expected context — the inspector shows the exact messages array the provider would receive.

See also ​

Released under the MIT License.