use warble_claude_code::emit_claude_code;
use warble_claude_code::ir::{WarbleIr, SUPPORTED_IR_VERSION};
const VERSION_MISMATCH_FIXTURE: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../conformance-fixtures/ir-version-mismatch.json"
);
fn load_ir(relative: &str) -> WarbleIr {
let path = format!("{}/{relative}", env!("CARGO_MANIFEST_DIR"));
let raw = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {path}: {e}"));
serde_json::from_str(&raw).unwrap_or_else(|e| panic!("parse {path}: {e}"))
}
fn extract_all_quoted_after(haystack: &str, needle: &str) -> Vec<String> {
let Some(line) = haystack.lines().find(|l| l.contains(needle)) else {
return Vec::new();
};
let Some(after_needle) = line.find(needle).map(|i| &line[i + needle.len()..]) else {
return Vec::new();
};
let mut out = Vec::new();
let mut rest = after_needle;
while let Some(start) = rest.find('"') {
let tail = &rest[start + 1..];
let Some(end) = tail.find('"') else { break };
out.push(tail[..end].to_string());
rest = &tail[end + 1..];
}
out
}
fn extract_one_quoted_after(haystack: &str, needle: &str) -> String {
let all = extract_all_quoted_after(haystack, needle);
assert_eq!(
all.len(),
1,
"expected exactly one quoted value on the line containing `{needle}` (found {all:?})"
);
all[0].clone()
}
fn extract_after(haystack: &str, needle: &str) -> Option<String> {
let line = haystack.lines().find(|l| l.contains(needle))?;
let after = line[line.find(needle)? + needle.len()..]
.trim()
.trim_start_matches('`');
let token: String = after
.chars()
.take_while(|c| c.is_ascii_digit() || *c == '.')
.collect();
(!token.is_empty()).then_some(token)
}
#[test]
fn ir_version_is_in_lockstep_across_rust_ts_and_doc() {
let crate_dir = env!("CARGO_MANIFEST_DIR");
let core_src = std::fs::read_to_string(format!("{crate_dir}/../../core/src/compile.rs"))
.expect("read core/src/compile.rs");
let core_emitted = extract_one_quoted_after(&core_src, "\"warble_ir_version\":");
assert_eq!(
SUPPORTED_IR_VERSION, core_emitted,
"core's emitted warble_ir_version literal has drifted from this back-end's enforced version — bump both together"
);
let ts_src = std::fs::read_to_string(format!("{crate_dir}/../claude-agent-sdk/src/ir.ts"))
.expect("read TS ir.ts");
let ts_versions = extract_all_quoted_after(&ts_src, "export const SUPPORTED_IR_VERSIONS");
assert_eq!(
ts_versions.len(),
1,
"TS SUPPORTED_IR_VERSIONS must declare exactly one supported version (found {ts_versions:?}) \
— this back-end's warble_ir_version contract is a single exact-match value, not a range; if TS ever \
needs to widen its accepted set, that is a deliberate cross-back-end version-support decision, not \
something to slip in silently"
);
let ts_version = ts_versions[0].clone();
let doc = std::fs::read_to_string(format!("{crate_dir}/../../docs/spec/ir-schema.md"))
.expect("read ir-schema.md");
let doc_version =
extract_after(&doc, "warble_ir_version:").expect("doc warble_ir_version in the title");
assert_eq!(
SUPPORTED_IR_VERSION, ts_version,
"Rust and TS supported IR version disagree — bump both together"
);
assert_eq!(
SUPPORTED_IR_VERSION, doc_version,
"Rust const and docs/spec/ir-schema.md version disagree — bump both together"
);
let vercel_src = std::fs::read_to_string(format!("{crate_dir}/../vercel/src/emit.rs"))
.expect("read vercel emit.rs");
let vercel_min = extract_one_quoted_after(&vercel_src, "const MIN_SUPPORTED_IR_VERSION");
let vercel_max = extract_one_quoted_after(&vercel_src, "const MAX_SUPPORTED_IR_VERSION");
assert_eq!(
SUPPORTED_IR_VERSION, vercel_min,
"vercel's advisory MIN_SUPPORTED_IR_VERSION has drifted from the enforced IR version — bump both together"
);
assert_eq!(
SUPPORTED_IR_VERSION, vercel_max,
"vercel's advisory MAX_SUPPORTED_IR_VERSION has drifted from the enforced IR version — bump both together"
);
let ts_manifest_src =
std::fs::read_to_string(format!("{crate_dir}/../claude-agent-sdk/src/manifest.ts"))
.expect("read TS manifest.ts");
let ts_manifest_min =
extract_one_quoted_after(&ts_manifest_src, "const MIN_SUPPORTED_IR_VERSION");
let ts_manifest_max =
extract_one_quoted_after(&ts_manifest_src, "const MAX_SUPPORTED_IR_VERSION");
assert_eq!(
SUPPORTED_IR_VERSION, ts_manifest_min,
"TS manifest's advisory MIN_SUPPORTED_IR_VERSION has drifted from the enforced IR version — bump both together"
);
assert_eq!(
SUPPORTED_IR_VERSION, ts_manifest_max,
"TS manifest's advisory MAX_SUPPORTED_IR_VERSION has drifted from the enforced IR version — bump both together"
);
}
#[test]
fn emit_rejects_an_out_of_range_ir_version_explicitly() {
let mut ir = load_ir("../../genbi-default/ir.golden.json");
ir.warble_ir_version = "0.2".to_string();
let tmp = tempfile::tempdir().expect("tempdir");
let err = emit_claude_code(
&ir,
tmp.path(),
"claude-code:headless",
warble_claude_code::DEFAULT_RENDER_FLAVOR,
)
.expect_err("an out-of-range IR version must not silently emit");
let message = err.to_string();
assert!(
message.contains("0.2"),
"error should name the rejected version, got: {message}"
);
assert!(
message.contains(SUPPORTED_IR_VERSION),
"error should name the supported version, got: {message}"
);
assert!(
!tmp.path().join("RUN.md").exists(),
"a rejected version must not have written any output"
);
}
#[test]
fn emit_rejects_the_shared_cross_back_end_version_mismatch_fixture() {
let raw = std::fs::read_to_string(VERSION_MISMATCH_FIXTURE)
.unwrap_or_else(|e| panic!("read {VERSION_MISMATCH_FIXTURE}: {e}"));
let fixture: serde_json::Value = serde_json::from_str(&raw).expect("fixture is valid JSON");
let ir: WarbleIr =
serde_json::from_value(fixture["ir"].clone()).expect("fixture ir deserializes");
let expected: Vec<&str> = fixture["expected_error_contains"]
.as_array()
.expect("expected_error_contains is an array")
.iter()
.map(|v| {
v.as_str()
.expect("expected_error_contains entries are strings")
})
.collect();
let tmp = tempfile::tempdir().expect("tempdir");
let err = emit_claude_code(
&ir,
tmp.path(),
"claude-code:headless",
warble_claude_code::DEFAULT_RENDER_FLAVOR,
)
.expect_err("the shared fixture's out-of-range version must not silently emit");
let message = err.to_string();
for substring in expected {
assert!(
message.contains(substring),
"error should contain '{substring}', got: {message}"
);
}
}