Summary
With a TIA graph present and no coverage driver enabled, a file that was modified during
one run and then reverted is treated as changed by every run afterwards, indefinitely. Each
of those runs takes the no coverage driver branch, which runs the whole suite and returns
before the snapshot that would clear the stale entry. The result is a permanent
full-suite fallback with a WARN that reads like a one-off.
Observed (pest 5.1.0, PHP 8.4.23, Xdebug present but xdebug.mode empty)
- Record a graph with
XDEBUG_MODE=coverage vendor/bin/pest --tia.
- Edit one broadly-covered file, run
XDEBUG_MODE=coverage vendor/bin/pest --tia --filtered
to completion, then git checkout -- that file. Tree is clean and HEAD is unchanged
from recorded_at_sha.
vendor/bin/pest --tia (no XDEBUG_MODE):
WARN Detected PHP source changes but no coverage driver is available.
─ Running the full suite to avoid using a stale dependency graph.
Tests: 2032 passed Duration: 361.80s
- Run it again, still a clean tree — identical WARN, full suite again, 353.89s.
graph.json shows the cause: the baseline's tree holds exactly one entry, the
pre-revert hash of the file edited in step 2.
- With a driver enabled, the same clean tree still reports
164 affected test files (from 1 changed file).
Mechanism
ChangedFiles::filterUnchangedSinceLastRun() (src/Plugins/Tia/ChangedFiles.php:27-50)
seeds its candidate set with every file in lastRunTree, not only the files git
reports, and keeps any whose current hash differs from the snapshot. A reverted file
differs, so it is reported as changed on a clean tree.
setLastRunTree() is called only from Tia.php:639, :727 and :1654 — all on paths
that complete a record or a replay.
Tia::enterReplayMode() returns at :1051, inside the
$hasProjectPhpSourceChanges && ! $coverageAvailable branch, before reaching any of them.
So the only branch that can clear the stale entry is unreachable from the state that the
stale entry itself forces you into, unless you install/enable a driver.
Impact
A contributor without pcov/Xdebug who has ever had a TIA graph gets full-suite runs forever,
with a warning that suggests a transient condition. --tia is silently a no-op plus
overhead for them.
Suggested fix
Snapshot the tree on the no-driver path too, or drop entries from lastRunTree once their
file matches its committed content. Either removes the trap without weakening the guard —
the guard's purpose (do not filter using a stale graph) is served by running the full suite,
which already happened.
Provenance note
The stuck state and both full-suite runs are directly observed. That a completed run
with a driver clears the entry is inferred from the source above and not measured — it
costs a 27-minute run on the reporter's suite and the verdict it fed did not turn on it.
Summary
With a TIA graph present and no coverage driver enabled, a file that was modified during
one run and then reverted is treated as changed by every run afterwards, indefinitely. Each
of those runs takes the
no coverage driverbranch, which runs the whole suite and returnsbefore the snapshot that would clear the stale entry. The result is a permanent
full-suite fallback with a WARN that reads like a one-off.
Observed (pest 5.1.0, PHP 8.4.23, Xdebug present but
xdebug.modeempty)XDEBUG_MODE=coverage vendor/bin/pest --tia.XDEBUG_MODE=coverage vendor/bin/pest --tia --filteredto completion, then
git checkout --that file. Tree is clean and HEAD is unchangedfrom
recorded_at_sha.vendor/bin/pest --tia(noXDEBUG_MODE):graph.jsonshows the cause: the baseline'streeholds exactly one entry, thepre-revert hash of the file edited in step 2.
164 affected test files (from 1 changed file).Mechanism
ChangedFiles::filterUnchangedSinceLastRun()(src/Plugins/Tia/ChangedFiles.php:27-50)seeds its candidate set with every file in
lastRunTree, not only the files gitreports, and keeps any whose current hash differs from the snapshot. A reverted file
differs, so it is reported as changed on a clean tree.
setLastRunTree()is called only fromTia.php:639,:727and:1654— all on pathsthat complete a record or a replay.
Tia::enterReplayMode()returns at:1051, inside the$hasProjectPhpSourceChanges && ! $coverageAvailablebranch, before reaching any of them.So the only branch that can clear the stale entry is unreachable from the state that the
stale entry itself forces you into, unless you install/enable a driver.
Impact
A contributor without pcov/Xdebug who has ever had a TIA graph gets full-suite runs forever,
with a warning that suggests a transient condition.
--tiais silently a no-op plusoverhead for them.
Suggested fix
Snapshot the tree on the no-driver path too, or drop entries from
lastRunTreeonce theirfile matches its committed content. Either removes the trap without weakening the guard —
the guard's purpose (do not filter using a stale graph) is served by running the full suite,
which already happened.
Provenance note
The stuck state and both full-suite runs are directly observed. That a completed run
with a driver clears the entry is inferred from the source above and not measured — it
costs a 27-minute run on the reporter's suite and the verdict it fed did not turn on it.