From bbffb7fa60330b406e5c2ce43c1d8b28d76fffe5 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Tue, 22 Sep 2026 06:17:39 +0200 Subject: [PATCH] 0.3.5-alpha.5 --- Cargo.toml | 5 +- README.md | 8 +- .../Cargo.toml | 21 +++ .../src/main.rs | 134 ++++++++++++++++++ deltas/0.3.5/alpha.5.md | 126 ++++++++++++++++ .../005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md | 24 ++-- history/0.3.5/alpha.4.fix.1.md | 75 ++++++++++ 7 files changed, 378 insertions(+), 15 deletions(-) create mode 100644 crates/apps/game-realtime-webtransport-smoke/Cargo.toml create mode 100644 crates/apps/game-realtime-webtransport-smoke/src/main.rs create mode 100644 deltas/0.3.5/alpha.5.md create mode 100644 history/0.3.5/alpha.4.fix.1.md diff --git a/Cargo.toml b/Cargo.toml index 3e5519e..9641d7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 100 +# version: 101 [workspace] resolver = "3" @@ -10,6 +10,7 @@ members = [ "crates/games/game-reflex-poc", "crates/games/game-snake-poc", "crates/apps/game-realtime-websocket-smoke", + "crates/apps/game-realtime-webtransport-smoke", "crates/apps/game-reflex-poc-desktop", "crates/apps/game-snake-poc-desktop", "crates/common/game-assets-lib", @@ -25,7 +26,7 @@ members = [ ] [workspace.package] -version = "0.3.5-alpha.4.fix.1" +version = "0.3.5-alpha.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index 3580728..fb56731 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # games.sasedev @@ -27,9 +27,9 @@ Workspace expérimental puis productif pour des jeux multiplateformes principale Version stable de référence : `0.3.4`. -Version active : `0.3.5-alpha.4.fix.1`. `0.3.2` reste différée. +Version active : `0.3.5-alpha.5`. `0.3.2` reste différée. -La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.4.fix.1` consolide le backend WebTransport natif avec limite de message configurable, deadlines connexion/stream/send, mapping d'erreurs, reset/abort/drop explicites et parseur de framing incrémental résistant à l'annulation ; le fix ferme uniquement le lint strict `implicit-return` révélé par la gate de `alpha.4` sans changer le comportement runtime. Navigateur/WASM, smoke public, fallback et datagrams restent réservés aux tranches suivantes. +La stable `0.3.4` livre la première baseline realtime : contrat binaire transport-neutral, backend WebSocket Tokio/tokio-tungstenite, limites et deadlines, tests loopback/robustesse, smoke runtime localhost public et frontières de dépendances empêchant moteurs et gameplay de dépendre d'un backend concret. `0.3.5-alpha.5` ajoute au backend WebTransport natif déjà robuste un smoke runtime localhost hors harness : identité TLS locale en mémoire, pin SHA-256 exact, session WebTransport réelle, stream fiable principal, round-trip binaire bidirectionnel et fermeture propre avec verdict `PASS` déterministe. Navigateur/WASM, fallback et datagrams restent réservés aux tranches suivantes. Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-snake-poc`. Ils existent d'abord pour valider les frontières du workspace, le moteur, les assets et le packaging multiplateforme. @@ -45,4 +45,4 @@ Les deux premiers jeux sont des POC structurels : `game-reflex-poc` et `game-sna ## Diagnostics et tests -Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Le realtime est séparé entre `game-realtime-transport-lib`, contrat binaire transport-neutral, `game-realtime-websocket-lib`, backend Tokio/tokio-tungstenite, et `game-realtime-webtransport-lib`, backend WebTransport/QUIC candidat dont le chemin natif couvre désormais TLS/pinning, établissement de session et stream fiable principal adapté au contrat commun. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` fournit la preuve runtime localhost hors harness de test ; le smoke WebTransport public est planifié après la tranche de robustesse. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`. +Les socles transverses `crates/common/game-assets-lib` et `crates/common/game-logging-lib` fournissent respectivement la résolution logique des assets et le tracing commun. Le realtime est séparé entre `game-realtime-transport-lib`, contrat binaire transport-neutral, `game-realtime-websocket-lib`, backend Tokio/tokio-tungstenite, et `game-realtime-webtransport-lib`, backend WebTransport/QUIC candidat dont le chemin natif couvre désormais TLS/pinning, établissement de session et stream fiable principal adapté au contrat commun. Leurs responsabilités sont documentées dans leurs README/USAGE locaux lorsqu'un guide d'usage est justifié. `game-realtime-websocket-smoke` et `game-realtime-webtransport-smoke` fournissent les preuves runtime localhost hors harness des deux backends fiables natifs. Les tests unitaires résident hors `src/` sous `unit_tests/`; les tests d’intégration/environnement résident sous `tests/`. diff --git a/crates/apps/game-realtime-webtransport-smoke/Cargo.toml b/crates/apps/game-realtime-webtransport-smoke/Cargo.toml new file mode 100644 index 0000000..fd5871e --- /dev/null +++ b/crates/apps/game-realtime-webtransport-smoke/Cargo.toml @@ -0,0 +1,21 @@ +# file: crates/apps/game-realtime-webtransport-smoke/Cargo.toml +# version: 1 + +[package] +name = "game-realtime-webtransport-smoke" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +publish.workspace = true + +[dependencies] +game-logging-lib = { path = "../../common/game-logging-lib" } +game-realtime-transport-lib = { path = "../../common/game-realtime-transport-lib" } +game-realtime-webtransport-lib = { path = "../../common/game-realtime-webtransport-lib" } +tokio = { workspace = true, features = ["macros", "rt", "time"] } +tracing.workspace = true + +[lints] +workspace = true diff --git a/crates/apps/game-realtime-webtransport-smoke/src/main.rs b/crates/apps/game-realtime-webtransport-smoke/src/main.rs new file mode 100644 index 0000000..8172303 --- /dev/null +++ b/crates/apps/game-realtime-webtransport-smoke/src/main.rs @@ -0,0 +1,134 @@ +// file: crates/apps/game-realtime-webtransport-smoke/src/main.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Executable localhost smoke for the public games.sasedev native WebTransport realtime path. + +use game_realtime_transport_lib::RealtimeConnection; // rust-rules: trait-import +use game_realtime_transport_lib::RealtimeReceiver; // rust-rules: trait-import +use game_realtime_transport_lib::RealtimeSender; // rust-rules: trait-import + +const CLIENT_PAYLOAD: &[u8] = b"games.sasedev-webtransport-client-smoke"; +const SERVER_PAYLOAD: &[u8] = b"games.sasedev-webtransport-server-smoke"; +const SMOKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); +const TRACING_TARGET: &str = "games::realtime::webtransport::smoke"; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> std::process::ExitCode { + let _logging_guard = match game_logging_lib::init_console_tracing() { + std::result::Result::Ok(guard) => guard, + std::result::Result::Err(error) => { + eprintln!("failed to initialize realtime WebTransport smoke tracing: {error}"); + return std::process::ExitCode::FAILURE; + }, + }; + tracing::info!(target: TRACING_TARGET, "realtime WebTransport smoke started"); + let result = tokio::time::timeout(SMOKE_TIMEOUT, run_smoke()).await; + return match result { + std::result::Result::Ok(std::result::Result::Ok(())) => { + tracing::info!(target: TRACING_TARGET, "realtime WebTransport smoke passed"); + println!("game-realtime-webtransport-smoke: PASS"); + std::process::ExitCode::SUCCESS + }, + std::result::Result::Ok(std::result::Result::Err(error)) => { + tracing::error!(target: TRACING_TARGET, detail = error.as_str(), "realtime WebTransport smoke failed"); + eprintln!("game-realtime-webtransport-smoke: FAIL: {error}"); + std::process::ExitCode::FAILURE + }, + std::result::Result::Err(_) => { + tracing::error!(target: TRACING_TARGET, timeout_ms = SMOKE_TIMEOUT.as_millis(), "realtime WebTransport smoke timed out"); + eprintln!("game-realtime-webtransport-smoke: FAIL: smoke timed out"); + std::process::ExitCode::FAILURE + }, + }; +} + +async fn run_smoke() -> std::result::Result<(), String> { + let identity = match game_realtime_webtransport_lib::WebTransportServerIdentity::generate_loopback() { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("loopback identity generation failed: {error}")), + }; + let certificate_hash = identity.certificate_hash().clone(); + let bind_address = std::net::SocketAddr::from(([127, 0, 0, 1], 0)); + let server_config = game_realtime_webtransport_lib::WebTransportServerConfig::new(bind_address, identity); + let mut listener = match game_realtime_webtransport_lib::WebTransportListener::bind(server_config) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("listener bind failed: {error}")), + }; + let endpoint = format!("https://{}/smoke", listener.local_addr()); + let client_config = match game_realtime_webtransport_lib::WebTransportClientConfig::new(endpoint.as_str(), certificate_hash) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("client configuration failed: {error}")), + }; + tracing::info!(target: TRACING_TARGET, endpoint = endpoint.as_str(), "loopback endpoint bound with exact certificate pin"); + let (server_result, client_result) = tokio::join!(listener.accept(), game_realtime_webtransport_lib::connect(&client_config)); + let server_session = match server_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("server accept failed: {error}")), + }; + let client_session = match client_result { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("client connect failed: {error}")), + }; + let client_connection = match client_session.open_primary_connection().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("client primary stream open failed: {error}")), + }; + let server_connection = match server_session.accept_primary_connection().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("server primary stream accept failed: {error}")), + }; + let (mut server_sender, mut server_receiver) = server_connection.split(); + let (mut client_sender, mut client_receiver) = client_connection.split(); + if let std::result::Result::Err(error) = client_sender.send(game_realtime_transport_lib::TransportMessage::new(CLIENT_PAYLOAD.to_vec())).await { + return std::result::Result::Err(format!("client send failed: {error}")); + } + let server_received = match server_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("server receive failed: {error}")), + }; + if !receive_matches(server_received, CLIENT_PAYLOAD) { + return std::result::Result::Err(String::from("server did not receive the expected client payload")); + } + if let std::result::Result::Err(error) = server_sender.send(game_realtime_transport_lib::TransportMessage::new(SERVER_PAYLOAD.to_vec())).await { + return std::result::Result::Err(format!("server send failed: {error}")); + } + let client_received = match client_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("client receive failed: {error}")), + }; + if !receive_matches(client_received, SERVER_PAYLOAD) { + return std::result::Result::Err(String::from("client did not receive the expected server payload")); + } + if let std::result::Result::Err(error) = client_sender.close().await { + return std::result::Result::Err(format!("client close failed: {error}")); + } + let server_close = match server_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("server close observation failed: {error}")), + }; + if server_close != game_realtime_transport_lib::TransportReceive::Closed { + return std::result::Result::Err(String::from("server did not observe the client FIN")); + } + if let std::result::Result::Err(error) = server_sender.close().await { + return std::result::Result::Err(format!("server close failed: {error}")); + } + let client_close = match client_receiver.receive().await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("client close observation failed: {error}")), + }; + if client_close != game_realtime_transport_lib::TransportReceive::Closed { + return std::result::Result::Err(String::from("client did not observe the server FIN")); + } + return std::result::Result::Ok(()); +} + +fn receive_matches(receive: game_realtime_transport_lib::TransportReceive, expected: &[u8]) -> bool { + return match receive { + game_realtime_transport_lib::TransportReceive::Message(message) => message.as_bytes() == expected, + game_realtime_transport_lib::TransportReceive::Closed => false, + }; +} diff --git a/deltas/0.3.5/alpha.5.md b/deltas/0.3.5/alpha.5.md new file mode 100644 index 0000000..3d9d479 --- /dev/null +++ b/deltas/0.3.5/alpha.5.md @@ -0,0 +1,126 @@ + + + +# Delta 0.3.5-alpha.5 + +## Base requise + +`0.3.5-alpha.4.fix.1`, validée par l'utilisateur le 2026-09-21 avec fmt, audits, `cargo check`, Clippy strict, les sept tests du contrat realtime commun et les vingt tests WebTransport. + +La preuve est enregistrée dans `history/0.3.5/alpha.4.fix.1.md`. + +## Objectif + +Fermer la preuve runtime native WebTransport hors harness de test sans ajouter de fonctionnalité à la bibliothèque. + +Cette tranche doit démontrer avec les APIs publiques réellement distribuées que le chemin localhost complet fonctionne depuis un exécutable normal : TLS local, pinning, session WebTransport, stream fiable principal, contrat realtime commun, round-trip bidirectionnel et fermeture propre. + +## Version + +La version workspace passe à : + +```text +0.3.5-alpha.5 +``` + +## Launcher technique + +Nouvelle crate : + +```text +crates/apps/game-realtime-webtransport-smoke +``` + +Elle dépend uniquement de : + +```text +game-logging-lib +game-realtime-transport-lib +game-realtime-webtransport-lib +tokio +tracing +``` + +Aucune nouvelle dépendance externe n'est introduite. + +Le launcher : + +1. initialise le tracing commun ; +2. génère une identité TLS loopback ECDSA P-256 courte durée en mémoire ; +3. dérive le hash SHA-256 exact exposé par cette identité ; +4. bind le listener WebTransport sur `127.0.0.1:0` ; +5. construit le client `https:///smoke` avec ce hash pin ; +6. établit simultanément les sessions client et serveur ; +7. ouvre/accepte le stream bidirectionnel primaire ; +8. échange un payload client puis un payload serveur via le contrat `RealtimeConnection` ; +9. ferme proprement le sender client et vérifie `Closed` côté serveur ; +10. ferme proprement le sender serveur et vérifie `Closed` côté client ; +11. imprime exactement : + +```text +game-realtime-webtransport-smoke: PASS +``` + +Une deadline globale de 10 secondes empêche le smoke de rester bloqué indéfiniment. + +## Frontière de dépendances + +Le backend WebTransport est désormais consommé par un launcher technique public, ce qui est intentionnel. Il doit toujours rester absent des crates engine et gameplay. + +Le graphe inverse attendu peut donc contenir : + +```text +game-realtime-webtransport-lib +game-realtime-webtransport-smoke +``` + +mais aucune crate `engine-v1-*`, `game-snake-poc` ou `game-reflex-poc`. + +## Fichiers modifiés + +```text +Cargo.toml +README.md +docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md +``` + +Nouveaux fichiers : + +```text +crates/apps/game-realtime-webtransport-smoke/Cargo.toml +crates/apps/game-realtime-webtransport-smoke/src/main.rs +deltas/0.3.5/alpha.5.md +history/0.3.5/alpha.4.fix.1.md +``` + +## Validation attendue + +```bash +cargo fmt --all +cargo fmt --all -- --check + +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history +python3 scripts/audit_distribution_layout.py + +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings + +cargo test -p game-realtime-transport-lib --all-targets --all-features +cargo test -p game-realtime-webtransport-lib --all-targets --all-features + +cargo run -p game-realtime-webtransport-smoke + +cargo tree -p game-realtime-webtransport-smoke --edges normal +cargo tree -i game-realtime-webtransport-lib --workspace --edges normal +``` + +Le smoke doit se terminer avec : + +```text +game-realtime-webtransport-smoke: PASS +``` + +## Après validation + +Créer `history/0.3.5/alpha.5.md` à partir des sorties réellement fournies, puis ouvrir `0.3.5-alpha.6` pour le chemin WASM compilable. Le smoke navigateur/TLS réel reste réservé à `alpha.7`. diff --git a/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md b/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md index 7c97224..01d0b8c 100644 --- a/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md +++ b/docs/plans/005-V0_3_5_WEBTRANSPORT_QUIC_POC_PLAN.md @@ -1,11 +1,11 @@ - + # Plan 0.3.5 — POC WebTransport/QUIC et fallback WebSocket ## Statut -Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2`, son correctif de validation `0.3.5-alpha.2.fix.1`, le chemin fiable validé de `0.3.5-alpha.3`, la robustesse candidate de `0.3.5-alpha.4` et son correctif de gate `0.3.5-alpha.4.fix.1`. +Plan actif créé pendant `0.3.5-alpha.1` à partir de l'archive taggée `v0.3.4`, puis réconcilié pour l'implémentation native de `0.3.5-alpha.2`, son correctif de validation `0.3.5-alpha.2.fix.1`, le chemin fiable validé de `0.3.5-alpha.3`, la robustesse validée par `0.3.5-alpha.4.fix.1` et le smoke natif candidat de `0.3.5-alpha.5`. Le cadrage détaillé et la comparaison des stacks actuelles sont conservés dans `docs/studies/026-V0_3_5_WEBTRANSPORT_QUIC_STACK_AUDIT.md`. Le présent document porte les décisions opérationnelles, le scope, les gates et le forecast vivant de la version. @@ -457,17 +457,23 @@ Tranche candidate matérialisée avec : Un timeout d'idle de `receive()` n'est volontairement pas imposé au contrat : l'appelant peut borner/annuler l'attente sans corrompre l'état du frame parser. Un test artificiel de saturation/backpressure n'est pas ajouté tant qu'il n'est pas déterministe. -La gate utilisateur de `alpha.4` a confirmé fmt, audits, `cargo check`, tous les tests du contrat commun et les vingt tests WebTransport, mais Clippy strict a détecté un unique `clippy::implicit-return` dans la future retournée par `RealtimeReceiver::receive()`. `alpha.4.fix.1` explicite ce `return` sans changement sémantique, d'API, de framing, de lifecycle ou de dépendance. La gate complète du fix reste requise avant d'ouvrir `alpha.5`. +La gate utilisateur de `alpha.4` a confirmé fmt, audits, `cargo check`, tous les tests du contrat commun et les vingt tests WebTransport, mais Clippy strict a détecté un unique `clippy::implicit-return` dans la future retournée par `RealtimeReceiver::receive()`. `alpha.4.fix.1` a explicité ce `return` sans changement sémantique, d'API, de framing, de lifecycle ou de dépendance. Sa gate complète est ensuite passée : audits, check workspace, Clippy strict, sept tests du contrat commun et vingt tests WebTransport. La preuve est conservée dans `history/0.3.5/alpha.4.fix.1.md`. ### `0.3.5-alpha.5` — smoke natif hors harness -- créer ou finaliser `game-realtime-webtransport-smoke` ; -- round-trip réel localhost ; -- certificat/hash de développement reproductible ; -- `PASS` déterministe ; -- contrôle du graphe de dépendances. +Tranche candidate matérialisée avec : -Cette tranche reste distincte pour ne pas mélanger robustesse de bibliothèque et preuve runtime publique. +- nouvelle app technique `game-realtime-webtransport-smoke` ; +- bind UDP loopback sur port éphémère ; +- identité TLS ECDSA P-256 locale générée en mémoire et pin SHA-256 dérivé de cette identité ; +- établissement WebTransport client/server réel sur `https://127.0.0.1:/smoke` ; +- ouverture/accept du stream fiable principal via l'API publique du backend ; +- round-trip binaire bidirectionnel via `RealtimeConnection` ; +- FIN propre client puis serveur, chacun observé comme `TransportReceive::Closed` par le pair ; +- timeout global du launcher et verdict terminal déterministe `game-realtime-webtransport-smoke: PASS` ; +- aucune nouvelle dépendance externe et aucun couplage engine/gameplay. + +Cette tranche reste distincte pour ne pas mélanger robustesse de bibliothèque et preuve runtime publique. La gate doit exécuter le smoke hors harness puis contrôler les graphes direct et inverse : l'inverse de `game-realtime-webtransport-lib` peut désormais contenir le launcher technique, mais aucune crate engine ou gameplay. ### `0.3.5-alpha.6` — chemin WASM compilable diff --git a/history/0.3.5/alpha.4.fix.1.md b/history/0.3.5/alpha.4.fix.1.md new file mode 100644 index 0000000..bcd6722 --- /dev/null +++ b/history/0.3.5/alpha.4.fix.1.md @@ -0,0 +1,75 @@ + + + +# Historique 0.3.5-alpha.4.fix.1 + +## Statut + +`0.3.5-alpha.4.fix.1` a été validée par l'utilisateur le 2026-09-21. La tranche robustesse/lifecycle WebTransport est donc fermée : configuration, deadlines, framing négatif, cancellation, reset/abort/drop et mapping d'erreurs passent avec Clippy strict. + +La suite peut ouvrir `0.3.5-alpha.5` pour le smoke natif hors harness sans réouvrir le scope de la bibliothèque. + +## Gates statiques et compilation + +Les commandes utilisateur ont terminé proprement : + +```text +cargo fmt --all +cargo fmt --all -- --check +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates Android Web deltas history +python3 scripts/audit_distribution_layout.py +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +``` + +La sortie confirme : + +```text +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +games.sasedev workspace audit: clean +Markdown table audit: clean (5 table(s), 292 file(s)) +Distribution layout audit: clean (50 required path(s), 8 forbidden path(s) absent) +cargo check --workspace: clean +cargo clippy --workspace --all-targets --all-features -- -D warnings: clean +``` + +Le workspace compilé porte `0.3.5-alpha.4.fix.1`, y compris `game-realtime-webtransport-lib` et l'ensemble des targets workspace affichées par la gate. + +## Tests du contrat realtime commun + +La commande : + +```bash +cargo test -p game-realtime-transport-lib --all-targets --all-features +``` + +est entièrement propre : + +```text +7 passed; 0 failed +``` + +## Tests WebTransport + +La commande : + +```bash +cargo test -p game-realtime-webtransport-lib --all-targets --all-features +``` + +est entièrement propre : + +```text +unit tests: 11 passed; 0 failed +establishment integration tests: 2 passed; 0 failed +realtime_connection integration tests: 1 passed; 0 failed +robustness integration tests: 6 passed; 0 failed +``` + +Les vingt tests WebTransport couvrent donc l'établissement et le pinning, le framing fiable, le round-trip primaire, les limites configurables, les frames tronquées, la cancellation de receive, les reset/abort/drop et la deadline d'accept du stream primaire. + +## Conséquence + +`alpha.5` peut se limiter à une preuve runtime publique hors harness : launcher technique dédié, bind éphémère, identité TLS locale en mémoire, pin SHA-256 exact, round-trip binaire bidirectionnel, FIN propre dans les deux directions et verdict `PASS` déterministe.