use std::path::{Path, PathBuf};
use std::process::{Command, Output};
fn analysis_agent_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("examples")
.join("analysis-agent")
}
fn uncomposed_analysis_ir_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("analysis-agent-uncomposed.ir.json")
}
fn sample_provider_path() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("..")
.join("dispatcher")
.join("vercel")
.join("tests")
.join("fixtures")
.join("sample-provider.yaml")
}
fn run_warble(args: &[&std::ffi::OsStr]) -> Output {
Command::new(env!("CARGO_BIN_EXE_warble"))
.args(args)
.output()
.expect("warble runs")
}
fn compile_analysis_agent_ir(dir: &Path) -> PathBuf {
let ir_path = dir.join("ir.json");
let output = run_warble(&[
"compile".as_ref(),
analysis_agent_dir().as_os_str(),
"--out".as_ref(),
ir_path.as_os_str(),
]);
assert!(
output.status.success(),
"warble compile examples/analysis-agent should succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
ir_path
}
#[test]
fn explicit_host_contract_emits_real_compiled_composition_without_losing_authority() {
let tmp = tempfile::tempdir().unwrap();
let ir = compile_analysis_agent_ir(tmp.path());
let host = sample_provider_path().with_file_name("host-contract.json");
for target in ["vercel", "vercel:interactive"] {
let out = tmp.path().join(target.replace(':', "-"));
let result = run_warble(&[
"dispatch".as_ref(),
ir.as_os_str(),
"--target".as_ref(),
target.as_ref(),
"--out".as_ref(),
out.as_os_str(),
"--provider".as_ref(),
sample_provider_path().as_os_str(),
"--host-contract".as_ref(),
host.as_os_str(),
]);
assert!(
result.status.success(),
"{}",
String::from_utf8_lossy(&result.stderr)
);
let bundle: serde_json::Value =
serde_json::from_slice(&std::fs::read(out.join("bundle.json")).unwrap()).unwrap();
assert_eq!(bundle["vercel_bundle_version"], "0.2");
let dashboard = &bundle["components"]["generate_dashboard"];
assert_eq!(
dashboard["declaration"]["llm_calls"][1]["component_calls"][0]["component"],
"answer_query"
);
assert_eq!(dashboard["steps"][1]["tools"], serde_json::json!([]));
assert_eq!(bundle["execution_status"], "not_executed");
}
}
#[test]
fn host_contract_cannot_be_ignored_by_native_target_or_legacy_reader() {
let tmp = tempfile::tempdir().unwrap();
let ir = compile_analysis_agent_ir(tmp.path());
let host = tmp.path().join("old-host.json");
std::fs::write(
&host,
r#"{"protocol":"warble-component-host/1","bundle_version":"0.1"}"#,
)
.unwrap();
for target in ["vercel", "claude-code:interactive", "codex:interactive"] {
let out = tmp.path().join(target.replace(':', "-"));
let result = run_warble(&[
"dispatch".as_ref(),
ir.as_os_str(),
"--target".as_ref(),
target.as_ref(),
"--out".as_ref(),
out.as_os_str(),
"--host-contract".as_ref(),
host.as_os_str(),
]);
assert!(!result.status.success());
assert!(!out.exists());
}
}
#[test]
fn target_vercel_emits_a_bundle_with_a_version_field() {
let tmp = tempfile::tempdir().expect("tempdir");
let ir_path = uncomposed_analysis_ir_path();
let out_dir = tmp.path().join("out");
let output = run_warble(&[
"dispatch".as_ref(),
ir_path.as_os_str(),
"--target".as_ref(),
"vercel".as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
"--provider".as_ref(),
sample_provider_path().as_os_str(),
]);
assert!(
output.status.success(),
"warble dispatch --target vercel should succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let bundle_path = out_dir.join("bundle.json");
assert!(
bundle_path.exists(),
"bundle.json should be written to --out"
);
let raw = std::fs::read_to_string(&bundle_path).expect("read bundle.json");
let bundle: serde_json::Value =
serde_json::from_str(&raw).expect("bundle.json must parse as JSON");
assert!(
bundle.get("vercel_bundle_version").is_some(),
"bundle should carry a vercel_bundle_version field; bundle: {bundle}"
);
}
#[test]
fn target_vercel_interactive_selects_the_interactive_mode() {
let tmp = tempfile::tempdir().expect("tempdir");
let ir_path = uncomposed_analysis_ir_path();
let out_dir = tmp.path().join("out");
let output = run_warble(&[
"dispatch".as_ref(),
ir_path.as_os_str(),
"--target".as_ref(),
"vercel:interactive".as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
"--provider".as_ref(),
sample_provider_path().as_os_str(),
]);
assert!(
output.status.success(),
"warble dispatch --target vercel:interactive should succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let raw = std::fs::read_to_string(out_dir.join("bundle.json")).expect("read bundle.json");
let bundle: serde_json::Value =
serde_json::from_str(&raw).expect("bundle.json must parse as JSON");
assert_eq!(
bundle.get("target").and_then(|v| v.as_str()),
Some("vercel:interactive"),
"bundle's target field should reflect the selected mode; bundle: {bundle}"
);
}
#[test]
fn canonical_analysis_agent_wall_hits_on_vercel_before_bundle_output() {
let tmp = tempfile::tempdir().expect("tempdir");
let ir_path = compile_analysis_agent_ir(tmp.path());
let out_dir = tmp.path().join("out");
for target in ["vercel", "vercel:interactive"] {
let output = run_warble(&[
"dispatch".as_ref(),
ir_path.as_os_str(),
"--target".as_ref(),
target.as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
"--provider".as_ref(),
sample_provider_path().as_os_str(),
]);
assert!(
!output.status.success(),
"{target} unexpectedly accepted composition"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("component_invocation") && stderr.contains("wall-hit"),
"{target}: {stderr}"
);
assert!(!out_dir.exists() || std::fs::read_dir(&out_dir).unwrap().count() == 0);
}
}
#[test]
fn unknown_vercel_target_fails_loudly_naming_the_known_targets() {
let tmp = tempfile::tempdir().expect("tempdir");
let ir_path = compile_analysis_agent_ir(tmp.path());
let out_dir = tmp.path().join("out");
let output = run_warble(&[
"dispatch".as_ref(),
ir_path.as_os_str(),
"--target".as_ref(),
"vercel:bogus".as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
]);
assert!(
!output.status.success(),
"an unknown vercel target should fail rather than silently fall back"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("vercel:bogus"),
"error should name the bad target; stderr: {stderr}"
);
assert!(
stderr.contains("vercel:headless"),
"error should list vercel:headless as a known target; stderr: {stderr}"
);
assert!(
stderr.contains("vercel:interactive"),
"error should list vercel:interactive as a known target; stderr: {stderr}"
);
}
#[test]
fn bare_dispatch_with_no_provider_loud_fails_naming_a_domain_capability() {
let tmp = tempfile::tempdir().expect("tempdir");
let ir_path = uncomposed_analysis_ir_path();
let out_dir = tmp.path().join("out");
let output = run_warble(&[
"dispatch".as_ref(),
ir_path.as_os_str(),
"--target".as_ref(),
"vercel".as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
]);
assert!(
!output.status.success(),
"a bare dispatch with no --provider must fail: examples/analysis-agent requires domain \
capabilities the base vercel target does not resolve on its own"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("semantic_introspection"),
"error should name the unresolved domain capability; stderr: {stderr}"
);
assert!(
!out_dir.exists() || std::fs::read_dir(&out_dir).unwrap().count() == 0,
"out_dir must not contain a partial bundle when dispatch fails"
);
}
#[test]
fn provider_supplied_capability_tools_are_sourced_from_the_provider_fragment() {
let tmp = tempfile::tempdir().expect("tempdir");
let ir_path = uncomposed_analysis_ir_path();
let out_dir = tmp.path().join("out");
let output = run_warble(&[
"dispatch".as_ref(),
ir_path.as_os_str(),
"--target".as_ref(),
"vercel".as_ref(),
"--out".as_ref(),
out_dir.as_os_str(),
"--provider".as_ref(),
sample_provider_path().as_os_str(),
]);
assert!(
output.status.success(),
"warble dispatch --target vercel --provider ... should succeed; stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let raw = std::fs::read_to_string(out_dir.join("bundle.json")).expect("read bundle.json");
let bundle: serde_json::Value =
serde_json::from_str(&raw).expect("bundle.json must parse as JSON");
let agents = bundle
.get("agents")
.and_then(|v| v.as_array())
.expect("bundle should have an agents array");
let answer_query = agents
.iter()
.find(|a| a.get("id").and_then(|v| v.as_str()) == Some("answer_query"))
.expect("answer_query agent should be present");
let tools = answer_query
.get("tools")
.and_then(|v| v.as_array())
.expect("answer_query should have a tools array");
let sources: Vec<&str> = tools
.iter()
.filter_map(|t| t.get("source").and_then(|v| v.as_str()))
.collect();
assert!(
sources.iter().any(|s| s.starts_with("mcp:sample/")),
"answer_query's tools should include a provider-sourced tool (mcp:sample/...); sources: {sources:?}"
);
}