update($release, $target, $work); eq($content, file_get_contents($target), 'binario reemplazado por el nuevo'); ok(is_executable($target), 'queda ejecutable'); rrmdir($release); rrmdir($work); }); Harness::it('self-update aborta ante sha256 inválido sin tocar el binario', function () { $release = tmpdir('release'); $work = tmpdir('work'); write_file($release . '/duckbrain.phar', "contenido nuevo"); write_file($release . '/duckbrain.phar.sha256', 'deadbeef'); $target = $work . '/duckbrain.phar'; write_file($target, 'viejo'); throws(function () use ($release, $target, $work) { (new SelfUpdater(new Toolbox\Console()))->update($release, $target, $work); }); eq('viejo', file_get_contents($target), 'el binario actual queda intacto'); rrmdir($release); rrmdir($work); }); Harness::it('self-update en modo API reemplaza cuando el tag es más nuevo', function () { $rel = tmpdir('rel'); $work = tmpdir('work'); $content = "#!/usr/bin/env php\n 'v9.9.9', 'assets' => [ ['name' => 'duckbrain.phar', 'browser_download_url' => 'file://' . $rel . '/duckbrain.phar'], ['name' => 'duckbrain.phar.sha256', 'browser_download_url' => 'file://' . $rel . '/duckbrain.phar.sha256'], ], ]); write_file($rel . '/releases.json', $json); $target = $work . '/duckbrain.phar'; write_file($target, 'viejo'); (new SelfUpdater(new Toolbox\Console()))->update($rel . '/releases.json', $target, $work); eq($content, file_get_contents($target), 'binario reemplazado desde la release'); eq('v9.9.9', (new SelfUpdater(new Toolbox\Console()))->available($rel . '/releases.json'), 'available lee el tag'); rrmdir($rel); rrmdir($work); }); Harness::it('self-update en modo API no toca el binario si la versión ya es actual', function () { $rel = tmpdir('rel'); $work = tmpdir('work'); write_file($rel . '/duckbrain.phar', "nuevo"); write_file($rel . '/duckbrain.phar.sha256', hash('sha256', 'nuevo')); $json = json_encode([ 'tag_name' => 'v' . Toolbox\Config::VERSION, 'assets' => [ ['name' => 'duckbrain.phar', 'browser_download_url' => 'file://' . $rel . '/duckbrain.phar'], ['name' => 'duckbrain.phar.sha256', 'browser_download_url' => 'file://' . $rel . '/duckbrain.phar.sha256'], ], ]); write_file($rel . '/releases.json', $json); $target = $work . '/duckbrain.phar'; write_file($target, 'actual'); (new SelfUpdater(new Toolbox\Console()))->update($rel . '/releases.json', $target, $work); eq('actual', file_get_contents($target), 'sin cambios cuando el tag no es más nuevo'); (new SelfUpdater(new Toolbox\Console()))->update($rel . '/releases.json', $target, $work, true); eq('nuevo', file_get_contents($target), 'con --force reemplaza igualmente'); rrmdir($rel); rrmdir($work); }); Harness::it('self-update elige el mayor tag semver de la lista e ignora latest y drafts', function () { $rel = tmpdir('rel'); $work = tmpdir('work'); $winner = "#!/usr/bin/env php\n 'duckbrain.phar', 'browser_download_url' => 'file://' . $file], ['name' => 'duckbrain.phar.sha256', 'browser_download_url' => 'file://' . $file . '.sha256'], ]; }; $list = [ ['tag_name' => 'latest', 'draft' => false, 'assets' => $pair($rel . '/p1')], ['tag_name' => 'v99.0.0', 'draft' => true, 'assets' => $pair($rel . '/p1')], ['tag_name' => 'v1.2.3', 'draft' => false, 'assets' => $pair($rel . '/p1')], ['tag_name' => 'v10.0.0', 'draft' => false, 'assets' => $pair($rel . '/p10')], ]; write_file($rel . '/releases.json', json_encode($list)); $target = $work . '/duckbrain.phar'; write_file($target, 'semver'); $updater = new SelfUpdater(new Toolbox\Console()); eq('v10.0.0', $updater->available($rel . '/releases.json'), 'el mayor semver gana sobre latest, drafts y v1.2.3'); $updater->update($rel . '/releases.json', $target, $work); eq($winner, file_get_contents($target), 'se instala el phar del mayor semver'); rrmdir($rel); rrmdir($work); }); Harness::it('build-phar genera un phar ejecutable con su sha256', function () { $repo = $GLOBALS['g10_repo']; $dist = tmpdir('dist'); exec('DUCKBRAIN_DIST=' . escapeshellarg($dist) . ' bash ' . escapeshellarg($repo . '/scripts/build-phar') . ' 2>&1', $out, $code); eq(0, $code, 'build-phar termina bien: ' . implode("\n", $out)); $phar = $dist . '/duckbrain.phar'; ok(is_file($phar), 'phar generado'); ok(is_file($phar . '.sha256'), 'sha256 publicado'); eq(hash_file('sha256', $phar), trim((string) file_get_contents($phar . '.sha256'))); exec('php ' . escapeshellarg($phar) . ' --version 2>&1', $ver, $vcode); eq(0, $vcode, 'el phar arranca'); ok(str_contains(implode("\n", $ver), 'duckbrain-toolbox'), 'el phar imprime su versión'); rrmdir($dist); }); Harness::it('el phar resuelve su catálogo embebido y su raíz sin --toolbox', function () { $repo = $GLOBALS['g10_repo']; $dist = tmpdir('dist'); exec('DUCKBRAIN_DIST=' . escapeshellarg($dist) . ' bash ' . escapeshellarg($repo . '/scripts/build-phar') . ' 2>&1', $out, $code); eq(0, $code, 'build-phar termina bien'); $phar = $dist . '/duckbrain.phar'; // Ejecutado desde $dist (cwd sin packages/), sin --toolbox: la raíz debe // resolverse al propio phar y el catálogo debe leerse bajo phar://. $lines = []; exec('cd ' . escapeshellarg($dist) . ' && php ' . escapeshellarg($phar) . ' catalog 2>&1', $lines, $ccode); eq(0, $ccode, 'catalog desde el phar termina bien'); $text = implode("\n", $lines); ok(str_contains($text, 'commands'), 'descubre paquetes embebidos bajo phar://'); ok(str_contains($text, 'curlRequest'), 'los componentes embebidos aparecen sin flags'); // Alta de un componente desde el catálogo embebido, sin --toolbox. $proj = $dist . '/proj'; $lines2 = []; exec('cd ' . escapeshellarg($dist) . ' && php ' . escapeshellarg($phar) . ' add id --root=' . escapeshellarg($proj) . ' --yes 2>&1', $lines2, $acode); eq(0, $acode, 'add desde el phar sin --toolbox'); ok(is_file($proj . '/src/Libs/Ulid.php'), 'el archivo del componente quedó instalado'); rrmdir($dist); });