i've been working on a simulator for an industrial operations and compliance platform.
The application handles inspections, equipment maintenance, discrepancies, work orders, inventory, and a bunch of related operational paperwork.
The simulator operates a fake facility inside the actual production application. It has equipment, employees, maintenance problems, inspections that need doing, and other things that would normally require people to log in and do their jobs.
Except there aren't any people.
It's running in production now, against an isolated synthetic facility, on a schedule. The records are fake. The application and its workflows are real.
I put most of this together in my spare time over a few long sessions. It's been getting incremental improvements since, but the core system was built in a surprisingly small number of sittings.
why i wanted this thing
Testing a system like this is annoying because everything is connected to something else.
An inspection creates a discrepancy. A discrepancy can take equipment out of service. Somebody has to repair it, possibly consume inventory, close a work order and return the equipment to service. Meanwhile there are compliance periods ticking away, reports being generated, and more inspections becoming due.
You can test each piece individually. We already do plenty of that.
But a pile of unit tests doesn't necessarily tell you what happens when the system has been running for months, users have been doing things in different orders, and somebody goes back and changes a record from three weeks ago.
And manually maintaining a test environment full of believable operational history is tedious as shit.
There's also the customer demo problem. Getting a facility ready to show somebody is a pain in the ass. You need equipment, inspection history, discrepancies, work orders, and enough activity to make the thing look like it's actually being used. Otherwise half the screens are empty, and somebody has to waste hours setting everything up by hand.
So the idea was to build something that would operate the application the way an actual facility might be operated, and keep doing it.
Not just fill a database with random crap.
it has to use the actual application
The simulator uses Node.js and Playwright to interact with the application's frontend.
It authenticates with dedicated test accounts using the normal login process. Different accounts have different roles, and those roles have different jobs and permissions.
The simulator discovers work from the same screens a human would use. If something isn't available to that user, or the application won't allow an action, the simulator doesn't get to override that decision.
I could have made the whole thing substantially faster by having it write directly into MySQL or call private application methods.
That would also defeat a good portion of the reason it exists.
I want to know whether the application actually works, including the ridiculous chain of frontend code, backend validation, permissions, saves, and state changes between clicking something and having it show up correctly afterward.
A successful button click isn't enough. The simulator goes back and verifies what the application actually recorded.
the equipment needs a memory
This was probably the more interesting part of the project.
If a simulated piece of equipment has a damaged component, that component needs to remain damaged tomorrow.
It can't suddenly become fine because the script started over.
The simulator maintains its own synthetic physical world in SQLite, separate from the application's database. It tracks equipment characteristics, readings, usage, defects, repair state, and other information that needs to survive between runs.
The application knows which inspections are due and what the official workflow state is.
The simulator knows what condition the imaginary equipment is in.
Those are two different things.
For example, a piece of equipment might gradually develop an abnormal measurement as it accumulates operating hours. Eventually that reading becomes a discrepancy. The inspection records it, and the problem remains until the simulated repair actually happens.
A maintenance worker then has to go through the appropriate work-order or return-to-service workflow. The equipment doesn't get magically repaired because some function returned successfully.
I actually managed to screw this up during development.
The return-to-service code was using the same answer-selection path as an active discrepancy. So after repairing something, the simulator could answer the repaired check with another failing value and reopen the damn problem it had just fixed.
That needed a separate clean-pass path, proper readings, and verification that the discrepancy and any associated work order were actually closed.
making it behave less like a test script
A simulator that completes every inspection perfectly, every day, is not particularly useful.
Real facilities have idle equipment, missed inspections, delayed maintenance, and things that break at inconvenient times.
The simulator makes these decisions using a configurable realism model.
Some equipment gets used more than other equipment. Some inspections are completed, others are marked as not used, and some are deliberately missed. Safety observations and preventive maintenance have their own workflows. Critical defects can take equipment out of service.
Missed inspections are especially useful because the application has its own compliance logic. The simulator doesn't just insert a noncompliance record to make a test pass. It leaves eligible work undone and lets the application's normal process determine whether a compliance finding should exist.
It also knows about existing findings and can run their explanation workflow.
Defects have their own persistent lifecycle. Some require parts, some can be closed directly, and some repairs take longer than others.
The parts aren't just random inventory entries either. The simulator attempts to select something related to the actual problem. A repair that needs a part shouldn't consume whatever happened to be first in a database query.
I also added a material-flow model.
Inventory has to come from somewhere and go somewhere. Receipts, transfers, replenishment, and consumption are represented as movements in a ledger. The quantities have to balance, and an unconfirmed browser action doesn't get to move material merely because the simulator intended it to happen.
If the process dies halfway through creating a transaction, that needs to be reconciled against the application's records before trying again. Otherwise you've built a machine for duplicating imaginary inventory.
Which would be funny, but not particularly helpful for testing accounting.
the browser was its own problem
One of the earlier approaches involved discovering the available inspections, loading all their check data, and then processing them.
This could mean dragging around roughly 180 inspections' worth of check context before the simulator actually completed anything.
It also hammered a single browser page with repeated navigation, which appears to have contributed to a hang during a live run.
So that went away.
Now it discovers the work, takes an item, loads what it needs, performs it, verifies the result, persists the state, and moves to the next one.
Individual inspections can also be surprisingly slow. Some of the larger maintenance wizards have multiple pages, interconnected equipment, and a considerable amount of application-side processing.
You can't just put a 30-second timeout on everything and call it broken when one of these takes longer.
The runner has bounded retries for transient problems, page recovery, and separate handling for actual validation failures. A legitimate rejection from the application isn't something to keep retrying until it magically becomes valid.
the AI part, or lack thereof at runtime
I used coding agents heavily while building the simulator. The operational skills are documented in SKILL.md files, organized around the roles and workflows they apply to.
The original architecture also leaves room for swapping in a language-model-driven agent runtime.
But the production runner currently uses a local adapter with deterministic application logic.
There isn't an LLM sitting there every time cron fires, improvising its way through a bunch of inspection questions.
The local adapter knows how to operate the supported browser workflows, and the skills document their behavior. It can't interpret arbitrary skill prose and invent a new workflow it hasn't been programmed to handle.
That's intentional for now. Having something decide that an inspection probably succeeded because the screen looked vaguely correct would be a pretty stupid way to test compliance software.
The AI was useful for developing the thing, researching the application behavior, and generating and correcting code. The running simulator needs predictable behavior and observable failures.
production, but not real operations
The simulator runs against a dedicated synthetic environment in the application's normal production deployment.
It uses its own accounts and equipment. None of this is supposed to touch actual customer operations.
The code lives on a separate Git branch rather than being merged into the application's main branch. The scheduled job pulls that branch and runs the simulator directly.
It doesn't need to be part of the web application's release to do its job.
Cron starts a run, the simulator works through the appropriate operational activities, records what happened, and exits. Subsequent runs pick up the persistent state.
There are safeguards against overlapping runs and accidentally repeating completed work. The operational date follows the configured timezone, which is another detail that gets ugly around midnight and daylight saving time.
The simulator also records work it couldn't perform. An unsupported inspection, missing permissions, a failed save, or an uncertain inventory transaction shouldn't disappear into a cheerful success summary.
now it just keeps running
The core project is finished and deployed. I'm still working on it, mostly improving how realistically the facility behaves and dealing with the weird cases that only appear when you keep exercising a large application.
The initial implementation came together over a handful of long sessions outside my normal work. What takes more time now is tuning the behavior, finding the edge cases, and dealing with everything the application does that nobody thought to account for.
The more of the normal application it operates, the more likely it is to encounter the same strange state combinations that real users eventually produce.
It isn't a replacement for unit tests or human QA. It's another way of keeping the software under load, with persistent state and actual workflows instead of completely isolated test cases.
And I don't have to sit there doing the same inspections every morning just to keep the test environment alive.
The imaginary employees can deal with that.