Files
telemt/src/web/manager/carrier_learning/tests.rs
T
2026-08-27 21:42:14 +03:00

272 lines
7.7 KiB
Rust

use super::*;
use crate::web::manager::{CarrierCapabilities, CarrierRequest};
fn request(hash: u8) -> CarrierRequest {
CarrierRequest::automatic(
CarrierClientClass::Bridge,
CarrierCapabilities::all(),
1,
None,
[hash; 32],
)
}
fn context(hash: u8) -> CarrierLearningContext {
CarrierLearningContext {
profile_key: [1; 32],
client_ip: IpAddr::V4(std::net::Ipv4Addr::new(192, 0, 2, hash)),
class: CarrierClientClass::Bridge,
user_agent_hash: [hash; 32],
epoch: 1,
ip_learning_eligible: true,
}
}
#[test]
fn policy_epoch_rejects_late_outcomes_and_clears_state() {
let now = Instant::now();
let mut learning = CarrierLearning::new(6);
let epoch = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
learning.record_chain(now, epoch, context(1), &[], WebCarrier::Websocket);
assert_eq!(learning.entries.len(), 3);
let next = learning
.apply_policy(
now,
false,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
assert_ne!(epoch, next);
learning.record_chain(now, epoch, context(1), &[], WebCarrier::Https);
assert!(learning.entries.is_empty());
}
#[test]
fn aggressive_policy_ranks_one_atomic_chain_sample() {
let now = Instant::now();
let mut learning = CarrierLearning::new(6);
let epoch = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
learning.record_chain(now, epoch, context(2), &[], WebCarrier::Websocket);
let (ranked, scores) = learning.rank(
now,
&[
WebCarrier::Https,
WebCarrier::Websocket,
WebCarrier::HttpsLanes,
],
request(2),
[1; 32],
context(2).client_ip,
true,
);
assert_eq!(
ranked,
[
WebCarrier::Websocket,
WebCarrier::Https,
WebCarrier::HttpsLanes,
]
);
assert_eq!(scores[WebCarrier::Websocket.index()], 33);
}
#[test]
fn two_half_windows_expire_without_sliding_updates() {
let start = Instant::now();
let mut learning = CarrierLearning::new(6);
let epoch = learning
.apply_policy(
start,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
learning.record_chain(start, epoch, context(3), &[], WebCarrier::Websocket);
learning.record_chain(
start + Duration::from_secs(6),
epoch,
context(3),
&[],
WebCarrier::Websocket,
);
learning.prune(start + Duration::from_secs(11));
assert_eq!(learning.entries.len(), 3);
learning.prune(start + Duration::from_secs(16));
assert!(learning.entries.is_empty());
}
#[test]
fn exhausted_epoch_and_insertion_identifiers_fail_closed() {
let now = Instant::now();
let mut learning = CarrierLearning::new(3);
learning.epoch = Some(u64::MAX);
assert_eq!(
learning.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
),
None
);
learning.record_chain(now, u64::MAX, context(4), &[], WebCarrier::Https);
assert!(learning.entries.is_empty());
learning.epoch = Some(1);
learning.insertion_sequence = u64::MAX;
learning.record_chain(now, 1, context(4), &[], WebCarrier::Https);
assert!(learning.entries.is_empty());
}
#[test]
fn client_reported_failures_do_not_create_negative_evidence() {
let now = Instant::now();
let mut learning = CarrierLearning::new(3);
let epoch = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
learning.record_chain(
now,
epoch,
context(5),
&[WebCarrier::Websocket],
WebCarrier::Https,
);
let (_, scores) = learning.rank(
now,
&[WebCarrier::Websocket, WebCarrier::HttpsLanes],
request(5),
[1; 32],
context(5).client_ip,
true,
);
assert_eq!(scores[WebCarrier::Websocket.index()], 0);
}
#[test]
fn old_new_old_policy_rejects_both_stale_epochs() {
let now = Instant::now();
let mut learning = CarrierLearning::new(3);
let old = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
let middle = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Balanced,
Duration::from_secs(10),
)
.unwrap();
let current = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
assert_ne!(old, middle);
assert_ne!(middle, current);
assert_ne!(old, current);
learning.record_chain(now, old, context(6), &[], WebCarrier::Https);
learning.record_chain(now, middle, context(6), &[], WebCarrier::Https);
assert!(learning.entries.is_empty());
learning.record_chain(now, current, context(6), &[], WebCarrier::Https);
assert_eq!(learning.entries.len(), 3);
}
#[test]
fn fifo_metadata_stays_within_the_entry_capacity() {
let now = Instant::now();
let mut learning = CarrierLearning::new(3);
let epoch = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
for hash in 1..=32 {
learning.record_chain(now, epoch, context(hash), &[], WebCarrier::Https);
assert!(learning.entries.len() <= 3);
assert!(learning.insertion_order.len() <= 3);
}
}
#[tokio::test]
async fn explicit_reset_preserves_policy_and_rejects_old_epoch_outcomes() {
let generation = crate::maestro::generation::test_runtime_generation(
1,
crate::config::ProxyConfig::default(),
);
let runtime = crate::web::manager::WebProcessRuntime::start(std::sync::Arc::new(
arc_swap::ArcSwap::from(generation.clone()),
));
let now = Instant::now();
let old_epoch = {
let mut learning = runtime.learning.lock();
let epoch = learning
.apply_policy(
now,
true,
WebCarrierNegotiationAggressiveness::Aggressive,
Duration::from_secs(10),
)
.unwrap();
learning.record_chain(now, epoch, context(7), &[], WebCarrier::Websocket);
epoch
};
let outcome = runtime.reset_carrier_learning().unwrap();
{
let mut learning = runtime.learning.lock();
learning.record_chain(
Instant::now(),
old_epoch,
context(7),
&[],
WebCarrier::Https,
);
let status = learning.status(Instant::now());
assert!(status.enabled);
assert_eq!(
status.aggressiveness,
WebCarrierNegotiationAggressiveness::Aggressive
);
assert_eq!(status.entries, 0);
assert_eq!(status.epoch, Some(outcome.epoch));
}
runtime.shutdown().await;
generation.stop_sessions().await;
generation.stop_background_tasks().await;
}