Agentic PHPUnit output
I was made a aware of PAO . And while it think it is a good tool I think we can do better by making it more useful for an LLM. The package has options for PHPUnit, Pest and ParaTest. I'm only going to focus on PHPUnit, version 12 in particular. The setup PHPUnit has an option to add extensions . The best way to let PHPUnit know your extension is in the phpunit.xml file. class= "Tests\Extensions\AgentAwareOutputExtension" /> To detect when PHPunit is run inside an agent I used the shipfastlabs/agent-detector library (I saw it in PAO). This library uses well known config variables to detect multiple agents. Because I'm trying out Mistral Vibe now I added a new script to composer.json. "test:agent" : "AI_AGENT=1 vendor/bin/phpunit" While PAO uses json as output, I want to use markdown. From t
I was made a aware of PAO. And while it think it is a good tool I think we can do better by making it more useful for an LLM.
The package has options for PHPUnit, Pest and ParaTest. I'm only going to focus on PHPUnit, version 12 in particular.
The setup
PHPUnit has an option to add extensions. The best way to let PHPUnit know your extension is in the phpunit.xml file.
`
`
Enter fullscreen mode
Exit fullscreen mode
To detect when PHPunit is run inside an agent I used the shipfastlabs/agent-detector library (I saw it in PAO). This library uses well known config variables to detect multiple agents. Because I'm trying out Mistral Vibe now I added a new script to composer.json.
"test:agent": "AI_AGENT=1 vendor/bin/phpunit"
Enter fullscreen mode
Exit fullscreen mode
While PAO uses json as output, I want to use markdown. From the documentation I got that it doesn't show the errors. Which strikes me as odd because you want your coding agent to be able to fix the failing tests, not? So that is on my todo list.
The code
In the PHPUnit I saw an example where the used a intermediate class to collect the needed data, so that is what I did.
class TestDataCollector { public function __construct( public int $failed = 0, public int $passed = 0, public int $total = 0, public array $messages = [], ) {}__class TestDataCollector { public function __construct( public int $failed = 0, public int $passed = 0, public int $total = 0, public array $messages = [], ) {}__public function write() : void { $text = '# Test results'. PHP_EOL . PHP_EOL; $text .= '## Summary' . PHP_EOL. PHP_EOL; $text .= 'failed: ' . $this->failed . PHP_EOL; $text .= 'passed: ' . $this->passed . PHP_EOL; $text .= 'total: ' . $this->total . PHP_EOL;
if(count($this->messages) > 0) { $text .= PHP_EOL . '## Failed tests' . PHP_EOL. PHP_EOL; $text .= '| Test | Message |' . PHP_EOL; $text .= '| --- | --- |' . PHP_EOL;
foreach($this->messages as $message) { $text .= '| ' . $message['test'].' | ' . $message['message'] . ' |' . PHP_EOL; } }
fwrite(STDOUT, $text); } }`
Enter fullscreen mode
Exit fullscreen mode
In the constructor I setup all the properties I needed to for PHPUnit to manipulate them based on the status of the tests.
The write method is used to display the result of the tests. I choose to use concatenation to make it easy to maintain.
Next up is the extension, the glue that holds the different parts together.
use AgentDetector\AgentDetector; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; use Tests\Extensions\Subscribers\FailSubsrciber; use Tests\Extensions\Subscribers\ErrorSubscriber; use Tests\Extensions\Subscribers\TestsDoneSubscriber; use Tests\Extensions\Subscribers\PassSubsrciber;use AgentDetector\AgentDetector; use PHPUnit\Runner\Extension\Extension; use PHPUnit\Runner\Extension\Facade; use PHPUnit\Runner\Extension\ParameterCollection; use PHPUnit\TextUI\Configuration\Configuration; use Tests\Extensions\Subscribers\FailSubsrciber; use Tests\Extensions\Subscribers\ErrorSubscriber; use Tests\Extensions\Subscribers\TestsDoneSubscriber; use Tests\Extensions\Subscribers\PassSubsrciber;class AgentAwareOutputExtension implements Extension { public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void { if ($configuration->noOutput()) { return; }
$agentDetector = new AgentDetector();
if (!$agentDetector->detect()->isAgent) { return; }
$facade->replaceOutput(); $facade->replaceProgressOutput(); $facade->replaceResultOutput();
$testDataCollector = new TestDataCollector();
$facade->registerSubscribers( new PassSubsrciber($testDataCollector), new FailSubscriber($testDataCollector), new ErrorSubscriber($testDataCollector), new TestsDoneSubscriber($testDataCollector), ); } }`
Enter fullscreen mode
Exit fullscreen mode
A better way would be to have the extension in its own directory, but for the purpose of the test I kept the directory structure flatter.
The Extension has a single method bootstrap, which mirrors the configuration in phpunit.xml.
PHPUnit has a --no-output CLI option that is why the first lines in bootstrap exist.
The AgentDetector lines are, as you can guess, to create a guard when an AI agent is not detected.
The replace methods are a bit unfortunately named because they prevent the display of the default output.
PHPUnit has quite a few Subscriber interfaces for all the events that can happen. So it is up to us to pick the ones we need.
use PHPUnit\Event\Test\Passed; use PHPUnit\Event\Test\PassedSubscriber; use Tests\Extensions\TestDataCollector;use PHPUnit\Event\Test\Passed; use PHPUnit\Event\Test\PassedSubscriber; use Tests\Extensions\TestDataCollector;class PassSubsrciber implements PassedSubscriber {
public function construct(private TestDataCollector $testDataCollector) {}
public function notify(Passed $event): void { $this->testDataCollector->passed++; $this->testDataCollector->total++; } }`
Enter fullscreen mode
Exit fullscreen mode
Because I don't need much data you will see most subscribers have little content.
use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; use Tests\Extensions\TestDataCollector;use PHPUnit\Event\Test\Failed; use PHPUnit\Event\Test\FailedSubscriber; use Tests\Extensions\TestDataCollector;class FailSubscriber implements FailedSubscriber {
public function construct(private TestDataCollector $testDataCollector) {}
public function notify(Failed $event): void { $this->testDataCollector->failed++; $this->testDataCollector->total++;
$this->testDataCollector->messages[] = [ 'test' => $event->test()->className().'::'.$event->test()->methodName(), 'message' => $event->throwable()->message(), ]; } }`
Enter fullscreen mode
Exit fullscreen mode
The main difference between this subscriber and the next one is that this catches the failed tests and the next one the PHP errors.
use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; use Tests\Extensions\TestDataCollector;use PHPUnit\Event\Test\Errored; use PHPUnit\Event\Test\ErroredSubscriber; use Tests\Extensions\TestDataCollector;class ErrorSubscriber implements ErroredSubscriber {
public function construct(private TestDataCollector $testDataCollector) {}
public function notify(Errored $event): void { $this->testDataCollector->failed++; $this->testDataCollector->total++;
$this->testDataCollector->messages[] = [ 'test' => $event->test()->className().'::'.$event->test()->methodName(), 'message' => $event->throwable()->message(), ]; } }`
Enter fullscreen mode
Exit fullscreen mode
The last subscriber is where the output happens.
use PHPUnit\Event\TestRunner\Finished; use PHPUnit\Event\TestRunner\FinishedSubscriber; use Tests\Extensions\TestDataCollector;use PHPUnit\Event\TestRunner\Finished; use PHPUnit\Event\TestRunner\FinishedSubscriber; use Tests\Extensions\TestDataCollector;class TestsDoneSubscriber implements FinishedSubscriber {
public function construct(private TestDataCollector $testDataCollector) {}
public function notify(Finished $event): void { $this->testDataCollector->write(); } }`
Enter fullscreen mode
Exit fullscreen mode
And this can now give an example output of
# Test results
Summary
failed: 1 passed: 24 total: 25
Messages
| Test | Message |
|---|---|
| App\Tests\AnswerTest::testFail | Failed asserting that false is true. |
Enter fullscreen mode
Exit fullscreen mode
Conclusion
Even if you don't need to format the output for AI agents I think you now have a better idea of what is possible.
Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
mistralversioninterface
Cleaned 10k customer records. One emoji crashed my entire pipeline.
Cleaned 10k customer records. One emoji crashed my entire pipeline. Was scraping ecommerce product reviews last month. Got 10k records, ran a cleaning script to normalize text before feeding it to a sentiment analysis tool. Script ran fine on test data (500 rows). Pushed it to production. 48 minutes in, the whole thing just stops. No error message. Just frozen. Thought it was memory. 10k rows shouldn't be a problem, but maybe something leaked. Restarted the process, added memory tracking. Same thing. Froze at exactly the same spot (row 6,842). Checked the CSV manually. Row 6,842 looked fine. Customer name, review text, rating. Nothing weird. Then I noticed it. The review had a 💩 emoji in it. Specifically: "This product is 💩 don't buy it" Encoding hell My script was using basic text encod

From MLOps to LLMOps: A Practical AWS GenAI Operations Guide
The vibe at AWS Student Community Day Tirupati on November 1, 2025, was different from what I thought it would be like. There were lots of students, cloud fans, and builders in the room. They were all there to learn, meet, and geek out about AWS. Throughout the day, there were several classes, and each one added something new. One lesson, though, made me sit up and pay more attention. Raghul Gopal , a Data Scientist and AWS Community Builder (ML), walked up to the stage to talk about something that most people don't think much about: how do you run AI models in real life? Not just make them on a laptop and be happy about it; consistently test, watch, and scale them. " Generative AI Operations: FMOps, LLMOps Integration with MLOps Maturity Model " was the title of the talk. When it was over
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Models

Built a script to categorize expenses automatically. Saved 3 hours/month.
Built a script to categorize expenses automatically. Saved 3 hours/month. Spent every Sunday sorting bank transactions into categories for my freelance accounting. Business meals, software subscriptions, travel, office supplies. Copying stuff from my bank CSV into a spreadsheet. After 6 months of this I finally snapped and wrote a Python script. Before (the painful way) Every week I'd download my bank CSV export. Then open it and categorize each transaction myself: Transaction at "Starbucks" → Business meal "AWS Invoice" → Software/tools "United Airlines" → Travel "Office Depot" → Office supplies For maybe 40 to 60 transactions per week this took about 45 minutes. Hated it. The script Basic Python that reads the bank CSV and categorizes based on keywords. Nothing fancy. import pandas as pd




Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!