use std::path::PathBuf;
use std::sync::Once;
use prompty::model::Prompty;
use prompty::model::context::LoadContext;
use serde_json::Value;
static INIT: Once = Once::new();
fn ensure_renderers() {
INIT.call_once(|| {
prompty::register_defaults();
});
}
fn vectors_path() -> PathBuf {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest
.parent() .unwrap()
.parent() .unwrap()
.parent() .unwrap()
.join("spec")
.join("vectors")
.join("render")
.join("render_vectors.json")
}
fn load_vectors() -> Vec<Value> {
let raw = std::fs::read_to_string(vectors_path()).expect("failed to read render_vectors.json");
serde_json::from_str::<Vec<Value>>(&raw).expect("failed to parse render_vectors.json")
}
const MUSTACHE_VECTORS: &[&str] = &[
"mustache_simple",
"mustache_section",
"mustache_inverted",
"mustache_loop",
];
fn build_agent(name: &str, template: &str, engine: &str) -> Prompty {
if name == "thread_nonce_injection" {
let data = serde_json::json!({
"name": "test",
"kind": "prompt",
"model": { "id": "test" },
"instructions": template,
"inputs": [
{ "name": "question", "kind": "string" },
{ "name": "conversation", "kind": "thread" }
],
"template": {
"format": { "kind": engine },
"parser": { "kind": "prompty" }
}
});
Prompty::load_from_value(&data, &LoadContext::default())
} else {
let data = serde_json::json!({
"name": "test",
"kind": "prompt",
"model": { "id": "test" },
"instructions": template,
"template": {
"format": { "kind": engine },
"parser": { "kind": "prompty" }
}
});
Prompty::load_from_value(&data, &LoadContext::default())
}
}
#[tokio::test]
async fn test_render_vectors() {
ensure_renderers();
let vectors = load_vectors();
assert_eq!(vectors.len(), 23, "expected 23 render vectors");
let has_mustache = prompty::has_renderer("mustache");
for vec in &vectors {
let name = vec["name"].as_str().expect("vector missing 'name'");
let input = &vec["input"];
let expected = &vec["expected"];
let template = input["template"]
.as_str()
.expect("vector missing 'template'");
let engine = input["engine"].as_str().expect("vector missing 'engine'");
let inputs = &input["inputs"];
if engine == "mustache" && !has_mustache {
assert!(
MUSTACHE_VECTORS.contains(&name),
"unexpected mustache vector: {name}"
);
println!("SKIP: {name} — mustache renderer not implemented");
continue;
}
let agent = build_agent(name, template, engine);
let rendered = prompty::render(&agent, inputs)
.await
.unwrap_or_else(|e| panic!("[{name}] render failed: {e}"));
if let Some(exp) = expected.get("rendered").and_then(Value::as_str) {
assert_eq!(rendered, exp, "[{name}] rendered output mismatch");
}
if let Some(pattern) = expected.get("nonce_pattern").and_then(Value::as_str) {
let re = regex::Regex::new(pattern)
.unwrap_or_else(|e| panic!("[{name}] invalid nonce_pattern regex: {e}"));
assert!(
re.is_match(&rendered),
"[{name}] nonce pattern mismatch\n pattern: {pattern}\n got: {rendered}"
);
}
}
}