mod common;
use common::*;
use lonca_core::ops::Op;
fn bind(e: &mut Env, token: &str, harness: &str) -> Value {
let p = json!({"harness": harness, "harness_session": "h", "sudo_without_password": false});
e.ok("hook.bind", None, Some(token), p)
}
#[test]
fn a_profile_with_no_table_keeps_the_built_in_one() {
let mut e = Env::new();
e.init(None);
let t = e.session("s1", &["src/**"]);
e.bind(&t);
let table = r#"
[harnesses.claude-code]
events = { session_start = "SessionStart", before_tool = "PreToolUse", after_tool = "PostToolUse" }
deny = { why = "{reason}" }
context = { say = "{context}" }
"#;
let config = format!("{}\n{table}", lonca_policy::config::DEFAULT_CONFIG);
let p = json!({"config": config, "policy": lonca_policy::config::DEFAULT_POLICY});
let prep = e.ok("human.prepare_policy", Some("demo"), None, p);
let op: Op = serde_json::from_value(prep["op"].clone()).unwrap();
assert_eq!(e.human(op)["decision"]["outcome"], "allow");
assert_eq!(bind(&mut e, &t, "claude-code")["level"], "hooks_sandbox");
}
const LOCKED: &str = r#"
[harnesses.codex.locked]
file = { linux = "/nowhere", macos = "/nowhere" }
format = "json"
hook_lists = ["/hooks/PreToolUse", "/hooks/PostToolUse"]
hooks = [{ at = "/allow_managed_hooks_only", equals = true }]
"#;
#[test]
fn a_table_in_the_signed_policy_gives_a_harness_its_level() {
let mut e = Env::new();
e.init(None);
let t = e.session_under("s1", &["src/**"], Some("codex"));
let hook = json!([{"hooks": [{"command": "/usr/bin/lonca hook"}]}]);
let file = json!({
"allow_managed_hooks_only": true,
"hooks": {"PreToolUse": hook, "PostToolUse": hook},
});
let path = e.settings.managed_settings.clone().unwrap();
std::fs::write(&path, serde_json::to_vec(&file).unwrap()).unwrap();
let r = bind(&mut e, &t, "codex");
assert_eq!(r["level"], "unobserved", "{r}");
assert_eq!(r["notes"], json!(["no locked profile for harness codex"]));
let table = lonca_policy::harness::built_in_table("codex")
.unwrap()
.unwrap();
let config = format!("{}\n{table}{LOCKED}", lonca_policy::config::DEFAULT_CONFIG);
let p = json!({"config": config, "policy": lonca_policy::config::DEFAULT_POLICY});
let prep = e.ok("human.prepare_policy", Some("demo"), None, p);
let op: Op = serde_json::from_value(prep["op"].clone()).unwrap();
assert_eq!(e.human(op)["decision"]["outcome"], "allow");
let t2 = e.session_under("s2", &["src/**"], Some("codex"));
let r = bind(&mut e, &t2, "codex");
assert_eq!(r["level"], "hooks", "{r}");
let mut open = file.clone();
open["allow_managed_hooks_only"] = json!(false);
std::fs::write(&path, serde_json::to_vec(&open).unwrap()).unwrap();
let r = bind(&mut e, &t2, "codex");
assert_eq!(r["level"], "unobserved", "{r}");
assert!(
r["notes"].to_string().contains("allow_managed_hooks_only"),
"{r}"
);
}
#[test]
fn a_level_is_read_for_the_harness_the_session_started_under() {
let mut e = Env::new();
e.init(None);
let locked = e.session("locked", &["src/**"]);
e.bind(&locked);
let t = e.session_under("s1", &["src/**"], Some("codex"));
let r = bind(&mut e, &t, "claude-code");
assert_eq!(r["level"], "unobserved", "{r}");
assert_eq!(r["decision"]["outcome"], "allow", "{r}");
assert_eq!(
r["notes"],
json!(["the session was started for codex, not claude-code"])
);
let t = e.session_under("s2", &["src/**"], None);
let r = bind(&mut e, &t, "claude-code");
assert_eq!(r["level"], "unobserved", "{r}");
assert_eq!(
r["notes"],
json!(["the session was started with no harness named"])
);
let st = e.ok("status", Some("demo"), None, json!({}));
let of = |id: &str| {
let s = st["sessions"].as_array().unwrap().iter();
s.clone().find(|s| s["id"] == id).unwrap()["harness"].clone()
};
assert_eq!(of("s1"), "codex");
assert_eq!(of("s2"), Value::Null);
}
#[test]
fn a_sandbox_on_another_port_is_not_locked_for_lonca() {
let mut e = Env::new();
e.init(None);
let locked = e.session("locked", &["src/**"]);
e.bind(&locked);
assert_eq!(
e.ok("ping", None, None, json!({}))["proxy"],
"127.0.0.1:8471"
);
let file = e.settings.managed_settings.clone().unwrap();
let mut settings: Value = serde_json::from_slice(&std::fs::read(&file).unwrap()).unwrap();
settings["sandbox"]["network"]["httpProxyPort"] = json!(9000);
std::fs::write(&file, serde_json::to_vec(&settings).unwrap()).unwrap();
let t = e.session("s1", &["src/**"]);
let r = bind(&mut e, &t, "claude-code");
assert_eq!(r["level"], "hooks", "{r}");
let notes = r["notes"].as_array().unwrap();
assert!(
notes
.iter()
.any(|n| n.as_str().unwrap().contains("httpProxyPort")),
"{r}"
);
}
#[test]
fn a_session_that_bound_low_stays_low() {
let mut e = Env::new();
e.init(None);
let locked = e.session("locked", &["src/**"]);
e.bind(&locked);
let t = e.session("s1", &["src/**"]);
let sudo = |open| json!({"harness": "claude-code", "harness_session": "h", "sudo_without_password": open});
let r = e.ok("hook.bind", None, Some(&t), sudo(true));
assert_eq!(r["level"], "unobserved", "{r}");
let r = e.ok("hook.bind", None, Some(&t), sudo(false));
assert_eq!(r["decision"]["outcome"], "deny", "{r}");
assert_eq!(
r["decision"]["reasons"],
json!(["bind_raises_level"]),
"{r}"
);
assert_eq!(r["level"], "unobserved", "{r}");
}
#[test]
fn a_continuation_binds_under_the_harness_it_took_over() {
let mut e = Env::new();
e.init(None);
let old = e.session("s1", &["src/**"]);
e.bind(&old);
let r = e.continue_session("s2", "s1", &["src/**"]);
let t = r["token"].as_str().unwrap().to_string();
assert_eq!(bind(&mut e, &t, "claude-code")["level"], "hooks_sandbox");
}