use async_trait::async_trait;
use std::sync::Arc;
use std::time::Duration;
use wm_core::{
Args, BrainWave, Context, CoreError, EffectRow, Gana, Output, Resource, Tool, ToolStats,
};
use wm_dispatch::{CircuitBreakerRegistry, RateLimiter};
use wm_dispatch::{DispatchPipeline, ToolRegistry};
use wm_governance::{DharmaGate, KarmaLedger};
struct LyingTool {
stats: ToolStats,
}
#[async_trait]
impl Tool for LyingTool {
fn name(&self) -> &str {
"lying_tool"
}
fn gana(&self) -> Gana {
Gana::Horn
}
fn effects(&self) -> &EffectRow {
use std::sync::OnceLock;
static EFFECTS: OnceLock<EffectRow> = OnceLock::new();
EFFECTS.get_or_init(EffectRow::pure)
}
async fn call(&self, _ctx: &mut Context, _args: Args) -> wm_core::Result<Output> {
Ok(serde_json::json!({
"writes": [1, 2, 3], "result": "sneaky"
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
struct DestructiveTool {
stats: ToolStats,
}
#[async_trait]
impl Tool for DestructiveTool {
fn name(&self) -> &str {
"destructive_tool"
}
fn gana(&self) -> Gana {
Gana::Horn
}
fn effects(&self) -> &EffectRow {
use std::sync::OnceLock;
static EFFECTS: OnceLock<EffectRow> = OnceLock::new();
EFFECTS.get_or_init(|| EffectRow {
writes: vec![Resource::Filesystem],
..Default::default()
})
}
async fn call(&self, _ctx: &mut Context, _args: Args) -> wm_core::Result<Output> {
Ok(serde_json::json!({"result": "done"}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
struct LongNameTool {
name: String,
stats: ToolStats,
}
#[async_trait]
impl Tool for LongNameTool {
fn name(&self) -> &str {
&self.name
}
fn gana(&self) -> Gana {
Gana::Horn
}
fn effects(&self) -> &EffectRow {
use std::sync::OnceLock;
static EFFECTS: OnceLock<EffectRow> = OnceLock::new();
EFFECTS.get_or_init(EffectRow::pure)
}
async fn call(&self, _ctx: &mut Context, _args: Args) -> wm_core::Result<Output> {
Ok(serde_json::json!("ok"))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
#[tokio::test]
async fn lying_tool_accumulates_karma_debt() {
let tmp = tempfile::tempdir().unwrap();
let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
let ledger = Arc::new(KarmaLedger::new(store).unwrap());
let pipeline = DispatchPipeline::new(
Arc::new(RateLimiter::default()),
Arc::new(CircuitBreakerRegistry::default()),
Arc::new(DharmaGate::default()),
Some(ledger.clone()),
);
let mut ctx = Context::new(BrainWave::Gamma);
let tool = LyingTool {
stats: ToolStats::default(),
};
let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
assert!(
result.is_ok(),
"Pipeline should succeed (Dharma gate sees pure effects)"
);
let entries = ledger.scan_entries().unwrap();
assert_eq!(entries.len(), 1);
assert!(
entries[0].mismatch,
"Karma ledger must detect the effect mismatch"
);
assert!(
entries[0].debt_delta > 0.0,
"Lying tool must accumulate karma debt, got {}",
entries[0].debt_delta
);
}
#[tokio::test]
async fn delta_blocks_all_tools_even_with_perfect_context() {
let pipeline = DispatchPipeline::with_defaults();
let mut ctx = Context::new(BrainWave::Delta);
ctx.karma_debt = 0.0;
ctx.intent_score = 1.0;
ctx.citta_coherence = 1.0;
ctx.self_model_confidence = 1.0;
let tool = DestructiveTool {
stats: ToolStats::default(),
};
let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
assert!(
result.is_err(),
"Delta must block all tools regardless of context"
);
}
#[tokio::test]
async fn pipeline_handles_extremely_long_tool_name() {
let pipeline = DispatchPipeline::with_defaults();
let mut ctx = Context::new(BrainWave::Gamma);
let tool = LongNameTool {
name: "A".repeat(10_000),
stats: ToolStats::default(),
};
let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
assert!(result.is_ok(), "Long name should not cause failure");
}
#[tokio::test]
async fn rate_limit_is_per_tool_not_global() {
let rate_limiter = Arc::new(RateLimiter::new(10000, 2, 0));
let pipeline = DispatchPipeline::new(
rate_limiter,
Arc::new(CircuitBreakerRegistry::default()),
Arc::new(DharmaGate::default()),
None,
);
let mut ctx = Context::new(BrainWave::Gamma);
let tool_a = LongNameTool {
name: "tool_a".to_string(),
stats: ToolStats::default(),
};
assert!(
pipeline
.dispatch(&tool_a, &mut ctx, Args::default())
.await
.is_ok()
);
assert!(
pipeline
.dispatch(&tool_a, &mut ctx, Args::default())
.await
.is_ok()
);
assert!(
pipeline
.dispatch(&tool_a, &mut ctx, Args::default())
.await
.is_err()
);
let tool_b = LongNameTool {
name: "tool_b".to_string(),
stats: ToolStats::default(),
};
assert!(
pipeline
.dispatch(&tool_b, &mut ctx, Args::default())
.await
.is_ok(),
"Different tool should have its own rate limit bucket"
);
}
#[tokio::test]
async fn rate_limited_call_does_not_execute_tool() {
let tmp = tempfile::tempdir().unwrap();
let store = Arc::new(wm_memory::MemoryStore::open_default(tmp.path()).unwrap());
let ledger = Arc::new(KarmaLedger::new(store).unwrap());
let rate_limiter = Arc::new(RateLimiter::new(10000, 1, 0)); let pipeline = DispatchPipeline::new(
rate_limiter,
Arc::new(CircuitBreakerRegistry::default()),
Arc::new(DharmaGate::default()),
Some(ledger.clone()),
);
let mut ctx = Context::new(BrainWave::Gamma);
let tool = LongNameTool {
name: "rate_limited".to_string(),
stats: ToolStats::default(),
};
assert!(
pipeline
.dispatch(&tool, &mut ctx, Args::default())
.await
.is_ok()
);
let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
assert!(matches!(result, Err(CoreError::RateLimited(_))));
let entries = ledger.scan_entries().unwrap();
assert_eq!(
entries.len(),
1,
"Rate-limited call must not create karma entry"
);
assert_eq!(
tool.stats()
.success_count
.load(std::sync::atomic::Ordering::Relaxed),
1
);
}
#[tokio::test]
async fn circuit_breaker_tracks_by_name_not_identity() {
let breakers = Arc::new(CircuitBreakerRegistry::new(
wm_dispatch::circuit_breaker::BreakerConfig {
failure_threshold: 3,
window: Duration::from_secs(10),
cooldown: Duration::from_secs(30),
},
));
let pipeline = DispatchPipeline::new(
Arc::new(RateLimiter::new(10000, 100, 100)),
breakers.clone(),
Arc::new(DharmaGate::default()),
None,
);
let flaky = FailTool::new("flaky");
let mut ctx = Context::new(BrainWave::Gamma);
for _ in 0..3 {
let _ = pipeline.dispatch(&flaky, &mut ctx, Args::default()).await;
}
assert_eq!(
breakers.state("flaky"),
wm_dispatch::circuit_breaker::BreakerState::Open
);
let flaky2 = FailTool::new("flaky");
let result = pipeline.dispatch(&flaky2, &mut ctx, Args::default()).await;
assert!(
matches!(result, Err(CoreError::CircuitBreaker(_))),
"Circuit breaker must block by name, not tool identity"
);
let other = FailTool::new("other_tool");
let result = pipeline.dispatch(&other, &mut ctx, Args::default()).await;
assert!(
!matches!(result, Err(CoreError::CircuitBreaker(_))),
"Different tool name should not be affected by another tool's breaker"
);
}
#[tokio::test]
async fn brain_wave_cannot_be_manipulated_mid_dispatch() {
let pipeline = DispatchPipeline::with_defaults();
let mut ctx = Context::new(BrainWave::Delta);
let tool = LongNameTool {
name: "test_tool".to_string(),
stats: ToolStats::default(),
};
let result = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
assert!(result.is_err(), "Delta must block before tool execution");
ctx.brain_wave = BrainWave::Gamma;
let result2 = pipeline.dispatch(&tool, &mut ctx, Args::default()).await;
assert!(result2.is_ok(), "New dispatch with Gamma should succeed");
}
struct FailTool {
name: String,
stats: ToolStats,
}
impl FailTool {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
stats: ToolStats::default(),
}
}
}
#[async_trait]
impl Tool for FailTool {
fn name(&self) -> &str {
&self.name
}
fn gana(&self) -> Gana {
Gana::Heart
}
fn effects(&self) -> &EffectRow {
use std::sync::OnceLock;
static EFFECTS: OnceLock<EffectRow> = OnceLock::new();
EFFECTS.get_or_init(EffectRow::pure)
}
async fn call(&self, _ctx: &mut Context, _args: Args) -> wm_core::Result<Output> {
Err(CoreError::Internal("intentional failure".into()))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
#[tokio::test]
async fn dispatch_by_name_rejects_unknown_tools() {
let pipeline = DispatchPipeline::with_defaults();
let registry = ToolRegistry::new();
let mut ctx = Context::new(BrainWave::Gamma);
let result = pipeline
.dispatch_by_name(®istry, "nonexistent_tool", &mut ctx, Args::default())
.await;
assert!(
matches!(result, Err(CoreError::NotFound(_))),
"Unknown tool must return NotFound, got {result:?}"
);
}
#[tokio::test]
async fn dispatch_by_name_rejects_empty_string() {
let pipeline = DispatchPipeline::with_defaults();
let registry = ToolRegistry::new();
let mut ctx = Context::new(BrainWave::Gamma);
let result = pipeline
.dispatch_by_name(®istry, "", &mut ctx, Args::default())
.await;
assert!(
matches!(result, Err(CoreError::NotFound(_))),
"Empty tool name must return NotFound"
);
}