From 0b98b7a26b0669fc43fe828029d179886cd7a686 Mon Sep 17 00:00:00 2001 From: kj Date: Mon, 5 Oct 2026 17:36:08 -0300 Subject: [PATCH] feat(compat): sweep of components against the real core with strict diagnostics --- Makefile | 5 +- scripts/compat-sweep | 197 +++++++++++++++++++++++++++ tests/compat/cache/assert.sh | 18 +++ tests/compat/cache/router.php | 15 ++ tests/compat/commands/assert.sh | 50 +++++++ tests/compat/crypto/assert.sh | 20 +++ tests/compat/crypto/router.php | 30 ++++ tests/compat/htmx/assert.sh | 19 +++ tests/compat/htmx/router.php | 33 +++++ tests/compat/http/assert.sh | 21 +++ tests/compat/http/router.php | 36 +++++ tests/compat/id/assert.sh | 18 +++ tests/compat/id/router.php | 13 ++ tests/compat/levels.json | 35 +++++ tests/compat/report.json | 79 +++++++++++ tests/compat/test/assert.sh | 29 ++++ tests/compat/text/assert.sh | 20 +++ tests/compat/text/router.php | 28 ++++ tests/group11_compat_stamps_test.php | 75 ++++++++++ 19 files changed, 740 insertions(+), 1 deletion(-) create mode 100755 scripts/compat-sweep create mode 100755 tests/compat/cache/assert.sh create mode 100644 tests/compat/cache/router.php create mode 100755 tests/compat/commands/assert.sh create mode 100755 tests/compat/crypto/assert.sh create mode 100644 tests/compat/crypto/router.php create mode 100755 tests/compat/htmx/assert.sh create mode 100644 tests/compat/htmx/router.php create mode 100755 tests/compat/http/assert.sh create mode 100644 tests/compat/http/router.php create mode 100755 tests/compat/id/assert.sh create mode 100644 tests/compat/id/router.php create mode 100644 tests/compat/levels.json create mode 100644 tests/compat/report.json create mode 100755 tests/compat/test/assert.sh create mode 100755 tests/compat/text/assert.sh create mode 100644 tests/compat/text/router.php create mode 100644 tests/group11_compat_stamps_test.php diff --git a/Makefile b/Makefile index e2113d1..f3e482b 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ CONFIG := packages/commands/src/Toolbox/Config.php VERSION := $(shell grep -oP "const VERSION = '\K[^']+" $(CONFIG)) -.PHONY: help test build release publish check clean +.PHONY: help test build compat release publish check clean help: ## Muestra esta ayuda @grep -hE '^[a-z-]+:.*## ' $(MAKEFILE_LIST) | sed -E 's/^([a-z-]+):.*## (.+)$$/ make \1 \xe2\x80\x94 \2/' @@ -15,6 +15,9 @@ test: ## Corre la suite de pruebas (harness propio, sin phpunit) build: ## Construye dist/duckbrain.phar y su .sha256 scripts/build-phar +compat: ## Barrido de compatibilidad contra el core real (requiere red) + scripts/compat-sweep $(if $(ARGS),$(ARGS),) + release: ## Asistente interactivo para versionar un paquete (scripts/release) ./scripts/release $(if $(ARGS),$(ARGS),) diff --git a/scripts/compat-sweep b/scripts/compat-sweep new file mode 100755 index 0000000..87bb08f --- /dev/null +++ b/scripts/compat-sweep @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# Barrido de compatibilidad componente <-> core real (change verify-core-compat). +# Monta un proyecto temporal por paquete contra el core real publicado, lo +# ejercita segun su nivel de evidencia (tests/compat/levels.json) con criterios +# estrictos (cualquier Deprecated/Warning/Notice/Fatal cuenta FAIL) y escribe +# tests/compat/report.json. +# +# Uso: scripts/compat-sweep [--only ...] +# Env: DUCKBRAIN_CORE_URL repositorio del core (default: Config::coreUrl()) +# DUCKBRAIN_CACHE_DIR caché donde CoreProvider mantiene el clon del core +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +COMPAT="$ROOT/tests/compat" +LEVELS="$COMPAT/levels.json" +REPORT="$COMPAT/report.json" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +ONLY=() +args=("$@") +i=0 +while [ "$i" -lt "${#args[@]}" ]; do + case "${args[i]}" in + --only) i=$((i + 1)); ONLY+=("${args[i]}") ;; + *) echo "Uso: scripts/compat-sweep [--only ...]" >&2; exit 2 ;; + esac + i=$((i + 1)) +done + +# --- resolver el core real (clon + checkout del mayor tag semver) -------------- +CORE_INFO="$(php -d error_reporting=-1 -r ' +require "'"$ROOT"'/src/autoload.php"; +try { + $p = Toolbox\CoreProvider::fromUrl( + Toolbox\Config::coreUrl(), Toolbox\Config::cacheDir(), new Toolbox\Console() + ); + $v = $p->version(); + if ($v === null) { + fwrite(STDERR, "El core no tiene tags semver publicados; nada que verificar.\n"); + exit(1); + } + echo $p->dir(), "\n", $v, "\n", (string) $p->commit(), "\n"; +} catch (Throwable $e) { + fwrite(STDERR, "No se pudo acceder al core real: " . $e->getMessage() . "\n"); + exit(1); +}' 2>"$WORK/core.err")" || { + echo "== ERROR: barrido abortado ==" >&2 + cat "$WORK/core.err" >&2 + exit 1 +} +CORE_DIR="$(printf '%s\n' "$CORE_INFO" | sed -n 1p)" +CORE_VER="$(printf '%s\n' "$CORE_INFO" | sed -n 2p)" +CORE_SHA="$(printf '%s\n' "$CORE_INFO" | sed -n 3p)" +echo "== core real: v$CORE_VER ($CORE_SHA) en $CORE_DIR ==" +echo + +LEVELS_PATH="$LEVELS" php -r ' +$levels = json_decode((string) file_get_contents(getenv("LEVELS_PATH")), true); +echo implode("\n", array_keys($levels)), "\n";' > "$WORK/pkgs" + +RESULTS="$WORK/results" +: > "$RESULTS" +FAILED=0 + +wanted() { + [ "${#ONLY[@]}" -eq 0 ] && return 0 + local w + for w in "${ONLY[@]}"; do [ "$w" = "$1" ] && return 0; done + return 1 +} + +stop_server() { + if [ -n "${SERVER_PID:-}" ]; then + kill "$SERVER_PID" 2>/dev/null || true + wait "$SERVER_PID" 2>/dev/null || true + SERVER_PID="" + fi +} + +# --- por paquete --------------------------------------------------------------- +while read -r PKG; do + wanted "$PKG" || continue + PROJ="$WORK/proj-$PKG" + LOG="$WORK/$PKG.server.log" + echo "── $PKG" + + php -d error_reporting=-1 "$ROOT/bin/duckbrain" install "$PROJ" --core-dir="$CORE_DIR" --yes >/dev/null + php -d error_reporting=-1 "$ROOT/bin/duckbrain" add "$PKG" --root="$PROJ" --yes >/dev/null \ + || { echo "$PKG:add|fail" >> "$RESULTS"; echo " add fallo"; FAILED=1; continue; } + + # Config del proyecto temporal: sqlite, clave para crypto y db local. + PROJ_PATH="$PROJ" php -r ' + $proj = getenv("PROJ_PATH"); + $f = "$proj/config.php"; + $cfg = (string) file_get_contents($f); + $cfg = str_replace("define(\x27DB_TYPE\x27, \x27mysql\x27);", "define(\x27DB_TYPE\x27, \x27sqlite\x27);", $cfg); + $cfg = str_replace("define(\x27DB_NAME\x27, \x27\x27);", "define(\x27DB_NAME\x27, \x27$proj/app.sqlite\x27);", $cfg); + if (!str_contains($cfg, "PRIVATE_KEY")) { + $cfg .= "\ndefine(\x27PRIVATE_KEY\x27, \x27compat-sweep-clave-de-prueba-32!\x27);\n"; + } + file_put_contents($f, $cfg);' + + # Router de compatibilidad del fixture (si lo hay). + if [ -f "$COMPAT/$PKG/router.php" ]; then + mkdir -p "$PROJ/src/Routers" + sed "s|@COMPAT@|$COMPAT/$PKG|g" "$COMPAT/$PKG/router.php" > "$PROJ/src/Routers/Compat.php" + fi + + # Servidor con diagnósticos al máximo y capturados. + BASE="" + : > "$LOG" + for PORT in $(seq 18100 18120); do + PHP_CLI_SERVER_WORKERS=4 php -d error_reporting=-1 -d display_errors=1 \ + -d log_errors=1 -d error_log="$LOG" -S "127.0.0.1:$PORT" -t "$PROJ" "$PROJ/index.php" \ + >/dev/null 2>&1 & + SERVER_PID=$! + for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do + sleep 0.15 + if curl -s -o /dev/null "http://127.0.0.1:$PORT/compat/ping"; then + BASE="http://127.0.0.1:$PORT" + break 2 + fi + done + stop_server + done + if [ -z "$BASE" ]; then + echo " no hubo servidor en 18100-18120" >&2 + echo "$PKG:server|fail" >> "$RESULTS"; FAILED=1; continue + fi + + # Aserciones del fixture (si lo hay; los niveles "none" no tienen). + if [ -f "$COMPAT/$PKG/assert.sh" ]; then + BASE="$BASE" PROJ="$PROJ" PKG="$PKG" LOG="$LOG" RESULTS="$RESULTS" COMPAT="$COMPAT" \ + bash "$COMPAT/$PKG/assert.sh" || true + fi + stop_server + + # Criterio estricto: cualquier diagnostico en el log del servidor es fallo. + if grep -qE 'Deprecated|Warning:|Notice:|Fatal error|Parse error' "$LOG"; then + echo " diagnostico PHP detectado en el servidor (ver $LOG):" + grep -E 'Deprecated|Warning:|Notice:|Fatal error|Parse error' "$LOG" | sed 's/^/ /' + FAILED=1 + fi +done < "$WORK/pkgs" + +# --- merge de resultados con levels.json y reporte ------------------------------ +LEVELS_PATH="$LEVELS" RESULTS_PATH="$RESULTS" REPORT_PATH="$REPORT" \ +CORE_VER="$CORE_VER" CORE_SHA="$CORE_SHA" php -r ' +$levels = json_decode((string) file_get_contents(getenv("LEVELS_PATH")), true); +$results = []; +foreach (file(getenv("RESULTS_PATH"), FILE_IGNORE_NEW_LINES) as $line) { + [$id, $status] = array_pad(explode("|", $line, 2), 2, ""); + $results[$id] = $status; +} +// Corre parciales (--only): hereda del reporte previo los ok del mismo core. +$prior = is_file(getenv("REPORT_PATH")) + ? (json_decode((string) file_get_contents(getenv("REPORT_PATH")), true) ?: []) + : []; +$stale = ($prior["core"]["version"] ?? null) !== getenv("CORE_VER"); +$out = []; +$fail = 0; +foreach ($levels as $pkg => $comps) { + foreach ($comps as $comp => $level) { + $reason = null; + if (is_array($level)) { $reason = $level["reason"] ?? null; $level = $level["level"]; } + $id = "$pkg:$comp"; + $status = $results[$id] ?? ($level === "none" ? "skipped" : "not_attempted"); + if ($status === "not_attempted" && !$stale && ($prior["components"][$id]["status"] ?? "") === "ok") { + $status = "ok"; + } + if (!in_array($status, ["ok", "skipped"], true)) { $fail++; } + $out[$id] = array_filter([ + "status" => $status, + "level" => $level, + "reason" => $reason, + ], fn ($v) => $v !== null); + } +} +$report = [ + "core" => ["version" => getenv("CORE_VER"), "commit" => getenv("CORE_SHA")], + "generated" => gmdate("Y-m-d\TH:i:s\Z"), + "components" => $out, +]; +file_put_contents(getenv("REPORT_PATH"), json_encode($report, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); +foreach ($out as $id => $r) { + printf(" %-24s %-13s %s%s\n", $id, $r["status"], $r["level"], isset($r["reason"]) ? " (" . $r["reason"] . ")" : ""); +} +exit($fail === 0 ? 0 : 1);' + +STATUS=$? +if [ "$STATUS" = 0 ]; then + echo "OK: barrido de compatibilidad completo contra core v$CORE_VER" +else + echo "FAIL: hay componentes sin verificar o con fallos (ver $REPORT)" >&2 +fi +exit $STATUS diff --git a/tests/compat/cache/assert.sh b/tests/compat/cache/assert.sh new file mode 100755 index 0000000..b86529f --- /dev/null +++ b/tests/compat/cache/assert.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Smoke HTTP del paquete cache (APCu sólo existe en SAPI web). +set -u + +check() { + local comp="$1" path="$2" expect="$3" body + if ! body="$(curl -fsS "$BASE$path" 2>/dev/null)"; then + echo "$PKG:$comp|fail" >> "$RESULTS"; return + fi + if printf '%s' "$body" | grep -qF "$expect"; then + echo "$PKG:$comp|ok" >> "$RESULTS" + else + echo " inesperado en $path: $(printf '%s' "$body" | head -c 200)" >&2 + echo "$PKG:$comp|fail" >> "$RESULTS" + fi +} + +check apcache /compat/cache/apcache 'APC_OK' diff --git a/tests/compat/cache/router.php b/tests/compat/cache/router.php new file mode 100644 index 0000000..e5fbcb1 --- /dev/null +++ b/tests/compat/cache/router.php @@ -0,0 +1,15 @@ +> "$RESULTS"; } + +mkdir -p "$PROJ/migrations" + +cat > "$PROJ/migrations/2026_01_01_000000_compat_demo.php" <<'PHP' +create('compat_table', [ + 'id INTEGER PRIMARY KEY AUTOINCREMENT', + $this->string('name'), + ]); + } + + public function down(): void + { + $this->dropTable('compat_table'); + } +}; +PHP + +if ! (cd "$PROJ" && php duckbrain migration migrate >"$PROJ/../migrate.out" 2>&1); then + echo " migrate fallo:" >&2 + sed 's/^/ /' "$PROJ/../migrate.out" >&2 + res migration fail + exit 0 +fi + +PROJ_PATH="$PROJ" php -r ' +$proj = getenv("PROJ_PATH"); +$db = new PDO("sqlite:$proj/app.sqlite"); +$table = (bool) $db->query("SELECT name FROM sqlite_master WHERE type=" . chr(39) . "table" . chr(39) . " AND name=" . chr(39) . "compat_table" . chr(39))->fetchColumn(); +$ledger = glob("$proj/migrations/migrations-*.sqlite")[0] ?? null; +$done = 0; +if ($ledger !== null) { + $l = new PDO("sqlite:" . $ledger); + $done = (int) $l->query("SELECT COUNT(*) FROM migrations")->fetchColumn(); +} +exit($table && $done === 1 ? 0 : 1);' \ + && res migration ok \ + || { echo " compat_table o registro de migracion ausentes" >&2; res migration fail; } diff --git a/tests/compat/crypto/assert.sh b/tests/compat/crypto/assert.sh new file mode 100755 index 0000000..4dfe426 --- /dev/null +++ b/tests/compat/crypto/assert.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Smoke HTTP del paquete crypto. Recibe BASE, PKG, RESULTS. +set -u + +check() { + local comp="$1" path="$2" expect="$3" body + if ! body="$(curl -fsS "$BASE$path" 2>/dev/null)"; then + echo "$PKG:$comp|fail" >> "$RESULTS"; return + fi + if printf '%s' "$body" | grep -qF "$expect"; then + echo "$PKG:$comp|ok" >> "$RESULTS" + else + echo " inesperado en $path: $(printf '%s' "$body" | head -c 200)" >&2 + echo "$PKG:$comp|fail" >> "$RESULTS" + fi +} + +check crypto /compat/crypto/roundtrip 'CRYPTO_OK' +check nonce /compat/crypto/nonce 'NONCE_OK' +check twoFactor /compat/crypto/2fa 'TFA_OK' diff --git a/tests/compat/crypto/router.php b/tests/compat/crypto/router.php new file mode 100644 index 0000000..e40815d --- /dev/null +++ b/tests/compat/crypto/router.php @@ -0,0 +1,30 @@ +createSecret(16); + $code = $tfa->getCode($secret); + echo ($tfa->verifyCode($secret, $code, 0) && !$tfa->verifyCode($secret, '000000', 0)) ? 'TFA_OK' : 'TFA_FAIL'; +}); diff --git a/tests/compat/htmx/assert.sh b/tests/compat/htmx/assert.sh new file mode 100755 index 0000000..85affb3 --- /dev/null +++ b/tests/compat/htmx/assert.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Smoke HTTP del paquete htmx. Recibe BASE, PKG, RESULTS. +set -u + +check() { + local comp="$1" path="$2" expect="$3" body + if ! body="$(curl -fsS "$BASE$path" 2>/dev/null)"; then + echo "$PKG:$comp|fail" >> "$RESULTS"; return + fi + if printf '%s' "$body" | grep -qF "$expect"; then + echo "$PKG:$comp|ok" >> "$RESULTS" + else + echo " inesperado en $path: $(printf '%s' "$body" | head -c 200)" >&2 + echo "$PKG:$comp|fail" >> "$RESULTS" + fi +} + +check htmlComponent /compat/htmx/render '

A<b>

' +check htmx /compat/htmx/static 'HTMX:false:true' diff --git a/tests/compat/htmx/router.php b/tests/compat/htmx/router.php new file mode 100644 index 0000000..368821f --- /dev/null +++ b/tests/compat/htmx/router.php @@ -0,0 +1,33 @@ +escapedProperty('name'); ?> +VIEW); + +file_put_contents($views . '/CompatButton.php', <<<'VIEW' + +VIEW); + +Router::get('/compat/htmx/render', function () { + echo 'GREET:'; + HTMLComponent::render('CompatGreet', ['name' => 'A']); +}); + +Router::get('/compat/htmx/static', function () { + $instance = new HTMX([], 'ok'); + header('X-Marker: ' . (HTMX::isHtmx() ? 'yes' : 'no')); + echo 'HTMX:' . var_export(HTMX::isPartialRequest(), true) + . ':' . var_export($instance->getContent() !== '', true); +}); diff --git a/tests/compat/http/assert.sh b/tests/compat/http/assert.sh new file mode 100755 index 0000000..340d5f3 --- /dev/null +++ b/tests/compat/http/assert.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Smoke del paquete http. Recibe BASE, PKG, RESULTS. +set -u + +check() { + local comp="$1" path="$2" expect="$3" body + if ! body="$(curl -fsS "$BASE$path" 2>/dev/null)"; then + echo "$PKG:$comp|fail" >> "$RESULTS"; return + fi + if printf '%s' "$body" | grep -qF "$expect"; then + echo "$PKG:$comp|ok" >> "$RESULTS" + else + echo " inesperado en $path: $(printf '%s' "$body" | head -c 200)" >&2 + echo "$PKG:$comp|fail" >> "$RESULTS" + fi +} + +check curlRequest /compat/http/curl 'CURL_OK' +check aria2 '/compat/http/load?class=Aria2' 'LOAD_OK:Libs\Aria2' +check proxy '/compat/http/load?class=Proxy' 'LOAD_OK:Libs\Proxy' +check compreFace '/compat/http/load?class=CompreFace' 'LOAD_OK:Libs\CompreFace' diff --git a/tests/compat/http/router.php b/tests/compat/http/router.php new file mode 100644 index 0000000..82db0db --- /dev/null +++ b/tests/compat/http/router.php @@ -0,0 +1,36 @@ +get("http://$host/compat/http/echo?who=42"); + echo str_contains($req->body(), 'ECHO:42') ? 'CURL_OK' : 'CURL_FAIL:' . substr($req->body(), 0, 80); +}); + +Router::get('/compat/http/load', function () { + $class = 'Libs\\' . basename((string) ($_GET['class'] ?? '')); + if (!class_exists($class)) { + echo 'LOAD_MISSING'; + return; + } + $instance = match ($class) { + 'Libs\\Aria2' => new Aria2('http://127.0.0.1:6800/jsonrpc'), + 'Libs\\CompreFace' => new CompreFace('compat-key', 'http://127.0.0.1:18080'), + default => new $class(), + }; + echo 'LOAD_OK:' . get_class($instance); +}); diff --git a/tests/compat/id/assert.sh b/tests/compat/id/assert.sh new file mode 100755 index 0000000..634097e --- /dev/null +++ b/tests/compat/id/assert.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Smoke HTTP del paquete id. Recibe BASE, PKG, RESULTS. +set -u + +check() { + local comp="$1" path="$2" expect="$3" body + if ! body="$(curl -fsS "$BASE$path" 2>/dev/null)"; then + echo "$PKG:$comp|fail" >> "$RESULTS"; return + fi + if printf '%s' "$body" | grep -qF "$expect"; then + echo "$PKG:$comp|ok" >> "$RESULTS" + else + echo " inesperado en $path: $(printf '%s' "$body" | head -c 200)" >&2 + echo "$PKG:$comp|fail" >> "$RESULTS" + fi +} + +check ulid /compat/id/ulid 'ULID_OK' diff --git a/tests/compat/id/router.php b/tests/compat/id/router.php new file mode 100644 index 0000000..8f07d11 --- /dev/null +++ b/tests/compat/id/router.php @@ -0,0 +1,13 @@ +> "$RESULTS"; } + +cd "$PROJ" || { res fail; exit 0; } + +if [ ! -f composer.json ]; then + printf '{"name":"duckbrain/compat-probe","require":{}}\n' > composer.json +fi + +if ! COMPOSER_NO_INTERACTION=1 composer require --dev phpunit/phpunit:^11.0 fakerphp/faker \ + --no-interaction >/dev/null 2>"$PROJ/../composer.err"; then + echo " composer require fallo:" >&2 + sed 's/^/ /' "$PROJ/../composer.err" >&2 + res fail + exit 0 +fi + +if ! ./vendor/bin/phpunit >/dev/null 2>"$PROJ/../phpunit.err"; then + echo " phpunit fallo:" >&2 + sed 's/^/ /' "$PROJ/../phpunit.err" >&2 + res fail + exit 0 +fi + +res ok diff --git a/tests/compat/text/assert.sh b/tests/compat/text/assert.sh new file mode 100755 index 0000000..80c965f --- /dev/null +++ b/tests/compat/text/assert.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Smoke HTTP del paquete text. Recibe BASE, PROJ, PKG, LOG, RESULTS. +set -u + +check() { + local comp="$1" path="$2" expect="$3" body + if ! body="$(curl -fsS "$BASE$path" 2>/dev/null)"; then + echo "$PKG:$comp|fail" >> "$RESULTS" + return + fi + if printf '%s' "$body" | grep -qF "$expect"; then + echo "$PKG:$comp|ok" >> "$RESULTS" + else + echo " respuesta inesperada en $path: $(printf '%s' "$body" | head -c 200)" >&2 + echo "$PKG:$comp|fail" >> "$RESULTS" + fi +} + +check filter /compat/text/filter 'HOLA MUNDIN' +check replacement /compat/text/replacement 'bar y food' diff --git a/tests/compat/text/router.php b/tests/compat/text/router.php new file mode 100644 index 0000000..bed2a34 --- /dev/null +++ b/tests/compat/text/router.php @@ -0,0 +1,28 @@ + Router::apply()). + +use Libs\Filter; +use Libs\Replacement; +use Libs\Router; + +Router::get('/compat/text/filter', function () { + Filter::add( + fn (string $content): string => str_replace('mundo', 'MUNDIN', $content), + 'compat', + 5 + ); + Filter::add( + fn (string $content): string => strtoupper($content) . '|', + 'compat', + 20 + ); + echo 'FILTER_OK:' . Filter::apply('hola mundo', 'compat'); +}); + +Router::get('/compat/text/replacement', function () { + Replacement::add('/\bfoo\b/', 'bar'); + echo 'REPL_OK:' . Replacement::apply('foo y food y foo'); +}); diff --git a/tests/group11_compat_stamps_test.php b/tests/group11_compat_stamps_test.php new file mode 100644 index 0000000..a682f67 --- /dev/null +++ b/tests/group11_compat_stamps_test.php @@ -0,0 +1,75 @@ +packages() as $manifest) { + foreach (array_keys($manifest->components()) as $component) { + $declared[] = $manifest->name . ':' . $component; + } + } + + $planned = []; + foreach ($levels as $package => $components) { + foreach (array_keys($components) as $component) { + $planned[] = $package . ':' . $component; + } + } + + sort($declared); + sort($planned); + eq($declared, $planned, 'cada componente del catalogo tiene su nivel de evidencia planeado'); +}); + +Harness::it('los stamps tested estan respaldados por el reporte del barrido', function () { + $repo = $GLOBALS['g11_repo']; + $reportPath = $repo . '/tests/compat/report.json'; + + if (!is_file($reportPath)) { + return; // offline sin barrido previo: no se inventa evidencia ni se exige. + } + + $report = json_decode((string) file_get_contents($reportPath), true); + ok(is_array($report) && isset($report['components']), 'report.json valido'); + + $levels = json_decode((string) file_get_contents($repo . '/tests/compat/levels.json'), true); + $catalog = Catalog::fromRoot($repo); + + foreach ($catalog->packages() as $manifest) { + foreach (array_keys($manifest->components()) as $component) { + $id = $manifest->name . ':' . $component; + $level = $levels[$manifest->name][$component] ?? null; + $level = is_array($level) ? $level['level'] : $level; + $status = $report['components'][$id]['status'] ?? 'missing'; + $tested = $manifest->testedCore($component); + + if ($level === 'none') { + eq(null, $tested, "$id esta deliberadamente sin datos y no debe declarar tested"); + continue; + } + + if ($tested !== null) { + eq('ok', $status, "$id declara tested='$tested' pero su evidencia en el reporte es '$status'"); + } + } + } +});