use super::*;
#[test]
fn parses_a_graph_reply_whose_first_bracket_is_nested() {
let reply = r#"{ "facts": [ { "fact": "Zephyrin is the father of Kaltar.", "entities": ["zephyrin", "kaltar"] } ], "relations": [ { "subject": "zephyrin", "predicate": "pere de", "object": "kaltar" } ], "attributes": [ { "entity": "kaltar", "key": "age", "value": 15 } ] }"#;
let raw: RawExtraction = json_slice_object(reply).expect("the object is sliced whole");
let extraction = raw.into_extraction();
assert_eq!(extraction.facts.len(), 1);
assert_eq!(extraction.relations.len(), 1);
assert_eq!(extraction.relations[0].subject, "zephyrin");
assert_eq!(extraction.relations[0].predicate, "pere de");
assert_eq!(extraction.relations[0].object, "kaltar");
assert_eq!(extraction.attributes.len(), 1);
assert_eq!(extraction.attributes[0].value, serde_json::json!(15));
}
#[test]
fn parses_a_graph_reply_wrapped_in_prose_and_fences() {
let reply = "Here you go:\n```json\n{\"facts\": [], \"relations\": [{\"subject\": \"a\", \"predicate\": \"knows\", \"object\": \"b\"}], \"attributes\": []}\n```";
let raw: RawExtraction = json_slice_object(reply).expect("sliced past the fence");
assert_eq!(raw.into_extraction().relations.len(), 1);
}
#[test]
fn fact_only_slicing_still_prefers_the_array() {
let reply = "Result {ok}: [{\"fact\": \"A ships B.\", \"entities\": [\"b\"]}]";
let raw: Vec<RawFact> = json_slice(reply).expect("array sliced despite the stray brace");
assert_eq!(raw.len(), 1);
}
#[test]
fn graph_prompt_demands_numeric_values_and_the_three_sections() {
let prompt = build_graph_prompt("Kaltar a 15 ans.");
assert!(prompt.contains("Kaltar a 15 ans."));
assert!(prompt.contains("\"relations\""));
assert!(prompt.contains("\"attributes\""));
assert!(prompt.contains("15, not \"15\""));
}
#[test]
fn prompt_carries_the_passage_and_json_contract() {
let prompt = build_prompt("Alice adopted a dog in 2021.");
assert!(prompt.contains("Alice adopted a dog in 2021."));
assert!(prompt.contains("\"fact\": string"));
}
#[test]
fn graph_prompt_bounds_the_predicate_and_demands_edges() {
let prompt = build_graph_prompt("Ahmia is an onion search engine.");
assert!(prompt.contains("Ahmia is an onion search engine."));
assert!(
prompt.contains("at most 3 words"),
"the predicate length must be a hard bound, not a suggestion"
);
assert!(
prompt.contains("NEVER restate the sentence"),
"the counter-example is what stops a restated sentence"
);
assert!(
prompt.contains("at least one triple"),
"an entity with attributes but no edge is a dead end — the prompt \
must ask for the edge"
);
}
#[test]
fn graph_prompt_ties_every_output_to_the_passage_language() {
let prompt = build_graph_prompt("Sarah Miller has a brother, Tom Miller.");
assert!(
prompt.contains("Identify the passage's language"),
"the language rule must be an explicit first step, not an aside"
);
assert!(
prompt.contains("match the PASSAGE, never the example"),
"mixed-language examples are load-bearing — the rule must say they \
are examples of SHAPE, not of language"
);
}
#[test]
fn graph_prompt_forbids_both_directions_of_one_predicate() {
let prompt = build_graph_prompt("Sarah Miller has a brother, Tom Miller.");
assert!(
prompt.contains("emit exactly one"),
"the one-per-predicate rule must be stated as a hard bound"
);
assert!(
prompt.contains("keep both"),
"the rule must carry its POSITIVE half too: two different \
predicates stated separately are two facts — without it a model \
collapses a real converse pair into one edge (measured 3/3 on \
the bench's converse case)"
);
assert!(
prompt.contains("is a contradiction, not a converse"),
"the counter-example is what makes the rule unambiguous — the same \
device the carrier rule below relies on"
);
}
#[test]
fn graph_prompt_states_which_side_carries_the_relation() {
let prompt = build_graph_prompt("Theo Durand a une soeur, Camille Durand.");
assert!(
prompt.contains("whoever CARRIES the relation"),
"the rule must name the carrier, not just \"the direction\""
);
assert!(
prompt.contains("never A/\"soeur de\"/B"),
"the counter-example is what makes the rule unambiguous"
);
}
#[test]
fn parses_facts_from_a_fenced_reply() {
let reply = "Sure!\n```json\n[{\"fact\":\"Alice adopted a dog.\",\"entities\":[\"Adoption\",\"adoption\",\"\"]}]\n```";
let facts: Vec<RawFact> = json_slice(reply).expect("slice json");
let facts: Vec<ExtractedFact> = facts.into_iter().filter_map(RawFact::into_fact).collect();
assert_eq!(facts.len(), 1);
assert_eq!(facts[0].text, "Alice adopted a dog.");
assert_eq!(facts[0].entities, vec!["adoption".to_string()]);
}
#[test]
fn drops_a_textless_fact() {
let raw = RawFact {
fact: " ".to_string(),
entities: vec!["x".to_string()],
};
assert!(raw.into_fact().is_none());
}
#[test]
fn parses_response_envelope() {
let text = parse_generate_response(r#"{"response":" [] "}"#).expect("parse");
assert_eq!(text, "[]");
}
#[test]
fn rejects_response_without_field() {
assert!(matches!(
parse_generate_response(r#"{"oops":true}"#),
Err(ExtractError::Backend(_))
));
}