mod common;
use common::*;
use std::io::Write;
use std::process::{Command, Stdio};
struct DaemonGuard(std::process::Child);
impl Drop for DaemonGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
#[test]
fn export_validates_against_json_schema() {
let repo = copy_fixture("monorepo-acceptance");
std::fs::create_dir_all(workdir(repo.path()).join(".scc")).unwrap();
std::fs::write(
workdir(repo.path()).join(".scc/intent.yaml"),
"components:\n api:\n owns: [transcript]\n",
)
.unwrap();
run_ok(&workdir(repo.path()), &["index", "--quiet"]);
let ir = run_ok(&workdir(repo.path()), &["export", "system-ir.json"]);
let value: serde_json::Value = serde_json::from_str(&ir).unwrap();
let schema_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join("docs/system-ir.schema.json");
let schema_text = std::fs::read_to_string(&schema_path).unwrap();
let schema: serde_json::Value = serde_json::from_str(&schema_text).unwrap();
let compiled = jsonschema::validator_for(&schema).expect("schema compiles");
let result = compiled.validate(&value);
assert!(
result.is_ok(),
"System IR export violates the documented schema: {:?}",
result.err().map(|e| e.to_string())
);
}
#[test]
fn mcp_server_exposes_ten_semantic_tools() {
let repo = copy_fixture("http-service-python");
run_ok(&workdir(repo.path()), &["index", "--quiet"]);
let mut child = Command::new(scc())
.arg("mcp")
.current_dir(workdir(repo.path()))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let send = |stdin: &mut std::process::ChildStdin, msg: &str| {
writeln!(stdin, "{msg}").unwrap();
stdin.flush().unwrap();
};
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test"}}}"#,
);
send(&mut stdin, r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"system_overview","arguments":{}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"task_context","arguments":{"goal":"transcript normalization"}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"impact_context","arguments":{"files":["main.py"]}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"system_context","arguments":{}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"surface_map","arguments":{"goal":"transcript normalization"}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"structural_source","arguments":{"files":["main.py"]}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"surface_map","arguments":{}}}"#,
);
drop(stdin);
let mut stdout = String::new();
use std::io::Read;
child.stdout.take().unwrap().read_to_string(&mut stdout).unwrap();
let _ = child.wait();
let mut by_id: std::collections::BTreeMap<i64, serde_json::Value> = Default::default();
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
let v: serde_json::Value = serde_json::from_str(line).unwrap();
if let Some(id) = v.get("id").and_then(|i| i.as_i64()) {
by_id.insert(id, v);
}
}
assert_eq!(by_id.len(), 9, "all requests answered: {stdout}");
let tools = &by_id[&2]["result"]["tools"];
let names: Vec<&str> = tools
.as_array()
.unwrap()
.iter()
.filter_map(|t| t["name"].as_str())
.collect();
assert_eq!(
names,
vec![
"system_overview",
"system_atlas",
"task_context",
"component_context",
"flow_context",
"impact_context",
"verify_context",
"system_context",
"surface_map",
"structural_source"
],
"the ten semantic tools"
);
let overview_text = by_id[&3]["result"]["content"][0]["text"].as_str().unwrap();
assert!(overview_text.contains("IDENTITY"));
let task_text = by_id[&4]["result"]["content"][0]["text"].as_str().unwrap();
assert!(task_text.contains("TASK"));
assert!(task_text.contains("Normalizer"));
let impact_text = by_id[&5]["result"]["content"][0]["text"].as_str().unwrap();
assert!(impact_text.contains("AFFECTED COMPONENTS"));
let startup_text = by_id[&6]["result"]["content"][0]["text"].as_str().unwrap();
assert!(startup_text.contains("## SYSTEM ATLAS"), "{startup_text}");
assert!(startup_text.contains("## SYSTEM SURFACE MAP"), "{startup_text}");
assert!(startup_text.contains("## OMISSIONS"), "{startup_text}");
let surface_text = by_id[&7]["result"]["content"][0]["text"].as_str().unwrap();
let global_surface = by_id[&9]["result"]["content"][0]["text"].as_str().unwrap();
assert_ne!(surface_text, global_surface, "task surface must differ from global");
assert!(
surface_text.contains("transcript normalization"),
"{surface_text}"
);
assert!(surface_text.contains("handle_transcripts"), "{surface_text}");
let structural_text = by_id[&8]["result"]["content"][0]["text"].as_str().unwrap();
assert!(structural_text.contains("source: main.py:L"), "{structural_text}");
}
#[test]
fn mcp_stdio_negotiates_2025_11_25_then_lists_tools() {
let repo = copy_fixture("http-service-python");
run_ok(&workdir(repo.path()), &["index", "--quiet"]);
let mut child = Command::new(scc())
.arg("mcp")
.current_dir(workdir(repo.path()))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let send = |stdin: &mut std::process::ChildStdin, msg: &str| {
writeln!(stdin, "{msg}").unwrap();
stdin.flush().unwrap();
};
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"omp-e2e"}}}"#,
);
send(
&mut stdin,
r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
);
send(&mut stdin, r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#);
drop(stdin);
let mut stdout = String::new();
use std::io::Read;
child.stdout.take().unwrap().read_to_string(&mut stdout).unwrap();
let _ = child.wait();
let mut by_id: std::collections::BTreeMap<i64, serde_json::Value> = Default::default();
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
let v: serde_json::Value = serde_json::from_str(line).unwrap();
if let Some(id) = v.get("id").and_then(|i| i.as_i64()) {
by_id.insert(id, v);
}
}
assert!(by_id.contains_key(&1), "initialize answered: {stdout}");
assert_eq!(
by_id[&1]["result"]["protocolVersion"].as_str(),
Some("2025-11-25"),
"server must echo 2025-11-25: {stdout}"
);
assert!(by_id.contains_key(&2), "tools/list answered after initialized: {stdout}");
let tools = &by_id[&2]["result"]["tools"];
let names: Vec<&str> = tools
.as_array()
.unwrap()
.iter()
.filter_map(|t| t["name"].as_str())
.collect();
assert!(names.contains(&"system_context"), "{names:?}");
assert!(names.contains(&"surface_map"), "{names:?}");
assert!(names.contains(&"structural_source"), "{names:?}");
let ctx_tool = tools
.as_array()
.unwrap()
.iter()
.find(|t| t["name"] == "system_context")
.unwrap();
let desc = ctx_tool["description"].as_str().unwrap_or("");
let budget_desc = ctx_tool["inputSchema"]["properties"]["token_budget"]["description"]
.as_str()
.unwrap_or("");
assert!(
!budget_desc.contains("13:7") && !desc.contains("13:7"),
"system_context must not advertise the retired 13:7 split: {budget_desc} / {desc}"
);
assert!(
budget_desc.contains("adaptive") || budget_desc.contains("complexity"),
"system_context budget description must name adaptive allocation: {budget_desc}"
);
}
#[test]
fn mcp_unknown_tool_returns_error() {
let repo = copy_fixture("http-service-python");
run_ok(&workdir(repo.path()), &["index", "--quiet"]);
let mut child = Command::new(scc())
.arg("mcp")
.current_dir(workdir(repo.path()))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
{
let mut stdin = child.stdin.take().unwrap();
writeln!(
stdin,
r#"{{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{{"name":"query_graph","arguments":{{}}}}}}"#
)
.unwrap();
stdin.flush().unwrap();
}
let mut stdout = String::new();
use std::io::Read;
child.stdout.take().unwrap().read_to_string(&mut stdout).unwrap();
let _ = child.wait();
let v: serde_json::Value = serde_json::from_str(stdout.lines().next().unwrap()).unwrap();
assert_eq!(v["result"]["isError"], true, "advanced tools are not exposed");
}
#[test]
fn context_parity_across_cli_http_mcp() {
let repo = copy_fixture("http-service-python");
let dir = workdir(repo.path());
let addr = test_listen_addr();
std::fs::create_dir_all(dir.join(".scc")).unwrap();
std::fs::create_dir_all(dir.join(".beads")).unwrap();
std::fs::write(
dir.join(".scc/config.yaml"),
format!(
"schema: 1\nindex:\n watch: false\ninference:\n enabled: true\n provider: local\n base_url: http://127.0.0.1:1\nintegrations:\n hindsight: true\nsecurity:\n listen: {addr}\n"
),
)
.unwrap();
std::fs::write(
dir.join(".beads/issues.jsonl"),
"{\"id\":\"b1\",\"title\":\"Fix normalization retry\",\"status\":\"in_progress\",\"dependencies\":[]}\n",
)
.unwrap();
std::fs::write(
dir.join(".scc/lessons.jsonl"),
"{\"id\":\"l1\",\"text\":\"raw transcripts are immutable\",\"created_at\":\"2026-01-01T00:00:00Z\"}\n",
)
.unwrap();
run_ok(
&dir,
&["import", "hindsight", ".scc/lessons.jsonl"],
);
run_ok(&dir, &["index", "--quiet"]);
let goal = "change transcript normalization";
let cli_json = run_ok(&dir, &["context", "task", "--json", goal]);
let cli: serde_json::Value = serde_json::from_str(&cli_json).unwrap();
let cli_content = cli["pack"]["content"].as_str().unwrap().to_string();
assert!(
cli_content.contains("ACTIVE TASK STATE"),
"beads enrichment present on CLI: {cli_content}"
);
assert!(
cli_content.contains("HINDSIGHT LESSONS"),
"hindsight enrichment present on CLI: {cli_content}"
);
let _child = DaemonGuard(
Command::new(scc())
.arg("serve")
.current_dir(&dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap(),
);
let mut ready = false;
for _ in 0..40 {
if std::net::TcpStream::connect(addr.as_str()).is_ok() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
assert!(ready, "daemon did not start");
let post = |path: &str, body: &str| -> (u16, String) {
let mut stream = std::net::TcpStream::connect(addr.as_str()).unwrap();
write!(
stream,
"POST {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.unwrap();
let mut buf = String::new();
use std::io::Read;
stream.read_to_string(&mut buf).unwrap();
let status: u16 = buf.split_whitespace().nth(1).unwrap().parse().unwrap();
let body = buf.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
(status, body)
};
let (s, http_body) = post(
"/v1/context/task",
&format!(r#"{{"goal":"{goal}"}}"#),
);
assert_eq!(s, 200);
let http: serde_json::Value = serde_json::from_str(&http_body).unwrap();
let http_content = http["pack"]["content"].as_str().unwrap().to_string();
assert!(
http["delta"].as_str().is_some_and(|d| !d.is_empty()),
"HTTP artifact must carry the surface delta"
);
drop(_child);
let mut child = Command::new(scc())
.arg("mcp")
.current_dir(&dir)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let send = |stdin: &mut std::process::ChildStdin, msg: &str| {
writeln!(stdin, "{msg}").unwrap();
stdin.flush().unwrap();
};
send(
&mut stdin,
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"parity"}}}"#,
);
send(
&mut stdin,
&format!(
r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"task_context","arguments":{{"goal":"{goal}"}}}}}}"#
),
);
drop(stdin);
let mut stdout = String::new();
use std::io::Read;
child.stdout.take().unwrap().read_to_string(&mut stdout).unwrap();
let _ = child.wait();
let mut mcp_content = String::new();
for line in stdout.lines() {
let v: serde_json::Value = serde_json::from_str(line).unwrap();
if v.get("id").and_then(|i| i.as_i64()) == Some(2) {
mcp_content = v["result"]["content"][0]["text"].as_str().unwrap().to_string();
}
}
assert!(!mcp_content.is_empty(), "MCP answered: {stdout}");
assert_eq!(cli_content, http_content, "CLI and HTTP packs differ");
assert!(
mcp_content.starts_with(&cli_content),
"CLI and MCP packs differ (MCP must lead with the same pack)"
);
let engine_out = scc_engine::invoke(
&dir,
"context.task",
serde_json::json!({"goal": goal, "files": [], "symbols": [], "budget": null, "hook": false}),
)
.unwrap();
assert!(
engine_out["pack"]["content"].as_str().is_some_and(|c| !c.is_empty()),
"engine invoke must produce a pack"
);
assert!(
engine_out["pack"]["content"].as_str().unwrap().contains("ACTIVE TASK STATE"),
"engine pack carries the same enrichment"
);
}
#[test]
fn structural_source_cli_renders_file_units() {
let repo = copy_fixture("http-service-python");
let dir = workdir(repo.path());
run_ok(&dir, &["index", "--quiet"]);
let out = run_ok(&dir, &["context", "structural", "--files", "main.py"]);
assert!(out.contains("source: main.py:L"), "unit provenance: {out}");
assert!(out.contains("revision:"), "revision header: {out}");
assert!(out.contains("handle_transcripts"), "symbol rendered: {out}");
let task = run_ok(
&dir,
&["context", "structural", "--task", "transcript normalization"],
);
assert!(task.contains("representation:"), "task-matched units: {task}");
assert!(task.contains("transcripts.py"), "task matched the service file: {task}");
let capped = run_ok(&dir, &["context", "structural", "--files", "main.py", "--budget", "1000"]);
assert!(capped.contains("source: main.py:L"), "budgeted render: {capped}");
}
#[test]
fn structural_stale_handle_is_refused_on_cli() {
let repo = copy_fixture("http-service-python");
let dir = workdir(repo.path());
run_ok(&dir, &["index", "--quiet"]);
let stale = "scc://repo/epoch/file/main.py@aaaaaaaaaaaaaaaa";
let out = run_ok(&dir, &["context", "structural", "--files", stale]);
assert!(
out.contains("HANDLE REFUSED"),
"CLI must refuse a stale handle: {out}"
);
assert!(out.contains("stale"), "refusal must name staleness: {out}");
assert!(
!out.contains("handle_transcripts"),
"must not guess and serve the file: {out}"
);
}
#[test]
fn corrupt_index_cache_refuses_instead_of_empty_atlas() {
let repo = copy_fixture("http-service-python");
let dir = workdir(repo.path());
run_ok(&dir, &["index", "--quiet"]);
let db = dir.join(".scc/scc.db");
let _ = std::fs::remove_file(dir.join(".scc/scc.db-wal"));
let _ = std::fs::remove_file(dir.join(".scc/scc.db-shm"));
std::fs::write(&db, b"not a sqlite database").unwrap();
let out = run(&dir, &["status"]);
assert!(
!out.status.success(),
"corrupt cache must fail, not print an empty index"
);
let err = format!(
"{}{}",
String::from_utf8_lossy(&out.stderr),
String::from_utf8_lossy(&out.stdout)
);
assert!(
err.contains("corrupt") || err.contains("not a SQLite"),
"must disclose corruption: {err}"
);
assert!(
!err.contains("components: 0") && !err.to_lowercase().contains("not indexed yet"),
"must not fabricate a fresh empty index: {err}"
);
}
#[test]
fn http_daemon_endpoints() {
let repo = copy_fixture("http-service-python");
run_ok(&workdir(repo.path()), &["index", "--quiet"]);
let addr = test_listen_addr();
std::fs::write(
workdir(repo.path()).join(".scc/config.yaml"),
format!("schema: 1\nindex:\n watch: false\nsecurity:\n listen: {addr}\n"),
)
.unwrap();
let _child = DaemonGuard(
Command::new(scc())
.arg("serve")
.current_dir(workdir(repo.path()))
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap(),
);
let mut ready = false;
for _ in 0..40 {
if std::net::TcpStream::connect(addr.as_str()).is_ok() {
ready = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
assert!(ready, "daemon did not start");
let get = |path: &str| -> (u16, String) {
let mut stream = std::net::TcpStream::connect(addr.as_str()).unwrap();
write!(stream, "GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n").unwrap();
let mut buf = String::new();
use std::io::Read;
stream.read_to_string(&mut buf).unwrap();
let status: u16 = buf
.split_whitespace()
.nth(1)
.unwrap()
.parse()
.unwrap();
let body = buf.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
(status, body)
};
let post = |path: &str, body: &str| -> (u16, String) {
let mut stream = std::net::TcpStream::connect(addr.as_str()).unwrap();
write!(
stream,
"POST {path} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
)
.unwrap();
let mut buf = String::new();
use std::io::Read;
stream.read_to_string(&mut buf).unwrap();
let status: u16 = buf.split_whitespace().nth(1).unwrap().parse().unwrap();
let body = buf.split("\r\n\r\n").nth(1).unwrap_or("").to_string();
(status, body)
};
let (s, _) = get("/healthz");
assert_eq!(s, 200);
let (s, body) = get("/v1/index/status");
assert_eq!(s, 200);
assert!(body.contains("\"indexed\":true"), "{body}");
let (s, body) = get("/v1/system");
assert_eq!(s, 200);
assert!(body.contains("IDENTITY"), "{body}");
let (s, body) = post("/v1/context/task", r#"{"goal":"transcript normalization"}"#);
assert_eq!(s, 200);
let v: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(v["pack"]["kind"], "task");
assert!(v["delta"].is_string(), "artifact carries the surface delta");
let (s, _body) = post("/v1/runtime/traces", r#"[{"source":"a","target":"b","count":3}]"#);
assert_eq!(s, 202);
let (s, body) = post("/v1/impact", r#"{"files":["main.py"]}"#);
assert_eq!(s, 200);
assert!(body.contains("AFFECTED COMPONENTS"), "{body}");
let (s, body) = post("/v1/verify", "{}");
assert_eq!(s, 200);
assert!(body.contains("FRESHNESS"), "{body}");
let (s, _) = get("/v1/nope");
assert_eq!(s, 404);
drop(_child);
}
fn test_listen_addr() -> String {
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("bind ephemeral");
listener.local_addr().expect("local_addr").to_string()
}
#[test]
fn transports_implement_nothing() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
for file in ["src/httpd.rs", "src/mcp.rs"] {
let text = std::fs::read_to_string(root.join(file)).unwrap();
for banned in [
"ContextCompiler",
"SystemRanker",
"insert_entity",
"insert_relationship",
"insert_evidence",
"final_importance",
"build_surface",
"open_engine",
"inference::rankers",
] {
assert!(
!text.contains(banned),
"{file} must not reference engine internals ({banned}): transports dispatch via invoke"
);
}
}
let cli = std::fs::read_to_string(root.join("src/commands.rs")).unwrap()
+ &std::fs::read_to_string(root.join("src/httpd.rs")).unwrap()
+ &std::fs::read_to_string(root.join("src/mcp.rs")).unwrap();
let mut targets: Vec<String> = Vec::new();
let mut pos = 0;
while let Some(i) = cli[pos..].find("invoke(") {
let seg = &cli[pos + i..];
if let Some(q1) = seg.find('"') {
let rest = &seg[q1 + 1..];
if let Some(q2) = rest.find('"') {
let op: String = rest[..q2].to_string();
if op.contains('.') {
targets.push(op);
}
}
}
pos += i + 7;
}
targets.sort();
targets.dedup();
assert!(!targets.is_empty(), "expected invoke targets in the CLI crate");
for op in &targets {
assert!(
scc_engine::ops::OPERATIONS.iter().any(|d| d.id == op),
"CLI invokes unregistered operation '{op}' (see `scc operations`)"
);
}
}