#![allow(clippy::result_large_err)]
use gemel::store::{InitOptions, Repo};
use gemel::workflow::{self, BeginOptions, ClaimSpec, EvidenceSpec, FinishOptions, ResidualSpec};
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn temp_root(tag: &str) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let dir = std::env::temp_dir().join(format!("gemel-p7-{tag}-{}-{n}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
dir
}
fn write_file(root: &Path, rel: &str, content: &str) {
let path = root.join(rel);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, content).unwrap();
}
fn seed(root: &Path) -> Repo {
write_file(root, "src/lib.rs", "pub fn f() {}\n");
Repo::init(root, &InitOptions::default()).unwrap()
}
fn make_change(repo: &Repo, root: &Path, summary: &str) {
workflow::begin_change(
repo,
&BeginOptions {
intent_summary: Some(summary.into()),
..Default::default()
},
)
.unwrap();
write_file(root, "src/lib.rs", "pub fn f() { /* changed */ }\n");
workflow::finish_change(
repo,
&FinishOptions {
summary: summary.into(),
..Default::default()
},
)
.unwrap();
}
fn run_protocol(root: &Path, requests: &str) -> Vec<serde_json::Value> {
let bin = env!("CARGO_BIN_EXE_gemel");
let mut child = Command::new(bin)
.arg("--repo")
.arg(root)
.arg("protocol")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.expect("spawn gemel protocol");
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(requests.as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
String::from_utf8_lossy(&out.stdout)
.lines()
.map(|l| serde_json::from_str(l).expect("response line is JSON"))
.collect()
}
#[test]
fn protocol_session_routes_queries_and_errors() {
let root = temp_root("session");
let repo = seed(&root);
make_change(&repo, &root, "first");
let responses = run_protocol(
&root,
"{\"id\":1,\"query\":\"status\"}\n\
{\"id\":2,\"query\":\"log\"}\n\
{\"id\":3,\"query\":\"next\"}\n\
{\"id\":4,\"query\":\"frobnicate\"}\n\
{\"id\":5,\"query\":\"why\",\"params\":{\"subject\":\"src/lib.rs\"}}\n",
);
assert_eq!(responses.len(), 5);
for r in &responses {
assert_eq!(r["schema"], "gemel.agent.v1");
}
assert_eq!(responses[0]["id"], 1);
assert_eq!(responses[0]["result"]["readiness"], "READY");
assert_eq!(
responses[1]["result"]["changes"].as_array().unwrap().len(),
1
);
assert_eq!(responses[3]["error"]["code"], "query_failed");
assert_eq!(responses[4]["result"]["subject"], "src/lib.rs");
let again = run_protocol(&root, "{\"id\":1,\"query\":\"status\"}\n");
assert_eq!(responses[0]["result"], again[0]["result"]);
}
#[test]
fn protocol_rejects_malformed_and_oversized_requests() {
let root = temp_root("bounds");
let repo = seed(&root);
make_change(&repo, &root, "first");
let mut huge = "{\"id\":1,\"query\":\"x\",\"params\":{\"pad\":\"".to_string();
huge.push_str(&"a".repeat(128 * 1024));
huge.push_str("\"}}}\n");
let responses = run_protocol(&root, "not json\n{\"query\":\"status\"}\n");
assert_eq!(responses[0]["error"]["code"], "invalid_request");
assert_eq!(responses[1]["error"]["code"], "invalid_request");
let bin = env!("CARGO_BIN_EXE_gemel");
let mut child = Command::new(bin)
.arg("--repo")
.arg(&root)
.arg("protocol")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(huge.as_bytes())
.unwrap();
let out = child.wait_with_output().unwrap();
let line = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(line.trim()).unwrap();
assert_eq!(v["error"]["code"], "limit_exceeded");
}
#[test]
fn next_derives_honest_recommendations() {
let root = temp_root("next");
let repo = seed(&root);
let plan = gemel::query::next_plan(&repo).unwrap();
assert!(plan.recommendations.iter().any(|r| r.kind == "continue"));
assert!(plan
.recommendations
.iter()
.any(|r| r.certainty == "observed"));
workflow::begin_change(
&repo,
&BeginOptions {
intent_summary: Some("with residual".into()),
..Default::default()
},
)
.unwrap();
write_file(&root, "src/lib.rs", "pub fn f() { /* v2 */ }\n");
workflow::finish_change(
&repo,
&FinishOptions {
summary: "change with residual".into(),
residuals: vec![ResidualSpec {
summary: "FreeBSD divergence".into(),
severity: "high".into(),
classification: "platform_divergence".into(),
..Default::default()
}],
..Default::default()
},
)
.unwrap();
let plan = gemel::query::next_plan(&repo).unwrap();
assert!(
plan.recommendations
.iter()
.any(|r| r.kind == "resolve" && r.rationale.contains("FreeBSD divergence")),
"expected resolve recommendation: {:?}",
plan.recommendations
);
assert!(
plan.recommendations.iter().any(|r| r.kind == "index"),
"expected index recommendation: {:?}",
plan.recommendations
);
let again = gemel::query::next_plan(&repo).unwrap();
assert_eq!(
plan.recommendations
.iter()
.map(|r| (r.kind.clone(), r.subject.clone(), r.rationale.clone()))
.collect::<Vec<_>>(),
again
.recommendations
.iter()
.map(|r| (r.kind.clone(), r.subject.clone(), r.rationale.clone()))
.collect::<Vec<_>>()
);
}
#[test]
fn next_flags_blocked_claims_for_verification() {
let root = temp_root("blocked");
let repo = seed(&root);
workflow::begin_change(
&repo,
&BeginOptions {
intent_summary: Some("claims".into()),
..Default::default()
},
)
.unwrap();
write_file(&root, "src/lib.rs", "pub fn f() { /* v3 */ }\n");
workflow::finish_change(
&repo,
&FinishOptions {
summary: "change with claims".into(),
evidence: vec![
EvidenceSpec {
subject: Some("src/lib.rs".into()),
outcome: "pass".into(),
kind: "test_result".into(),
},
EvidenceSpec {
subject: Some("src/lib.rs".into()),
outcome: "fail".into(),
kind: "test_result".into(),
},
],
claims: vec![
ClaimSpec {
subject: Some("src/lib.rs".into()),
predicate: "parser is stable".into(),
kind: "correctness".into(),
evidence: vec![0],
},
ClaimSpec {
subject: Some("src/lib.rs".into()),
predicate: "parser is stable".into(),
kind: "correctness".into(),
evidence: vec![1],
},
],
..Default::default()
},
)
.unwrap();
let plan = gemel::query::next_plan(&repo).unwrap();
assert!(
plan.recommendations.iter().any(|r| r.kind == "verify"),
"blocked claims must surface a verify recommendation: {:?}",
plan.recommendations
);
}
#[test]
fn policy_matrix_drives_readiness() {
let root = temp_root("policy");
let repo = seed(&root);
make_change(&repo, &root, "first");
let st = gemel::query::status(&repo).unwrap();
assert!(gemel::query::required_verification(&repo)
.unwrap()
.is_empty());
assert!(gemel::query::required_verification_gaps(&repo)
.unwrap()
.is_empty());
assert_eq!(st.readiness.as_str(), "READY");
use gemel::value::{Field, Object, Value};
let platform = |platform: &str, arch: &str| {
Value::Record(vec![
Field::new(0x01, Value::Str(platform.into())),
Field::new(0x02, Value::Str(arch.into())),
])
};
let entry = Value::Record(vec![
Field::new(0x01, Value::Str("correctness".into())),
Field::new(
0x02,
Value::Array(vec![
platform("linux", "x86_64"),
platform("freebsd", "x86_64"),
]),
),
]);
let matrix = Object::fields(
gemel::family::Family::Config,
vec![
Field::new(0x04, Value::Str("never_auto_execute".into())), Field::new(
0x08,
Value::Record(vec![Field::new(0x01, Value::Array(vec![entry]))]),
),
],
);
let cfg_gid = repo.insert_object(&matrix).unwrap();
repo.with_write_lock(|| {
repo.apply_refs_unlocked(&gemel::store::refs::RefTransaction {
ops: vec![gemel::store::refs::RefOp::set(
gemel::store::REF_CONFIG,
cfg_gid,
)],
})
.unwrap();
Ok::<(), gemel::store::Error>(())
})
.unwrap();
let matrix = gemel::query::required_verification(&repo).unwrap();
assert_eq!(
matrix,
vec![
(
"correctness".to_string(),
"freebsd".to_string(),
"x86_64".to_string()
),
(
"correctness".to_string(),
"linux".to_string(),
"x86_64".to_string()
),
]
);
let gaps = gemel::query::required_verification_gaps(&repo).unwrap();
assert_eq!(gaps.len(), 2);
let st = gemel::query::status(&repo).unwrap();
assert_eq!(st.readiness.as_str(), "NOT_READY");
}
#[test]
fn protocol_exposes_next_and_policy() {
let root = temp_root("proto-next");
let repo = seed(&root);
make_change(&repo, &root, "first");
let responses = run_protocol(
&root,
"{\"id\":1,\"query\":\"next\"}\n\
{\"id\":2,\"query\":\"policy\"}\n",
);
assert!(responses[0]["result"]["recommendations"].is_array());
let bin = env!("CARGO_BIN_EXE_gemel");
let out = Command::new(bin)
.arg("--repo")
.arg(&root)
.args(["policy", "--json"])
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert!(v["result"]["required_verification"].is_array());
}