use std::path::PathBuf;
use contextgraph_conformance::{check_budget, check_frames};
use contextgraph_host::wire::{Envelope, decode_line, encode_line};
use contextgraph_host::{ContextProvider, IngestConfig, PasteIngest, ingest_paste};
use contextgraph_types::{ContextQuery, Representation};
fn schema_required_keys(definition: &str) -> Vec<String> {
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("workspace root")
.join("schema")
.join("contextgraph-envelope.schema.json");
let raw =
std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let schema: serde_json::Value = serde_json::from_str(&raw).expect("schema is valid JSON");
schema["$defs"][definition]["required"]
.as_array()
.unwrap_or_else(|| panic!("$defs.{definition}.required is an array"))
.iter()
.map(|v| v.as_str().expect("a required key is a string").to_string())
.collect()
}
fn schema_required_frame_keys() -> Vec<String> {
schema_required_keys("ContextFrame")
}
#[test]
fn an_unfiltered_unanchored_query_satisfies_the_schemas_required_keys() {
let query = ContextQuery {
goal: "why does the retry loop give up".into(),
query_text: None,
embedding: None,
kinds: Vec::new(),
anchors: Vec::new(),
max_frames: 8,
max_tokens: 2000,
as_of: None,
representation_preferences: Vec::new(),
};
let value = serde_json::to_value(&query).expect("query serializes");
let object = value.as_object().expect("a query serializes to an object");
assert!(
!object.contains_key("kinds") && !object.contains_key("anchors"),
"the reference serializer no longer elides empty kinds/anchors — \
this guard's premise changed: {value}"
);
for key in schema_required_keys("ContextQuery") {
assert!(
object.contains_key(&key),
"an ordinary query omits schema-required key `{key}`, making it \
un-representable in conformant JSON: {value}"
);
}
}
fn motivating_paste() -> PasteIngest {
let log: String = (0..80)
.map(|i| {
let level = if i == 40 {
"ERROR"
} else if i % 9 == 0 {
"WARN "
} else {
"INFO "
};
format!(
"2026-07-20T18:{:02}:{:02}Z {level} retry attempt {i}\n",
i / 60,
i % 60
)
})
.collect();
let table = "\
| endpoint | calls | p99_ms | errors |
|----------|-------|--------|--------|
| /query | 1841 | 210 | 3 |
| /resolve | 92 | 1204 | 41 |";
PasteIngest {
intent: "figure out why the retry loop gives up".to_string(),
anchors: vec![],
attachments: vec![
log,
table.to_string(),
"a note: backoff may be too aggressive".to_string(),
"./src/net".to_string(),
],
}
}
#[tokio::test]
async fn ingested_frames_pass_frame_budget_and_schema_conformance_in_every_representation() {
let required = schema_required_frame_keys();
assert!(required.iter().any(|k| k == "id"));
assert!(required.iter().any(|k| k == "token_cost"));
let bundle = ingest_paste(motivating_paste(), IngestConfig::default());
assert!(bundle.provider.len() >= 3, "log, table, and note frames");
for representation in [
Representation::Full,
Representation::Compact,
Representation::Reference,
] {
let query = ContextQuery {
representation_preferences: vec![representation],
max_frames: u32::MAX,
max_tokens: u32::MAX,
..bundle.query.clone()
};
let result = bundle.provider.query(&query).await.unwrap();
assert!(
!result.frames.is_empty(),
"{representation:?} served no frames"
);
let (frames_ok, evidence) = check_frames(&result);
assert!(frames_ok, "{representation:?} frame-validity: {evidence}");
let (budget_ok, evidence) = check_budget(&result, &query);
assert!(budget_ok, "{representation:?} budget: {evidence}");
for frame in &result.frames {
frame
.representation_invariants()
.unwrap_or_else(|e| panic!("{representation:?} frame {}: {e}", frame.id));
let value = serde_json::to_value(frame).expect("frame serializes");
let object = value.as_object().expect("a frame serializes to an object");
for key in &required {
assert!(
object.contains_key(key),
"{representation:?} frame {} omits schema-required key `{key}`",
frame.id
);
}
}
let envelope = Envelope::Frames { id: None, result };
let line = encode_line(&envelope).expect("frames envelope encodes");
assert!(matches!(
decode_line(&line).expect("frames envelope decodes"),
Envelope::Frames { .. }
));
}
}