From 25e3cebb8211a007bf5db8e2c72389ae0e834ac8 Mon Sep 17 00:00:00 2001 From: Lars Moelleken Date: Mon, 17 Aug 2026 08:12:20 +0200 Subject: [PATCH 1/3] ci: reject duplicate root dependency declarations --- tools/agent-loop/verify-release-set.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/agent-loop/verify-release-set.php b/tools/agent-loop/verify-release-set.php index a5ad658..31f087e 100644 --- a/tools/agent-loop/verify-release-set.php +++ b/tools/agent-loop/verify-release-set.php @@ -29,6 +29,17 @@ $toolchain = requireArray($issue, 'toolchain', $argv[1]); $require = stringRequirements($composer['require'] ?? [], 'require', $argv[2]); $requireDev = stringRequirements($composer['require-dev'] ?? [], 'require-dev', $argv[2]); + + $duplicatePackages = array_keys(array_intersect_key($require, $requireDev)); + if ($duplicatePackages !== []) { + sort($duplicatePackages, SORT_STRING); + throw new \RuntimeException(sprintf( + '%s must not declare the same package in both require and require-dev: %s.', + $argv[2], + implode(', ', $duplicatePackages), + )); + } + $rootRequirements = $require + $requireDev; $expectedAgentLoop = requireString($toolchain, 'agent_loop_release', $argv[1]); From 4740ffc5317c97e4f5ee326793efd89d05f8193b Mon Sep 17 00:00:00 2001 From: Lars Moelleken Date: Mon, 17 Aug 2026 08:12:34 +0200 Subject: [PATCH 2/3] test: reject duplicate release-set root constraints --- tests/AgentLoopReleaseSetVerifierTest.php | 92 +++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/AgentLoopReleaseSetVerifierTest.php diff --git a/tests/AgentLoopReleaseSetVerifierTest.php b/tests/AgentLoopReleaseSetVerifierTest.php new file mode 100644 index 0000000..d6132ec --- /dev/null +++ b/tests/AgentLoopReleaseSetVerifierTest.php @@ -0,0 +1,92 @@ + [ + 'agent_loop_release' => '0.16.5', + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); + file_put_contents($composerPath, json_encode([ + 'require' => [ + 'voku/agent-loop' => '0.16.5', + ], + 'require-dev' => [ + 'voku/agent-loop' => '0.16.4', + ], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); + + [$exitCode, $stdout, $stderr] = $this->execute([ + PHP_BINARY, + dirname(__DIR__) . '/tools/agent-loop/verify-release-set.php', + $issuePath, + $composerPath, + ]); + + self::assertSame(1, $exitCode, $stdout . $stderr); + self::assertSame('', $stdout); + self::assertStringContainsString( + 'must not declare the same package in both require and require-dev: voku/agent-loop', + $stderr, + ); + } finally { + @unlink($issuePath); + @unlink($composerPath); + @rmdir($directory); + } + } + + /** + * @param list $command + * + * @return array{0: int, 1: string, 2: string} + */ + private function execute(array $command): array + { + $pipes = []; + $process = proc_open( + $command, + [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ], + $pipes, + ); + if (!is_resource($process)) { + throw new RuntimeException('Unable to start verifier process.'); + } + + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exitCode = proc_close($process); + + return [ + $exitCode, + is_string($stdout) ? $stdout : '', + is_string($stderr) ? $stderr : '', + ]; + } +} From 0843fd65b47cd5623387a8db8c83717e09596098 Mon Sep 17 00:00:00 2001 From: Lars Moelleken Date: Mon, 17 Aug 2026 08:13:36 +0200 Subject: [PATCH 3/3] test: clean verifier fixtures without suppression --- tests/AgentLoopReleaseSetVerifierTest.php | 30 +++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/AgentLoopReleaseSetVerifierTest.php b/tests/AgentLoopReleaseSetVerifierTest.php index d6132ec..dd009b6 100644 --- a/tests/AgentLoopReleaseSetVerifierTest.php +++ b/tests/AgentLoopReleaseSetVerifierTest.php @@ -21,19 +21,19 @@ public function testRejectsPackageDeclaredInRequireAndRequireDev(): void $composerPath = $directory . '/composer.json'; try { - file_put_contents($issuePath, json_encode([ + self::writeJson($issuePath, [ 'toolchain' => [ 'agent_loop_release' => '0.16.5', ], - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); - file_put_contents($composerPath, json_encode([ + ]); + self::writeJson($composerPath, [ 'require' => [ 'voku/agent-loop' => '0.16.5', ], 'require-dev' => [ 'voku/agent-loop' => '0.16.4', ], - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); + ]); [$exitCode, $stdout, $stderr] = $this->execute([ PHP_BINARY, @@ -49,9 +49,25 @@ public function testRejectsPackageDeclaredInRequireAndRequireDev(): void $stderr, ); } finally { - @unlink($issuePath); - @unlink($composerPath); - @rmdir($directory); + foreach ([$issuePath, $composerPath] as $path) { + if (is_file($path) && !unlink($path)) { + throw new RuntimeException('Unable to remove test file: ' . $path); + } + } + if (is_dir($directory) && !rmdir($directory)) { + throw new RuntimeException('Unable to remove test directory: ' . $directory); + } + } + } + + /** + * @param array $data + */ + private static function writeJson(string $path, array $data): void + { + $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); + if (file_put_contents($path, $json . "\n") === false) { + throw new RuntimeException('Unable to write test file: ' . $path); } }