use std::fs;
use std::path::Path;
use warble_cli::compile_project_to_ir;
const TIMESTAMPLESS_LAYER: &str = r#"{
"context_version": 2,
"parseable": true,
"metrics": [
{
"name": "id",
"owner": "widgets",
"declared": false,
"additivity": null
},
{
"name": "amount",
"owner": "widgets",
"declared": false,
"additivity": null
}
],
"dimensions": [
{
"name": "category",
"owner": "widgets",
"is_temporal": false
}
],
"models": [
{
"name": "widgets",
"has_timestamp": false,
"columns": [
"id",
"category",
"amount"
]
}
],
"lineage": {
"nodes": [
{
"id": "model:widgets",
"kind": "model"
}
],
"edges": []
},
"lineage_diagnostics": [],
"analysis": {
"impact": {
"model:widgets": {
"downstream": [],
"severity": {
"rank": 0,
"name": "none"
}
}
},
"consumers": {
"queries": 0,
"dashboards": 0
}
}
}"#;
fn write_monitor_fixture(dir: &Path) {
fs::create_dir_all(dir.join("context")).unwrap();
fs::create_dir_all(dir.join("components/monitor/steps")).unwrap();
fs::write(
dir.join("profile.yml"),
"profile: fixture\ncontext:\n project: ./context/binding.yml\ncomponents:\n - use: monitor\n bind:\n model: widgets\n",
)
.unwrap();
fs::write(dir.join("context/context.json"), TIMESTAMPLESS_LAYER).unwrap();
fs::write(
dir.join("context/binding.yml"),
"kind: prepared\nproject: widgets-layer\ndocument: context/context.json\n",
)
.unwrap();
fs::write(
dir.join("components/monitor/component.yml"),
r#"
id: monitor
verb: monitor_freshness
type: assertive
realization_kind: tool
binding_mode: pinned
context_precondition:
- { predicate: model_has_timestamp, args: { model: "$param:model" } }
params:
- { name: model, bind: required }
llm_steps:
# Deliberately still single-step, unlike the real hub/components/monitor_freshness (which now
# splits into read_freshness + assess_severity so assess_severity's consumes/when.target resolve
# against a real producer): this fixture's compile call is meant to fail at the earlier
# `model_has_timestamp` context_precondition gate, strictly before llm_steps consumes/produces are
# ever resolved, so assess_severity's dangling `freshness_reading` reference here is inert and does
# not need the same fix.
- { name: assess_severity, tier: cheap, conditional: true, prompt_ref: steps/assess_severity.md,
when: { guard: on_flag, target: freshness_reading.stale } }
trigger: { kind: scheduled }
guardrails:
- { name: read_only_execution, locked: true }
required_capabilities: [scheduler, sql_execution:read_only, notify_channel, llm:cheap]
borrowed_actions: [notify_slack]
effect:
render_blocks: [status]
outcome:
kind: assertion
verdict_type: freshness_verdict
emits: [freshness_breach]
"#,
)
.unwrap();
fs::write(
dir.join("components/monitor/steps/assess_severity.md"),
"Classify the breach severity.\n",
)
.unwrap();
}
#[test]
fn monitoring_a_model_without_a_timestamp_fails_to_compile() {
let project = tempfile::tempdir().unwrap();
write_monitor_fixture(project.path());
let err = compile_project_to_ir(project.path())
.expect_err("monitoring freshness on a model with no timestamp column must be refused");
assert!(
err.contains("model_has_timestamp") && err.contains("not satisfied"),
"expected a model_has_timestamp rejection, got: {err}"
);
}