Summary
When recording a TIA graph with --tia --parallel --max-batch-size=N, each respawned worker process overwrites the previous worker's edge partial for the same slot. Only the last batch per worker slot survives into graph.json, so the dependency graph is silently incomplete. Subsequent --tia runs then re-execute most of the suite as "uncached" on every run, and (because replay runs don't record) the graph never heals.
There is no warning — recording appears to succeed.
Versions
- Pest: v5.0.1
- PHP: 8.4.23 (Xdebug 3.x,
xdebug.mode=coverage, forwarded to workers via --passthru-php)
- OS: macOS (arm64)
Steps to reproduce
On any suite large enough to need more than one batch per worker (i.e. more than processes × max-batch-size tests):
# 1. Record with batch recycling (fresh graph):
vendor/bin/pest --tia --fresh --parallel --max-batch-size=40 \
--passthru-php="-d zend_extension=xdebug -d xdebug.mode=coverage"
# 2. Inspect the graph:
php -r '$g = json_decode(file_get_contents(getenv("HOME")."/.pest/tia/<project-key>/graph.json"), true);
echo count($g["edges"]), " test files with edges\n";'
# 3. Re-run without --fresh: most of the suite executes as "uncached" every time.
Observed on our suite (~9,000 tests across ~900 test files, 12 processes): the recording run completes green, but the graph contains edges for only 202 of ~900 test files — roughly the final batches of each worker slot. Re-recording with --parallel but without --max-batch-size produces the complete graph (922 test files) and replay then works as designed (768s → 3.7s).
Cause
Pest\Plugins\Tia::flushWorkerPartial() writes each worker's edges to a state file keyed by workerToken():
$this->state->write(self::KEY_WORKER_EDGES_PREFIX.$this->workerToken().'.json', $json);
and workerToken() prefers ParaTest's TEST_TOKEN:
$raw = $_SERVER['TEST_TOKEN'] ?? $_ENV['TEST_TOKEN'] ?? null;
TEST_TOKEN is stable per worker slot, not per worker process. With --max-batch-size=N, ParaTest recycles the process in each slot every N tests, and every recycled process flushes its partial to the same filename, clobbering the edges recorded by its predecessors in that slot. The parent's merge at the end of the run only ever sees the last batch per slot.
The same keying is used for KEY_WORKER_RESULTS_PREFIX and KEY_WORKER_NO_DRIVER_PREFIX, which presumably lose data the same way.
Suggested fix
Key the partials by ParaTest's UNIQUE_TEST_TOKEN (unique per worker process, e.g. 2_6a690d236824c) instead of TEST_TOKEN, falling back to TEST_TOKEN/PID as now:
$raw = $_SERVER['UNIQUE_TEST_TOKEN'] ?? $_ENV['UNIQUE_TEST_TOKEN']
?? $_SERVER['TEST_TOKEN'] ?? $_ENV['TEST_TOKEN'] ?? null;
The parent already merges all files matching the prefix (keysWithPrefix), so accumulating one partial per process rather than per slot should merge cleanly without further changes.
Workaround for anyone else hitting this
Record without --max-batch-size (long-lived workers → one partial per slot is then correct). Note this removes the worker-recycling memory relief, so suites that rely on batching to stay under memory_limit may need a higher limit for recording runs.
Summary
When recording a TIA graph with
--tia --parallel --max-batch-size=N, each respawned worker process overwrites the previous worker's edge partial for the same slot. Only the last batch per worker slot survives intograph.json, so the dependency graph is silently incomplete. Subsequent--tiaruns then re-execute most of the suite as "uncached" on every run, and (because replay runs don't record) the graph never heals.There is no warning — recording appears to succeed.
Versions
xdebug.mode=coverage, forwarded to workers via--passthru-php)Steps to reproduce
On any suite large enough to need more than one batch per worker (i.e. more than
processes × max-batch-sizetests):Observed on our suite (~9,000 tests across ~900 test files, 12 processes): the recording run completes green, but the graph contains edges for only 202 of ~900 test files — roughly the final batches of each worker slot. Re-recording with
--parallelbut without--max-batch-sizeproduces the complete graph (922 test files) and replay then works as designed (768s → 3.7s).Cause
Pest\Plugins\Tia::flushWorkerPartial()writes each worker's edges to a state file keyed byworkerToken():and
workerToken()prefers ParaTest'sTEST_TOKEN:TEST_TOKENis stable per worker slot, not per worker process. With--max-batch-size=N, ParaTest recycles the process in each slot every N tests, and every recycled process flushes its partial to the same filename, clobbering the edges recorded by its predecessors in that slot. The parent's merge at the end of the run only ever sees the last batch per slot.The same keying is used for
KEY_WORKER_RESULTS_PREFIXandKEY_WORKER_NO_DRIVER_PREFIX, which presumably lose data the same way.Suggested fix
Key the partials by ParaTest's
UNIQUE_TEST_TOKEN(unique per worker process, e.g.2_6a690d236824c) instead ofTEST_TOKEN, falling back toTEST_TOKEN/PID as now:The parent already merges all files matching the prefix (
keysWithPrefix), so accumulating one partial per process rather than per slot should merge cleanly without further changes.Workaround for anyone else hitting this
Record without
--max-batch-size(long-lived workers → one partial per slot is then correct). Note this removes the worker-recycling memory relief, so suites that rely on batching to stay undermemory_limitmay need a higher limit for recording runs.