use std::sync::Arc;
use nika::ast::{InferParams, TaskAction};
use nika::binding::ResolvedBindings;
use nika::error::NikaError;
use nika::event::EventLog;
use nika::runtime::TaskExecutor;
use nika::store::RunContext;
use pretty_assertions::assert_eq;
use serde_json::json;
fn create_executor() -> TaskExecutor {
TaskExecutor::new("claude", None, None, EventLog::new())
}
fn create_mock_executor() -> TaskExecutor {
TaskExecutor::new("claude", None, None, EventLog::new())
}
fn infer_params(prompt: &str) -> InferParams {
InferParams {
prompt: prompt.to_string(),
..Default::default()
}
}
struct EnvGuard {
vars: Vec<(String, Option<String>)>,
}
impl EnvGuard {
fn new(var_names: &[&str]) -> Self {
let vars = var_names
.iter()
.map(|name| {
let current = std::env::var(name).ok();
(name.to_string(), current)
})
.collect();
Self { vars }
}
fn clear_all(&self) {
for (name, _) in &self.vars {
std::env::remove_var(name);
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (name, value) in &self.vars {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| match value {
Some(v) => std::env::set_var(name, v),
None => std::env::remove_var(name),
}));
}
}
}
#[tokio::test]
#[ignore = "rig-core panics on missing API key - run separately without env vars"]
#[should_panic(expected = "ANTHROPIC_API_KEY not set")]
async fn test_infer_missing_api_key_panics() {
let guard = EnvGuard::new(&["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]);
guard.clear_all();
let executor = create_executor();
let task_id: Arc<str> = "test_task".into();
let action = TaskAction::Infer {
infer: infer_params("Generate a headline"),
};
let bindings = ResolvedBindings::new();
let datastore = RunContext::new();
let _ = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
}
#[test]
fn test_missing_api_key_error_type() {
let err = NikaError::MissingApiKey {
provider: "claude".to_string(),
};
assert_eq!(err.code(), "NIKA-032");
let msg = err.to_string();
assert!(msg.contains("NIKA-032"));
assert!(msg.contains("claude"));
assert!(msg.contains("Missing API key"));
}
#[tokio::test]
async fn test_infer_template_resolution_failure() {
let executor = create_mock_executor();
let task_id: Arc<str> = "test_template".into();
let action = TaskAction::Infer {
infer: infer_params("Generate based on: {{with.context}}"),
};
let bindings = ResolvedBindings::new(); let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err(), "Should fail with missing binding");
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("context") || err_str.contains("Alias"),
"Error should mention missing alias 'context': {}",
err_str
);
assert!(
matches!(err, NikaError::TemplateError { .. }),
"Expected NikaError::TemplateError, got: {:?}",
err
);
}
#[tokio::test]
async fn test_infer_template_multiple_missing_aliases() {
let executor = create_mock_executor();
let task_id: Arc<str> = "test_multi".into();
let action = TaskAction::Infer {
infer: infer_params("Combine {{with.first}} with {{with.second}} and {{with.third}}"),
};
let bindings = ResolvedBindings::new();
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("first")
|| err_str.contains("second")
|| err_str.contains("third")
|| err_str.contains("Alias"),
"Error should mention missing aliases: {}",
err_str
);
}
#[tokio::test]
async fn test_infer_template_nested_path_failure() {
let executor = create_mock_executor();
let task_id: Arc<str> = "test_nested".into();
let action = TaskAction::Infer {
infer: infer_params("Value: {{with.data.nonexistent.field}}"),
};
let mut bindings = ResolvedBindings::new();
bindings.set("data", json!({"name": "test", "value": 42}));
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("nonexistent")
|| err_str.contains("not found")
|| err_str.contains("NIKA-052") || err_str.contains("NIKA-073"), "Error should indicate path not found: {}",
err_str
);
}
#[tokio::test]
async fn test_infer_template_null_value_error() {
let executor = create_mock_executor();
let task_id: Arc<str> = "test_null".into();
let action = TaskAction::Infer {
infer: infer_params("Result: {{with.result}}"),
};
let mut bindings = ResolvedBindings::new();
bindings.set("result", json!(null));
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("NIKA-072") || err_str.contains("Null value") || err_str.contains("null"),
"Error should indicate null value: {}",
err_str
);
assert!(
matches!(err, NikaError::NullValue { .. }),
"Expected NikaError::NullValue, got: {:?}",
err
);
}
#[tokio::test]
async fn test_infer_unknown_provider() {
let executor = create_executor();
let task_id: Arc<str> = "test_unknown".into();
let action = TaskAction::Infer {
infer: InferParams {
prompt: "Test prompt".to_string(),
provider: Some("unknown_provider".to_string()),
..Default::default()
},
};
let bindings = ResolvedBindings::new();
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("unknown_provider")
|| err_str.contains("Unknown")
|| err_str.contains("not configured"),
"Error should mention unknown provider: {}",
err_str
);
}
#[tokio::test]
async fn test_infer_template_resolution_success() {
let _guard = EnvGuard::new(&["ANTHROPIC_API_KEY"]);
if std::env::var("ANTHROPIC_API_KEY").is_err() {
return;
}
let executor = create_executor();
let task_id: Arc<str> = "test_success".into();
let action = TaskAction::Infer {
infer: infer_params("Generate headline for: {{with.product}}"),
};
let mut bindings = ResolvedBindings::new();
bindings.set("product", json!("QR Code AI"));
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
match result {
Ok(response) => {
assert!(!response.is_empty(), "Response should not be empty");
}
Err(e) => {
let err_str = e.to_string();
assert!(
!err_str.contains("Template")
&& !err_str.contains("Alias")
&& !err_str.contains("NIKA-04"),
"Template resolution should succeed, got template error: {}",
err_str
);
}
}
}
#[tokio::test]
async fn test_infer_template_invalid_traversal_on_string() {
let executor = create_mock_executor();
let task_id: Arc<str> = "test_traverse_string".into();
let action = TaskAction::Infer {
infer: infer_params("Get field: {{with.name.field}}"),
};
let mut bindings = ResolvedBindings::new();
bindings.set("name", json!("just a string"));
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("NIKA-073") || err_str.contains("string") || err_str.contains("traverse"),
"Error should indicate invalid traversal on string: {}",
err_str
);
assert!(
matches!(err, NikaError::InvalidTraversal { .. }),
"Expected NikaError::InvalidTraversal, got: {:?}",
err
);
}
#[tokio::test]
async fn test_infer_template_invalid_traversal_on_number() {
let executor = create_mock_executor();
let task_id: Arc<str> = "test_traverse_number".into();
let action = TaskAction::Infer {
infer: infer_params("Get value: {{with.count.property}}"),
};
let mut bindings = ResolvedBindings::new();
bindings.set("count", json!(42));
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = err.to_string();
assert!(
err_str.contains("NIKA-073") || err_str.contains("number"),
"Error should indicate invalid traversal on number: {}",
err_str
);
}
#[tokio::test]
#[ignore = "rig-core panics without API key - run with ANTHROPIC_API_KEY set"]
async fn test_infer_empty_prompt() {
if std::env::var("ANTHROPIC_API_KEY").is_err() {
return; }
let executor = create_executor();
let task_id: Arc<str> = "test_empty".into();
let action = TaskAction::Infer {
infer: infer_params(""),
};
let bindings = ResolvedBindings::new();
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
if let Err(e) = result {
let err_str = e.to_string();
assert!(
!err_str.contains("panic"),
"Should not panic on empty prompt"
);
}
}
#[tokio::test]
#[ignore = "rig-core panics without API key - run with ANTHROPIC_API_KEY set"]
async fn test_infer_whitespace_in_template() {
if std::env::var("ANTHROPIC_API_KEY").is_err() {
return; }
let executor = create_mock_executor();
let task_id: Arc<str> = "test_whitespace".into();
let action = TaskAction::Infer {
infer: infer_params("Value: {{ with.data }}"),
};
let mut bindings = ResolvedBindings::new();
bindings.set("data", json!("test value"));
let datastore = RunContext::new();
let result = executor
.execute(&task_id, &action, &bindings, &datastore, None)
.await;
if let Err(e) = result {
let err_str = e.to_string();
assert!(
err_str.contains("Provider") || err_str.contains("API") || err_str.contains("401"),
"Error should be from provider, not template: {}",
err_str
);
}
}
#[test]
fn test_template_whitespace_parsing() {
use nika::binding::template_resolve;
let mut bindings = ResolvedBindings::new();
bindings.set("data", json!("resolved_value"));
let datastore = RunContext::new();
let template = "Value: {{with.data}}";
let result = template_resolve(template, &bindings, &datastore);
assert!(result.is_ok(), "Standard template should resolve");
assert_eq!(result.unwrap().as_ref(), "Value: resolved_value");
let template2 = "Value: {{with.data }}";
let result2 = template_resolve(template2, &bindings, &datastore);
assert!(
result2.is_ok(),
"Template with trailing whitespace should resolve"
);
assert_eq!(result2.unwrap().as_ref(), "Value: resolved_value");
}
#[test]
fn test_template_no_whitespace() {
use nika::binding::template_resolve;
let mut bindings = ResolvedBindings::new();
bindings.set("value", json!(42));
let datastore = RunContext::new();
let template = "Number: {{with.value}}";
let result = template_resolve(template, &bindings, &datastore);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_ref(), "Number: 42");
}
#[test]
fn test_error_codes() {
let null_err = NikaError::NullValue {
path: "test.path".to_string(),
alias: "test".to_string(),
};
assert_eq!(null_err.code(), "NIKA-072");
let traversal_err = NikaError::InvalidTraversal {
segment: "field".to_string(),
value_type: "string".to_string(),
full_path: "data.field".to_string(),
};
assert_eq!(traversal_err.code(), "NIKA-073");
let path_err = NikaError::PathNotFound {
path: "data.missing".to_string(),
};
assert_eq!(path_err.code(), "NIKA-052");
let key_err = NikaError::MissingApiKey {
provider: "claude".to_string(),
};
assert_eq!(key_err.code(), "NIKA-032");
let provider_err = NikaError::ProviderNotConfigured {
provider: "openai".to_string(),
};
assert_eq!(provider_err.code(), "NIKA-030");
}