open-standards/eoc/eoc-rs/crates/eoc-route-learned/tests/learned_cascade.rs
Transaction Science 01d4ad9c7b Ingest batch 2: 9 new crates across openpay / smart-byte / eoc
openpay/ (+3 crates, ~8400 LOC, 162 tests)
  op-iso8583/         ISO 8583 messages for direct acquirer / card-network
                      connectivity. MTIs, primary+secondary bitmaps, full
                      DE catalogue, BCD/ASCII/EBCDIC/LLVAR/LLLVAR codecs,
                      five dialects (Visa Base I, Mastercard MDS, Amex GNS,
                      Discover, JCB), network-management messages
                      (sign-on/echo/cutover), DE 55 EMV TLV.
  op-mobile-wallets/  Apple Pay (ECDH-P256 + AES-256-GCM), Google Pay ECv2
                      (ECDH + HKDF + AES-256-CTR + HMAC), Samsung Pay. PAN
                      never leaves the decryption boundary as a String;
                      lands in VaultRef. No-PAN-leakage assertions per
                      wallet.
  op-routing/         Least-cost routing + MCC-aware routing + intelligent
                      retry / soft-decline recovery. 231-MCC catalogue.
                      Real ISO 8583 DE-39 soft/hard decline taxonomies.

smart-byte/smart-byte-rs/ (+3 crates, ~7100 LOC, 96 tests)
  smart-byte-vc/      W3C Verifiable Credentials 2.0: VCDM, presentations,
                      Data Integrity proofs (eddsa-jcs-2022), VC-JWT,
                      SD-JWT (selective disclosure), Bitstring Status
                      List 2021. Cargo-bridge maps VCs to Smart Byte
                      envelopes with SAID stability.
  smart-byte-did/     W3C DID resolution: did:key (Ed25519/P-256/secp256k1),
                      did:web (HTTPS), did:peer (numalgo 2+4), did:jwk,
                      did:ion (feature-gated stub). DID URL dereferencer.
  smart-byte-pq/      Post-quantum signatures: ML-DSA-44/65/87 (FIPS 204),
                      SLH-DSA across 12 parameter sets (FIPS 205), FN-DSA
                      (draft FIPS 206, feature-gated). Hybrid Ed25519 +
                      ML-DSA pattern for the transition. Secret-key
                      zeroize-on-drop.

eoc/eoc-rs/ (+3 crates, ~6450 LOC, 76 tests)
  eoc-vendor-api/     NeuralBackend implementations: Anthropic, OpenAI,
                      Google, Mistral, Cohere, Groq, Together, Fireworks.
                      SSE streaming. Prompt-caching where supported.
                      Joule estimates per published energy coefficient
                      (12-model curated table).
  eoc-embeddings/     21 embedding models behind one Embedder trait:
                      OpenAI v3 (3), Cohere v3 (3), Voyage (5), Mistral,
                      Jina, plus local ONNX (BGE x3, nomic, mxbai, GTE,
                      E5) behind 'local' feature. Matryoshka-style
                      dimension projection. Per-content-hash cache.
  eoc-route-learned/  RouteLLM-style matrix-factorisation router +
                      multinomial-logreg classifier + LinUCB / Thompson
                      bandits for online learning. LearnedCascade wraps
                      the rule-based cascade with a confidence-threshold
                      skip policy. Synthetic-eval accuracy: MF 0.935,
                      LogReg 0.980.

Quality:
- cargo check across all three workspaces: clean
- cargo clippy --all-targets -- -D warnings on every new crate: clean
- 334 new tests total; all passing
- #![forbid(unsafe_code)] across every new crate
- No unwrap/expect outside #[cfg(test)]

Rust 1.95.0 edition 2024 (inherited via root rust-toolchain.toml).
2026-05-23 18:12:24 -04:00

114 lines
4.2 KiB
Rust

//! End-to-end LearnedCascade test.
//!
//! Build a real four-stage cascade, route a "novel" query through it twice
//! and compare joule cost between:
//!
//! - the baseline `Cascade` (which walks cache → kv → graph → neural), and
//! - a `LearnedCascade` whose router picks `Neural` with confidence 1.0.
//!
//! Both must produce the same answer; the learned variant must be cheaper
//! (or at worst equal) because it skips three doomed cache/kv/graph misses.
use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use eoc_cache::LruCache;
use eoc_cascade::Cascade;
use eoc_core::{Query, Stage};
use eoc_graph::GraphStage;
use eoc_kv::{KvStage, MemoryKvBackend};
use eoc_neural::{EchoBackend, NeuralStage};
use eoc_route_learned::inference::LearnedCascade;
use eoc_route_learned::router::{LearnedRouter, RouterState, StagePrediction};
use eoc_route_learned::threshold::ThresholdPolicy;
/// A trivial router that always picks `Neural` with full confidence.
struct AlwaysNeural;
#[async_trait]
impl LearnedRouter for AlwaysNeural {
async fn route(&self, _query: &Query) -> StagePrediction {
StagePrediction::new(Stage::Neural, 1.0)
}
async fn observe(&mut self, _q: &Query, _s: Stage, _ok: bool, _j: u64) {}
fn export_state(&self) -> RouterState {
RouterState {
algorithm: "always_neural".into(),
weights_blob: vec![],
metadata: Default::default(),
}
}
fn import_state(_state: RouterState) -> eoc_route_learned::Result<Self> {
Ok(AlwaysNeural)
}
}
fn fixture() -> Cascade {
let cache = Arc::new(LruCache::new(8));
let kv_backend = Box::new(MemoryKvBackend::new());
let kv = Arc::new(KvStage::new(kv_backend));
let graph = Arc::new(GraphStage::new());
let neural = Arc::new(NeuralStage::new(Box::new(
EchoBackend::new().with_cost(50_000),
)));
Cascade::new(cache, kv, graph, neural)
}
#[tokio::test]
async fn learned_cascade_routes_to_neural_when_confident() {
let cascade = Arc::new(fixture());
let learned = LearnedCascade::new(cascade, AlwaysNeural, ThresholdPolicy::new(0.9));
let resp = learned.resolve(Query::new("novel query")).await;
assert_eq!(resp.stage, Stage::Neural);
assert!(resp.payload.contains("novel query"));
}
#[tokio::test]
async fn learned_cascade_skip_saves_joules_vs_baseline() {
// Baseline path: full cascade, neural answers — pays neural cost only.
// Both should answer with the neural stage in this fixture, so this is
// mostly a smoke test that skip-mode doesn't *increase* cost.
let baseline = fixture();
let baseline_response = baseline.resolve(Query::new("brand new prompt")).await;
let cascade = Arc::new(fixture());
let learned = LearnedCascade::new(cascade, AlwaysNeural, ThresholdPolicy::new(0.9));
let learned_response = learned.resolve(Query::new("brand new prompt")).await;
assert_eq!(learned_response.stage, Stage::Neural);
// The learned cascade can't be more expensive than the baseline.
assert!(
learned_response.joule_cost.microjoules <= baseline_response.joule_cost.microjoules + 1
);
}
#[tokio::test]
async fn learned_cascade_falls_through_to_full_cascade_when_unconfident() {
/// Low-confidence router → policy says FullCascade.
struct LowConf;
#[async_trait]
impl LearnedRouter for LowConf {
async fn route(&self, _q: &Query) -> StagePrediction {
StagePrediction::new(Stage::Neural, 0.1)
}
async fn observe(&mut self, _q: &Query, _s: Stage, _ok: bool, _j: u64) {}
fn export_state(&self) -> RouterState {
RouterState {
algorithm: "low".into(),
weights_blob: vec![],
metadata: Default::default(),
}
}
fn import_state(_s: RouterState) -> eoc_route_learned::Result<Self> {
Ok(LowConf)
}
}
let cascade = Arc::new(fixture());
let learned = LearnedCascade::new(cascade, LowConf, ThresholdPolicy::new(0.9));
let resp = learned.resolve(Query::new("anything")).await;
// Baseline cascade resolves novel queries with the neural stage.
assert_eq!(resp.stage, Stage::Neural);
let _ = HashMap::<u32, u32>::new(); // suppress unused-import warnings
}