From 4b619d12e93883fffc99da347ae449e4724d6c94 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Wed, 30 Sep 2026 02:59:27 +0200 Subject: [PATCH] fix: write reference sources before fingerprints.json, atomic on failure Co-Authored-By: Claude Opus 5.5 (1M context) --- src/Cli/DiviCheckCommand.php | 20 +++++++++----- tests/Unit/Cli/DiviCheckCommandTest.php | 35 ++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/Cli/DiviCheckCommand.php b/src/Cli/DiviCheckCommand.php index 43bf24f..0ec9779 100644 --- a/src/Cli/DiviCheckCommand.php +++ b/src/Cli/DiviCheckCommand.php @@ -172,13 +172,21 @@ final class DiviCheckCommand return 1; } - DiviReference::writeExpected( - array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints), - $this->expectedFile - ); $sourceDir = ($this->sourceDir ?? DiviReference::sourceDir(...))(); - foreach ($sourceDir === null ? [] : $prints as $id => $print) { - DiviReference::writeSource($id, $print->source, $sourceDir); + try { + // Sources first, fingerprints.json last: the fingerprints decide the release gate, so a write that + // fails before this last one leaves the gate exactly as it was. + foreach ($sourceDir === null ? [] : $prints as $id => $print) { + DiviReference::writeSource($id, $print->source, $sourceDir); + } + DiviReference::writeExpected( + array_map(static fn (SourceFingerprint $print): string => $print->hash, $prints), + $this->expectedFile + ); + } catch (RuntimeException $e) { + ($this->out)('Referenz nicht vollständig aktualisiert: ' . $e->getMessage()); + + return 1; } delete_transient(ReleaseResolver::CACHE); ReleaseGate::forget(); diff --git a/tests/Unit/Cli/DiviCheckCommandTest.php b/tests/Unit/Cli/DiviCheckCommandTest.php index b3dc4ff..859e49e 100644 --- a/tests/Unit/Cli/DiviCheckCommandTest.php +++ b/tests/Unit/Cli/DiviCheckCommandTest.php @@ -44,8 +44,12 @@ final class DiviCheckCommandTest extends TestCase parent::tearDown(); } - /** @param array> $problems Step key => problems. */ - private function command(array $problems = []): DiviCheckCommand + /** + * @param array> $problems Step key => problems. + * @param bool $expectSave True asserts save() is called exactly once (the green happy path); otherwise it is + * merely allowed, since some tests (e.g. no admin) never reach the save() call. + */ + private function command(array $problems = [], bool $expectSave = false): DiviCheckCommand { $steps = array_map( static fn (string $key): CheckStep => new CheckStep($key, $problems[$key] ?? []), @@ -54,7 +58,11 @@ final class DiviCheckCommandTest extends TestCase $check = Mockery::mock(CompatibilityCheck::class); $check->allows('run')->andReturn(new CheckReport('r1', 'd', 1, '5.3.0', '0.6.0', 'k', $steps)); $reports = Mockery::mock(CheckReportStore::class); - $reports->allows('save'); + if ($expectSave) { + $reports->expects('save')->once(); + } else { + $reports->allows('save'); + } return new DiviCheckCommand( static fn () => $check, @@ -78,7 +86,7 @@ final class DiviCheckCommandTest extends TestCase public function testPrintsTheStepsAndSucceedsWhenGreen(): void { - self::assertSame(0, $this->command()->run([])); + self::assertSame(0, $this->command(expectSave: true)->run([])); self::assertContains('✓ Schreibtest auf einer Wegwerfseite', $this->lines); } @@ -134,6 +142,25 @@ final class DiviCheckCommandTest extends TestCase self::assertFileDoesNotExist($this->expectedFile, 'all or nothing'); } + public function testUpdateReferenceWritesNothingWhenASourceIsUnwritable(): void + { + $this->writeDivi(); + chmod($this->sourceDir, 0500); + + try { + $code = $this->command([CheckStep::MIRRORS => ['geändert']])->run(['update-reference' => true]); + + self::assertSame(1, $code); + self::assertStringContainsString( + 'Referenz nicht vollständig aktualisiert:', + implode("\n", $this->lines) + ); + self::assertFileDoesNotExist($this->expectedFile, 'sources are written before fingerprints.json'); + } finally { + chmod($this->sourceDir, 0777); + } + } + public function testUpdateReferenceWritesFingerprintsAndSources(): void { $this->writeDivi();