use layer0::*;
use rust_decimal::Decimal;
use serde_json::json;
fn _assert_send_sync<T: Send + Sync>() {}
#[test]
fn operator_is_object_safe_send_sync() {
_assert_send_sync::<Box<dyn Operator>>();
}
#[test]
fn arc_operator_is_send_sync() {
_assert_send_sync::<std::sync::Arc<dyn Operator>>();
}
#[test]
fn arc_orchestrator_is_send_sync() {
_assert_send_sync::<std::sync::Arc<dyn Orchestrator>>();
}
#[test]
fn arc_state_store_is_send_sync() {
_assert_send_sync::<std::sync::Arc<dyn StateStore>>();
}
#[test]
fn arc_state_reader_is_send_sync() {
_assert_send_sync::<std::sync::Arc<dyn StateReader>>();
}
#[test]
fn arc_environment_is_send_sync() {
_assert_send_sync::<std::sync::Arc<dyn Environment>>();
}
#[test]
fn orchestrator_is_object_safe_send_sync() {
_assert_send_sync::<Box<dyn Orchestrator>>();
}
#[test]
fn state_store_is_object_safe_send_sync() {
_assert_send_sync::<Box<dyn StateStore>>();
}
#[test]
fn state_reader_is_object_safe_send_sync() {
_assert_send_sync::<Box<dyn StateReader>>();
}
#[test]
fn environment_is_object_safe_send_sync() {
_assert_send_sync::<Box<dyn Environment>>();
}
#[test]
fn agent_id_from_str() {
let id = OperatorId::from("agent-1");
assert_eq!(id.as_str(), "agent-1");
assert_eq!(id.to_string(), "agent-1");
}
#[test]
fn session_id_from_string() {
let id = SessionId::from(String::from("sess-abc"));
assert_eq!(id.as_str(), "sess-abc");
}
#[test]
fn workflow_id_new() {
let id = WorkflowId::new("wf-123");
assert_eq!(id.0, "wf-123");
}
#[test]
fn scope_id_equality() {
let a = ScopeId::new("scope-1");
let b = ScopeId::new("scope-1");
assert_eq!(a, b);
}
#[test]
fn typed_id_serde_round_trip() {
let id = OperatorId::new("test-agent");
let json = serde_json::to_string(&id).unwrap();
let back: OperatorId = serde_json::from_str(&json).unwrap();
assert_eq!(id, back);
}
#[test]
fn content_text_helper() {
let c = Content::text("hello");
assert_eq!(c.as_text(), Some("hello"));
}
#[test]
fn content_blocks_as_text_returns_first_text() {
let c = Content::Blocks(vec![
ContentBlock::Text {
text: "first".into(),
},
ContentBlock::Text {
text: "second".into(),
},
]);
assert_eq!(c.as_text(), Some("first"));
}
#[test]
fn content_blocks_as_text_skips_non_text() {
let c = Content::Blocks(vec![
ContentBlock::ToolResult {
tool_use_id: "id".into(),
content: "result".into(),
is_error: false,
},
ContentBlock::Text {
text: "found".into(),
},
]);
assert_eq!(c.as_text(), Some("found"));
}
#[test]
fn content_text_serde_round_trip() {
let c = Content::text("hello world");
let json = serde_json::to_string(&c).unwrap();
let back: Content = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
#[test]
fn content_blocks_serde_round_trip() {
let c = Content::Blocks(vec![
ContentBlock::Text {
text: "hello".into(),
},
ContentBlock::Image {
source: layer0::content::ImageSource::Url {
url: "https://example.com/img.png".into(),
},
media_type: "image/png".into(),
},
ContentBlock::ToolUse {
id: "tu_1".into(),
name: "read_file".into(),
input: json!({"path": "/tmp/test"}),
},
ContentBlock::ToolResult {
tool_use_id: "tu_1".into(),
content: "file contents".into(),
is_error: false,
},
]);
let json = serde_json::to_string(&c).unwrap();
let back: Content = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
#[test]
fn content_custom_block_round_trip() {
let c = Content::Blocks(vec![ContentBlock::Custom {
content_type: "audio".into(),
data: json!({"codec": "opus", "samples": 48000}),
}]);
let json = serde_json::to_string(&c).unwrap();
let back: Content = serde_json::from_str(&json).unwrap();
assert_eq!(c, back);
}
fn sample_operator_input() -> OperatorInput {
let mut config = OperatorConfig::default();
config.max_turns = Some(10);
config.max_cost = Some(Decimal::new(100, 2)); config.max_duration = Some(DurationMs::from_secs(60));
config.model = Some("claude-sonnet-4-20250514".into());
config.allowed_operators = Some(vec!["read_file".into()]);
config.system_addendum = Some("Be concise.".into());
let mut input = OperatorInput::new(
Content::text("do something"),
layer0::operator::TriggerType::User,
);
input.session = Some(SessionId::new("sess-1"));
input.config = Some(config);
input.metadata = json!({"trace_id": "abc123"});
input
}
#[test]
fn operator_input_serde_round_trip() {
let input = sample_operator_input();
let json = serde_json::to_string(&input).unwrap();
let back: OperatorInput = serde_json::from_str(&json).unwrap();
assert_eq!(input.message, back.message);
assert_eq!(input.trigger, back.trigger);
assert_eq!(input.session, back.session);
assert_eq!(input.metadata, back.metadata);
}
fn sample_operator_output() -> OperatorOutput {
let mut meta = OperatorMetadata::default();
meta.tokens_in = 100;
meta.tokens_out = 50;
meta.cost = Decimal::new(5, 3); meta.turns_used = 1;
meta.sub_dispatches = vec![SubDispatchRecord::new(
"read_file",
DurationMs::from_millis(150),
true,
)];
meta.duration = DurationMs::from_secs(2);
let mut output = OperatorOutput::new(Content::text("done"), ExitReason::Complete);
output.metadata = meta;
output
}
#[test]
fn operator_output_serde_round_trip() {
let output = sample_operator_output();
let json = serde_json::to_string(&output).unwrap();
let back: OperatorOutput = serde_json::from_str(&json).unwrap();
assert_eq!(output.message, back.message);
assert_eq!(output.exit_reason, back.exit_reason);
}
#[test]
fn operator_metadata_default() {
let m = OperatorMetadata::default();
assert_eq!(m.tokens_in, 0);
assert_eq!(m.tokens_out, 0);
assert_eq!(m.cost, Decimal::ZERO);
assert_eq!(m.turns_used, 0);
assert!(m.sub_dispatches.is_empty());
assert_eq!(m.duration, DurationMs::ZERO);
}
#[test]
fn trigger_type_custom_round_trip() {
let t = layer0::operator::TriggerType::Custom("webhook".into());
let json = serde_json::to_string(&t).unwrap();
let back: layer0::operator::TriggerType = serde_json::from_str(&json).unwrap();
assert_eq!(t, back);
}
#[test]
fn exit_reason_custom_round_trip() {
let e = ExitReason::Custom("user_cancelled".into());
let json = serde_json::to_string(&e).unwrap();
let back: ExitReason = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn exit_reason_observer_halt_round_trip() {
let e = ExitReason::InterceptorHalt {
reason: "budget exceeded".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: ExitReason = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn exit_reason_safety_stop_round_trip() {
let e = ExitReason::SafetyStop {
reason: "refusal".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: ExitReason = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn effect_write_memory_round_trip() {
let e = Effect::WriteMemory {
scope: Scope::Session(SessionId::new("s1")),
key: "notes".into(),
value: json!({"text": "remember this"}),
tier: None,
lifetime: None,
content_kind: None,
salience: None,
ttl: None,
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_signal_round_trip() {
let e = Effect::Signal {
target: WorkflowId::new("wf-1"),
payload: SignalPayload::new("user_feedback", json!({"rating": 5})),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_delegate_round_trip() {
let e = Effect::Delegate {
operator: OperatorId::new("helper"),
input: Box::new(sample_operator_input()),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_custom_round_trip() {
let e = Effect::Custom {
effect_type: "send_email".into(),
data: json!({"to": "user@example.com", "subject": "done"}),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn scope_session_round_trip() {
let s = Scope::Session(SessionId::new("s1"));
let json = serde_json::to_string(&s).unwrap();
let back: Scope = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn scope_agent_round_trip() {
let s = Scope::Operator {
workflow: WorkflowId::new("wf-1"),
operator: OperatorId::new("a-1"),
};
let json = serde_json::to_string(&s).unwrap();
let back: Scope = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn scope_global_round_trip() {
let s = Scope::Global;
let json = serde_json::to_string(&s).unwrap();
let back: Scope = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn scope_custom_round_trip() {
let s = Scope::Custom("tenant:acme".into());
let json = serde_json::to_string(&s).unwrap();
let back: Scope = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn isolation_boundary_custom_round_trip() {
let b = layer0::environment::IsolationBoundary::Custom {
boundary_type: "firecracker".into(),
config: json!({"kernel": "vmlinux", "rootfs": "rootfs.ext4"}),
};
let json = serde_json::to_string(&b).unwrap();
let back: layer0::environment::IsolationBoundary = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn isolation_boundary_all_variants_round_trip() {
let variants: Vec<layer0::environment::IsolationBoundary> = vec![
layer0::environment::IsolationBoundary::Process,
layer0::environment::IsolationBoundary::Container {
image: Some("ubuntu:24.04".into()),
},
layer0::environment::IsolationBoundary::Gvisor,
layer0::environment::IsolationBoundary::MicroVm,
layer0::environment::IsolationBoundary::Wasm {
runtime: Some("wasmtime".into()),
},
layer0::environment::IsolationBoundary::NetworkPolicy {
rules: vec![{
let mut rule = layer0::environment::NetworkRule::new(
"10.0.0.0/8",
layer0::environment::NetworkAction::Allow,
);
rule.port = Some(443);
rule
}],
},
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let back: layer0::environment::IsolationBoundary = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
}
#[test]
fn environment_spec_round_trip() {
let mut resources = layer0::environment::ResourceLimits::default();
resources.cpu = Some("1.0".into());
resources.memory = Some("2Gi".into());
let mut api_rule = layer0::environment::NetworkRule::new(
"api.anthropic.com",
layer0::environment::NetworkAction::Allow,
);
api_rule.port = Some(443);
let mut spec = EnvironmentSpec::default();
spec.isolation = vec![layer0::environment::IsolationBoundary::Container {
image: Some("python:3.12".into()),
}];
spec.credentials = vec![layer0::environment::CredentialRef::new(
"api-key",
layer0::secret::SecretSource::OsKeystore {
service: "test".into(),
},
layer0::environment::CredentialInjection::EnvVar {
var_name: "API_KEY".into(),
},
)];
spec.resources = Some(resources);
spec.network = Some(layer0::environment::NetworkPolicy::new(
layer0::environment::NetworkAction::Deny,
vec![api_rule],
));
let json = serde_json::to_string(&spec).unwrap();
let back: EnvironmentSpec = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn budget_event_cost_incurred_round_trip() {
let e = BudgetEvent::CostIncurred {
operator: OperatorId::new("a1"),
cost: Decimal::new(5, 3),
cumulative: Decimal::new(150, 3),
};
let json = serde_json::to_string(&e).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn compaction_event_round_trip() {
let e = CompactionEvent::ContextPressure {
operator: OperatorId::new("a1"),
fill_percent: 0.85,
tokens_used: 85000,
tokens_available: 15000,
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn query_payload_round_trip() {
let q = QueryPayload::new("status", json!({"verbose": true}));
let json = serde_json::to_string(&q).unwrap();
let back: QueryPayload = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn search_result_round_trip() {
let mut r = SearchResult::new("notes/meeting", 0.95);
r.snippet = Some("discussed the architecture...".into());
let json = serde_json::to_string(&r).unwrap();
let back: SearchResult = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn log_level_round_trip() {
let levels = vec![
layer0::effect::LogLevel::Trace,
layer0::effect::LogLevel::Debug,
layer0::effect::LogLevel::Info,
layer0::effect::LogLevel::Warn,
layer0::effect::LogLevel::Error,
];
for l in levels {
let json = serde_json::to_string(&l).unwrap();
let back: layer0::effect::LogLevel = serde_json::from_str(&json).unwrap();
assert_eq!(l, back);
}
}
#[test]
fn decimal_serializes_as_string_not_number() {
let cost = Decimal::new(123, 2); let json = serde_json::to_value(cost).unwrap();
assert!(
json.is_string(),
"Decimal must serialize as a JSON string, got: {json}"
);
assert_eq!(json.as_str().unwrap(), "1.23");
}
#[test]
fn decimal_zero_serializes_as_string() {
let cost = Decimal::ZERO;
let json = serde_json::to_value(cost).unwrap();
assert!(
json.is_string(),
"Decimal::ZERO must serialize as string, got: {json}"
);
assert_eq!(json.as_str().unwrap(), "0");
}
#[test]
fn decimal_in_operator_metadata_wire_format() {
let mut meta = OperatorMetadata::default();
meta.tokens_in = 100;
meta.tokens_out = 50;
meta.cost = Decimal::new(5, 3); meta.turns_used = 1;
let json = serde_json::to_value(&meta).unwrap();
let cost_val = &json["cost"];
assert!(
cost_val.is_string(),
"cost in OperatorMetadata must be string, got: {cost_val}"
);
assert_eq!(cost_val.as_str().unwrap(), "0.005");
}
#[test]
fn content_text_serializes_as_bare_string() {
let c = Content::text("hello");
let json = serde_json::to_value(&c).unwrap();
assert!(
json.is_string(),
"Content::Text must serialize as bare string, got: {json}"
);
assert_eq!(json.as_str().unwrap(), "hello");
}
#[test]
fn content_blocks_serializes_as_array() {
let c = Content::Blocks(vec![ContentBlock::Text {
text: "hello".into(),
}]);
let json = serde_json::to_value(&c).unwrap();
assert!(
json.is_array(),
"Content::Blocks must serialize as array, got: {json}"
);
}
#[test]
fn content_text_and_blocks_are_structurally_distinct() {
let text = Content::text("hello");
let blocks = Content::Blocks(vec![ContentBlock::Text {
text: "hello".into(),
}]);
let text_json = serde_json::to_string(&text).unwrap();
let blocks_json = serde_json::to_string(&blocks).unwrap();
let text_back: Content = serde_json::from_str(&text_json).unwrap();
let blocks_back: Content = serde_json::from_str(&blocks_json).unwrap();
assert_eq!(text, text_back);
assert_eq!(blocks, blocks_back);
assert_ne!(text_json, blocks_json);
}
#[test]
fn duration_ms_serializes_as_integer() {
let d = DurationMs::from_millis(1500);
let json = serde_json::to_value(d).unwrap();
assert!(
json.is_u64(),
"DurationMs must serialize as integer, got: {json}"
);
assert_eq!(json.as_u64().unwrap(), 1500);
}
#[test]
fn duration_ms_zero_serializes_as_zero() {
let d = DurationMs::ZERO;
let json = serde_json::to_value(d).unwrap();
assert_eq!(json.as_u64().unwrap(), 0);
}
#[test]
fn trigger_type_custom_preserves_unknown_variant() {
let json = r#"{"custom":"iot_sensor_event"}"#;
let t: layer0::operator::TriggerType = serde_json::from_str(json).unwrap();
assert_eq!(
t,
layer0::operator::TriggerType::Custom("iot_sensor_event".into())
);
}
#[test]
fn exit_reason_custom_preserves_unknown_variant() {
let json = r#"{"custom":"human_takeover"}"#;
let e: ExitReason = serde_json::from_str(json).unwrap();
assert_eq!(e, ExitReason::Custom("human_takeover".into()));
}
#[test]
fn scope_custom_preserves_unknown_scope() {
let json = r#"{"custom":"tenant:acme-corp"}"#;
let s: Scope = serde_json::from_str(json).unwrap();
assert_eq!(s, Scope::Custom("tenant:acme-corp".into()));
}
#[test]
fn effect_custom_preserves_unknown_effect() {
let json = r##"{"type":"custom","effect_type":"send_slack","data":{"channel":"#ops"}}"##;
let e: Effect = serde_json::from_str(json).unwrap();
let reserialized = serde_json::to_string(&e).unwrap();
let reparsed: serde_json::Value = serde_json::from_str(&reserialized).unwrap();
assert_eq!(reparsed["type"], "custom");
assert_eq!(reparsed["effect_type"], "send_slack");
}
#[test]
fn content_block_custom_preserves_unknown_modality() {
let json =
r#"{"type":"custom","content_type":"audio","data":{"codec":"opus","sample_rate":48000}}"#;
let b: ContentBlock = serde_json::from_str(json).unwrap();
let reserialized = serde_json::to_string(&b).unwrap();
let reparsed: serde_json::Value = serde_json::from_str(&reserialized).unwrap();
assert_eq!(reparsed["type"], "custom");
assert_eq!(reparsed["content_type"], "audio");
assert_eq!(reparsed["data"]["codec"], "opus");
}
#[test]
fn isolation_boundary_custom_preserves_unknown_isolation() {
let json = r#"{"type":"custom","boundary_type":"kata_container","config":{"runtime":"qemu"}}"#;
let b: layer0::environment::IsolationBoundary = serde_json::from_str(json).unwrap();
let reserialized = serde_json::to_string(&b).unwrap();
let reparsed: serde_json::Value = serde_json::from_str(&reserialized).unwrap();
assert_eq!(reparsed["type"], "custom");
assert_eq!(reparsed["boundary_type"], "kata_container");
}
fn _takes_state_reader<T: StateReader + ?Sized>(_r: &T) {}
fn _takes_state_store<T: StateStore>(s: &T) {
_takes_state_reader(s);
}
#[test]
fn operator_error_display() {
let e = OperatorError::Model("rate limited".into());
assert_eq!(e.to_string(), "model error: rate limited");
let e = OperatorError::SubDispatch {
operator: "bash".into(),
message: "command failed".into(),
};
assert_eq!(e.to_string(), "sub-dispatch error in bash: command failed");
}
#[test]
fn orch_error_display() {
let e = OrchError::OperatorNotFound("missing-agent".into());
assert_eq!(e.to_string(), "operator not found: missing-agent");
}
#[test]
fn state_error_display() {
let e = StateError::NotFound {
scope: "session".into(),
key: "notes".into(),
};
assert_eq!(e.to_string(), "not found: session/notes");
}
#[test]
fn env_error_display() {
let e = EnvError::ProvisionFailed("docker not available".into());
assert_eq!(e.to_string(), "provisioning failed: docker not available");
}
#[test]
fn operator_error_display_remaining_variants() {
assert_eq!(
OperatorError::ContextAssembly("bad ctx".into()).to_string(),
"context assembly failed: bad ctx"
);
assert_eq!(
OperatorError::Retryable("timeout".into()).to_string(),
"retryable: timeout"
);
assert_eq!(
OperatorError::NonRetryable("invalid".into()).to_string(),
"non-retryable: invalid"
);
let boxed: Box<dyn std::error::Error + Send + Sync> = "inner error".into();
assert_eq!(OperatorError::Other(boxed).to_string(), "inner error");
}
#[test]
fn orch_error_display_remaining_variants() {
assert_eq!(
OrchError::WorkflowNotFound("wf-1".into()).to_string(),
"workflow not found: wf-1"
);
assert_eq!(
OrchError::DispatchFailed("timeout".into()).to_string(),
"dispatch failed: timeout"
);
assert_eq!(
OrchError::SignalFailed("no handler".into()).to_string(),
"signal delivery failed: no handler"
);
let inner = OperatorError::Model("provider down".into());
assert_eq!(
OrchError::OperatorError(inner).to_string(),
"operator error: model error: provider down"
);
let boxed: Box<dyn std::error::Error + Send + Sync> = "orch inner".into();
assert_eq!(OrchError::Other(boxed).to_string(), "orch inner");
}
#[test]
fn state_error_display_remaining_variants() {
assert_eq!(
StateError::WriteFailed("disk full".into()).to_string(),
"write failed: disk full"
);
assert_eq!(
StateError::Serialization("invalid json".into()).to_string(),
"serialization error: invalid json"
);
let boxed: Box<dyn std::error::Error + Send + Sync> = "state inner".into();
assert_eq!(StateError::Other(boxed).to_string(), "state inner");
}
#[test]
fn env_error_display_remaining_variants() {
assert_eq!(
EnvError::IsolationViolation("escaped sandbox".into()).to_string(),
"isolation violation: escaped sandbox"
);
assert_eq!(
EnvError::CredentialFailed("secret not found".into()).to_string(),
"credential injection failed: secret not found"
);
assert_eq!(
EnvError::ResourceExceeded("OOM".into()).to_string(),
"resource limit exceeded: OOM"
);
let inner = OperatorError::Model("provider down".into());
assert_eq!(
EnvError::OperatorError(inner).to_string(),
"operator error: model error: provider down"
);
let boxed: Box<dyn std::error::Error + Send + Sync> = "env inner".into();
assert_eq!(EnvError::Other(boxed).to_string(), "env inner");
}
#[test]
fn budget_decision_downgrade_round_trip() {
let d = layer0::lifecycle::BudgetDecision::DowngradeModel {
from: "claude-opus-4-20250514".into(),
to: "claude-haiku-4-5-20251001".into(),
};
let json = serde_json::to_string(&d).unwrap();
let back: layer0::lifecycle::BudgetDecision = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_log_round_trip() {
let e = Effect::Log {
level: layer0::effect::LogLevel::Info,
message: "turn completed".into(),
data: Some(json!({"duration_ms": 1500})),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_handoff_round_trip() {
let e = Effect::Handoff {
operator: OperatorId::new("specialist"),
state: json!({"context": "user needs help with billing"}),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_delete_memory_round_trip() {
let e = Effect::DeleteMemory {
scope: Scope::Global,
key: "temp_notes".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn memory_tier_round_trip() {
use layer0::MemoryTier;
let tiers = [MemoryTier::Hot, MemoryTier::Warm, MemoryTier::Cold];
for tier in tiers {
let json = serde_json::to_string(&tier).unwrap();
let back: MemoryTier = serde_json::from_str(&json).unwrap();
assert_eq!(tier, back);
}
}
#[test]
fn store_options_default() {
use layer0::StoreOptions;
let opts = StoreOptions::default();
assert!(opts.tier.is_none());
}
#[test]
fn write_memory_with_tier_round_trip() {
use layer0::MemoryTier;
let e = Effect::WriteMemory {
scope: Scope::Global,
key: "k".into(),
value: json!(1),
tier: Some(MemoryTier::Warm),
lifetime: None,
content_kind: None,
salience: None,
ttl: None,
};
let json = serde_json::to_value(&e).unwrap();
let back: Effect = serde_json::from_value(json.clone()).unwrap();
let json2 = serde_json::to_value(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn write_memory_tier_omitted_when_none() {
let e = Effect::WriteMemory {
scope: Scope::Global,
key: "k".into(),
value: json!(1),
tier: None,
lifetime: None,
content_kind: None,
salience: None,
ttl: None,
};
let json = serde_json::to_value(&e).unwrap();
assert!(
json.get("tier").is_none(),
"tier: None must not appear in serialized JSON, got: {json}"
);
}
#[test]
fn test_lifetime_serde() {
use layer0::Lifetime;
for v in [Lifetime::Transient, Lifetime::Session, Lifetime::Durable] {
let json = serde_json::to_string(&v).unwrap();
let back: Lifetime = serde_json::from_str(&json).unwrap();
assert_eq!(v, back);
}
}
#[test]
fn test_content_kind_serde() {
use layer0::ContentKind;
let cases = [
ContentKind::Episodic,
ContentKind::Semantic,
ContentKind::Procedural,
ContentKind::Structural,
ContentKind::Custom("domain::MyKind".into()),
];
for v in cases {
let json = serde_json::to_string(&v).unwrap();
let back: ContentKind = serde_json::from_str(&json).unwrap();
assert_eq!(v, back);
}
}
#[test]
fn test_store_options_serde() {
use layer0::state::StoreOptions;
use layer0::{ContentKind, DurationMs, Lifetime, MemoryTier};
let opts = StoreOptions {
tier: Some(MemoryTier::Hot),
lifetime: Some(Lifetime::Durable),
content_kind: Some(ContentKind::Semantic),
salience: Some(0.9),
ttl: Some(DurationMs::from_secs(3600)),
};
let json = serde_json::to_string(&opts).unwrap();
let back: StoreOptions = serde_json::from_str(&json).unwrap();
assert_eq!(back.lifetime, Some(Lifetime::Durable));
assert_eq!(back.content_kind, Some(ContentKind::Semantic));
assert_eq!(back.salience, Some(0.9));
}
#[test]
fn test_write_memory_effect_with_new_fields_serde() {
use layer0::{ContentKind, DurationMs, Lifetime};
let e = Effect::WriteMemory {
scope: Scope::Global,
key: "k".into(),
value: serde_json::json!(42),
tier: None,
lifetime: Some(Lifetime::Session),
content_kind: Some(ContentKind::Episodic),
salience: Some(0.5),
ttl: Some(DurationMs::from_millis(60_000)),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
if let Effect::WriteMemory {
lifetime,
content_kind,
salience,
ttl,
..
} = back
{
assert_eq!(lifetime, Some(Lifetime::Session));
assert_eq!(content_kind, Some(ContentKind::Episodic));
assert_eq!(salience, Some(0.5));
assert!(ttl.is_some());
} else {
panic!("wrong variant");
}
}
#[test]
fn operator_config_default_all_none() {
let c = OperatorConfig::default();
assert!(c.max_turns.is_none());
assert!(c.max_cost.is_none());
assert!(c.max_duration.is_none());
assert!(c.model.is_none());
assert!(c.allowed_operators.is_none());
assert!(c.system_addendum.is_none());
}
#[test]
fn credential_injection_variants_round_trip() {
let variants: Vec<layer0::environment::CredentialInjection> = vec![
layer0::environment::CredentialInjection::EnvVar {
var_name: "API_KEY".into(),
},
layer0::environment::CredentialInjection::File {
path: "/run/secrets/key".into(),
},
layer0::environment::CredentialInjection::Sidecar,
];
for v in variants {
let json = serde_json::to_string(&v).unwrap();
let back: layer0::environment::CredentialInjection = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
}
#[test]
fn compaction_pre_flush_round_trip() {
let e = CompactionEvent::PreCompactionFlush {
operator: OperatorId::new("a1"),
scope: Scope::Session(SessionId::new("s1")),
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn compaction_complete_round_trip() {
let e = CompactionEvent::CompactionComplete {
operator: OperatorId::new("a1"),
strategy: "sliding_window".into(),
tokens_freed: 50000,
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn compaction_provider_managed_round_trip() {
let e = CompactionEvent::ProviderManaged {
operator: OperatorId::new("a1"),
provider: "anthropic".into(),
tokens_before: 100000,
tokens_after: 50000,
summary: Some(Content::text("Summarized prior conversation")),
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn compaction_failed_round_trip() {
let e = CompactionEvent::CompactionFailed {
operator: OperatorId::new("a1"),
error: "timeout during summarization".into(),
strategy: "sliding_window".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn compaction_skipped_round_trip() {
let e = CompactionEvent::CompactionSkipped {
operator: OperatorId::new("a1"),
reason: "below threshold".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn flush_failed_round_trip() {
let e = CompactionEvent::FlushFailed {
operator: OperatorId::new("a1"),
scope: Scope::Session(SessionId::new("s1")),
key: "notes".into(),
error: "write timeout".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn compaction_quality_round_trip() {
let e = CompactionEvent::CompactionQuality {
operator: OperatorId::new("a1"),
tokens_before: 80000,
tokens_after: 40000,
items_preserved: 10,
items_lost: 5,
};
let json = serde_json::to_string(&e).unwrap();
let back: CompactionEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn budget_event_all_variants_round_trip() {
let events: Vec<BudgetEvent> = vec![
BudgetEvent::BudgetWarning {
workflow: WorkflowId::new("wf-1"),
spent: Decimal::new(800, 2),
limit: Decimal::new(1000, 2),
},
BudgetEvent::BudgetAction {
workflow: WorkflowId::new("wf-1"),
action: layer0::lifecycle::BudgetDecision::Continue,
},
BudgetEvent::BudgetAction {
workflow: WorkflowId::new("wf-1"),
action: layer0::lifecycle::BudgetDecision::HaltWorkflow,
},
BudgetEvent::BudgetAction {
workflow: WorkflowId::new("wf-1"),
action: layer0::lifecycle::BudgetDecision::RequestIncrease {
amount: Decimal::new(500, 2),
},
},
];
for event in events {
let json = serde_json::to_string(&event).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
}
#[test]
fn budget_event_step_limit_approaching_round_trip() {
let e = BudgetEvent::StepLimitApproaching {
operator: OperatorId::new("a1"),
current: 8,
max: 10,
};
let json = serde_json::to_string(&e).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn budget_event_step_limit_reached_round_trip() {
let e = BudgetEvent::StepLimitReached {
operator: OperatorId::new("a1"),
total_sub_dispatches: 10,
};
let json = serde_json::to_string(&e).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn budget_event_loop_detected_round_trip() {
let e = BudgetEvent::LoopDetected {
operator: OperatorId::new("a1"),
operator_name: "search".to_string(),
consecutive_count: 3,
max: 3,
};
let json = serde_json::to_string(&e).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn budget_event_timeout_approaching_round_trip() {
let e = BudgetEvent::TimeoutApproaching {
operator: OperatorId::new("a1"),
elapsed: DurationMs::from_millis(8000),
max_duration: DurationMs::from_millis(10000),
};
let json = serde_json::to_string(&e).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn budget_event_timeout_reached_round_trip() {
let e = BudgetEvent::TimeoutReached {
operator: OperatorId::new("a1"),
elapsed: DurationMs::from_millis(10050),
};
let json = serde_json::to_string(&e).unwrap();
let back: BudgetEvent = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn exit_reason_all_variants_round_trip() {
let reasons = vec![
ExitReason::Complete,
ExitReason::MaxTurns,
ExitReason::BudgetExhausted,
ExitReason::CircuitBreaker,
ExitReason::Timeout,
ExitReason::InterceptorHalt {
reason: "safety".into(),
},
ExitReason::Error,
ExitReason::Custom("special".into()),
];
for reason in &reasons {
let json = serde_json::to_string(reason).unwrap();
let back: ExitReason = serde_json::from_str(&json).unwrap();
assert_eq!(*reason, back);
}
}
use layer0::secret::{SecretAccessEvent, SecretAccessOutcome, SecretSource};
#[test]
fn secret_source_all_variants_round_trip() {
let sources = vec![
SecretSource::Vault {
mount: "secret".into(),
path: "data/api-key".into(),
},
SecretSource::AwsSecretsManager {
secret_id: "arn:aws:secretsmanager:us-east-1:123:secret:api-key".into(),
region: Some("us-east-1".into()),
},
SecretSource::GcpSecretManager {
project: "my-project".into(),
secret_id: "api-key".into(),
},
SecretSource::AzureKeyVault {
vault_url: "https://myvault.vault.azure.net".into(),
secret_name: "api-key".into(),
},
SecretSource::OsKeystore {
service: "skg-test".into(),
},
SecretSource::Kubernetes {
namespace: "default".into(),
name: "api-secrets".into(),
key: "anthropic-key".into(),
},
SecretSource::Hardware { slot: "9a".into() },
SecretSource::Custom {
provider: "1password".into(),
config: json!({"vault": "Engineering"}),
},
];
for source in sources {
let json = serde_json::to_string(&source).unwrap();
let back: SecretSource = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
}
#[test]
fn secret_access_outcome_all_variants_round_trip() {
let outcomes = vec![
SecretAccessOutcome::Resolved,
SecretAccessOutcome::Denied,
SecretAccessOutcome::Failed,
SecretAccessOutcome::Renewed,
SecretAccessOutcome::Released,
];
for outcome in &outcomes {
let json = serde_json::to_string(outcome).unwrap();
let back: SecretAccessOutcome = serde_json::from_str(&json).unwrap();
assert_eq!(*outcome, back);
}
}
#[test]
fn secret_access_event_round_trip() {
let event = SecretAccessEvent::new(
"anthropic-api-key",
SecretSource::Vault {
mount: "secret".into(),
path: "data/api-key".into(),
},
SecretAccessOutcome::Resolved,
1740000000000,
);
let json = serde_json::to_string(&event).unwrap();
let back: SecretAccessEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back.credential_name, "anthropic-api-key");
assert_eq!(back.outcome, SecretAccessOutcome::Resolved);
assert_eq!(back.timestamp_ms, 1740000000000);
}
#[test]
fn secret_access_event_with_all_fields() {
let mut event = SecretAccessEvent::new(
"db-password",
SecretSource::AwsSecretsManager {
secret_id: "prod/db/password".into(),
region: Some("us-west-2".into()),
},
SecretAccessOutcome::Denied,
1740000000000,
);
event.lease_id = Some("lease-abc-123".into());
event.lease_ttl_secs = Some(3600);
event.reason = Some("policy: requires mfa".into());
event.workflow_id = Some("wf-001".into());
event.operator_id = Some("agent-research".into());
event.trace_id = Some("trace-xyz".into());
let json = serde_json::to_string(&event).unwrap();
let back: SecretAccessEvent = serde_json::from_str(&json).unwrap();
assert_eq!(back.lease_id.as_deref(), Some("lease-abc-123"));
assert_eq!(back.lease_ttl_secs, Some(3600));
assert_eq!(back.reason.as_deref(), Some("policy: requires mfa"));
assert_eq!(back.workflow_id.as_deref(), Some("wf-001"));
assert_eq!(back.operator_id.as_deref(), Some("agent-research"));
assert_eq!(back.trace_id.as_deref(), Some("trace-xyz"));
}
#[test]
fn secret_source_kind_tags() {
assert_eq!(
SecretSource::Vault {
mount: "s".into(),
path: "p".into()
}
.kind(),
"vault"
);
assert_eq!(
SecretSource::AwsSecretsManager {
secret_id: "x".into(),
region: None
}
.kind(),
"aws"
);
assert_eq!(
SecretSource::GcpSecretManager {
project: "p".into(),
secret_id: "s".into()
}
.kind(),
"gcp"
);
assert_eq!(
SecretSource::AzureKeyVault {
vault_url: "u".into(),
secret_name: "n".into()
}
.kind(),
"azure"
);
assert_eq!(
SecretSource::OsKeystore {
service: "s".into()
}
.kind(),
"os_keystore"
);
assert_eq!(
SecretSource::Kubernetes {
namespace: "n".into(),
name: "n".into(),
key: "k".into()
}
.kind(),
"kubernetes"
);
assert_eq!(
SecretSource::Hardware { slot: "9a".into() }.kind(),
"hardware"
);
assert_eq!(
SecretSource::Custom {
provider: "p".into(),
config: json!({})
}
.kind(),
"custom"
);
}
#[test]
fn credential_ref_with_source_round_trip() {
use layer0::environment::{CredentialInjection, CredentialRef};
let cred = CredentialRef::new(
"anthropic-api-key",
SecretSource::Vault {
mount: "secret".into(),
path: "data/anthropic".into(),
},
CredentialInjection::EnvVar {
var_name: "ANTHROPIC_API_KEY".into(),
},
);
let json = serde_json::to_string(&cred).unwrap();
let back: CredentialRef = serde_json::from_str(&json).unwrap();
assert_eq!(back.name, "anthropic-api-key");
}
#[test]
fn search_options_default_all_none() {
use layer0::SearchOptions;
let opts = SearchOptions::default();
assert!(opts.min_score.is_none());
assert!(opts.content_kind.is_none());
assert!(opts.tier.is_none());
assert!(opts.max_depth.is_none());
}
#[test]
fn search_options_full_round_trip() {
use layer0::{ContentKind, MemoryTier, SearchOptions};
let opts = SearchOptions {
min_score: Some(0.75),
content_kind: Some(ContentKind::Semantic),
tier: Some(MemoryTier::Hot),
max_depth: Some(3),
};
let json = serde_json::to_string(&opts).unwrap();
let back: SearchOptions = serde_json::from_str(&json).unwrap();
assert_eq!(back.min_score, Some(0.75));
assert_eq!(back.content_kind, Some(ContentKind::Semantic));
assert_eq!(back.tier, Some(MemoryTier::Hot));
assert_eq!(back.max_depth, Some(3));
}
#[test]
fn search_options_empty_omits_all_fields() {
use layer0::SearchOptions;
let opts = SearchOptions::default();
let json = serde_json::to_value(&opts).unwrap();
assert!(json.get("min_score").is_none());
assert!(json.get("content_kind").is_none());
assert!(json.get("tier").is_none());
assert!(json.get("max_depth").is_none());
}
#[test]
fn memory_link_new_round_trip() {
use layer0::MemoryLink;
let link = MemoryLink::new("key/a", "key/b", "references");
assert_eq!(link.from_key, "key/a");
assert_eq!(link.to_key, "key/b");
assert_eq!(link.relation, "references");
assert!(link.metadata.is_none());
let json = serde_json::to_string(&link).unwrap();
let back: MemoryLink = serde_json::from_str(&json).unwrap();
assert_eq!(back.from_key, link.from_key);
assert_eq!(back.to_key, link.to_key);
assert_eq!(back.relation, link.relation);
assert!(back.metadata.is_none());
}
#[test]
fn memory_link_with_metadata_round_trip() {
use layer0::MemoryLink;
let mut link = MemoryLink::new("a", "b", "supersedes");
link.metadata = Some(json!({"weight": 0.9}));
let json = serde_json::to_string(&link).unwrap();
let back: MemoryLink = serde_json::from_str(&json).unwrap();
assert_eq!(back.metadata.as_ref().unwrap()["weight"], json!(0.9));
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
}
#[test]
fn effect_link_memory_round_trip() {
use layer0::MemoryLink;
let e = Effect::LinkMemory {
scope: Scope::Global,
link: MemoryLink::new("notes/meeting", "decisions/arch", "references"),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["type"], "link_memory");
}
#[test]
fn effect_unlink_memory_round_trip() {
let e = Effect::UnlinkMemory {
scope: Scope::Session(SessionId::new("s1")),
from_key: "notes/meeting".into(),
to_key: "decisions/arch".into(),
relation: "references".into(),
};
let json = serde_json::to_string(&e).unwrap();
let back: Effect = serde_json::from_str(&json).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
assert_eq!(json, json2);
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(val["type"], "unlink_memory");
}
fn _assert_state_store_still_object_safe(_: &dyn StateStore) {}
fn _assert_state_reader_still_object_safe(_: &dyn StateReader) {}