Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/scripts/check-test-filters.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# Guard contra «verificaciones fantasma» de la clase filtro-de-test (FU-020, CHARTER-12).
#
# El fallo que cierra: un comando `dotnet test --filter X` documentado o en CI que no casa con
# ningún test pasa EN VERDE ejecutando cero tests — su síntoma es idéntico al del éxito, así que
# sobrevive indefinidamente. Ocurrió con `Category=Concurrency` (quickstart US2, 0 tests durante
# meses hasta CHARTER-11) y sigue latente en `FullyQualifiedName~RedisDocumentStoreContractTests`
# (ci.yml): si alguien renombra esa clase, el job Redis pasa verde sin correr nada.
#
# Qué hace: descubre cada `dotnet test … --filter <expr>` en los archivos vigilados, y para cada
# uno exige que `--list-tests --filter <expr>` case con ≥1 test. `.NET` emite el texto exacto
# «No test matches the given testcase filter» cuando son cero — ese es el detector.
#
# Qué NO cubre (acotado a propósito, R3 del Charter): solo filtros de test. Un `dotnet run
# --project X` inexistente falla en ROJO, que es molesto pero no engañoso — otra clase.
set -euo pipefail

# Archivos donde un filtro documentado/ejecutado puede quedar huérfano.
FILES=(
".github/workflows/ci.yml"
".github/workflows/release.yml"
"specs/001-weft-crdt-versioning/quickstart.md"
"CONTRIBUTING.md"
"README.md"
)

fail=0
found_any=0

# Extrae, de cada línea con `dotnet test … --filter …`: el proyecto (primer token `tests/…`) y el
# filtro (token tras --filter, con o sin comillas).
while IFS= read -r line; do
[[ "$line" == *"dotnet test"* && "$line" == *"--filter"* ]] || continue

proj="$(sed -n 's/.*dotnet test[[:space:]]\+\(tests\/[A-Za-z0-9._\/-]*\).*/\1/p' <<<"$line")"
filter="$(sed -n 's/.*--filter[[:space:]]\+"\([^"]*\)".*/\1/p' <<<"$line")"
[[ -z "$filter" ]] && filter="$(sed -n 's/.*--filter[[:space:]]\+\([^[:space:]]*\).*/\1/p' <<<"$line")"

[[ -z "$proj" || -z "$filter" ]] && continue
found_any=1

echo "· ${proj%/} --filter ${filter}"
out="$(dotnet test "$proj" --configuration Release --list-tests --filter "$filter" 2>&1 || true)"
if grep -qF "No test matches the given testcase filter" <<<"$out"; then
echo " ✗ FANTASMA: el filtro no casa con ningún test → el comando pasaría en verde con 0 tests"
fail=1
else
echo " ✓ casa con ≥1 test"
fi
done < <(grep -rhnE "dotnet test.*--filter" "${FILES[@]}" 2>/dev/null || true)

# El guard mismo no debe ser una verificación fantasma: si dejó de encontrar filtros, algo cambió
# (¿se renombró un archivo? ¿cambió la sintaxis?) y hay que revisarlo, no pasar en silencio.
if [[ "$found_any" -eq 0 ]]; then
echo "::error::el guard no encontró NINGÚN 'dotnet test --filter' en los archivos vigilados;" \
"si de verdad ya no hay filtros documentados, actualiza este script — no lo dejes ciego"
exit 1
fi

exit $fail
81 changes: 52 additions & 29 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ jobs:
with:
dotnet-version: "10.0.x"
# El shim se compila CON test-hooks para la suite de panic-safety (SC-009); el binario de
# release empaquetado (US4) se compila SIN la feature — pack-smoke verifica la ausencia.
# release empaquetado (US4) se compila SIN la feature — el job `native` de release.yml
# verifica con `nm` que los símbolos de test no están exportados en los cdylibs.
- name: Build shim (test-hooks)
working-directory: native
run: cargo build --release --features test-hooks
Expand All @@ -62,7 +63,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
workspaces: native
# Matriz de sanitizers sobre AMBOS shims (T035): sin -p corre todo el workspace nativo.
# AddressSanitizer + LeakSanitizer sobre AMBOS shims (T035): sin -p corre todo el workspace.
- name: cargo test bajo AddressSanitizer + LeakSanitizer (yrs + loro)
working-directory: native
env:
Expand All @@ -73,15 +74,16 @@ jobs:
--target x86_64-unknown-linux-gnu

# ── Fuzzing de la frontera FFI (research R14): smoke de 60 s por target en cada PR ─────────
# INFORMATIVO en M0 (continue-on-error): el fuzz destapó un DoS por amplificación de memoria en el
# decoder de yrs (upstream) — un update malformado de pocos bytes declara una longitud gigante y
# yrs hace `with_capacity(N)` sin cota; en un entorno con memoria limitada la asignación falla y
# Rust aborta (`handle_alloc_error`, NO capturable por catch_unwind). El shim FFI es correcto
# (contiene panics, sin UB). La mitigación real (validar el update / límite de recursos) llega en
# M2, donde entra input de red no confiable — ver AILOG-2026-07-10-001 §R6 y el follow-up. El job
# sigue corriendo y reportando en los logs, sin bloquear el cierre de M0.
# El job BLOQUEA A MEDIAS, y es deliberado: un fallo de COMPILACIÓN de los targets (o de instalar
# cargo-fuzz) pone el job rojo; un CRASH ENCONTRADO solo emite `::warning` (los `|| echo` de los
# steps `fuzz run`). NO hay `continue-on-error` en el job. La razón del trato informativo del
# crash es R6: el decoder de yrs amplifica memoria sobre un update malformado de pocos bytes
# (`with_capacity(N)` sin cota → `handle_alloc_error`, que catch_unwind no puede contener). El shim
# es correcto (contiene panics, sin UB); el fallo está aguas abajo, en yrs. La mitigación llegó en
# CHARTER-08 (PR upstream y-crdt#639 + caveat en GOVERNANCE.md §Seguridad); lo que resta es
# adoptar el fix vía bump (FU-015). Ver AILOG-2026-07-10-001 §R6.
fuzz:
name: fuzz (smoke, informativo)
name: fuzz (smoke; crash informativo)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
Expand All @@ -105,33 +107,35 @@ jobs:
# El objetivo del smoke es que ningún input adversarial cause panic-through / UB / crash en
# NUESTRO shim (research R14). Dos características del decoder de yrs sobre input malformado
# obligan a configurar el harness para que mida eso y no artefactos de yrs (ver AILOG §R6):
# 1. `-s none` (sin ASan): yrs hace `with_capacity(N)` según una longitud declarada sin cota;
# glibc reserva esa memoria VIRTUAL (nunca se llena → RSS real ~150 MB) y yrs falla el
# decode → WEFT_ERR_DECODE. El allocator de ASan, en cambio, aborta reservas gigantes.
# La memory-safety la cubre el job `asan` sobre la suite determinista.
# 1. `-s none` (sin ASan): en las rutas de yrs que aún usan `with_capacity(N)` según una
# longitud declarada sin cota (el residual es id_set/state_vector vía export_since;
# Update::decode ya usa try_reserve en 0.27.2), glibc reserva esa memoria VIRTUAL (nunca se
# llena → RSS real ~150 MB) y yrs falla el decode → WEFT_ERR_DECODE. El allocator de ASan,
# en cambio, aborta reservas gigantes. La memory-safety la cubre el job `asan`.
# 2. Hook de panic silenciado en el harness (fuzz_targets/*.rs): yrs usa `assert!` en algunos
# updates malformados; el shim los contiene con `catch_unwind` → WEFT_ERR_PANIC (verificado
# en tests). libfuzzer-sys aborta en su panic hook antes de que catch_unwind actúe, así que
# el harness lo silencia para ejercitar el mismo camino que producción. Un SIGSEGV/UB real
# no pasa por el hook y sigue detectándose.
# La resistencia a amplificación con input no confiable se endurece en la capa de servidor
# (M2, límites de tamaño de mensaje + recursos del proceso).
# Informativo (R6): un crash del fuzz sobre la amplificación de memoria del decoder de yrs no
# bloquea M0. El comando captura ese crash y emite una ANOTACIÓN de warning visible en el PR;
# el step sale con éxito (check verde, no rojo). Un fallo de infraestructura (compilación de
# los targets, instalación) rompe el job en los steps anteriores. La mitigación real va en M2.
# La resistencia a amplificación con input de red no confiable ya está en la capa de servidor
# (FU-002 cerrado: cap de tamaño de mensaje + límites de recursos por conexión, ver
# WeftServerOptions). Para la RUTA DIRECTA del FFI, el caveat está en GOVERNANCE.md §Seguridad.
# Crash informativo (R6): un crash del fuzz sobre la amplificación del decoder de yrs no rompe
# el job. El `|| echo` captura ese crash y emite una ANOTACIÓN de warning visible en el PR; el
# step sale con éxito (check verde). Un fallo de infraestructura (compilar los targets,
# instalar cargo-fuzz) rompe el job en los steps anteriores, que NO llevan `|| echo`.
- name: Fuzz weft_doc_load (60 s)
working-directory: native/weft-yrs-ffi
run: >-
cargo +nightly fuzz run -s none doc_load --
-max_total_time=60 -rss_limit_mb=0 -max_len=8192
|| echo "::warning title=fuzz informativo (R6)::doc_load halló un crash por amplificación de memoria del decoder de yrs — no bloquea M0; mitigación en M2 (ver AILOG R6)"
|| echo "::warning title=fuzz informativo (R6)::doc_load halló un crash por amplificación de memoria del decoder de yrs — no rompe el job; fix upstream y-crdt#639, adopción vía FU-015"
- name: Fuzz weft_doc_apply_update (60 s)
working-directory: native/weft-yrs-ffi
run: >-
cargo +nightly fuzz run -s none apply_update --
-max_total_time=60 -rss_limit_mb=0 -max_len=8192
|| echo "::warning title=fuzz informativo (R6)::apply_update halló un crash por amplificación de memoria del decoder de yrs — no bloquea M0; mitigación en M2 (ver AILOG R6)"
|| echo "::warning title=fuzz informativo::apply_update halló un crash — no rompe el job. Nota: Update::decode ya usa try_reserve en yrs 0.27.2, así que un crash aquí probablemente NO sea amplificación R6; revisar el artefacto"
# Regresión R6 (CHARTER-08): ruta RESIDUAL state_vector::decode vía weft_doc_export_since — el
# único sitio de yrs que aún usa with_capacity sin acotar (id_set/state_vector; Update::decode ya
# usa try_reserve). Informativo hasta adoptar el fix upstream (FU-015); prueba el fix al bumpear.
Expand All @@ -147,15 +151,14 @@ jobs:
run: >-
cargo +nightly fuzz run -s none loro_doc_load --
-max_total_time=60 -rss_limit_mb=0 -max_len=8192
|| echo "::warning title=fuzz informativo (loro)::loro_doc_load halló un crash — no bloquea M0 (ver AILOG)"
|| echo "::warning title=fuzz informativo (loro)::loro_doc_load halló un crash — no rompe el job (ver AILOG)"
- name: Fuzz loro_apply_update (60 s)
working-directory: native/weft-loro-ffi
run: >-
cargo +nightly fuzz run -s none loro_apply_update --
-max_total_time=60 -rss_limit_mb=0 -max_len=8192
|| echo "::warning title=fuzz informativo (loro)::loro_apply_update halló un crash — no bloquea M0 (ver AILOG)"
|| echo "::warning title=fuzz informativo (loro)::loro_apply_update halló un crash — no rompe el job (ver AILOG)"

# ── Gates que se activan en fases posteriores (jobs nombrados, T004) ──────────────────────
# ── Gate P-III: determinismo del encoding (bloqueante desde US1) ──────────────────────────
determinism:
name: determinism
Expand All @@ -175,7 +178,10 @@ jobs:
run: cargo build --release --features test-hooks
- name: Determinism gate
run: dotnet test tests/Weft.Determinism.Tests/ --configuration Release
# El cross-implementación vs Yjs JS (mismo hash en todos los RIDs) se añade en US4 (T058).
# La paridad cross-implementación yrs↔Yjs es BLOQUEANTE y vive AQUÍ, en Weft.Determinism.Tests
# (`Yrs_export_matches_yjs_golden` contra tests/determinism-yjs/golden.json) — se promovió en
# CHARTER-09/FU-012. El job Node `determinism-yjs` de release.yml es solo informativo (regenera
# el hash de Yjs para cazar drift del upstream), no la aserción de paridad.

# ── Gate P-IV: suite de versionado sobre yrs Y Loro (bloqueante desde US5, SC-008) ────────
dual-engine:
Expand Down Expand Up @@ -231,11 +237,28 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# El pack-smoke multi-RID real (SC-007/P-VI) + la verificación de ausencia de weft_test_panic
# (SC-009) viven en `release.yml` (CHARTER-07/T057): la matriz cross-compile es cara, así que
# corre por `workflow_dispatch` (dry-run), NO en cada PR. Este job por-PR queda como marcador.
# El pack-smoke multi-RID real (SC-007/P-VI) + la verificación de ausencia de los símbolos de
# test (SC-009) viven en `release.yml` (CHARTER-07/T057): la matriz cross-compile es cara, así
# que corre por `workflow_dispatch` (dry-run), NO en cada PR. Este job por-PR queda como
# marcador — y así se documenta con honestidad, en vez de fingir que valida el empaquetado.
- run: echo "pack-smoke multi-RID vive en release.yml (workflow_dispatch, dry_run) — CHARTER-07 T057."

# ── Guard contra «verificaciones fantasma» de la clase filtro-de-test (FU-020, CHARTER-12) ────
# BLOQUEANTE. Un `dotnet test --filter X` que no casa con ningún test pasa en verde ejecutando
# cero — su síntoma es idéntico al del éxito. Este guard exige que cada filtro documentado/en CI
# case con ≥1 test. Solo necesita .NET (usa --list-tests, no ejecuta la suite); ~1 min.
test-filters:
name: test-filters (guard fantasma)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v5
- uses: actions/setup-dotnet@v5
with:
dotnet-version: "10.0.x"
- name: Cada --filter documentado casa con ≥1 test
run: bash .github/scripts/check-test-filters.sh

# ── Gate M1 (P-V, SC-006): prueba de carga de concurrencia ────────────────────────────────
# Nightly (schedule) + manual (workflow_dispatch): NO corre en PR (no lo bloquea), pero es
# BLOQUEANTE para el cierre de M1 — el nightly debe estar verde. El harness sale con código ≠ 0
Expand Down
54 changes: 41 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,25 +88,53 @@ jobs:
# Quedarnos solo con el cdylib (descartar .d/.rlib/.pdb/.lib/.exp).
find out -type f ! -name '*.so' ! -name '*.dll' ! -name '*.dylib' -delete
ls -la out
- name: Verify weft_test_panic NOT exported (SC-009)
# SC-009: los test-hooks NUNCA viajan en release. Dos correcciones de CHARTER-12:
# (a) el patrón era `weft_test_panic`, que NO casa con `weft_loro_test_panic` (no es
# substring) → el shim de Loro quedaba fuera del gate. Ahora se buscan ambos.
# (b) el gate pasaba en verde si la herramienta faltaba: `nm ... 2>/dev/null | grep -q` no
# distingue «no está el símbolo» de «no está nm». Un gate que no sabe fallar es la
# verificación fantasma que este Charter cierra, y estaba dentro del propio gate.
# Ahora: la herramienta debe existir, y debe demostrar que ve un símbolo de control que
# SÍ está en el binario (weft_abi_version) antes de creerle una ausencia.
- name: Verify test hooks NOT exported (SC-009)
working-directory: native
shell: bash
run: |
set -euo pipefail

symbols() { # $1 = archivo → lista de símbolos, o falla si no hay herramienta
case "$1" in
*.so|*.dylib) nm -D "$1" 2>/dev/null || nm "$1" ;;
*.dll) strings "$1" ;;
esac
}

case "$RUNNER_OS" in
Windows) command -v strings >/dev/null || { echo "::error::falta 'strings': el gate SC-009 no puede verificar nada"; exit 1; } ;;
*) command -v nm >/dev/null || { echo "::error::falta 'nm': el gate SC-009 no puede verificar nada"; exit 1; } ;;
esac

fail=0
for f in out/*; do
case "$f" in
*.so|*.dylib)
if nm -D "$f" 2>/dev/null | grep -q weft_test_panic \
|| nm "$f" 2>/dev/null | grep -q weft_test_panic; then
echo "✗ $f exporta weft_test_panic (test-hooks filtrado en release)"; fail=1
else echo "✓ $f sin weft_test_panic"; fi ;;
*.dll)
# En Windows/bash usamos strings como verificación débil (test-hooks no compilado en release).
if strings "$f" 2>/dev/null | grep -q weft_test_panic; then
echo "✗ $f contiene weft_test_panic"; fail=1
else echo "✓ $f sin weft_test_panic"; fi ;;
esac
[ -f "$f" ] || continue
syms="$(symbols "$f")"

# Control positivo: si no vemos un símbolo que SÍ está, la ausencia de otro no prueba
# nada. Se busca `abi_version` (substring de weft_abi_version Y weft_loro_abi_version):
# buscar `weft_abi_version` fallaría en el cdylib de Loro por el mismo motivo de
# substring que dejaba fuera a `weft_loro_test_panic`.
if ! printf '%s' "$syms" | grep -q abi_version; then
echo "::error::$f — la herramienta no encuentra ni *abi_version: el gate está ciego, no limpio"
fail=1; continue
fi

found=0
for hook in weft_test_panic weft_loro_test_panic; do
if printf '%s' "$syms" | grep -q "$hook"; then
echo "✗ $f expone $hook (test-hooks filtrado en release)"; found=1; fail=1
fi
done
[ "$found" -eq 0 ] && echo "✓ $f sin test hooks (control positivo: weft_abi_version visible)"
done
exit $fail
- name: Upload native artifacts
Expand Down
Loading
Loading