Jean Carlo Benites PorrasEspañol

Automated tests inside Baldur’s Gate 3

A harness that runs test scenarios inside the real game, from one command, and leaves everything as it was. I built it for a modding project and released it as open source.

Period
2026
Role
Author
License
MIT
Stack
TypeScript, Lua, C++, Python

See the code on GitHub

bg3:test · crooked.dead-aim
$ node --import tsx tools/src/bg3-test.ts --scenario crooked.dead-aim
PASS mods-loaded: Required BG3 mods are loaded in order
PASS bonus-action-cost: Casting Dead Aim spent exactly one bonus action (expected "1", actual "1.0")
PASS marked-hit-bypasses-resistance: The marked attack ignored the target's Fire resistance (expected "8", actual "8")
PASS post-consume-resistant: Resistance applied again after Dead Aim was consumed (expected "4", actual "4")
TIME crooked.dead-aim: 26165 ms
PASS crooked.dead-aim
Excerpt from the real output of a run on October 24, 2026: 4 of its 23 checks, 26.2 seconds inside the game.

The problem

A BG3 mod changes game rules: what a spell costs, which resistance it ignores, when an effect is used up. Unit tests cannot tell you whether the engine applies those rules as expected. Checking by hand means opening the game, loading a save, setting up a fight and casting the spell: several minutes per case, every time something changes.

How it works

  1. Starts BG3 through Steam and turns Steam Cloud off for the run only.
  2. Loads an exact disposable save, verified by hash, or starts a new campaign.
  3. Hands the scenario to Script Extender over a JSON file channel.
  4. The scenario, written in Lua, creates what it needs, casts spells with their real cost, advances turn-based rounds and compares engine state with what it expects.
  5. Undoes what it created in reverse order, closes the game and restores the mod configuration byte for byte.
Baldur’s Gate 3 entering turn-based mode: the test character next to two wolves the scenario spawned.
The scenario spawns its targets and enters turn-based mode.
Baldur’s Gate 3 in combat: the log confirms the test character used Dead Aim on a wolf.
The spell is cast with its real cost; the assertion reads the result from the engine.

The hard parts

Leave your saves alone
The harness works on a disposable copy and locks the run so two processes cannot collide. A small C++ helper turns Steam Cloud off and puts it back the way it was.
Linux and Proton
It runs on Steam with Proton, with the game on a headless Hyprland display. LSLib, the standard tool for packing mods, rejects every path on Linux, so I wrote a packer on top of its library.
A crash is not a FAIL
If the game crashes, the run ends in ERROR, not FAIL. An environment problem never looks like a rule that was implemented wrong.

In numbers

The harness grew inside a private modding project; the public repo has the harness and an example mod.

  • 439in-game scenariosmodding project
  • 1,737Vitest testsmodding project
  • 194Lua test suitesmodding project
  • 440Vitest tests that do not open the gamepublic repo, Oct 24, 2026
  • PASSexample.runtime-smoke inside BG3, before releasepublic repo, Oct 20, 2026

What stays out

The modding project’s content comes from commercial books, so it is not published. The repo shows the harness, which is the engineering part.

Next case: CLX

Write to me

I am looking for a remote senior full-stack or backend role. I answer in English or Spanish.