| name | test-writer |
| description | Writes missing Drupal PHPUnit tests for the AI module based on missing-testing reports, runs them in ddev, creates an issue draft, and stores the variant. |
Test Writer Skill
You write missing PHPUnit tests for the Drupal AI module. You are pointed at a specific file from the missing-testing/ reports and you create the test, validate it, and package it up.
CRITICAL: This skill uses a shared ddev environment. NEVER spawn subagents for concurrent work. All steps MUST be performed sequentially.
Inputs
When invoked, you will be told:
- Which source file needs a test (e.g.,
src/Plugin/AiGuardrail/RegexpGuardrail.php)
- What test type is needed (Unit, Kernel, Functional, FunctionalJavascript)
- What to test (from the missing-testing report)
If not given explicit details, read the relevant missing-testing/*.md report to find the entry for the file.
Workflow
Step 1: Understand the source code
Read the source file in repos/ai/ to understand:
- What the class does
- Its public methods
- Its dependencies (constructor injection, services, etc.)
- Any parent class or interface it implements
- How similar classes in the same directory are structured
Also check if sibling classes have tests you can use as a pattern. For example, if writing a test for FloatConverter.php, look at the existing BoolConverterTest.php for structure.
Step 2: Create the variant folder
Create a folder under repos/variants/ named after the test. Use a short descriptive name based on the class being tested, e.g.:
repos/variants/regexp-guardrail-test/
Copy the entire repos/ai/ directory into it:
cp -r repos/ai repos/variants/regexp-guardrail-test/ai
Step 3: Write the issue draft
Write a Drupal issue to repos/variants/{folder}/issue.html following the ai-issue-writer format:
[Tracker]
<strong>Update Summary: </strong>[One-line status update for stakeholders]
<strong>Short Description: </strong>Add {test type} test for {ClassName}
<strong>Check-in Date: </strong>MM/DD/YYYY
[/Tracker]
<h3 id="summary-problem-motivation">Problem/Motivation</h3>
The <code>{ClassName}</code> in <code>{file path}</code> currently has no test coverage. {Brief explanation of what the class does and why testing it matters.}
<h3 id="summary-proposed-resolution">Proposed resolution</h3>
<ul>
<li>Add a {test type} test covering {what will be tested}.</li>
<li>{Additional bullet points for specific test scenarios.}</li>
</ul>
<h3 id="summary-ai-usage">AI usage (if applicable)</h3>
[ ] AI Assisted Issue
[ ] AI Assisted Code
[x] AI Generated Code
This code was mainly generated by an AI with human guidance, and reviewed, tested, and refined by a human.
[ ] Vibe Coded
- <strong>This issue was created with the help of AI
Step 4: Deploy to ddev environment
Copy the AI module to the running ddev Drupal environment:
rm -rf running-drupal/web/modules/contrib/ai
cp -r repos/ai running-drupal/web/modules/contrib/ai
Step 5: Create a branch
Inside the ai module directory under running-drupal, create a git branch:
cd running-drupal/web/modules/contrib/ai
git checkout -b add-{class-name-kebab}-test
Step 6: Write the test
Create the test file in the appropriate location following Drupal testing conventions:
File location:
- Unit tests:
tests/src/Unit/{mirror source path}/
- Kernel tests:
tests/src/Kernel/{mirror source path}/
- Functional tests:
tests/src/Functional/{mirror source path}/
- FunctionalJavascript tests:
tests/src/FunctionalJavascript/ (avoid unless specifically requested)
Always prefer Unit or Kernel tests. Only use Functional tests when you truly need a full Drupal bootstrap with browser simulation. Never use FunctionalJavascript unless specifically told to.
Test class conventions:
- Namespace:
Drupal\Tests\ai\{Unit|Kernel|Functional}\{path}
- Class name:
{SourceClass}Test
- Must have
@coversDefaultClass or @covers annotation
- Must have
@group ai
- Must use
declare(strict_types=1);
- For Kernel tests: extend
KernelTestBase, list required modules in $modules
- For Unit tests: extend
UnitTestCase
- For Functional tests: extend
BrowserTestBase
IMPORTANT: Use PHPUnit annotations, NOT PHP 8 attributes. The tests must be compatible with Drupal 10. Use docblock annotations like @covers, @coversDefaultClass, @group, @dataProvider, @depends, @expectedException, etc. Do NOT use PHP 8 attributes like #[CoversClass()], #[Group()], #[DataProvider()], #[Test], etc.
Correct:
class RegexpGuardrailTest extends KernelTestBase {
public function testProcessInput(): void {
Wrong (do NOT do this):
#[CoversClass(RegexpGuardrail::class)]
#[Group('ai')]
class RegexpGuardrailTest extends KernelTestBase {
#[Test]
public function processInput(): void {
Common modules for Kernel tests:
protected static $modules = [
'ai',
'key',
'file',
'system',
'node',
'media',
'user',
];
Add more modules only if the class under test requires them (check service definitions and dependencies).
When the class under test needs an AI provider (e.g., guardrails that call an LLM, services that use ai.provider): use the built-in EchoAI mock provider from the ai_test module. This is a full mock LLM provider that echoes inputs back or replays recorded responses. To use it:
- Add
'ai_test' to the $modules array.
- In
setUp(), install config and entity schema:
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', ['file_usage']);
$this->installConfig(['ai', 'ai_test']);
$this->installEntitySchema('ai_mock_provider_result');
}
- Create the provider instance:
$provider = \Drupal::service('ai.provider')->createInstance('echoai');
- Use model ID
'gpt-test' or 'gpt-awesome' for requests.
- The EchoAI provider supports chat, embeddings, moderation, speech_to_text, text_to_speech, image_classification, and text_to_image operations.
- For chat, it echoes back:
"Hello world! Input: {your text}. Config: {json config}." by default.
- For pre-recorded responses, place YAML files in
tests/resources/ai_test/requests/{operation_type}/ with request and response keys. The provider matches the serialized input and returns the recorded response.
Example - testing a guardrail plugin that needs to call an LLM:
protected static $modules = [
'ai',
'ai_test',
'key',
'file',
'system',
'user',
];
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('file');
$this->installSchema('file', ['file_usage']);
$this->installConfig(['ai', 'ai_test']);
$this->installEntitySchema('ai_mock_provider_result');
}
public function testGuardrailWithProvider(): void {
$provider = \Drupal::service('ai.provider')->createInstance('echoai');
$input = new ([ (, )]);
= ->(, );
}
Pre-recording specific responses: When your test needs the EchoAI provider to return a specific response (not just the default echo), create a YAML fixture file. Place it in tests/resources/ai_test/requests/{operation_type}/{TestClassName}.yml within the module being tested. The EchoAI provider auto-discovers these from any enabled module.
YAML fixture format (see repos/ai/modules/ai_automators/tests/resources/ai_test/requests/chat/AutoCompleteTagsTaxonomyTest.yml for a real example):
request:
messages:
- role: user
text: "Your exact prompt text here"
images: []
tools: null
tool_id: null
debug_data: {}
chat_tools: null
chat_structured_json_schema: {}
chat_strict_schema: false
response:
normalized:
role: assistant
text: >-
The exact response text you want back
images: []
tools: null
tool_id: null
rawOutput: []
metadata: {}
tokenUsage:
input: null
output: null
total: null
reasoning: null
cached: null
The provider matches by serializing the input and comparing it to the request key. If there's an exact match, the response is returned instead of the default echo. This is essential for testing plugins like RestrictToTopic that parse structured JSON from the LLM response.
Do NOT mock the AI provider manually when the EchoAI test provider is available. Always prefer using ai_test module's EchoAI provider over hand-rolled mocks, as it exercises the real provider infrastructure.
Test method naming: test{BehaviorDescription} - descriptive, not just testMethod.
What to cover:
- All public methods
- Edge cases and error conditions
- Both valid and invalid inputs
- Return types and values
Step 7: Run the test
Run the test using ddev:
cd /path/to/running-drupal
ddev exec -d /var/www/html ./vendor/bin/phpunit -c phpunit.xml web/modules/contrib/ai/tests/src/{path to test file}
If the test fails:
- Read the error output carefully
- Fix the test (not the source code - we are only writing tests)
- Re-run until it passes
If a test genuinely cannot pass because of a bug in the source code, note this in the issue draft and skip that specific test method, leaving a @todo comment explaining the issue.
Step 8: Run code quality checks
Run PHPCS on the test file:
ddev exec -d /var/www/html ./vendor/bin/phpcs --standard=Drupal,DrupalPractice web/modules/contrib/ai/tests/src/{path to test file}
Fix any coding standards violations. Re-run until clean.
Step 9: Move the module back
Copy the ai module (with the new test) back to the variant folder:
rm -rf repos/variants/{folder}/ai
cp -r running-drupal/web/modules/contrib/ai repos/variants/{folder}/ai
The variant folder should now contain:
repos/variants/{folder}/ai/ - the full AI module with the new test on a branch
repos/variants/{folder}/issue.html - the issue draft
Step 10: Summary
Report what was done:
- Which test was written and where
- Which branch it is on
- Whether the test passes
- Whether PHPCS is clean
- Any issues or notes
Important rules
- NEVER modify source code in
repos/ai/ - that is read-only reference
- NEVER spawn subagents - all work is sequential due to the shared ddev environment
- NEVER write FunctionalJavascript tests unless explicitly asked - prefer Unit or Kernel
- Always run tests before declaring done - a test that doesn't pass is not done
- Always run PHPCS before declaring done - coding standards matter
- Follow existing test patterns - look at sibling test files for conventions
- Use
declare(strict_types=1); in all test files
- The variant folder is the deliverable - it must contain both the module with the test branch and the issue draft