use std::fs;
use std::path::Path;
use warble_cli::compile_project_to_ir;
const PREPARED_WIDGETS: &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": [],
"models": [
{
"name": "widgets",
"has_timestamp": false,
"columns": [
"id",
"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_project(dir: &Path, prompt_ref: &str) {
fs::create_dir_all(dir.join("context")).unwrap();
fs::create_dir_all(dir.join("components/asker/steps")).unwrap();
fs::write(
dir.join("profile.yml"),
"profile: fixture\ncontext:\n project: ./context/binding.yml\ncomponents:\n - use: asker\n",
)
.unwrap();
fs::write(dir.join("context/context.json"), PREPARED_WIDGETS).unwrap();
fs::write(
dir.join("context/binding.yml"),
"kind: prepared\nproject: widgets-layer\ndocument: context/context.json\n",
)
.unwrap();
fs::write(
dir.join("components/asker/component.yml"),
format!(
r#"
id: asker
verb: asker
type: analytical
realization_kind: skill
binding_mode: runtime_selected
context_precondition: []
params: []
llm_steps:
- {{ name: ask, tier: cheap, prompt_ref: {prompt_ref} }}
trigger: {{ kind: one_shot }}
guardrails:
- {{ name: read_only_execution, locked: true }}
required_capabilities: [llm:cheap]
borrowed_actions: []
effect:
render_blocks: []
outcome:
kind: none
"#
),
)
.unwrap();
fs::write(
dir.join("components/asker/steps/ask.md"),
"Ask something.\n",
)
.unwrap();
}
#[test]
fn a_well_behaved_prompt_ref_compiles() {
let project = tempfile::tempdir().unwrap();
write_project(project.path(), "steps/ask.md");
compile_project_to_ir(project.path())
.expect("a relative prompt_ref inside the component directory must compile cleanly");
}
#[test]
fn a_prompt_ref_escaping_with_dotdot_is_rejected_on_the_real_compile_path() {
let project = tempfile::tempdir().unwrap();
write_project(project.path(), "../../escape.md");
let err = compile_project_to_ir(project.path())
.expect_err("an escaping prompt_ref must never compile, not even a single leading '..'");
assert!(
err.contains("prompt_ref"),
"the error must name which field was rejected: {err}"
);
assert!(
err.contains("../../escape.md"),
"the error must name the offending reference: {err}"
);
}
#[test]
fn an_absolute_prompt_ref_is_rejected_on_the_real_compile_path() {
let project = tempfile::tempdir().unwrap();
write_project(project.path(), "/etc/passwd");
let err = compile_project_to_ir(project.path()).expect_err(
"an absolute prompt_ref must never compile — it names a path outside any component",
);
assert!(
err.contains("prompt_ref"),
"the error must name which field was rejected: {err}"
);
assert!(
err.contains("/etc/passwd"),
"the error must name the offending reference: {err}"
);
}
#[test]
fn a_missing_prompt_ref_target_is_rejected_on_the_real_compile_path() {
let project = tempfile::tempdir().unwrap();
write_project(project.path(), "steps/missing.md");
let err = compile_project_to_ir(project.path())
.expect_err("a prompt_ref naming a file that does not exist must never compile");
assert!(
err.contains("prompt_ref"),
"the error must name which field was rejected: {err}"
);
assert!(
err.contains("steps/missing.md"),
"the error must name the offending reference: {err}"
);
assert!(
err.contains("does not exist"),
"the error must say the file is missing, not something else: {err}"
);
}