#[cfg(test)]
mod layer_tests {
use portail::config::Config;
use portail::*;
use std::sync::{Arc, RwLock};
fn global_metrics() -> metrics_exporter_prometheus::PrometheusHandle {
metrics_exporter_prometheus::PrometheusBuilder::new()
.install_recorder()
.expect("install metrics recorder")
}
fn test_state() -> AppState {
AppState {
config: RwLock::new(Config::default()),
event_log: Arc::new(events::EventLog::new(100)),
cdn_cache: None,
hooks: Arc::new(hooks::HookStore::new()),
a2a_tasks: Arc::new(a2a::TaskStore::new()),
a2a_registry: Arc::new(a2a::registry::AgentRegistry::new()),
dns_store: Arc::new(dns::DnsStore::new()),
doh_client: None,
network_isolation: Arc::new(dns::NetworkIsolation::default()),
tinyurl: Arc::new(plugins::TinyUrlStore::new(plugins::TinyUrlConfig::default())),
trace_store: Arc::new(plugins::TraceStore::new(100)),
redis_cache: Arc::new(plugins::RedisCache::new(
plugins::RedisCacheConfig::default(),
)),
discovery: Arc::new(discovery::DiscoveryStore::new(
discovery::DiscoveryConfig::default(),
)),
ci_status: Arc::new(ci::CiStatusStore::new(100, None)),
metrics_handle: global_metrics(),
rate_limiter: None,
auth_state: None,
event_store: None,
session_store: portail::sessions::SessionStore::new(20),
file_cache: portail::file_cache::FileCache::new(
&portail::file_cache::FileCacheConfig {
path: "/tmp/portail-test-cache".into(),
..Default::default()
},
),
config_watcher: portail::config_watcher::ConfigWatcher::new(std::path::PathBuf::from(
"portail.toml",
)),
supervisor: std::sync::Arc::new(portail::supervisor::Supervisor::new(
std::sync::Arc::new(portail::events::EventLog::new(100)),
)),
plugin_registry: portail::plugin_hooks::init_plugin_registry(std::path::Path::new(
"vaked",
)),
loop_manager: std::sync::Arc::new(loop_state_manager::LoopStateManager::new("3.0.0")),
loop_runner: loopeng::SharedLoopEngine::new(loopeng::LoopEngineConfig::default()),
inference_engine: None,
pkg_ctx_memory: tokio::sync::Mutex::new(pkg_ctx::memory::PkgCtxMemory::new().unwrap()),
}
}
#[test]
fn layer1_events_constructible() {
let log = events::EventLog::new(100);
log.publish(events::AgentEvent {
agent_id: "test".into(),
event_type: "ping".into(),
severity: "info".into(),
timestamp: 0,
metadata: portail::types::BoundedMeta::default(),
});
assert_eq!(log.recent(1).len(), 1);
}
#[test]
fn layer1_hooks_constructible() {
let store = hooks::HookStore::new();
store.add(hooks::Hook {
id: "h1".into(),
match_agent: None,
match_path: Some("/test".into()),
match_event_type: None,
inject: hooks::InjectMode::Prepend,
content: "test".into(),
enabled: true,
priority: 0,
});
assert_eq!(store.list().len(), 1);
}
#[test]
fn layer1_a2a_constructible() {
let store = a2a::TaskStore::new();
let task = store.create("t1".into());
assert_eq!(task.status.state, a2a::TaskState::Submitted);
}
#[test]
fn layer1_dns_constructible() {
let store = dns::DnsStore::new();
store.add_record(
"example.com".into(),
dns::DnsAnswer {
name: "example.com".into(),
record_type: dns::DnsRecordType::A,
data: "1.2.3.4".into(),
ttl: 300,
},
);
let answers = store.query("example.com", dns::DnsRecordType::A);
assert_eq!(answers.len(), 1);
}
#[test]
fn layer2_events_independent() {
let log = events::EventLog::new(100);
log.publish(events::AgentEvent {
agent_id: "test".into(),
event_type: "ping".into(),
severity: "info".into(),
timestamp: 0,
metadata: portail::types::BoundedMeta::default(),
});
let rx = log.subscribe();
assert_eq!(log.recent(1).len(), 1);
drop(rx);
}
#[test]
fn layer2_hooks_independent() {
let store = hooks::HookStore::new();
store.add(hooks::Hook {
id: "h1".into(),
match_agent: None,
match_path: Some("/test".into()),
match_event_type: None,
inject: hooks::InjectMode::Prepend,
content: "test".into(),
enabled: true,
priority: 0,
});
let matched = store.match_message("/test");
assert_eq!(matched.len(), 1);
}
#[test]
fn layer2_dns_independent() {
let store = dns::DnsStore::new();
store.add_hook(dns::DnsHook {
id: "h1".into(),
name: "block".into(),
pattern: "ads.example.com".into(),
action: dns::DnsHookAction::Block,
enabled: true,
});
let query = dns::DnsQuery {
name: "ads.example.com".into(),
record_type: dns::DnsRecordType::A,
source: "127.0.0.1".parse().unwrap(),
};
let action = store.apply_hooks(&query);
assert!(matches!(action, Some(dns::DnsHookAction::Block)));
}
#[test]
fn layer3_appstate_integrates_all() {
let state = test_state();
assert!(state.cdn_cache.is_none());
assert!(state.doh_client.is_none());
assert!(!state.network_isolation.enabled);
state.event_log.publish(events::AgentEvent {
agent_id: "test".into(),
event_type: "ping".into(),
severity: "info".into(),
timestamp: 0,
metadata: portail::types::BoundedMeta::default(),
});
assert_eq!(state.event_log.recent(1).len(), 1);
state.hooks.add(hooks::Hook {
id: "h1".into(),
match_agent: None,
match_path: Some("/test".into()),
match_event_type: None,
inject: hooks::InjectMode::Prepend,
content: "test".into(),
enabled: true,
priority: 0,
});
assert_eq!(state.hooks.list().len(), 1);
let task = state.a2a_tasks.create("t1".into());
assert_eq!(task.status.state, a2a::TaskState::Submitted);
state.dns_store.add_record(
"example.com".into(),
dns::DnsAnswer {
name: "example.com".into(),
record_type: dns::DnsRecordType::A,
data: "1.2.3.4".into(),
ttl: 300,
},
);
assert_eq!(
state
.dns_store
.query("example.com", dns::DnsRecordType::A)
.len(),
1
);
}
#[test]
fn layer4_network_isolation_allow() {
let iso = dns::NetworkIsolation {
enabled: true,
allowed_domains: vec!["example.com".into()],
..Default::default()
};
assert!(iso.is_allowed("api.example.com", None));
assert!(!iso.is_allowed("evil.com", None));
}
#[test]
fn layer4_network_isolation_block() {
let iso = dns::NetworkIsolation {
enabled: true,
blocked_domains: vec!["ads.example.com".into()],
..Default::default()
};
assert!(iso.is_allowed("api.example.com", None));
assert!(!iso.is_allowed("ads.example.com", None));
}
#[test]
fn layer4_network_isolation_disabled() {
let iso = dns::NetworkIsolation {
enabled: false,
blocked_domains: vec!["evil.com".into()],
..Default::default()
};
assert!(iso.is_allowed("evil.com", None));
}
#[test]
fn layer5_message_hooks() {
let body = serde_json::json!({
"messages": [
{ "role": "user", "content": "hello" }
]
});
let hooks = vec![hooks::Hook {
id: "h1".into(),
match_agent: None,
match_path: None,
match_event_type: None,
inject: hooks::InjectMode::Prepend,
content: "be helpful".into(),
enabled: true,
priority: 0,
}];
let modified = hooks::apply_message_hooks(&body, &hooks).unwrap();
let msgs = modified["messages"].as_array().unwrap();
assert_eq!(msgs.len(), 2);
assert_eq!(msgs[0]["content"], "be helpful");
assert_eq!(msgs[1]["content"], "hello");
}
#[test]
fn layer5_event_hooks() {
let store = hooks::HookStore::new();
store.add(hooks::Hook {
id: "h1".into(),
match_agent: Some("test-agent".into()),
match_path: None,
match_event_type: Some("started".into()),
inject: hooks::InjectMode::Prepend,
content: "injected".into(),
enabled: true,
priority: 0,
});
let matched = store.match_event("test-agent", "started");
assert_eq!(matched.len(), 1);
}
#[test]
fn layer5_dns_hooks() {
let store = dns::DnsStore::new();
store.add_hook(dns::DnsHook {
id: "h1".into(),
name: "redirect".into(),
pattern: "old.example.com".into(),
action: dns::DnsHookAction::Redirect("new.example.com".into()),
enabled: true,
});
let query = dns::DnsQuery {
name: "old.example.com".into(),
record_type: dns::DnsRecordType::A,
source: "127.0.0.1".parse().unwrap(),
};
let action = store.apply_hooks(&query);
assert!(matches!(action, Some(dns::DnsHookAction::Redirect(_))));
}
#[test]
fn layer6_cli_types_valid() {
use portail::cli::*;
let _ = OutputFormat::Text;
let _ = OutputFormat::Json;
let _ = InstallMethod::Auto;
let _ = InstallMethod::Cargo;
let _ = InstallMethod::Nix;
let _ = InstallMethod::Binary;
}
}