&1', escapeshellarg(self::PLUGIN_ROOT), escapeshellarg(self::ARCHIVE_PREFIX), escapeshellarg($zipPath) ); exec($cmd, $out, $rc); self::assertSame(0, $rc, 'git archive failed: ' . implode("\n", $out)); try { $listing = []; $unzipRc = 0; exec('unzip -l ' . escapeshellarg($zipPath), $listing, $unzipRc); self::assertSame(0, $unzipRc, 'unzip -l failed (is unzip installed?)'); $joined = implode("\n", $listing); $required = [ 'wp-multi-city/wp-multi-city.php', 'wp-multi-city/includes/lib/plugin-update-checker/plugin-update-checker.php', 'wp-multi-city/wp-plugin-info.json', 'wp-multi-city/readme.txt', 'wp-multi-city/README.md', ]; foreach ($required as $path) { self::assertStringContainsString($path, $joined, "missing in archive: {$path}"); } $forbidden = [ 'tests/', 'docs/', 'composer.json', 'phpunit.xml.dist', 'patchwork.json', 'release.sh', 'CLAUDE.md', '.gitattributes', '.gitea/', ]; foreach ($forbidden as $path) { self::assertStringNotContainsString( 'wp-multi-city/' . $path, $joined, "dev file leaked into archive: {$path}" ); } } finally { if (file_exists($zipPath)) { unlink($zipPath); } } } public function test_wp_plugin_info_json_is_valid(): void { $path = self::PLUGIN_ROOT . '/wp-plugin-info.json'; self::assertFileExists($path); $json = file_get_contents($path); self::assertIsString($json); $data = json_decode($json, true); self::assertIsArray($data, 'wp-plugin-info.json must be valid JSON'); self::assertSame('wp-multi-city', $data['slug']); self::assertArrayHasKey('version', $data); self::assertArrayHasKey('download_url', $data); self::assertStringContainsString( 'git.netranking.ru/bryzgalov/wp-multi-city', $data['download_url'] ); } }