use std::collections::HashMap;
use std::fs;
use std::path::Path;
use warble::{
Additivity, BindingFile, ComponentFile, ContextLoader, DimensionInfo, LineageGraph, MetricInfo,
ModelInfo, ProfileFile,
};
#[derive(Default)]
struct FakeContext {
parseable_flag: bool,
metrics: Vec<MetricInfo>,
dimensions: Vec<DimensionInfo>,
time_dimensions: Vec<DimensionInfo>,
models: Vec<ModelInfo>,
lineage: LineageGraph,
}
impl FakeContext {
fn parseable() -> Self {
Self {
parseable_flag: true,
..Default::default()
}
}
fn unparseable() -> Self {
Self::default()
}
fn with_metric(mut self, name: &str, declared: bool, additivity: Option<Additivity>) -> Self {
self.metrics.push(MetricInfo {
name: name.into(),
owner: "m".into(),
declared,
additivity,
});
self
}
fn with_dimension(mut self, name: &str, temporal: bool) -> Self {
let dim = DimensionInfo {
name: name.into(),
owner: "m".into(),
is_temporal: temporal,
};
if temporal {
self.time_dimensions.push(dim.clone());
}
self.dimensions.push(dim);
self
}
fn with_model(mut self, name: &str, has_timestamp: bool) -> Self {
self.models.push(ModelInfo {
name: name.into(),
has_timestamp,
columns: Vec::new(),
});
self
}
}
impl ContextLoader for FakeContext {
fn is_parseable(&self) -> bool {
self.parseable_flag
}
fn metrics(&self) -> &[MetricInfo] {
&self.metrics
}
fn dimensions(&self) -> &[DimensionInfo] {
&self.dimensions
}
fn time_dimensions(&self) -> &[DimensionInfo] {
&self.time_dimensions
}
fn models(&self) -> &[ModelInfo] {
&self.models
}
fn lineage(&self) -> &LineageGraph {
&self.lineage
}
}
fn compile_project(project_dir: &Path) -> Result<serde_json::Value, String> {
compile_project_with(project_dir, &FakeContext::parseable())
}
fn compile_project_with(
project_dir: &Path,
context: &dyn ContextLoader,
) -> Result<serde_json::Value, String> {
let profile: ProfileFile =
serde_yaml::from_str(&fs::read_to_string(project_dir.join("profile.yml")).unwrap())
.unwrap();
let binding_path = project_dir.join(&profile.context.project);
let binding: BindingFile =
serde_yaml::from_str(&fs::read_to_string(&binding_path).unwrap()).unwrap();
let mut components: HashMap<String, ComponentFile> = HashMap::new();
let mut step_contents: HashMap<String, HashMap<String, String>> = HashMap::new();
for mount in &profile.components {
let component_dir = project_dir.join("components").join(&mount.use_id);
let component: ComponentFile =
serde_yaml::from_str(&fs::read_to_string(component_dir.join("component.yml")).unwrap())
.unwrap();
let mut steps = HashMap::new();
for step in &component.llm_steps {
let content = fs::read_to_string(component_dir.join(&step.prompt_ref)).unwrap();
steps.insert(step.name.clone(), content);
}
step_contents.insert(component.id.clone(), steps);
components.insert(component.id.clone(), component);
}
warble::compile(
&profile,
&components,
&binding.project,
context,
&step_contents,
)
.map_err(|e| e.to_string())
}
fn write_required_bind_fixture(dir: &Path, bind_block: &str) {
fs::create_dir_all(dir.join("context")).unwrap();
fs::create_dir_all(dir.join("components/needs_bind/steps")).unwrap();
fs::create_dir_all(dir.join("wren_project")).unwrap();
fs::write(
dir.join("wren_project/wren_project.yml"),
"schema_version: 2\n",
)
.unwrap();
fs::write(
dir.join("profile.yml"),
format!(
"profile: fixture\ncontext:\n project: ./context/binding.yml\nconfig:\n tier_policy: null\ncomponents:\n - use: needs_bind\n{bind_block}"
),
)
.unwrap();
fs::write(dir.join("context/binding.yml"), "project: ./wren_project\n").unwrap();
fs::write(
dir.join("components/needs_bind/component.yml"),
r#"
id: needs_bind
verb: needs_bind
type: analytical
realization_kind: skill
binding_mode: runtime_selected
params:
- { name: topic, bind: required }
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
required_capabilities: []
borrowed_actions: []
effect:
render_blocks: []
outcome: { kind: none }
"#,
)
.unwrap();
fs::write(
dir.join("components/needs_bind/steps/only_step.md"),
"Do the thing.\n",
)
.unwrap();
}
fn write_selector_field_fixture(dir: &Path, selector_block: &str) {
write_required_bind_fixture(dir, " bind:\n topic: \"orders\"\n");
let path = dir.join("components/needs_bind/component.yml");
let existing = fs::read_to_string(&path).unwrap();
fs::write(path, format!("{existing}{selector_block}")).unwrap();
}
#[test]
fn examples_without_a_description_fail_loudly() {
let dir = tempfile::tempdir().unwrap();
write_selector_field_fixture(dir.path(), "examples:\n - \"Why did revenue drop?\"\n");
let err = compile_project(dir.path()).expect_err("examples with no description must fail");
assert!(
err.contains("authors 'examples' without a 'description'"),
"unexpected error: {err}"
);
}
#[test]
fn a_placeholder_in_a_selector_field_fails_loudly() {
for block in [
"description: \"Survey {{project}} and report.\"\n",
"description: \"Survey the model.\"\nexamples:\n - \"What is in {{project_name}}?\"\n",
] {
let dir = tempfile::tempdir().unwrap();
write_selector_field_fixture(dir.path(), block);
let err = compile_project(dir.path()).expect_err("an unrendered placeholder must fail");
assert!(
err.contains("placeholder") && err.contains("would never be rendered"),
"unexpected error: {err}"
);
}
}
#[test]
fn a_description_with_examples_compiles() {
let dir = tempfile::tempdir().unwrap();
write_selector_field_fixture(
dir.path(),
"description: \"Answer one question about the bound model.\"\nexamples:\n - \"How many orders?\"\n",
);
let ir = compile_project(dir.path()).expect("the paired form must compile");
let node = &ir["components"][0];
assert_eq!(
node["description"], "Answer one question about the bound model.",
"the authored description is emitted verbatim"
);
assert_eq!(node["examples"][0], "How many orders?");
}
#[test]
fn missing_required_bind_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_required_bind_fixture(dir.path(), "");
let err = compile_project(dir.path()).expect_err("missing required bind must fail");
assert!(
err.contains("missing required bind 'topic' for component 'needs_bind'"),
"unexpected error: {err}"
);
}
#[test]
fn supplied_required_bind_compiles() {
let dir = tempfile::tempdir().unwrap();
write_required_bind_fixture(dir.path(), " bind:\n topic: \"orders\"\n");
compile_project(dir.path()).expect("bind is supplied, compile should succeed");
}
#[test]
fn locked_guardrail_override_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_required_bind_fixture(
dir.path(),
" bind:\n topic: \"orders\"\n guardrails:\n read_only_execution:\n locked: false\n",
);
let err = compile_project(dir.path()).expect_err("locked guardrail override must fail");
assert!(
err.contains(
"cannot override locked guardrail 'read_only_execution' on component 'needs_bind'"
),
"unexpected error: {err}"
);
}
#[test]
fn unparseable_context_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_required_bind_fixture(dir.path(), " bind:\n topic: \"orders\"\n");
let err = compile_project_with(dir.path(), &FakeContext::unparseable())
.expect_err("unparseable context must fail");
assert!(
err.contains("is not a parseable wren project"),
"unexpected error: {err}"
);
}
fn write_component_fixture(dir: &Path, component_id: &str, component_yaml: &str) {
fs::create_dir_all(dir.join("context")).unwrap();
fs::create_dir_all(dir.join(format!("components/{component_id}/steps"))).unwrap();
fs::create_dir_all(dir.join("wren_project")).unwrap();
fs::write(
dir.join("wren_project/wren_project.yml"),
"schema_version: 2\n",
)
.unwrap();
fs::write(
dir.join("profile.yml"),
format!(
"profile: fixture\ncontext:\n project: ./context/binding.yml\nconfig:\n tier_policy: null\ncomponents:\n - use: {component_id}\n"
),
)
.unwrap();
fs::write(dir.join("context/binding.yml"), "project: ./wren_project\n").unwrap();
fs::write(
dir.join(format!("components/{component_id}/component.yml")),
component_yaml,
)
.unwrap();
fs::write(
dir.join(format!("components/{component_id}/steps/only_step.md")),
"Do the thing.\n",
)
.unwrap();
}
fn write_component_fixture_with_bind(
dir: &Path,
component_id: &str,
component_yaml: &str,
bind_block: &str,
) {
fs::create_dir_all(dir.join("context")).unwrap();
fs::create_dir_all(dir.join(format!("components/{component_id}/steps"))).unwrap();
fs::create_dir_all(dir.join("wren_project")).unwrap();
fs::write(
dir.join("wren_project/wren_project.yml"),
"schema_version: 2\n",
)
.unwrap();
fs::write(
dir.join("profile.yml"),
format!(
"profile: fixture\ncontext:\n project: ./context/binding.yml\nconfig:\n tier_policy: null\ncomponents:\n - use: {component_id}\n{bind_block}"
),
)
.unwrap();
fs::write(dir.join("context/binding.yml"), "project: ./wren_project\n").unwrap();
fs::write(
dir.join(format!("components/{component_id}/component.yml")),
component_yaml,
)
.unwrap();
fs::write(
dir.join(format!("components/{component_id}/steps/only_step.md")),
"Do the thing.\n",
)
.unwrap();
}
fn monitor_style_component(id: &str, param_block: &str) -> String {
format!(
r#"
id: {id}
verb: {id}
type: analytical
realization_kind: skill
binding_mode: pinned
context_precondition:
- {{ predicate: model_has_timestamp, args: {{ model: "$param:model" }} }}
params:
{param_block}
llm_steps:
- {{ name: only_step, tier: cheap, prompt_ref: steps/only_step.md }}
trigger: {{ kind: one_shot }}
guardrails:
- {{ name: read_only_execution, locked: true }}
effect:
render_blocks: []
outcome: {{ kind: none }}
"#
)
}
fn precondition_component(id: &str, precondition_block: &str) -> String {
format!(
r#"
id: {id}
verb: {id}
type: analytical
realization_kind: skill
binding_mode: runtime_selected
context_precondition:
{precondition_block}
llm_steps:
- {{ name: only_step, tier: cheap, prompt_ref: steps/only_step.md }}
trigger: {{ kind: one_shot }}
guardrails:
- {{ name: read_only_execution, locked: true }}
effect:
render_blocks: []
outcome: {{ kind: none }}
"#
)
}
#[test]
fn precondition_pass_records_structured_check() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_metric",
&precondition_component("needs_metric", " - { predicate: has_metric }"),
);
let ctx =
FakeContext::parseable().with_metric("total_revenue", true, Some(Additivity::Additive));
let ir = compile_project_with(dir.path(), &ctx).expect("has_metric is satisfied");
assert_eq!(
ir["components"][0]["precondition_result"]["checks"],
serde_json::json!([{ "predicate": "has_metric", "outcome": "pass" }]),
"a satisfied precondition is recorded as a structured pass check"
);
assert_eq!(ir["warble_ir_version"], "0.5");
assert_eq!(
ir["context_binding"]["resolved"]["metrics"][0]["name"],
"total_revenue"
);
}
#[test]
fn precondition_false_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_metric",
&precondition_component("needs_metric", " - { predicate: has_metric }"),
);
let err = compile_project_with(dir.path(), &FakeContext::parseable())
.expect_err("has_metric must fail on a project with no metrics");
assert!(err.contains("not satisfied"), "unexpected error: {err}");
}
#[test]
fn metric_additive_unanswerable_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_additive",
&precondition_component("needs_additive", " - { predicate: metric_additive }"),
);
let ctx = FakeContext::parseable().with_metric("amount", false, None);
let err = compile_project_with(dir.path(), &ctx)
.expect_err("metric_additive must be unanswerable without a declared metric");
assert!(
err.contains("cannot be evaluated") && err.contains("additivity"),
"unexpected error (expected the can_answer=false loud-fail): {err}"
);
}
#[test]
fn metric_additive_existential_passes_with_an_additive_metric() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_additive",
&precondition_component("needs_additive", " - { predicate: metric_additive }"),
);
let ctx = FakeContext::parseable()
.with_metric("total_revenue", true, Some(Additivity::Additive))
.with_metric("avg_order", true, Some(Additivity::NonAdditive));
compile_project_with(dir.path(), &ctx).expect("an additive declared metric satisfies it");
}
#[test]
fn metric_additive_pinned_to_nonadditive_metric_fails() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_additive",
&precondition_component(
"needs_additive",
" - { predicate: metric_additive, args: { metric: avg_order } }",
),
);
let ctx =
FakeContext::parseable().with_metric("avg_order", true, Some(Additivity::NonAdditive));
let err =
compile_project_with(dir.path(), &ctx).expect_err("a non-additive pinned metric must fail");
assert!(err.contains("not satisfied"), "unexpected error: {err}");
}
#[test]
fn bind_value_resolves_into_precondition_args() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&monitor_style_component("monitor_style", " - { name: model, bind: required }"),
" bind:\n model: \"orders\"\n",
);
let ctx = FakeContext::parseable().with_model("orders", true);
let ir = compile_project_with(dir.path(), &ctx).expect("bound model has a timestamp");
assert_eq!(
ir["components"][0]["context_precondition"],
serde_json::json!([{ "predicate": "model_has_timestamp", "args": { "model": "orders" } }]),
"the IR must carry the resolved bind value, not the '$param:model' template"
);
}
#[test]
fn unsupplied_optional_bind_falls_back_to_default() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&monitor_style_component(
"monitor_style",
" - { name: model, bind: optional, default: widgets }",
),
"",
);
let ctx = FakeContext::parseable().with_model("widgets", true);
let ir = compile_project_with(dir.path(), &ctx).expect("default model has a timestamp");
assert_eq!(
ir["components"][0]["context_precondition"][0]["args"]["model"],
"widgets"
);
assert_eq!(ir["components"][0]["binds"]["model"], "widgets");
}
#[test]
fn unbound_optional_param_with_no_default_is_unanswerable() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&monitor_style_component("monitor_style", " - { name: model, bind: optional }"),
"",
);
let err = compile_project(dir.path())
.expect_err("an unsupplied optional bind with no default must be unanswerable");
assert!(
err.contains("cannot be evaluated") && err.contains("no declared default"),
"unexpected error: {err}"
);
}
#[test]
fn dollar_param_referencing_undeclared_param_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&precondition_component(
"monitor_style",
" - { predicate: model_has_timestamp, args: { model: \"$param:ghost_param\" } }",
),
"",
);
let err = compile_project(dir.path())
.expect_err("a $param: reference to an undeclared param must loud-fail");
assert!(
err.contains("'ghost_param'") && err.contains("not a declared param"),
"unexpected error: {err}"
);
}
#[test]
fn binding_a_timestampless_model_fails_loudly_in_a_multi_model_project() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&monitor_style_component("monitor_style", " - { name: model, bind: required }"),
" bind:\n model: \"widgets\"\n",
);
let ctx = FakeContext::parseable()
.with_model("orders", true)
.with_model("widgets", false);
let err = compile_project_with(dir.path(), &ctx)
.expect_err("binding a timestampless model must fail even when another model has one");
assert!(err.contains("not satisfied"), "unexpected error: {err}");
}
#[test]
fn binding_a_nonexistent_model_no_longer_silently_passes() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&monitor_style_component("monitor_style", " - { name: model, bind: required }"),
" bind:\n model: \"ghost\"\n",
);
let ctx = FakeContext::parseable().with_model("orders", true);
let err = compile_project_with(dir.path(), &ctx)
.expect_err("binding a nonexistent model must fail, not silently pass");
assert!(
err.contains("cannot be evaluated") && err.contains("'ghost' is not a declared model"),
"unexpected error: {err}"
);
}
#[test]
fn model_has_timestamp_existential_fallback_is_unchanged() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"existential_timestamp_test",
&precondition_component(
"existential_timestamp_test",
" - { predicate: model_has_timestamp }",
),
);
let err = compile_project_with(
dir.path(),
&FakeContext::parseable().with_model("widgets", false),
)
.expect_err("existential model_has_timestamp must fail with no timestamped model");
assert!(err.contains("not satisfied"), "unexpected error: {err}");
let ctx = FakeContext::parseable()
.with_model("widgets", false)
.with_model("orders", true);
compile_project_with(dir.path(), &ctx)
.expect("existential model_has_timestamp passes when any model has a timestamp");
}
#[test]
fn two_mounts_with_different_binds_produce_different_ir() {
let dir_a = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir_a.path(),
"monitor_style",
&monitor_style_component("monitor_style", " - { name: model, bind: required }"),
" bind:\n model: \"orders\"\n",
);
let dir_b = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir_b.path(),
"monitor_style",
&monitor_style_component("monitor_style", " - { name: model, bind: required }"),
" bind:\n model: \"widgets\"\n",
);
let ctx = FakeContext::parseable()
.with_model("orders", true)
.with_model("widgets", true);
let ir_a = compile_project_with(dir_a.path(), &ctx).expect("orders bind compiles");
let ir_b = compile_project_with(dir_b.path(), &ctx).expect("widgets bind compiles");
assert_ne!(
ir_a["components"][0], ir_b["components"][0],
"binding different models must produce different IR component nodes"
);
assert_eq!(ir_a["components"][0]["binds"]["model"], "orders");
assert_eq!(ir_b["components"][0]["binds"]["model"], "widgets");
}
#[test]
fn runtime_injected_param_is_excluded_from_binds_facet() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"monitor_style",
&monitor_style_component(
"monitor_style",
" - { name: model, bind: required }\n - { name: connection, source: runtime-injected }",
),
" bind:\n model: \"orders\"\n",
);
let ctx = FakeContext::parseable().with_model("orders", true);
let ir =
compile_project_with(dir.path(), &ctx).expect("compiles with a runtime-injected param");
let binds = ir["components"][0]["binds"].as_object().unwrap();
assert_eq!(
binds.len(),
1,
"runtime-injected params must not appear in the binds facet: {binds:?}"
);
assert_eq!(ir["components"][0]["binds"]["model"], "orders");
}
#[test]
fn binds_facet_is_absent_when_no_bind_params_exist() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"no_binds_test",
&precondition_component("no_binds_test", " - { predicate: has_metric }"),
);
let ctx =
FakeContext::parseable().with_metric("total_revenue", true, Some(Additivity::Additive));
let ir = compile_project_with(dir.path(), &ctx).expect("has_metric is satisfied");
assert!(
ir["components"][0].get("binds").is_none(),
"binds facet must be absent (not an empty object) when there are no bind params: {:?}",
ir["components"][0]
);
}
#[test]
fn time_dimension_predicate_evaluates() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_time",
&precondition_component("needs_time", " - { predicate: has_time_dimension }"),
);
let err = compile_project_with(dir.path(), &FakeContext::parseable())
.expect_err("has_time_dimension must fail with no time dimension");
assert!(err.contains("not satisfied"), "unexpected error: {err}");
let ctx = FakeContext::parseable().with_dimension("order_date", true);
compile_project_with(dir.path(), &ctx).expect("a temporal dimension satisfies it");
}
#[test]
fn unknown_precondition_predicate_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"precon_test",
&precondition_component("precon_test", " - { predicate: not_a_real_predicate }"),
);
let err = compile_project(dir.path()).expect_err("unknown precondition predicate must fail");
assert!(
err.contains("unknown context_precondition predicate"),
"unexpected error: {err}"
);
}
#[test]
fn source_param_is_carried_into_ir() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"source_param_test",
r#"
id: source_param_test
verb: source_param_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
params:
- { name: connection, source: runtime-injected }
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let ir = compile_project(dir.path()).expect("source param should compile");
let params = ir["components"][0]["params"].as_array().unwrap();
assert_eq!(
params,
&vec![serde_json::json!({ "name": "connection", "source": "runtime-injected" })],
"source param must be carried into IR params verbatim: {params:?}"
);
}
#[test]
fn param_with_both_bind_and_source_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"both_test",
r#"
id: both_test
verb: both_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
params:
- { name: topic, bind: optional, source: runtime-injected }
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err = compile_project(dir.path()).expect_err("bind + source must fail");
assert!(err.contains("declares both"), "unexpected error: {err}");
}
#[test]
fn param_with_neither_bind_nor_source_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"neither_test",
r#"
id: neither_test
verb: neither_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
params:
- { name: topic }
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err = compile_project(dir.path()).expect_err("neither bind nor source must fail");
assert!(err.contains("declares neither"), "unexpected error: {err}");
}
#[test]
fn param_with_unknown_source_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"unknown_source_test",
r#"
id: unknown_source_test
verb: unknown_source_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
params:
- { name: topic, source: some-other-source }
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err = compile_project(dir.path()).expect_err("unknown source value must fail");
assert!(err.contains("unknown source"), "unexpected error: {err}");
}
#[test]
fn conditional_llm_step_is_carried_into_ir() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"conditional_test",
r#"
id: conditional_test
verb: conditional_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md, conditional: true,
when: { guard: on_failure, target: some_upstream_step } }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let ir =
compile_project(dir.path()).expect("conditional step with a when guard should compile");
assert_eq!(
ir["components"][0]["llm_calls"][0]["conditional"],
serde_json::json!(true)
);
assert_eq!(
ir["components"][0]["llm_calls"][0]["when"],
serde_json::json!({ "guard": "on_failure", "target": "some_upstream_step" })
);
}
#[test]
fn bare_conditional_without_when_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"bare_conditional_test",
r#"
id: bare_conditional_test
verb: bare_conditional_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md, conditional: true }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err =
compile_project(dir.path()).expect_err("bare conditional with no when must loud-fail");
assert!(
err.contains("has no 'when' guard"),
"unexpected error: {err}"
);
}
#[test]
fn when_guard_without_conditional_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"when_without_conditional_test",
r#"
id: when_without_conditional_test
verb: when_without_conditional_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md,
when: { guard: on_failure, target: some_step } }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err =
compile_project(dir.path()).expect_err("when guard without conditional must loud-fail");
assert!(
err.contains("is not 'conditional: true'"),
"unexpected error: {err}"
);
}
#[test]
fn unknown_guard_name_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"unknown_guard_test",
r#"
id: unknown_guard_test
verb: unknown_guard_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md, conditional: true,
when: { guard: on_vibes, target: some_step } }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err = compile_project(dir.path()).expect_err("unknown guard name must loud-fail");
assert!(
err.contains("unknown guard 'on_vibes'"),
"unexpected error: {err}"
);
}
#[test]
fn on_flag_guard_requires_dotted_target() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"on_flag_bad_target_test",
r#"
id: on_flag_bad_target_test
verb: on_flag_bad_target_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md, conditional: true,
when: { guard: on_flag, target: no_dot_here } }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err =
compile_project(dir.path()).expect_err("on_flag with a non-dotted target must loud-fail");
assert!(err.contains("expects a dotted"), "unexpected error: {err}");
}
#[test]
fn on_missing_guard_is_valid() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"on_missing_test",
r#"
id: on_missing_test
verb: on_missing_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md, conditional: true,
when: { guard: on_missing, target: some_artifact } }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let ir = compile_project(dir.path()).expect("on_missing guard should compile");
assert_eq!(
ir["components"][0]["llm_calls"][0]["when"],
serde_json::json!({ "guard": "on_missing", "target": "some_artifact" })
);
}
#[test]
fn unconditional_step_emits_null_when() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"unconditional_when_test",
r#"
id: unconditional_when_test
verb: unconditional_when_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let ir = compile_project(dir.path()).expect("unconditional step with no when should compile");
assert_eq!(
ir["components"][0]["llm_calls"][0]["when"],
serde_json::json!(null)
);
}
#[test]
fn authored_eval_block_is_carried_into_ir() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"eval_test",
r#"
id: eval_test
verb: eval_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
eval:
template_ref: eval/
metrics: [accuracy]
"#,
);
let ir = compile_project(dir.path()).expect("component with eval should compile");
assert_eq!(
ir["components"][0]["eval"],
serde_json::json!({ "template_ref": "eval/", "metrics": ["accuracy"] })
);
}
#[test]
fn component_file_rejects_unknown_fields() {
let yaml = r#"
id: bad
verb: bad
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true }
effect:
render_blocks: []
outcome: { kind: none }
totally_unknown_field: true
"#;
let err = serde_yaml::from_str::<ComponentFile>(yaml)
.expect_err("unknown field must be rejected at parse time");
assert!(
err.to_string().contains("unknown field"),
"unexpected error: {err}"
);
}
#[test]
fn overridable_guardrail_normalizes_to_locked_false() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"overridable_test",
r#"
id: overridable_test
verb: overridable_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, overridable: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let ir = compile_project(dir.path()).expect("overridable guardrail should compile");
assert_eq!(
ir["components"][0]["guardrails"][0],
serde_json::json!({ "name": "read_only_execution", "locked": false })
);
}
#[test]
fn contradictory_locked_and_overridable_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"contradictory_test",
r#"
id: contradictory_test
verb: contradictory_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution, locked: true, overridable: true }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err = compile_project(dir.path()).expect_err("contradictory locked/overridable must fail");
assert!(err.contains("contradictory"), "unexpected error: {err}");
}
#[test]
fn guardrail_with_neither_locked_nor_overridable_fails_loudly() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"no_lock_decl_test",
r#"
id: no_lock_decl_test
verb: no_lock_decl_test
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- { name: only_step, tier: cheap, prompt_ref: steps/only_step.md }
trigger: { kind: one_shot }
guardrails:
- { name: read_only_execution }
effect:
render_blocks: []
outcome: { kind: none }
"#,
);
let err = compile_project(dir.path()).expect_err("missing locked/overridable must fail");
assert!(err.contains("must declare"), "unexpected error: {err}");
}
struct DeclinesSchemaProbes(FakeContext);
impl ContextLoader for DeclinesSchemaProbes {
fn is_parseable(&self) -> bool {
self.0.is_parseable()
}
fn metrics(&self) -> &[MetricInfo] {
self.0.metrics()
}
fn dimensions(&self) -> &[DimensionInfo] {
self.0.dimensions()
}
fn time_dimensions(&self) -> &[DimensionInfo] {
self.0.time_dimensions()
}
fn models(&self) -> &[ModelInfo] {
self.0.models()
}
fn lineage(&self) -> &LineageGraph {
self.0.lineage()
}
fn can_answer(&self, predicate: &str) -> bool {
!matches!(predicate, "has_metric")
}
}
#[test]
fn a_declined_predicate_is_unanswerable_even_when_the_data_is_present() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"needs_metric",
&precondition_component("needs_metric", " - { predicate: has_metric }"),
);
let ctx = DeclinesSchemaProbes(FakeContext::parseable().with_metric("revenue", true, None));
let err = compile_project_with(dir.path(), &ctx)
.expect_err("a declined predicate must refuse rather than be evaluated anyway");
assert!(
err.contains("cannot be evaluated") && !err.contains("not satisfied"),
"expected the unanswerable loud-fail, not an answerable false: {err}"
);
}
fn brief_component(id: &str, brief_line: &str) -> String {
format!(
r#"
id: {id}
verb: {id}
type: analytical
realization_kind: skill
binding_mode: runtime_selected
llm_steps:
- {{ name: only_step, tier: cheap, prompt_ref: steps/only_step.md }}
trigger: {{ kind: one_shot }}
guardrails:
- {{ name: read_only_execution, locked: true }}
effect:
render_blocks: []
outcome: {{ kind: none }}
{brief_line}
"#
)
}
#[test]
fn absent_brief_produces_no_brief_key_in_ir() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(dir.path(), "no_brief", &brief_component("no_brief", ""));
let ir = compile_project(dir.path()).expect("component with no brief should compile");
assert!(
ir["components"][0].get("brief").is_none(),
"a component with no authored brief must not gain a 'brief' key: {:?}",
ir["components"][0]
);
}
#[test]
fn component_brief_is_rendered_with_placeholders_and_carried_into_ir() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture(
dir.path(),
"with_brief",
&brief_component(
"with_brief",
r#"brief: "Shared framing for {{project_name}} at {{project}}.""#,
),
);
let ir = compile_project(dir.path()).expect("component with brief should compile");
assert_eq!(
ir["components"][0]["brief"],
serde_json::json!("Shared framing for wren_project at ./wren_project."),
"brief must have its {{{{project}}}}/{{{{project_name}}}} placeholders substituted, \
same as a step body: {:?}",
ir["components"][0]["brief"]
);
}
#[test]
fn profile_mount_brief_replaces_component_brief_wholesale() {
let dir = tempfile::tempdir().unwrap();
write_component_fixture_with_bind(
dir.path(),
"overridden_brief",
&brief_component(
"overridden_brief",
r#"brief: "Component's own framing, must not appear.""#,
),
" brief: \"Mount-level framing for {{project_name}}.\"\n",
);
let ir = compile_project(dir.path()).expect("component with overridden brief should compile");
assert_eq!(
ir["components"][0]["brief"],
serde_json::json!("Mount-level framing for wren_project."),
"a profile-mount brief must replace the component's brief entirely: {:?}",
ir["components"][0]["brief"]
);
}