use chrono::Utc;
use serde_json::json;
use tempfile::TempDir;
use uuid::Uuid;
use crate::error::CorpFinanceError;
use crate::multi_agent::goap_adapter::{MCP_TOOL_NAMES, SLASH_COMMAND_NAMES};
use crate::self_learning::drift::{block_deploy_on_drift, detect_drift};
use crate::self_learning::golden_set::{freeze_golden_set, restore_golden_set};
use crate::self_learning::replay::{digest_output, run_replay};
use crate::self_learning::signing::{
ensure_keypair, generate_keypair, sign_manifest, verify_manifest,
};
use crate::self_learning::sona::{cluster_trajectories, CLUSTER_TRAINING_FLOOR};
use crate::self_learning::trajectory::{
capture_trajectory_step, complete_trajectory, lock_test_store, reset_store_for_tests,
retrieve_similar, MAX_TRAJECTORY_STEPS,
};
use crate::self_learning::types::{
DeltaKind, DriftVerdict, EvalGrade, GoldenInput, SignedManifest, SurfaceEventKind,
SurfaceEventRef, Trajectory,
};
use crate::self_learning::TrajectoryFilter;
use crate::surface::Surface;
fn step(name: &str) -> SurfaceEventRef {
SurfaceEventRef {
kind: SurfaceEventKind::McpTool,
name: name.into(),
input_hash: "h".into(),
output_hash: Some("o".into()),
duration_ms: 1,
}
}
fn traj(id_offset: u8, surface: Surface, grade: EvalGrade, tenant: Option<&str>) -> Trajectory {
Trajectory {
trajectory_id: Uuid::now_v7(),
surface,
surface_event_id: format!("ev{id_offset}"),
steps: vec![step("a"), step("b")],
eval_grade: Some(grade),
tenant_id: tenant.map(String::from),
ts: Utc::now(),
}
}
#[test]
fn ruf_learn_001_trajectory_captured_at_surface_boundary() {
let _guard = lock_test_store();
reset_store_for_tests();
capture_trajectory_step(Surface::Cli, "ic-memo", step("dcf_model")).unwrap();
capture_trajectory_step(Surface::Cli, "ic-memo", step("comps_table")).unwrap();
let t = complete_trajectory(Surface::Cli, "ic-memo", Some(EvalGrade::Excellent)).unwrap();
assert_eq!(t.surface, Surface::Cli);
assert_eq!(t.surface_event_id, "ic-memo");
assert_eq!(t.steps.len(), 2);
assert!(matches!(t.eval_grade, Some(EvalGrade::Excellent)));
}
#[test]
fn ruf_learn_002_kmeans_converges() {
let trajectories: Vec<Trajectory> = (0..6)
.map(|i| traj(i, Surface::Cli, EvalGrade::Good, None))
.collect();
let embeddings = vec![
vec![1.0, 0.0],
vec![0.95, 0.05],
vec![0.92, 0.08],
vec![0.0, 1.0],
vec![0.05, 0.95],
vec![0.08, 0.92],
];
let clusters = cluster_trajectories(&trajectories, &embeddings, 2, 50).unwrap();
assert_eq!(clusters.len(), 2);
let total: usize = clusters.iter().map(|c| c.sample_size).sum();
assert_eq!(total, 6);
}
#[test]
fn ruf_learn_003_astar_planner_action_space_includes_mcp_and_slash() {
assert!(!MCP_TOOL_NAMES.is_empty());
assert!(!SLASH_COMMAND_NAMES.is_empty());
assert!(MCP_TOOL_NAMES.contains(&"dcf_model"));
assert!(SLASH_COMMAND_NAMES.contains(&"initiate-coverage"));
}
#[test]
fn ruf_learn_004_replay_dispatcher_invocation_captures_outputs() {
let out = json!({"price": 123.45});
let digest = digest_output(&out);
let gs = crate::self_learning::types::GoldenSet {
surface: Surface::Mcp,
surface_event_id: "dcf_model".into(),
inputs: vec![GoldenInput {
input_id: Uuid::now_v7(),
input_json: json!({"ticker": "AAPL"}),
expected_digest: digest,
}],
expected_output_digest: "stub".into(),
signed_manifest: SignedManifest {
content_hash: "h".into(),
signature: "s".into(),
public_key: "k".into(),
signed_at: Utc::now(),
},
};
let mut calls = 0;
let res = run_replay(&gs, |_| {
calls += 1;
Ok(out.clone())
})
.unwrap();
assert_eq!(res.passed, 1);
assert_eq!(res.failed, 0);
}
#[test]
fn ruf_learn_005_drift_byte_diff_under_5pct_is_within_tolerance() {
let a = json!({"summary": "the quick brown fox jumps over the lazy dog"});
let b = json!({"summary": "the quick brown fox jumps over the lazy dog!"});
let report = detect_drift(&a, &b, 0.05);
assert!(matches!(
report.verdict,
DriftVerdict::WithinTolerance { .. }
));
assert!(!block_deploy_on_drift(&report, 0.05));
}
#[test]
fn ruf_learn_006_drift_critical_on_structural_type_change() {
let a = json!({"price": 1.0});
let b = json!({"price": "1.0"});
let report = detect_drift(&a, &b, 0.05);
assert!(matches!(report.verdict, DriftVerdict::Critical { .. }));
assert!(block_deploy_on_drift(&report, 0.05));
}
#[test]
fn ruf_learn_007_golden_set_freeze_writes_signed_manifest() {
let dir = TempDir::new().unwrap();
let (sk, _) = generate_keypair().unwrap();
let inputs = vec![GoldenInput {
input_id: Uuid::now_v7(),
input_json: json!({"x": 1}),
expected_digest: "abc".into(),
}];
let gs = freeze_golden_set(Surface::Mcp, "ev1", inputs, dir.path(), &sk).unwrap();
let manifest_path = dir.path().join("mcp/ev1/manifest.json");
assert!(manifest_path.exists());
assert!(verify_manifest(&gs.signed_manifest).unwrap());
}
#[test]
fn ruf_learn_008_golden_set_restore_verifies_signature() {
let dir = TempDir::new().unwrap();
let (sk, _) = generate_keypair().unwrap();
freeze_golden_set(
Surface::Cli,
"evt",
vec![GoldenInput {
input_id: Uuid::now_v7(),
input_json: json!({"x": 1}),
expected_digest: "z".into(),
}],
dir.path(),
&sk,
)
.unwrap();
let manifest_path = dir.path().join("cli/evt/manifest.json");
let restored = restore_golden_set(&manifest_path).unwrap();
assert!(verify_manifest(&restored.signed_manifest).unwrap());
}
#[test]
fn ruf_learn_009_replay_drift_blocks_deploy_in_ci() {
let baseline = json!({"price": 100.0, "horizon_years": 5});
let current = json!({"price": "x", "horizon_years": "five"});
let report = detect_drift(&baseline, ¤t, 0.05);
assert!(block_deploy_on_drift(&report, 0.05));
}
#[test]
fn ruf_learn_010_trajectory_retrieval_returns_top_k_by_eval_grade() {
let _guard = lock_test_store();
reset_store_for_tests();
use crate::self_learning::trajectory::persist_with_embedding;
let t1 = traj(1, Surface::Cli, EvalGrade::Acceptable, None);
let t2 = traj(2, Surface::Cli, EvalGrade::Excellent, None);
let t3 = traj(3, Surface::Cli, EvalGrade::Good, None);
persist_with_embedding(t1, vec![1.0, 0.0]).unwrap();
persist_with_embedding(t2, vec![1.0, 0.0]).unwrap();
persist_with_embedding(t3, vec![1.0, 0.0]).unwrap();
let results = retrieve_similar(&[1.0, 0.0], &TrajectoryFilter::new(), 3).unwrap();
assert_eq!(results.len(), 3);
assert_eq!(results[0].eval_grade, Some(EvalGrade::Excellent));
assert_eq!(results[1].eval_grade, Some(EvalGrade::Good));
assert_eq!(results[2].eval_grade, Some(EvalGrade::Acceptable));
}
#[test]
fn ruf_learn_011_dispatcher_failure_recorded_as_replay_failure() {
let gs = crate::self_learning::types::GoldenSet {
surface: Surface::Mcp,
surface_event_id: "ev".into(),
inputs: vec![GoldenInput {
input_id: Uuid::now_v7(),
input_json: json!({}),
expected_digest: "x".into(),
}],
expected_output_digest: "stub".into(),
signed_manifest: SignedManifest {
content_hash: "h".into(),
signature: "s".into(),
public_key: "k".into(),
signed_at: Utc::now(),
},
};
let res = run_replay(&gs, |_| {
Err(CorpFinanceError::InsufficientData("dispatcher boom".into()))
})
.unwrap();
assert_eq!(res.failed, 1);
assert_eq!(res.failures.len(), 1);
assert!(res.failures[0].structural_delta.is_none());
}
#[test]
fn ruf_learn_012_trajectories_persisted_via_phase_26_memory() {
let _guard = lock_test_store();
reset_store_for_tests();
capture_trajectory_step(Surface::Mcp, "dcf_model", step("a")).unwrap();
let t = complete_trajectory(Surface::Mcp, "dcf_model", Some(EvalGrade::Good)).unwrap();
use crate::self_learning::trajectory::persist_with_embedding;
persist_with_embedding(t.clone(), vec![0.5, 0.5]).unwrap();
let results = retrieve_similar(
&[0.5, 0.5],
&TrajectoryFilter::new().with_surface(Surface::Mcp),
5,
)
.unwrap();
assert!(!results.is_empty());
assert!(results.iter().any(|r| r.surface == Surface::Mcp));
}
#[test]
fn ruf_learn_013_signing_key_round_trip() {
let dir = TempDir::new().unwrap();
let (sk1, _) = ensure_keypair(dir.path()).unwrap();
let (sk2, _) = ensure_keypair(dir.path()).unwrap();
assert_eq!(sk1.to_bytes(), sk2.to_bytes());
let m = sign_manifest("payload", &sk1);
assert!(verify_manifest(&m).unwrap());
}
#[test]
fn ruf_learn_inv_001_trajectory_step_count_within_max() {
assert!(MAX_TRAJECTORY_STEPS >= 1024);
let _guard = lock_test_store();
reset_store_for_tests();
for i in 0..16 {
capture_trajectory_step(
Surface::Cli,
"cap_unique_key_for_inv_001",
step(&format!("s{i}")),
)
.unwrap();
}
let _ = complete_trajectory(
Surface::Cli,
"cap_unique_key_for_inv_001",
Some(EvalGrade::Acceptable),
);
}
#[test]
fn ruf_learn_inv_002_trajectory_immutability_after_completion() {
let _guard = lock_test_store();
reset_store_for_tests();
capture_trajectory_step(Surface::Cli, "imm", step("a")).unwrap();
let t1 = complete_trajectory(Surface::Cli, "imm", Some(EvalGrade::Good)).unwrap();
let err = complete_trajectory(Surface::Cli, "imm", Some(EvalGrade::Excellent)).unwrap_err();
assert!(matches!(err, CorpFinanceError::InsufficientData(_)));
assert_eq!(t1.eval_grade, Some(EvalGrade::Good));
}
#[test]
fn ruf_learn_inv_003_cluster_training_floor_is_acceptable_or_higher() {
assert!(CLUSTER_TRAINING_FLOOR >= EvalGrade::Acceptable);
}
#[test]
fn ruf_learn_inv_004_plan_tree_action_space_is_typed_union() {
let mcp_count = MCP_TOOL_NAMES.len();
let slash_count = SLASH_COMMAND_NAMES.len();
assert!(mcp_count > 0 && slash_count > 0);
for n in MCP_TOOL_NAMES.iter().chain(SLASH_COMMAND_NAMES.iter()) {
assert!(!n.is_empty());
assert!(n.is_ascii());
}
}
#[test]
fn ruf_learn_inv_005_replan_upper_bound_default_is_three() {
use crate::multi_agent::planner::DEFAULT_MAX_REPLANS;
assert_eq!(DEFAULT_MAX_REPLANS, 3);
}
#[test]
fn ruf_learn_inv_006_golden_set_supports_ten_inputs() {
let dir = TempDir::new().unwrap();
let (sk, _) = generate_keypair().unwrap();
let inputs: Vec<GoldenInput> = (0..10)
.map(|i| GoldenInput {
input_id: Uuid::now_v7(),
input_json: json!({"i": i}),
expected_digest: format!("d{i}"),
})
.collect();
let gs = freeze_golden_set(Surface::Mcp, "ten", inputs, dir.path(), &sk).unwrap();
assert_eq!(gs.inputs.len(), 10);
}
#[test]
fn ruf_learn_inv_007_manifest_signing_uses_ed25519() {
let (sk, _) = generate_keypair().unwrap();
let m = sign_manifest("payload-bytes", &sk);
assert_eq!(m.signature.len(), 128);
assert_eq!(m.public_key.len(), 64);
assert!(verify_manifest(&m).unwrap());
}
#[test]
fn ruf_learn_inv_008_drift_threshold_default_is_5pct() {
let baseline = json!({"x": "abcdefghij"}); let current = json!({"x": "abcdefghij_"}); let report = detect_drift(&baseline, ¤t, 0.05);
assert!(matches!(
report.verdict,
DriftVerdict::BeyondTolerance { .. }
));
assert!(block_deploy_on_drift(&report, 0.05));
}
#[test]
fn ruf_learn_inv_009_tenant_scoping_filters_cross_tenant_reads() {
let _guard = lock_test_store();
reset_store_for_tests();
use crate::self_learning::trajectory::persist_with_embedding;
let t_a = traj(1, Surface::Cli, EvalGrade::Good, Some("tenant-a"));
let t_b = traj(2, Surface::Cli, EvalGrade::Good, Some("tenant-b"));
persist_with_embedding(t_a, vec![1.0, 0.0]).unwrap();
persist_with_embedding(t_b, vec![1.0, 0.0]).unwrap();
let results = retrieve_similar(
&[1.0, 0.0],
&TrajectoryFilter::new().with_tenant_id("tenant-a"),
10,
)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].tenant_id.as_deref(), Some("tenant-a"));
}
#[test]
fn ruf_learn_inv_010_delta_kind_is_enumerated() {
let kinds = [
DeltaKind::Added,
DeltaKind::Removed,
DeltaKind::Changed,
DeltaKind::TypeChanged,
];
assert_eq!(kinds.len(), 4);
}
#[test]
fn ruf_learn_inv_011_plan_hash_is_deterministic_for_fixed_inputs() {
use crate::multi_agent::planner::plan_hash;
use crate::multi_agent::types::{GoapPlan, PlanAction, PlanStep, StepStatus};
let p1 = GoapPlan {
plan_id: Uuid::nil(),
goal: "test".into(),
steps: vec![PlanStep {
step_id: Uuid::nil(),
action: PlanAction::McpTool {
name: "dcf_model".into(),
input_hint: serde_json::json!({}),
},
depends_on: vec![],
status: StepStatus::Pending,
result_summary: None,
}],
plan_hash: String::new(),
replan_count: 0,
max_replans: 3,
};
let p2 = p1.clone();
assert_eq!(plan_hash(&p1), plan_hash(&p2));
}
#[test]
fn ruf_learn_inv_012_trajectory_store_integration_point_documented() {
let _guard = lock_test_store();
reset_store_for_tests();
use crate::self_learning::trajectory::persist_with_embedding;
let t = traj(1, Surface::Cli, EvalGrade::Excellent, Some("local"));
persist_with_embedding(t.clone(), vec![1.0, 0.0]).unwrap();
let results = retrieve_similar(
&[1.0, 0.0],
&TrajectoryFilter::new().with_tenant_id("local"),
1,
)
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].trajectory_id, t.trajectory_id);
}