From 74cdb177e91c55d98a33dd60a0af954b8837ce4f Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Mon, 21 Sep 2026 18:22:19 +0200 Subject: [PATCH] 0.3.4-alpha.5 --- Cargo.toml | 5 +- README.md | 6 +- .../game-realtime-websocket-smoke/Cargo.toml | 21 +++ .../game-realtime-websocket-smoke/src/main.rs | 108 +++++++++++++++ deltas/0.3.4/alpha.5.md | 127 ++++++++++++++++++ ...0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md | 36 ++++- history/0.3.4/alpha.4.md | 62 +++++++++ scripts/audit_distribution_layout.py | 3 +- 8 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 crates/apps/game-realtime-websocket-smoke/Cargo.toml create mode 100644 crates/apps/game-realtime-websocket-smoke/src/main.rs create mode 100644 deltas/0.3.4/alpha.5.md create mode 100644 history/0.3.4/alpha.4.md diff --git a/Cargo.toml b/Cargo.toml index b60d4c9..5ee5249 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 90 +# version: 91 [workspace] resolver = "3" @@ -9,6 +9,7 @@ members = [ "crates/engines/engine-v1-sdl", "crates/games/game-reflex-poc", "crates/games/game-snake-poc", + "crates/apps/game-realtime-websocket-smoke", "crates/apps/game-reflex-poc-desktop", "crates/apps/game-snake-poc-desktop", "crates/common/game-assets-lib", @@ -23,7 +24,7 @@ members = [ ] [workspace.package] -version = "0.3.4-alpha.4" +version = "0.3.4-alpha.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/games" diff --git a/README.md b/README.md index 764f985..0e303f1 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.3`. -Version candidate active : `0.3.4-alpha.4`. `0.3.2` reste différée. +Version candidate active : `0.3.4-alpha.5`. `0.3.2` reste différée. -La stable `0.3.3` livre la voie Android SDL3 native multi-ABI : build Gradle/Cargo sans orchestrateur Python, APK Debug universal et AAB Release pour `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`, `minSdk 21` réellement fumé et compatibilité pages mémoire 16 KB validée sur les ABI 64 bits. `0.3.4-alpha.4` consolide la baseline realtime validée en `alpha.3.fix.1` avec une configuration WebSocket explicite : limite message/frame de 1 MiB, write buffer cible 64 KiB et maximum 2 MiB, deadlines connect/send/close de 10/5/2 s, rejet Text, dépassements de taille, handshake silencieux et fermeture abrupte couverts par des tests locaux bornés. +La stable `0.3.3` livre la voie Android SDL3 native multi-ABI : build Gradle/Cargo sans orchestrateur Python, APK Debug universal et AAB Release pour `arm64-v8a`, `armeabi-v7a`, `x86_64` et `x86`, `minSdk 21` réellement fumé et compatibilité pages mémoire 16 KB validée sur les ABI 64 bits. `0.3.4-alpha.4` a validé la robustesse de la baseline realtime : configuration WebSocket explicite, limites message/frame/write-buffer, deadlines connect/send/close et principaux cas négatifs. `0.3.4-alpha.5` ajoute le smoke runtime manquant sous la forme d'un launcher Cargo dédié qui exerce uniquement l'API publique sur un vrai socket loopback, échange dans les deux sens et valide le close handshake avant l'entrée en beta. 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. diff --git a/crates/apps/game-realtime-websocket-smoke/Cargo.toml b/crates/apps/game-realtime-websocket-smoke/Cargo.toml new file mode 100644 index 0000000..0782288 --- /dev/null +++ b/crates/apps/game-realtime-websocket-smoke/Cargo.toml @@ -0,0 +1,21 @@ +# file: crates/apps/game-realtime-websocket-smoke/Cargo.toml +# version: 1 + +[package] +name = "game-realtime-websocket-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-websocket-lib = { path = "../../common/game-realtime-websocket-lib" } +tokio = { workspace = true, features = ["macros", "rt", "time"] } +tracing.workspace = true + +[lints] +workspace = true diff --git a/crates/apps/game-realtime-websocket-smoke/src/main.rs b/crates/apps/game-realtime-websocket-smoke/src/main.rs new file mode 100644 index 0000000..238ba90 --- /dev/null +++ b/crates/apps/game-realtime-websocket-smoke/src/main.rs @@ -0,0 +1,108 @@ +// file: crates/apps/game-realtime-websocket-smoke/src/main.rs +// version: 1 + +#![warn(missing_docs)] +#![deny(unreachable_pub)] +#![forbid(unsafe_code)] + +//! Executable localhost smoke for the public games.sasedev WebSocket realtime transport 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-client-smoke"; +const SERVER_PAYLOAD: &[u8] = b"games.sasedev-server-smoke"; +const SMOKE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); +const TRACING_TARGET: &str = "games::realtime::websocket::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 WebSocket smoke tracing: {error}"); + return std::process::ExitCode::FAILURE; + }, + }; + tracing::info!(target: TRACING_TARGET, "realtime WebSocket 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 WebSocket smoke passed"); + println!("game-realtime-websocket-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 WebSocket smoke failed"); + eprintln!("game-realtime-websocket-smoke: FAIL: {error}"); + std::process::ExitCode::FAILURE + }, + std::result::Result::Err(_) => { + tracing::error!(target: TRACING_TARGET, timeout_ms = SMOKE_TIMEOUT.as_millis(), "realtime WebSocket smoke timed out"); + eprintln!("game-realtime-websocket-smoke: FAIL: smoke timed out"); + std::process::ExitCode::FAILURE + }, + }; +} + +async fn run_smoke() -> std::result::Result<(), String> { + let bind_address = std::net::SocketAddr::from(([127, 0, 0, 1], 0)); + let listener = match game_realtime_websocket_lib::WebSocketListener::bind(bind_address).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("listener bind failed: {error}")), + }; + let endpoint = format!("ws://{}/", listener.local_addr()); + tracing::info!(target: TRACING_TARGET, endpoint = endpoint.as_str(), "loopback endpoint bound"); + let (server_result, client_result) = tokio::join!(listener.accept(), game_realtime_websocket_lib::connect(endpoint.as_str())); + let server_connection = 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_connection = 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 (mut server_sender, mut server_receiver) = server_connection.split(); + let (mut client_sender, mut client_receiver) = client_connection.split(); + let client_message = game_realtime_transport_lib::TransportMessage::new(CLIENT_PAYLOAD.to_vec()); + if let std::result::Result::Err(error) = client_sender.send(client_message).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")); + } + let server_message = game_realtime_transport_lib::TransportMessage::new(SERVER_PAYLOAD.to_vec()); + if let std::result::Result::Err(error) = server_sender.send(server_message).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 close handshake")); + } + 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.4/alpha.5.md b/deltas/0.3.4/alpha.5.md new file mode 100644 index 0000000..b8446fc --- /dev/null +++ b/deltas/0.3.4/alpha.5.md @@ -0,0 +1,127 @@ + + + +# Delta 0.3.4-alpha.5 + +## Base + +Base directe : `0.3.4-alpha.4`, validée par l'utilisateur le 2026-09-21. + +La gate `alpha.4` est entièrement propre, mais la revue post-validation a identifié une omission de processus : les preuves runtime réseau restent sous le harness de test Cargo. Cette tranche ajoute le smoke réellement exécutable avant toute promotion en beta. + +## Historique du jalon précédent + +Ajout de : + +```text +history/0.3.4/alpha.4.md +``` + +L'historique enregistre les audits propres, check/Clippy propres, sept tests transport, onze tests WebSocket/robustesse et le graphe de dépendances sans TLS fournis par l'utilisateur. + +## Launcher de smoke runtime + +Ajout de la crate binaire : + +```text +crates/apps/game-realtime-websocket-smoke +``` + +Ce launcher n'introduit aucune logique session/gameplay et n'ajoute aucune dépendance tierce nouvelle. Il consomme uniquement : + +- `game-realtime-websocket-lib` ; +- `game-realtime-transport-lib` ; +- le logging commun ; +- Tokio pour posséder le runtime du processus de smoke. + +Le backend reste donc conforme à la décision d'ownership : `game-realtime-websocket-lib` ne crée toujours ni runtime ni thread privé. + +## Scénario fumé + +La commande : + +```bash +cargo run -p game-realtime-websocket-smoke +``` + +exécute hors harness `#[test]` le scénario suivant : + +1. initialise le tracing applicatif commun ; +2. bind un listener WebSocket sur `127.0.0.1:0` ; +3. construit l'endpoint `ws://` à partir du port réellement alloué ; +4. accepte un serveur et connecte un client via les API publiques ; +5. envoie un payload binaire client → serveur et vérifie son contenu ; +6. envoie un payload binaire serveur → client et vérifie son contenu ; +7. initie un close propre côté client ; +8. vérifie que le serveur observe `TransportReceive::Closed` ; +9. affiche `game-realtime-websocket-smoke: PASS` et sort avec le code zéro. + +Une deadline globale de cinq secondes empêche le smoke de rester bloqué en cas de régression. Toute erreur de bind/connect/send/receive/close, payload inattendu ou timeout produit un code de sortie non nul. + +Le smoke ne dépend ni d'Internet, ni d'un service externe, ni d'un port fixe. + +## Documentation locale + +`DOC-CRATE-*` est réévalué pour le nouveau launcher. Aucun `README.md` ou `USAGE.md` local n'est ajouté : le binaire n'a aucune configuration, aucun argument et un unique workflow opérateur (`cargo run -p game-realtime-websocket-smoke`), entièrement documenté par le plan actif et le présent delta. + +## Plan et version + +L'audit de distribution exige désormais aussi la présence du manifeste du launcher de smoke afin que cette preuve runtime ne puisse pas disparaître silencieusement d'une archive future. + +Le workspace passe à : + +```text +0.3.4-alpha.5 +``` + +Le plan `0.3.4` est corrigé pour distinguer explicitement les tests automatisés du smoke runtime et rendre ce dernier obligatoire avant l'entrée en beta. + +`CHANGELOG.md` et `ROADMAP.md` restent inchangés : aucun scope macro ou jalon RC/stable n'est modifié. + +## Fichiers + +Modifiés : + +```text +Cargo.toml +README.md +docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md +scripts/audit_distribution_layout.py +``` + +Ajoutés : + +```text +crates/apps/game-realtime-websocket-smoke/Cargo.toml +crates/apps/game-realtime-websocket-smoke/src/main.rs +history/0.3.4/alpha.4.md +deltas/0.3.4/alpha.5.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-websocket-lib --all-targets --all-features + +cargo run -p game-realtime-websocket-smoke +cargo tree -p game-realtime-websocket-smoke --edges normal +``` + +La validation attendue du smoke est explicitement : + +```text +game-realtime-websocket-smoke: PASS +``` + +avec code de sortie zéro. La compilation, les tests et le smoke restent exécutés côté utilisateur conformément à `CMD-BUILD-005`. diff --git a/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md b/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md index 3b4525a..055402e 100644 --- a/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md +++ b/docs/plans/004-V0_3_4_REALTIME_TRANSPORT_WEBSOCKET_PLAN.md @@ -1,5 +1,5 @@ - + # Plan 0.3.4 — transport realtime et baseline WebSocket @@ -7,7 +7,7 @@ Plan actif créé pendant `0.3.4-alpha.1` à partir de l'archive taggée `v0.3.3`. -Les tranches `alpha.1`, `alpha.2` et `alpha.3.fix.1` ont été validées le 2026-09-21. `alpha.3` avait été rejetée avant compilation à cause d'un héritage Cargo invalide de `default-features`; son fix a ensuite validé le backend WebSocket, le round-trip localhost et le graphe sans TLS. La tranche active `alpha.4` ferme maintenant les limites, timeouts, cas négatifs et lifecycle avant consolidation/beta. +Les tranches `alpha.1`, `alpha.2`, `alpha.3.fix.1` et `alpha.4` ont été validées le 2026-09-21. `alpha.3` avait été rejetée avant compilation à cause d'un héritage Cargo invalide de `default-features`; son fix a ensuite validé le backend WebSocket, le round-trip localhost et le graphe sans TLS. `alpha.4` a fermé les limites, timeouts, cas négatifs et lifecycle. La revue post-gate a toutefois identifié une preuve encore absente : un smoke runtime exécuté hors harness de test. La tranche active `alpha.5` ajoute donc ce smoke avant toute entrée en beta. ## Mission @@ -366,7 +366,7 @@ Tests d'intégration localhost déterministes avec `127.0.0.1:0` : Les tests ne dépendent ni d'Internet, ni d'un serveur externe, ni d'un port fixe. -Un petit demo/CLI n'est pas prévu par défaut : il sera créé uniquement si les tests publics ne prouvent pas suffisamment le chemin client/server. +Un petit launcher de smoke n'était pas prévu par défaut tant que les tests publics semblaient suffire. La revue après validation de `alpha.4` a distingué correctement test automatisé et smoke runtime : `alpha.5` ajoute donc `game-realtime-websocket-smoke` sous `crates/apps/`. Ce binaire reste un harness de validation, pas un produit ni un protocole applicatif. ## Gates par jalon @@ -426,6 +426,21 @@ cargo tree -p game-realtime-websocket-lib --edges normal Le test backend doit maintenant couvrir le loopback positif, les invariants de configuration, les mappings de capacité/backpressure et les cinq scénarios négatifs déterministes de `robustness.rs`. +### `alpha.5` + +Ajouter le smoke runtime public avant promotion de maturité. + +Gates ciblées : + +```bash +cargo test -p game-realtime-transport-lib --all-targets --all-features +cargo test -p game-realtime-websocket-lib --all-targets --all-features +cargo run -p game-realtime-websocket-smoke +cargo tree -p game-realtime-websocket-smoke --edges normal +``` + +Le smoke doit binder `127.0.0.1:0`, connecter un client via l'API publique, échanger un payload binaire dans chaque sens, effectuer un close propre, afficher `game-realtime-websocket-smoke: PASS` et sortir avec le code zéro. Il ne dépend ni d'Internet, ni d'un port fixe, ni d'un harness `#[test]`. + ### Beta La beta est le jalon large retenu pour : @@ -482,7 +497,19 @@ Aucune dépendance réseau n'est ajoutée. - lifecycle sans runtime/task backend privé ; - documentation API/backend et audit du graphe. -Si les gates de cette tranche sont propres, le demo/CLI conditionnel n'apporte plus de preuve supplémentaire : la suite doit privilégier une courte consolidation alpha uniquement si la revue documentaire/API révèle une dette réelle, sinon passer directement à la beta large. +La gate `alpha.4` est propre, mais elle ne constitue pas à elle seule un smoke runtime : tous ses scénarios restent exécutés sous le harness de test Cargo. + +### `0.3.4-alpha.5` — smoke runtime end-to-end + +- créer `game-realtime-websocket-smoke` comme launcher minimal sous `crates/apps/` ; +- utiliser uniquement les API publiques `game-realtime-websocket-lib` et `game-realtime-transport-lib` ; +- binder un listener loopback sur port éphémère et connecter un client réel ; +- vérifier client → serveur puis serveur → client ; +- initier et observer un close propre ; +- borner le smoke global et retourner un code de sortie non nul au moindre défaut ; +- ne créer ni protocole session/gameplay, ni serveur externe, ni port fixe. + +`DOC-CRATE-*` est réévalué pour ce launcher : le workflow se résume à une commande Cargo unique et le plan central décrit entièrement sa responsabilité, donc aucun `README.md`/`USAGE.md` local n'est ajouté. ### `0.3.4-beta.1` — validation large @@ -548,4 +575,5 @@ Un split vers une autre version est requis si l'une des conditions suivantes app - les targets tracing transport/WebSocket sont distinctes des futurs domaines session/sync/simulation ; - aucune crate gameplay ou engine ne dépend de `tokio-tungstenite` ; - les audits et tests ciblés sont propres ; +- le smoke `cargo run -p game-realtime-websocket-smoke` passe réellement côté utilisateur hors harness de test ; - la documentation décrit ce qui est réellement implémenté et ce qui reste reporté. diff --git a/history/0.3.4/alpha.4.md b/history/0.3.4/alpha.4.md new file mode 100644 index 0000000..6d0e010 --- /dev/null +++ b/history/0.3.4/alpha.4.md @@ -0,0 +1,62 @@ + + + +# Historique 0.3.4-alpha.4 + +## Statut + +`0.3.4-alpha.4` a été validée par l'utilisateur le 2026-09-21. Les limites, deadlines et cas négatifs du backend WebSocket compilent dans le workspace, passent Clippy strict et tous les tests ciblés sont propres. + +Aucun `alpha.4.fix.N` n'est requis. La revue suivant cette gate a néanmoins identifié qu'aucun smoke runtime distinct du harness de test n'avait encore été exécuté. La suite ouvre donc `0.3.4-alpha.5` pour cette preuve avant la beta. + +## Gates statiques et Rust + +La gate fournie est propre : + +```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), 267 file(s)) +Distribution layout audit: clean (49 required path(s), 8 forbidden path(s) absent) +cargo fmt --all: clean +cargo fmt --all -- --check: clean +cargo check --workspace: clean +cargo clippy --workspace --all-targets --all-features -- -D warnings: clean +``` + +Le workspace compilé porte `0.3.4-alpha.4`, y compris `game-realtime-transport-lib` et `game-realtime-websocket-lib`. + +## Tests transport et WebSocket + +Le contrat transport-neutral reste propre : + +```text +cargo test -p game-realtime-transport-lib --all-targets --all-features +7 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +Le backend WebSocket valide ensuite : + +```text +unit tests: 5 passed; 0 failed +loopback: 1 passed; 0 failed +robustness: 5 passed; 0 failed +``` + +Les onze tests WebSocket couvrent notamment les defaults et invariants de configuration, les mappings capacité/backpressure, le round-trip binaire et close propre, les tailles hors limite, Text interdit, peer drop abrupt et timeout de handshake silencieux. + +## Graphe de dépendances + +`cargo tree -p game-realtime-websocket-lib --edges normal` confirme toujours la frontière attendue : + +- `game-realtime-websocket-lib` dépend directement de `game-realtime-transport-lib`, `futures-util 0.3.34`, `tokio 1.53.1`, `tokio-tungstenite 0.30.0` et `tracing 0.1.44` ; +- `tokio-tungstenite 0.30.0` apporte `tungstenite 0.30.0` ; +- aucune pile TLS `native-tls`/`rustls` n'apparaît dans le graphe normal ; +- aucune crate gameplay ou moteur ne dépend du backend WebSocket. + +## Limite de la preuve + +Cette gate est suffisante pour valider techniquement `alpha.4`, mais elle reste composée d'audits, compilation, lint et tests Cargo. Même le round-trip localhost est exécuté sous `#[test]`. + +La validation runtime opérateur manque encore. `alpha.5` doit donc fournir un exécutable de smoke séparé qui utilise uniquement l'API publique sur un vrai socket loopback et dont le code de sortie atteste le résultat. diff --git a/scripts/audit_distribution_layout.py b/scripts/audit_distribution_layout.py index 43ff350..e3afc68 100644 --- a/scripts/audit_distribution_layout.py +++ b/scripts/audit_distribution_layout.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # file: scripts/audit_distribution_layout.py -# version: 17 +# version: 18 """Audit the static distribution layout expected by supported POCs.""" @@ -24,6 +24,7 @@ FORBIDDEN_PATHS = ( REQUIRED_PATHS = ( + "crates/apps/game-realtime-websocket-smoke/Cargo.toml", "crates/apps/game-reflex-poc-desktop/Cargo.toml", "crates/apps/game-snake-poc-desktop/Cargo.toml", "crates/apps/game-reflex-poc-tauri/Cargo.toml",