pestphp / pestphp/pest

TIA: the documented CI baseline command records no graph, and unlocking it would record an under-selecting one

Open
#1,917 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
11.7k
Forks
538
Avg merge
4d 11h
Merged PRs (30d)
8

Description

Summary

The Sharing The Baseline From CI workflow records the baseline with:

./vendor/bin/pest --parallel --tia --coverage --fresh

Both the GitHub Actions and the GitLab CI starter jobs in the docs use this line. On a CI runner — where no graph exists yet — it never records a graph. The run prints the "TIA is skipped" notice and exits normally, but the storage directory that pest --baseline points at contains only coverage.bin.gz.

The published artifact therefore has no graph.json, and every consumer that fetches it hits BaselineSync::validateDownloadedArtifact():

Baseline downloaded but the artifact is missing expected files (graph.json).
Your CI publish step is broken — check the job that uploads the TIA baseline artifact.

So the documented workflow cannot produce a usable baseline, and the failure surfaces on the developer's machine rather than in the job that is actually wrong.

Reproduced on v5.2.0 (PHP 8.5, Xdebug 3, XDEBUG_MODE=coverage).

Reproduction

# minimal project: src/{Calc,Other}.php, tests/{Add,Mul,Other}Test.php,
# phpunit.xml with <source><include><directory>src</directory></include></source>

BASE=$(./vendor/bin/pest --baseline)
rm -rf "$BASE"

XDEBUG_MODE=coverage ./vendor/bin/pest --parallel --tia --coverage --fresh
#   ─ Running in TIA mode, however TIA is skipped as an active coverage report
#     narrows the edges it could record.
#   ─ Record the baseline with a plain --tia run first; coverage runs then reuse it.

ls -1 "$BASE"
# coverage.bin.gz          <- no graph.json

Why

Tia::handleParent() bails before recording when no graph exists and coverage is active:

// src/Plugins/Tia.php
$coverageCacheOwned = $this->piggybackCoverage && $this->pestCoverageActive();

if ($coverageCacheOwned) {
    $this->state->write(self::KEY_COVERAGE_MARKER, '');   // cache will still be written
}

if (! $graph instanceof Graph && $this->piggybackCoverage) {
    $this->emitCoverageScopedRecordSkipped();

    return $arguments;                                    // graph never recorded
}

The marker is written immediately above the guard, which is why the artifact ends up populated-looking but unusable.

Note also that there is no coverage form that avoids this. coverageReportActive() checks pestCoverageActive() first, so plain --coverage trips the guard exactly like --coverage-cobertura does:

private function coverageReportActive(): bool
{
    if ($this->pestCoverageActive()) {
        return true;
    }

    return array_any(self::COVERAGE_REPORT_FLAGS, fn (string $flag): bool => $this->hasArgument($flag, $this->originalArguments));
}

Removing the guard is not the fix

The obvious patch — dropping the guard so a full run under --coverage records — does produce both files in one pass, and the result replays. But the graph it builds silently under-selects tests.

In piggyback mode the per-test file edges come from PHPUnit's CodeCoverage instance, which honours the phpunit.xml <source> filter. The recorder's own driver is built from SourceScope::fromProjectRoot(), which is phpunit's <source> plus every top-level project directory. So a piggyback-recorded graph is a strict subset, and it is still marked complete.

Demonstration — lib/helpers.php holds a function one test calls, and lib is outside <source>:

graph recorded by edit lib/helpers.php
--tia --fresh (recorder's own driver) 1 affected, 1 replayed
--tia --fresh --coverage, guard removed 2 replayed, 0 affected

The test that calls the changed function is not re-run, with no warning. For a Laravel application whose <source> is typically app, that silently covers database/, routes/, bootstrap/ and test helpers.

Suggested direction

  1. Docs: change both starter jobs to record with a plain --tia --fresh, since the graph alone is what makes a fetched baseline replay. If the coverage cache is wanted too it needs a second pass (--tia --coverage), which is worth stating explicitly rather than implying one command does both.
  2. Fail loudly in the publisher: when a run skips recording and the user asked for --fresh, exiting non-zero (or at least warning at --baseline time that no graph exists) would move this failure into the CI job that caused it.
  3. Longer term: decoupling the edge-collection filter from the report filter would let one pass do both, which is what the documented command already implies. That is the only version of "unlock the guard" that is safe.

Related

  • #1796 — the shipped coverage.bin.gz carries the recording machine's absolute paths and fails open cross-machine. Still reproduces on v5.2.0: with the recording root absent, a replayed --coverage --min=90 exits 0 on a project whose real coverage is 50%.

The two compose badly for anyone following the docs today: the published artifact is either missing its graph (this issue), or — if the guard were simply removed — carries a graph that under-selects and a cache that fails open.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the Sharing The Baseline From CI documentation and reproduce the published command using the minimal project described in the issue. Read src/Plugins/Tia.php, especially Tia::handleParent(), then trace BaselineSync::validateDownloadedArtifact() to understand the missing graph failure. Done means the documented workflow produces a validated baseline artifact without silently under-selecting tests.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
documentation, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.