use super::branch_context::BranchContext;
use super::shared_state_context::SharedStateContext;
use adk_core::{
Agent, CallbackContext, Content, Event, InvocationContext, Memory, Part, ReadonlyContext,
Result, RunConfig, Session, SharedState, ToolOutcome,
};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
const SENTINEL_SECRET_NAME: &str = "sentinel-secret";
const SENTINEL_SECRET_VALUE: &str = "sentinel-value";
const SENTINEL_SCOPE: &str = "sentinel:scope";
const SENTINEL_METADATA_KEY: &str = "sentinel-metadata";
const SENTINEL_STATE_KEY: &str = "sentinel-shared-key";
const SENTINEL_ARTIFACT: &str = "sentinel-artifact";
const SENTINEL_MEMORY_AUTHOR: &str = "sentinel-memory-author";
struct SentinelArtifacts;
#[async_trait]
impl adk_core::Artifacts for SentinelArtifacts {
async fn save(&self, _name: &str, _data: &Part) -> Result<i64> {
Ok(1)
}
async fn load(&self, _name: &str) -> Result<Part> {
Ok(Part::Text { text: "sentinel".to_string() })
}
async fn list(&self) -> Result<Vec<String>> {
Ok(vec![SENTINEL_ARTIFACT.to_string()])
}
}
struct SentinelMemory;
#[async_trait]
impl Memory for SentinelMemory {
async fn search(&self, _query: &str) -> Result<Vec<adk_core::MemoryEntry>> {
Ok(vec![adk_core::MemoryEntry {
content: Content {
role: "model".to_string(),
parts: vec![Part::Text { text: "sentinel".to_string() }],
},
author: SENTINEL_MEMORY_AUTHOR.to_string(),
}])
}
}
struct SentinelState;
impl adk_core::State for SentinelState {
fn get(&self, _key: &str) -> Option<serde_json::Value> {
None
}
fn set(&mut self, _key: String, _value: serde_json::Value) {}
fn all(&self) -> HashMap<String, serde_json::Value> {
HashMap::new()
}
}
struct SentinelSession {
state: SentinelState,
}
impl Session for SentinelSession {
fn id(&self) -> &str {
"sentinel-session"
}
fn app_name(&self) -> &str {
"sentinel-app"
}
fn user_id(&self) -> &str {
"sentinel-user"
}
fn state(&self) -> &dyn adk_core::State {
&self.state
}
fn conversation_history(&self) -> Vec<Content> {
Vec::new()
}
}
struct SentinelContext {
content: Content,
config: RunConfig,
session: SentinelSession,
shared: Arc<SharedState>,
}
impl SentinelContext {
fn with_shared(shared: Arc<SharedState>) -> Self {
Self {
content: Content {
role: "user".to_string(),
parts: vec![Part::Text { text: "sentinel".to_string() }],
},
config: RunConfig::default(),
session: SentinelSession { state: SentinelState },
shared,
}
}
}
async fn seeded_shared_state(key: &str) -> Arc<SharedState> {
let shared = Arc::new(SharedState::new());
shared.set_shared(key, serde_json::json!(true)).await.expect("seeding shared state failed");
shared
}
#[async_trait]
impl ReadonlyContext for SentinelContext {
fn invocation_id(&self) -> &str {
"sentinel-invocation"
}
fn agent_name(&self) -> &str {
"sentinel-agent"
}
fn user_id(&self) -> &str {
"sentinel-user"
}
fn app_name(&self) -> &str {
"sentinel-app"
}
fn session_id(&self) -> &str {
"sentinel-session"
}
fn branch(&self) -> &str {
"sentinel-branch"
}
fn user_content(&self) -> &Content {
&self.content
}
}
#[async_trait]
impl CallbackContext for SentinelContext {
fn artifacts(&self) -> Option<Arc<dyn adk_core::Artifacts>> {
Some(Arc::new(SentinelArtifacts))
}
fn tool_outcome(&self) -> Option<ToolOutcome> {
Some(ToolOutcome {
tool_name: "sentinel-tool".to_string(),
tool_args: serde_json::json!({}),
success: true,
duration: std::time::Duration::from_millis(1),
error_message: None,
attempt: 0,
})
}
fn tool_name(&self) -> Option<&str> {
Some("sentinel-tool")
}
fn shared_state(&self) -> Option<Arc<SharedState>> {
Some(self.shared.clone())
}
}
#[async_trait]
impl InvocationContext for SentinelContext {
fn agent(&self) -> Arc<dyn Agent> {
unimplemented!("not exercised by wrapper conformance")
}
fn memory(&self) -> Option<Arc<dyn Memory>> {
Some(Arc::new(SentinelMemory))
}
fn session(&self) -> &dyn Session {
&self.session
}
fn run_config(&self) -> &RunConfig {
&self.config
}
fn end_invocation(&self) {}
fn ended(&self) -> bool {
false
}
fn is_cancelled(&self) -> bool {
true
}
fn user_scopes(&self) -> Vec<String> {
vec![SENTINEL_SCOPE.to_string()]
}
fn request_metadata(&self) -> HashMap<String, serde_json::Value> {
HashMap::from([(SENTINEL_METADATA_KEY.to_string(), serde_json::json!(1))])
}
async fn get_secret(&self, name: &str) -> Result<Option<String>> {
Ok(if name == SENTINEL_SECRET_NAME {
Some(SENTINEL_SECRET_VALUE.to_string())
} else {
None
})
}
}
async fn assert_capabilities_preserved(label: &str, wrapped: Arc<dyn InvocationContext>) {
assert!(wrapped.is_cancelled(), "{label}: cancellation was dropped");
assert_eq!(
wrapped.user_scopes(),
vec![SENTINEL_SCOPE.to_string()],
"{label}: authenticated scopes were dropped"
);
assert!(
wrapped.request_metadata().contains_key(SENTINEL_METADATA_KEY),
"{label}: request metadata was dropped"
);
let secret = wrapped.get_secret(SENTINEL_SECRET_NAME).await.expect("secret lookup failed");
assert_eq!(
secret.as_deref(),
Some(SENTINEL_SECRET_VALUE),
"{label}: secret access was dropped"
);
let shared =
wrapped.shared_state().unwrap_or_else(|| panic!("{label}: shared state was dropped"));
assert!(
shared.get_shared(SENTINEL_STATE_KEY).await.is_some(),
"{label}: shared state was replaced rather than forwarded"
);
let artifacts =
wrapped.artifacts().unwrap_or_else(|| panic!("{label}: artifact service was dropped"));
assert_eq!(
artifacts.list().await.expect("artifact list failed"),
vec![SENTINEL_ARTIFACT.to_string()],
"{label}: artifact service was replaced rather than forwarded"
);
let memory = wrapped.memory().unwrap_or_else(|| panic!("{label}: memory service was dropped"));
let hits = memory.search("sentinel").await.expect("memory search failed");
assert_eq!(
hits.first().map(|e| e.author.as_str()),
Some(SENTINEL_MEMORY_AUTHOR),
"{label}: memory service was replaced rather than forwarded"
);
assert_eq!(wrapped.app_name(), "sentinel-app", "{label}: app name was lost");
assert_eq!(wrapped.user_id(), "sentinel-user", "{label}: user id was lost");
assert_eq!(wrapped.session_id(), "sentinel-session", "{label}: session id was lost");
}
async fn sentinel() -> Arc<dyn InvocationContext> {
Arc::new(SentinelContext::with_shared(seeded_shared_state(SENTINEL_STATE_KEY).await))
}
#[tokio::test]
async fn shared_state_context_preserves_capabilities() {
let injected = seeded_shared_state("injected").await;
let wrapped: Arc<dyn InvocationContext> =
Arc::new(SharedStateContext::new(sentinel().await, injected));
assert!(wrapped.is_cancelled(), "SharedStateContext: cancellation was dropped");
assert_eq!(wrapped.user_scopes(), vec![SENTINEL_SCOPE.to_string()]);
assert!(wrapped.request_metadata().contains_key(SENTINEL_METADATA_KEY));
let secret = wrapped.get_secret(SENTINEL_SECRET_NAME).await.unwrap();
assert_eq!(secret.as_deref(), Some(SENTINEL_SECRET_VALUE));
let shared = wrapped.shared_state().expect("shared state missing");
assert!(
shared.get_shared("injected").await.is_some(),
"SharedStateContext must expose the state it injects"
);
assert!(wrapped.artifacts().is_some(), "SharedStateContext: artifacts were dropped");
assert!(wrapped.memory().is_some(), "SharedStateContext: memory was dropped");
}
#[tokio::test]
async fn branch_context_preserves_capabilities() {
let wrapped: Arc<dyn InvocationContext> =
Arc::new(BranchContext::new(sentinel().await, "sentinel-branch.child".to_string()));
assert_capabilities_preserved("BranchContext", wrapped.clone()).await;
assert_eq!(wrapped.branch(), "sentinel-branch.child", "BranchContext must override branch");
}
#[tokio::test]
async fn skill_context_preserves_capabilities() {
let overridden = Content {
role: "user".to_string(),
parts: vec![Part::Text { text: "overridden".to_string() }],
};
let wrapped =
super::skill_context::with_user_content_override(sentinel().await, overridden.clone());
assert_capabilities_preserved("UserContentOverrideContext", wrapped.clone()).await;
assert_eq!(
wrapped.user_content().parts.len(),
overridden.parts.len(),
"UserContentOverrideContext must override user content"
);
}
#[tokio::test]
async fn loop_history_context_preserves_capabilities() {
let wrapped = super::loop_agent::history_tracking_context_for_test(sentinel().await);
assert_capabilities_preserved("HistoryTrackingContext", wrapped).await;
}
#[tokio::test]
async fn composed_wrappers_preserve_capabilities() {
let injected = seeded_shared_state(SENTINEL_STATE_KEY).await;
let inner: Arc<dyn InvocationContext> =
Arc::new(SharedStateContext::new(sentinel().await, injected));
let outer: Arc<dyn InvocationContext> =
Arc::new(BranchContext::new(inner, "sentinel-branch.parallel.a".to_string()));
assert_capabilities_preserved("BranchContext(SharedStateContext)", outer.clone()).await;
assert_eq!(outer.branch(), "sentinel-branch.parallel.a");
}
#[tokio::test]
async fn sentinel_reports_non_default_capabilities() {
let ctx = SentinelContext::with_shared(seeded_shared_state(SENTINEL_STATE_KEY).await);
assert!(ctx.is_cancelled());
assert!(!ctx.user_scopes().is_empty());
assert!(!ctx.request_metadata().is_empty());
assert!(ctx.shared_state().is_some());
assert!(ctx.artifacts().is_some());
assert!(ctx.memory().is_some());
let _ = Event::new("sentinel-invocation");
}