Files
projectsend/tests/Support/RecordingUpdate.php
denkfabrik-li 4469648d82 Stop the update tests emptying bootstrap/cache for every other worker
`UpdateWelcomeTest > staff who may not read system information are not
interrupted` fails on a parallel run roughly one time in six, with

    BindingResolutionException: Target [Inertia\Ssr\Gateway] is not
    instantiable

in a file that has nothing to do with updates. Run alone it is green
every time. The cause is not in that file.

`clear-compiled` deletes bootstrap/cache/packages.php and
bootstrap/cache/services.php. There is one of each for the whole
checkout, and `pest --parallel` gives eight worker processes the same
one. Instrumented over three full runs, the real command ran 12 times per
run -- 11 from UpdateCommandTest, 1 from StaleCodeNoticeTest -- and the
other workers observed the package manifest missing at boot 46 times.

What that costs is in PackageManifest::getManifest():

    if (! is_file($this->manifestPath)) {
        $this->build();
    }

    return $this->manifest = is_file($this->manifestPath) ?
        $this->files->getRequire($this->manifestPath) : [];

A worker that loses the second is_file() to another worker's unlink gets
`[]`: no discovered packages, so no package service providers, so
Inertia's is never registered and `Inertia\Ssr\Gateway` is never bound.
The next page it renders dies in the compiled root view, where
`@inertia` resolves that interface. Any test in any file, whichever one
happened to be booting.

Both halves measured. Building the manifest with inertia-laravel in
`dont-discover` reproduces the reported failure exactly -- same test,
same exception, same frame (`app('Inertia\Ssr\Gateway')` from the
compiled app.blade.php). And 12 real `clear-compiled` calls per run is
the count above.

UpdateCommandTest already owns a double for this, and says why in its own
docblock: the artisan call is a seam. Nine of its tests and one in
StaleCodeNoticeTest simply do not use it. None of them asserts that a
command ran -- they assert EnsureSystemRoles, the settings writes, the
activity log and the welcome marker, and the double touches none of
those. So the seam now covers the file, through a beforeEach rather than
per test, because the next test added here should not have to know any of
this.

The double moves to tests/Support and its helper to tests/Helpers.php,
for the reason that file documents: Pest hands whole files to workers, so
a class declared in one test file does not exist for another.

Not changed: UpdateInstallation. `clear-compiled` belongs in a real
update. Also not changed: giving each worker its own bootstrap/cache
through APP_PACKAGES_CACHE and friends. That would make the destruction
cheap rather than remove it, and nothing in the suite needs those
commands to run at all.

One new test, on the files rather than on the recorded call list -- a
future double that forgot to intercept one command would still satisfy a
call-list assertion. Counter-checked: with the beforeEach removed it goes
red on both manifests being gone (1 failed / 22 passed).

Eight consecutive parallel runs green after the change; the manifests'
mtimes are untouched by a full run, where before they were rewritten
every time. Full suite passes (2049 passed / 2 skipped). PHPStan level 8
clean -- it analyses `app` only, so it does not cover this change.

Pre-existing and left alone: pint reports `ordered_imports` on
UpdateCommandTest.php. Its import block is misordered on main too.
2026-08-28 00:32:57 +02:00

67 lines
2.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Support;
use App\Modules\Platform\Updates\UpdateInstallation;
use Illuminate\Console\OutputStyle;
/**
* The real update with its artisan calls written down instead of run.
*
* Two reasons, and the second one is why every test that runs the command
* uses this and not the genuine article.
*
* The ordering constraints inside UpdateInstallation are invisible in its
* result and expensive when wrong — a queue:restart before a cache clear
* leaves a worker on old code indefinitely, and config:cache breaks
* TRUSTED_PROXIES silently. An ordered list of the commands it ran is the
* only thing that can assert them, so the artisan call is a seam.
*
* And those commands are not local. `clear-compiled` deletes
* bootstrap/cache/packages.php and bootstrap/cache/services.php, `view:clear`
* empties storage/framework/views, `storage:link` rewrites public/storage —
* one copy of each, shared by all eight workers of a parallel run. A worker
* that boots its application in the window between the delete and the
* rebuild reads an empty package manifest, registers no package service
* providers at all, and dies on the next page it renders with
* "Target [Inertia\Ssr\Gateway] is not instantiable". See the test in
* UpdateCommandTest that pins this.
*
* Everything above the artisan call stays real: EnsureSystemRoles, the
* settings writes, the activity log and the welcome marker all run, which
* is what the tests in both files actually assert.
*/
class RecordingUpdate extends UpdateInstallation
{
/** @var list<string> */
public array $calls = [];
/** @var array<string, int> */
public array $exitCodes = [];
/** @var array{route: bool, event: bool, config: bool} */
public array $warm = ['route' => false, 'event' => false, 'config' => false];
/** The test database is always migrated, so this cannot be observed for real. */
public bool $existingInstall = true;
protected function artisan(string $command, array $parameters = [], ?OutputStyle $output = null): int
{
$this->calls[] = $command;
return $this->exitCodes[$command] ?? 0;
}
protected function warmCaches(): array
{
return $this->warm;
}
protected function hasRunMigrationsBefore(): bool
{
return $this->existingInstall;
}
}