diff --git a/Cargo.toml b/Cargo.toml index 1fd1302..8d744aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 228 +# version: 229 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.8-pre.7.fix.1" +version = "0.2.8-pre.7.fix.2" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs b/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs index 104e6e3..b1f15bf 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs -// version: 9 +// version: 10 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import @@ -189,6 +189,18 @@ async fn yield_runtime_steps() { return; } +async fn wait_for_observed_ping(ping_rx: &mut tokio::sync::mpsc::UnboundedReceiver<()>) { + for _ in 0..256 { + match ping_rx.try_recv() { + std::result::Result::Ok(()) => return, + std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty) => tokio::task::yield_now().await, + std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => panic!("heartbeat observation channel disconnected before Ping"), + } + } + assert!(matches!(ping_rx.try_recv(), std::result::Result::Ok(())), "heartbeat Ping was not observed after bounded scheduler progress"); + return; +} + #[tokio::test(flavor = "current_thread")] async fn websocket_session_connects_and_round_trips_internal_json_rpc() { let (listener, url) = bind_local_listener().await; @@ -457,14 +469,12 @@ async fn helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms() { yield_runtime_steps().await; assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty))); tokio::time::advance(std::time::Duration::from_secs(1)).await; - yield_runtime_steps().await; - assert!(matches!(ping_rx.try_recv(), std::result::Result::Ok(()))); + wait_for_observed_ping(&mut ping_rx).await; tokio::time::advance(std::time::Duration::from_secs(59)).await; yield_runtime_steps().await; assert!(matches!(ping_rx.try_recv(), std::result::Result::Err(tokio::sync::mpsc::error::TryRecvError::Empty))); tokio::time::advance(std::time::Duration::from_secs(1)).await; - yield_runtime_steps().await; - assert!(matches!(ping_rx.try_recv(), std::result::Result::Ok(()))); + wait_for_observed_ping(&mut ping_rx).await; session.close().await.expect("Helius heartbeat fixture session must close"); tokio::time::resume(); server.await.expect("Helius heartbeat fixture server must complete"); diff --git a/deltas/0.2.8/pre.007-fix.002.md b/deltas/0.2.8/pre.007-fix.002.md new file mode 100644 index 0000000..46eea4c --- /dev/null +++ b/deltas/0.2.8/pre.007-fix.002.md @@ -0,0 +1,132 @@ + + + +# Delta `0.2.8-pre.007-fix.002` — observation scheduler bornée du heartbeat + +## 1. Base et défaut restant + +Base appliquée : + +```text +0.2.8-pre.7.fix.1 +``` + +Le replay opérateur du `fix.001` ferme le lint Clippy et le canari close : `fmt`, audit Rust, `cargo check` et `cargo clippy` sont verts. Transport atteint **330/331 unit** ; un seul canari reste en échec : + +```text +helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms +``` + +Le reste des preuves heartbeat passe, notamment Helius-only, intervalle 60 s, absence standard, write failure/reconnect, close avant deadline et réarmement après reconnexion. Aucun défaut runtime supplémentaire n'est démontré. + +## 2. Version technique + +```text +workspace.package.version = 0.2.8-pre.7.fix.2 +commit attendu = v0.2.8-pre.007-fix.002 +Git tag = aucun tag prerelease +``` + +Le header root `Cargo.toml` passe en version `229`. + +## 3. Cause exacte + +Après : + +```rust +tokio::time::advance(std::time::Duration::from_secs(1)).await; +``` + +le deadline heartbeat est bien expiré, mais l'observation du Ping nécessite encore plusieurs étapes asynchrones : + +```text +actor WsSession + -> websocket.send(Ping) + -> socket local + -> serveur fixture websocket.next() + -> ping_tx.send(()) + -> ping_rx.try_recv() +``` + +Un `try_recv()` après un nombre fixe faible de `yield_now()` reste une course scheduler. Le défaut est donc dans le protocole d'observation du test, pas dans la cadence runtime. + +## 4. Correction déterministe + +Le fichier de tests ajoute un helper privé local : + +```rust +async fn wait_for_observed_ping(...) +``` + +Il effectue au maximum 256 tours : + +```text +try_recv() == Ok(()) -> succès immédiat +try_recv() == Empty -> yield_now().await puis nouvelle tentative +try_recv() == Disconnected -> échec immédiat +``` + +Après épuisement de la borne, le test échoue explicitement si aucun Ping n'est observé. + +Ce helper n'appelle volontairement : + +```text +ni tokio::time::advance() +ni tokio::time::sleep() +ni tokio::time::timeout() +``` + +L'horloge virtuelle reste donc exactement à `t=60 s` ou `t=120 s` pendant l'attente de propagation. Le canari ne peut pas réussir en observant accidentellement un heartbeat ultérieur. + +## 5. Cadence vérifiée inchangée + +Le scénario reste : + +```text +t=59 s aucun Ping +t=60 s premier Ping observé après propagation scheduler bornée +t=119 s aucun second Ping +t=120 s second Ping observé après propagation scheduler bornée +``` + +Aucune cadence de test spéciale n'est introduite. + +## 6. Runtime explicitement inchangé + +Ce fix ne modifie pas : + +```text +crates/ksp-onchain-transport-lib/src/ws_session.rs +HELIUS_WS_HEARTBEAT_INTERVAL = 60 s +WebSocket Ping control frame +WsProtocolKind +WsSessionSettings +Config / schema / env +transactionSubscribe / transactionNotification lifecycle +reconnect / remap / backpressure +``` + +Il n'ajoute aucune dépendance, feature ou visibilité. + +## 7. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs +docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +deltas/0.2.8/pre.007-fix.002.md +``` + +## 8. Gate opérateur + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +cargo test --workspace +``` + +Critère de fermeture : zéro warning, audit clean, **331 unit / 40 public API / 29 completeness / 4 doctests** et workspace vert. `pre.008` reste bloqué jusque-là. diff --git a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md index d1efb21..962919f 100644 --- a/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md +++ b/docs/plans/015-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET_PLAN.md @@ -1,9 +1,9 @@ - + # Plan `0.2.8` — Helius LaserStream WebSocket -> **Statut : `0.2.8-pre.007` est fonctionnellement implémenté mais son gate opérateur révèle un défaut de canaris Tokio pausés et un lint `implicit_return`. `0.2.8-pre.007-fix.001` corrige uniquement le protocole de test ; le runtime heartbeat Helius 60 s reste inchangé.** +> **Statut : `0.2.8-pre.007-fix.001` corrige le lint et le canari close, mais le replay laisse un seul canari périodique en course scheduler. `0.2.8-pre.007-fix.002` borne l'observation du Ping sans avancer l'horloge virtuelle ; le runtime heartbeat Helius 60 s reste inchangé.** ## 1. Objet, base et état courant @@ -35,10 +35,11 @@ pre.005-fix.001 correction types de canaris + visibilité/tests/règles val pre.005-fix.002 suppression warnings dead_code via cfg(test) validée pre.006 transactionNotification + lifecycle actor validé pre.007 heartbeat Helius WebSocket / idle — checkpoint gate à corriger -pre.007-fix.001 déterminisme canaris Tokio + lint test — préparé +pre.007-fix.001 lint + close canary corrigés ; un canari périodique reste non déterministe +pre.007-fix.002 observation bornée du Ping après deadline — préparé -workspace.package.version courant = 0.2.8-pre.7.fix.1 -commit attendu = v0.2.8-pre.007-fix.001 +workspace.package.version courant = 0.2.8-pre.7.fix.2 +commit attendu = v0.2.8-pre.007-fix.002 aucun tag prerelease ``` @@ -69,8 +70,8 @@ pre.005 DONE — transactionSubscribe request typed + filters/options/tokenAcco pre.006 DONE — transactionNotification + handle live + actor registry/reconnect/resubscribe/unsubscribe races + late notifications + backpressure ciblée, sans second actor/socket pre.007 CHECKPOINT — heartbeat Helius WebSocket/idle + timers + interaction reconnect/control frames/shutdown - fix.001 PREPARED — actor armé avant avance de l'horloge Tokio + close accepte canal d'observation fermé - + `return;` explicite du helper de yield pour `clippy::implicit_return` + fix.001 CHECKPOINT — lint et close canary corrigés ; 330/331 unit, seul le canari périodique reste en course scheduler + fix.002 PREPARED — observation positive du Ping bornée en scheduler steps, sans nouvelle avance du temps virtuel pre.008 provider adversarial lifecycle + capability guards + payload/backpressure + security/redaction pre.009 compliance Helius WebSocket + non-régressions Solana standard 18/18 + HTTP 52/14 + Config/API/dependency-firewall canaries @@ -1181,3 +1182,51 @@ helius_explicit_close_cancels_heartbeat_before_deadline La constante 60 s, la frame Ping, le branch actor, le reconnect, Config et `WsSessionSettings` restent byte-for-byte hors du fix. `pre.008` reste bloqué jusqu'au replay vert de `pre.007-fix.001`. +## 17. Replay `pre.007-fix.001` et préparation `pre.007-fix.002` + +Checkpoint opérateur `0.2.8-pre.7.fix.1` reçu le 2026-08-23 : + +```text +[x] cargo fmt --all +[x] python3 scripts/audit_rust_workspace_rules.py = clean / 0 candidate +[x] cargo check --workspace = vert, sans warning +[x] cargo clippy --workspace --all-targets = vert, sans warning +[!] cargo test -p ksp-onchain-transport-lib = 330/331 unit ; 1 seul échec heartbeat périodique +[ ] cargo test --workspace — bloqué tant que Transport n'est pas vert +``` + +Le `fix.001` a donc bien corrigé : + +```text +[x] `clippy::implicit_return` dans yield_runtime_steps +[x] close avant deadline : aucun Ping puis Empty/Disconnected acceptés après fermeture +[x] policy Helius-only + 60 s +[x] absence de heartbeat standard +[x] write failure -> reconnect +[x] réarmement après reconnect +``` + +Échec restant : + +```text +helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms + le timer est expiré à t=60 s, mais l'observation traverse encore plusieurs tâches : + actor -> écriture socket -> lecture serveur fixture -> ping_tx -> ping_rx. + + Un `try_recv()` après un nombre fixe faible de yields reste donc une course scheduler. +``` + +`fix.002` reste test-only et ne modifie pas `src/ws_session.rs`. Il ajoute un helper d'observation bornée qui : + +```text +- tente `try_recv()` ; +- si Empty, cède explicitement au scheduler ; +- répète au maximum 256 tours ; +- échoue immédiatement si le canal est Disconnected ; +- n'appelle ni `advance()`, ni `sleep()`, ni `timeout()` pendant cette attente. +``` + +Ainsi, le test peut laisser la propagation async finir **sans faire avancer l'horloge Tokio au-delà de t=60 s / t=120 s**. Le canari continue donc de vérifier la vraie cadence nominale, et ne peut pas réussir grâce à un heartbeat ultérieur. + +Le runtime reste byte-for-byte hors du fix : cadence 60 s, Ping control frame, actor unique, reconnect, shutdown, Config et lifecycle transaction inchangés. `pre.008` reste bloqué jusqu'au replay entièrement vert de `pre.007-fix.002`. + diff --git a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md index 9c80b3e..5cc8aa6 100644 --- a/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md +++ b/docs/validation/011-V0_2_8_HELIUS_LASERSTREAM_WEBSOCKET.md @@ -1,9 +1,9 @@ - + # Validation `0.2.8` — Helius LaserStream WebSocket -> **Statut : `pre.007` est implémenté mais son gate est bloqué par un lint de helper test et deux canaris Tokio pausés. `pre.007-fix.001` est préparé comme correction test-only, sans changement runtime heartbeat.** +> **Statut : `pre.007-fix.001` passe fmt/audit/check/Clippy et corrige le close canary ; un seul canari périodique reste non déterministe. `pre.007-fix.002` est préparé comme correction test-only d'observation scheduler, sans changement runtime heartbeat.** ## 1. Références @@ -26,6 +26,7 @@ pre.005 dead-code fix deltas/0.2.8/pre.005-fix.002.md pre.006 actor transaction deltas/0.2.8/pre.006.md pre.007 heartbeat Helius deltas/0.2.8/pre.007.md pre.007 test determinism fix deltas/0.2.8/pre.007-fix.001.md +pre.007 scheduler observation fix deltas/0.2.8/pre.007-fix.002.md validation standard WS docs/validation/010-V0_2_7_ONCHAIN_WEBSOCKET.md HTTP compliance docs/validation/007-V0_2_4_HTTP_FINAL_COMPLIANCE.md KSP-TRANSPORT-007 docs/validation/005-V0_2_3_KSP_TRANSPORT_007_RETRO_AUDIT.md @@ -344,6 +345,7 @@ Constats `pre.002-fix.001` : Aucun split en fichiers supplémentaires n'est retenu : les plans historiques `0.2.5`–`0.2.7` sont de taille comparable ou supérieure, et le contenu du plan `015` reste entièrement centré sur une seule release. Le problème identifié était **l'ordre interne et la duplication de responsabilités**, pas la nécessité d'un nouveau type de document. + ## 12. Gates opérateur `pre.004` / `fix.001` / `fix.002` Premier passage `pre.004` reçu le 2026-08-23 : @@ -679,3 +681,55 @@ Critères `pre.007-fix.001` : Verdict courant : **`pre.007` FIX REQUIRED ; `pre.007-fix.001` PREPARED ; `pre.008` bloqué.** +## 19. Replay `pre.007-fix.001` et gate `pre.007-fix.002` + +Résultat opérateur `0.2.8-pre.7.fix.1` : + +```text +[x] cargo fmt --all +[x] audit Rust workspace = clean / 0 candidate +[x] cargo check --workspace = vert, sans warning +[x] cargo clippy --workspace --all-targets = vert, sans warning +[!] Transport unit = 330 passed / 1 failed sur 331 +``` + +Canaris heartbeat verts : + +```text +[x] policy provider-owned Helius + 60 s +[x] close avant deadline annule le heartbeat +[x] SolanaStandard n'émet aucun heartbeat provider +[x] write failure du Ping suit le chemin reconnect existant +[x] reconnect réussi réarme depuis la connexion de remplacement +``` + +Seul échec : + +```text +[!] helius_heartbeat_sends_ping_at_sixty_seconds_and_rearms + `try_recv()` peut encore précéder la propagation actor/socket/fixture/channel après `advance(1 s)`. +``` + +Critères `pre.007-fix.002` : + +```text +[ ] aucune modification de `src/ws_session.rs` +[ ] cadence Helius exacte 60 s inchangée +[ ] aucune API/Config/dependency supplémentaire +[ ] attente positive bornée par scheduler steps uniquement +[ ] aucune nouvelle `advance()`, `sleep()` ou `timeout()` pour attendre le Ping +[ ] t=59 s reste strictement sans Ping +[ ] Ping observé à t=60 s après propagation scheduler bornée +[ ] t=119 s reste strictement sans second Ping +[ ] second Ping observé à t=120 s après propagation scheduler bornée +[ ] canal Disconnected avant Ping = échec explicite +[ ] cargo fmt --all +[ ] audit Rust = clean +[ ] cargo check = sans warning +[ ] cargo clippy = sans warning +[ ] Transport = 331 unit + 40 public API + 29 completeness + 4 doctests +[ ] cargo test --workspace = vert +``` + +Verdict courant : **`pre.007` FIX REQUIRED ; `pre.007-fix.001` CHECKPOINT ; `pre.007-fix.002` PREPARED ; `pre.008` bloqué.** +