AI does not replace system engineering. Stories abound about AI running amok, deleting production databases, exposing private data, failing to deliver on promises. And yet it's an incredibly useful tool for engineering -- quickly collecting and suggesting requirements, documenting architecture and processes, writing tests, and more.
I find that coding agents have unlocked a lot of power, and helped me build some impressive things quickly. Here's an example of how I recently used AI to get a complex Drupal module working correctly, through test-driven development.
The problem
Group PURL is a module that manages "Persistant URLs" associated with group entities, using Drupal's popular Group module. Group Purl takes the URL alias of a group, and sets it as either a persistant URL part after the domain name, or as a subdomain. A system like this was widely used with "Organic Groups" in Drupal 6 and 7, but never really got fully functional for Drupal 8 or later - I picked it up because we needed it on several sites, got it partially working, and never took it beyond that. Others updated it for Drupal 9 and 10, but didn't fix some of its underlying issues. And it was also only being used with Group 1.x, which is not being updated for Drupal 11.
With a new site being built in Drupal 11 that needed this functionality, I took on updating Group Purl to work with Drupal 11 and Group 3.x. And while doing so, I decided to use an AI coding assistant to help me get to the bottom of the longstanding issues with the module, and make it work reliably. As part of doing so, I decided it was high time to learn how to use Drupal.org's automated unit testing, and use that as the basis for making sure new development doesn't break anything.
The tools
PHPUnit - the fundamental testing tool used by Drupal is PHPUnit. While I've run it once or twice, I've never set up unit tests using it, and never hooked it up to automatically run in the Drupal CI environment.
Claude Code - For the past few months, Claude Code has been my choice for an AI coding assistant. It runs inside the project directory, so it can read and change everything in there. It also has full access to the current shell environment, it can browse the web, read content in other directories, and more.
Drupal Flake - a local development environment/dependency manager based for Drupal, based on Nix Flakes. I developed this as an experiment, and got it working for a presentation I gave at DrupalCon earlier this year -- and it has entirely replaced DDev for my local development. When I need to do something new in my local development, I've been adding or extending Drupal Flake so I can do it in a way that makes me happy.
The process
I started with a dev copy of the site in question, with 20ish groups already defined. I used the composer plugin mglaman/composer-drupal-lenient plugin so I could install the latest (incompatible) dev version of group_purl into the site.
Setup and basic fixes
First I had Claude fix the Drupal and Group version compatibility issues -- I asked it to find the changes, pointed it at the group api upgrade page, and let it churn away, giving it feedback on errors until I could at least have it (and the "PURL" module it depends upon) enabled without making the site whitescreen.
Create tests
Next, I wanted to specify exactly how the code is supposed to work. I gave Claude a rundown of exactly what the code is supposed to do: intercept the request, determine if there was a group path in the route, and if there is, store it as a "group context", strip it out and submit it as a subrequest for further processing. Then, when links are generated for the page, it's supposed to check whether the link target has its own group context that should be added, and if not, whether the link should have the current group context, or "exit" the context to the bare url.
I gave Claude a dozen specific scenarios of how URLs should behave, both for the current route and generating URLs on the page. And I asked it to write tests for me. It generated three different test classes to test different aspects, and for the most part, they all looked good.
Run tests locallyFix Drupal Core bug
Now came what turned out to be the hardest part: running PHPUnit tests.
This is where a coding assistant adds huge value: you can just ask it to set up and run the test framework. So I did. And... nothing but errors.
Why? Because PHPUnit could not find the socket to talk to the database.
Drupal Flake starts up the database and configures it to listen on a socket inside a project "data" directory. However, while Drupal itself can talk to the database through this socket just fine, if you specify the unit_socket path in the database connection string, Drupal's PHPUnit test setup turns out drops this parameter before handing off to PHPUnit.
I managed to get it working by hard-coding the absolute path to the socket in the PHP configuration setup in the flake -- and then continued to fixing the functionality based on the tests.
But after I had the project work delivered, I came back to solve the test running under Drupal Flake, and eventually filed a bug against core to fix it.
Fix code
Now that I had running tests, I was able to use those to guide Claude into fixing the issues. I prioritized the main cases, and very quickly got it working. But then we hit the biggest longstanding issue in the module: links were getting written with the current group getting added to the group that the link destination was in. And if you followed that link, you would have two groups in the path -- and links that added a third!
Claude quickly figured out that it was a service priority issue -- the Group Purl service needed to process the link before PURL. So it changed that priority -- which then broke a bunch of the basic routing. When I asked it to fix that, it changed the priority back!
Claude was not smart enough to figure out a solution on its own -- at least not until I pointed out the obvious -- split the service in two, so some functionality could run before PURL, and some after.
That, in fact, was the underlying issue that has been broken all along in this module - so setting up tests, and having an AI assistant diagnose and attempt to fix, combined with a bit of human creativity, got to the bottom of it much quicker than I think either of us would have alone.
Configure CI tests
The last thing to do is configure the Continuous Integration (CI) tests to run whenever the module code is updated on Drupal.org. This was another thing I have not done, so I asked Claude to hook this all up. It dutifully created a .gitlab-ci.yml file that loaded all the tests, and I pushed it all up!
... only to find lots of weird errors. It turns out Drupal.org's CI, like many different CI systems, expects some very particular parameters, so you can't just ship some random ci configuration and expect it to work.
It turns out the best way is to start from the Drupal Association's template. When I pointed Claude at this, it refactored the .gitlab-ci.yml file to use it, and now the tests run as expected.
At this writing, all the functional tests are failing on Drupal.org, but that's because some of the required updates to PURL are not yet released -- when there's a new release of PURL with Drupal 11 compatibility, the Group Purl tests should start passing.
And finally, I did end up having Claude make a bunch of tests to address some of those nested group links, but have not gotten those tests passing just yet. I focused on the tests that ensured those links would never get created in the first place, and then called it "good enough" for now.
Clean up documentation
Finally, this is a really complex module that is hard to wrap your head around. So I asked Claude to outline how it works, and include a Mermaid diagram that shows the flow. The module now has a README that can really help people trying to figure out what's going on, to understand how this works.
In the age of AI, documentation is even more important -- LLMs depend upon good documentation to work well and solve problems correctly. They are also good at documenting existing work, so it's easier than ever to provide decent documentation to go along with your work.
The result
Over the course of this project, with the help of Claude Code, I created a robust, stable release of the Group Purl module that is working better than it has since Drupal 7, with tests to ensure it stays working with new development.
Note that I only tested and worked with the "subdirectory" modifier, and not the "subdomain" version -- if we get a project that wants a Group site that uses a different subdomain for each project, that would be next for us to add.
The other major outcome of this project was enhancing Drupal Flake to make it easy to run PHPUnit tests.
Add new comment