use aspect_runtime::{
compare_orderings, detect_short_circuits, ChainSlot, Composition, Interaction, Phase,
};
use aspect_std::{
ActionAuditTrailAspect, AuditContext, LoggingAspect, RateLimitAspect, ToolCall,
ToolScopeSandboxAspect,
};
use std::time::Duration;
fn build_chain(
sandbox: ToolScopeSandboxAspect,
limiter: RateLimitAspect,
audit: ActionAuditTrailAspect,
logger: LoggingAspect,
) -> Composition {
Composition::new(vec![
ChainSlot::new("ToolScopeSandbox", sandbox),
ChainSlot::new("RateLimiter", limiter),
ChainSlot::new("ActionAuditTrail", audit),
ChainSlot::new("StructuredLogger", logger),
])
}
#[test]
fn allowed_tool_passes_through_all_four_aspects() {
let sandbox = ToolScopeSandboxAspect::new();
sandbox.allow("s1", ["read_file"]);
let limiter = RateLimitAspect::new(10, Duration::from_secs(1));
let audit = ActionAuditTrailAspect::new();
let logger = LoggingAspect::new();
let sandbox_key = sandbox.bind(ToolCall {
session: "s1".into(),
tool: "read_file".into(),
});
let audit_key = audit.bind(AuditContext {
session: "s1".into(),
tool: "read_file".into(),
});
let chain = build_chain(sandbox.clone(), limiter, audit.clone(), logger);
let result = chain.run("execute_tool_call", || Ok::<i32, _>(42));
sandbox.release(sandbox_key);
audit.release(audit_key);
assert!(result.is_ok());
let trace = chain.trace().snapshot();
let entries: Vec<_> = trace.iter().map(|t| (t.aspect.as_str(), t.phase)).collect();
assert_eq!(
entries,
vec![
("ToolScopeSandbox", Phase::AroundEnter),
("RateLimiter", Phase::AroundEnter),
("ActionAuditTrail", Phase::AroundEnter),
("StructuredLogger", Phase::AroundEnter),
("StructuredLogger", Phase::AroundExit),
("ActionAuditTrail", Phase::AroundExit),
("RateLimiter", Phase::AroundExit),
("ToolScopeSandbox", Phase::AroundExit),
]
);
let trail = audit.snapshot();
assert_eq!(trail.len(), 1);
assert!(trail[0].ok);
}
#[test]
fn sandbox_short_circuits_inner_three_aspects() {
let sandbox = ToolScopeSandboxAspect::new();
sandbox.allow("s1", ["read_file"]);
let limiter = RateLimitAspect::new(10, Duration::from_secs(1));
let audit = ActionAuditTrailAspect::new();
let logger = LoggingAspect::new();
let sandbox_key = sandbox.bind(ToolCall {
session: "s1".into(),
tool: "delete_file".into(),
});
let chain = build_chain(sandbox.clone(), limiter, audit.clone(), logger);
let result = chain.run("execute_tool_call", || Ok::<i32, _>(0));
sandbox.release(sandbox_key);
assert!(result.is_err());
let declared = vec![
"ToolScopeSandbox".to_string(),
"RateLimiter".to_string(),
"ActionAuditTrail".to_string(),
"StructuredLogger".to_string(),
];
let interactions = detect_short_circuits(&declared, &chain.trace().snapshot());
let short_circuit = interactions
.iter()
.find(|i| matches!(i, Interaction::ShortCircuit { .. }))
.expect("expected a short-circuit interaction");
if let Interaction::ShortCircuit { culprit, skipped } = short_circuit {
assert_eq!(culprit, "ToolScopeSandbox");
assert_eq!(
skipped,
&vec![
"RateLimiter".to_string(),
"ActionAuditTrail".to_string(),
"StructuredLogger".to_string()
]
);
}
assert!(audit.is_empty());
}
#[test]
fn moving_audit_outside_sandbox_changes_trace() {
let make_aspects = || {
let sandbox = ToolScopeSandboxAspect::new();
sandbox.allow("s1", ["read_file"]);
let limiter = RateLimitAspect::new(10, Duration::from_secs(1));
let audit = ActionAuditTrailAspect::new();
let logger = LoggingAspect::new();
(sandbox, limiter, audit, logger)
};
let (sa, la, aa, lga) = make_aspects();
let sa_key = sa.bind(ToolCall {
session: "s1".into(),
tool: "delete_file".into(),
});
let chain_a = Composition::new(vec![
ChainSlot::new("ToolScopeSandbox", sa.clone()),
ChainSlot::new("RateLimiter", la),
ChainSlot::new("ActionAuditTrail", aa.clone()),
ChainSlot::new("StructuredLogger", lga),
]);
let r_a = chain_a.run("execute_tool_call", || Ok::<i32, _>(0));
sa.release(sa_key);
let (sb, lb, ab, lgb) = make_aspects();
let sb_key = sb.bind(ToolCall {
session: "s1".into(),
tool: "delete_file".into(),
});
let ab_key = ab.bind(AuditContext {
session: "s1".into(),
tool: "delete_file".into(),
});
let chain_b = Composition::new(vec![
ChainSlot::new("ActionAuditTrail", ab.clone()),
ChainSlot::new("ToolScopeSandbox", sb.clone()),
ChainSlot::new("RateLimiter", lb),
ChainSlot::new("StructuredLogger", lgb),
]);
let r_b = chain_b.run("execute_tool_call", || Ok::<i32, _>(0));
sb.release(sb_key);
ab.release(ab_key);
assert!(r_a.is_err() && r_b.is_err());
let interactions = compare_orderings(
&chain_a.trace().snapshot(),
r_a.is_ok(),
&chain_b.trace().snapshot(),
r_b.is_ok(),
);
assert!(interactions
.iter()
.any(|i| matches!(i, Interaction::OrderSensitive { .. })));
assert!(aa.is_empty());
assert_eq!(ab.len(), 1);
assert!(!ab.snapshot()[0].ok);
}