mod common;
use common::*;
fn indexed_ir() -> (tempfile::TempDir, serde_json::Value) {
let repo = copy_fixture("ts-facts-service");
let dir = workdir(repo.path());
run_ok(&dir, &["index", "--quiet"]);
let ir: serde_json::Value =
serde_json::from_str(&run_ok(&dir, &["export", "system-ir.json"])).unwrap();
(repo, ir)
}
fn entities_by_kind<'a>(ir: &'a serde_json::Value, kind: &str) -> Vec<&'a serde_json::Value> {
ir["entities"]
.as_array()
.unwrap()
.iter()
.filter(|e| e["kind"] == kind)
.collect()
}
fn has_entity_name(ir: &serde_json::Value, kind: &str, name: &str) -> bool {
entities_by_kind(ir, kind).iter().any(|e| e["name"] == name)
}
fn has_relationship(ir: &serde_json::Value, predicate: &str) -> bool {
ir["relationships"]
.as_array()
.unwrap()
.iter()
.any(|r| r["predicate"] == predicate)
}
#[test]
fn ts_facts_export_entities_and_relationships() {
let (_repo, ir) = indexed_ir();
for name in [
"UsersController",
"UsersService",
"AppModule",
"health",
"App",
"handleTheme",
"handleClick",
"Page",
"VERSION",
"PageList",
] {
assert!(
has_entity_name(&ir, "export", name),
"missing export entity {name}: {ir}"
);
}
for (name, target) in [
("Controller", "UsersController"),
("Get", "UsersController.list"),
("Get", "UsersController.get"),
("Module", "AppModule"),
("Injectable", "UsersService"),
] {
let anns = entities_by_kind(&ir, "annotation");
assert!(
anns.iter().any(|e| e["name"] == name),
"missing annotation entity {name}: {ir}"
);
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "annotates"
&& r["object"].as_str().unwrap().contains(target)
}),
"missing ANNOTATES ->{target}: {ir}"
);
}
assert!(has_entity_name(&ir, "field", "UsersService.table"), "{ir}");
assert!(has_entity_name(&ir, "field", "UsersService.retries"), "{ir}");
let fields = entities_by_kind(&ir, "field");
let table = fields.iter().find(|e| e["name"] == "UsersService.table").unwrap();
assert_eq!(table["attributes"]["mutable"], serde_json::json!(false), "{ir}");
let retries = fields.iter().find(|e| e["name"] == "UsersService.retries").unwrap();
assert_eq!(retries["attributes"]["mutable"], serde_json::json!(true), "{ir}");
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "registers"
&& r["subject"].as_str().unwrap().contains("AppModule")
&& r["object"].as_str().unwrap().contains("UsersController")
}),
"missing REGISTERS AppModule->UsersController: {ir}"
);
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "registers"
&& r["subject"].as_str().unwrap().contains("AppModule")
&& r["object"].as_str().unwrap().contains("usersservice")
}),
"missing REGISTERS AppModule->UsersService: {ir}"
);
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "registers"
&& r["subject"].as_str().unwrap().contains("app")
&& r["object"].as_str().unwrap().contains("get-/health")
}),
"missing express route registration GET /health: {ir}"
);
assert!(
has_entity_name(&ir, "route", "GET /health"),
"route contract entity: {ir}"
);
assert!(
has_entity_name(&ir, "contract", "GET /health"),
"registered route contract: {ir}"
);
assert!(
has_entity_name(&ir, "contract", "next"),
"next config contract: {ir}"
);
let cfgs = entities_by_kind(&ir, "configuration");
assert!(
cfgs.iter().any(|e| e["name"] == "PORT"),
"missing configuration PORT: {ir}"
);
assert!(
cfgs.iter().any(|e| e["name"] == "API_URL"),
"missing configuration API_URL: {ir}"
);
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "handles_callback"
&& r["subject"].as_str().unwrap().contains("App")
&& r["object"].as_str().unwrap().contains("handleTheme")
}),
"missing HANDLES_CALLBACK App->handleTheme: {ir}"
);
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "handles_callback"
&& r["subject"].as_str().unwrap().contains("App")
&& r["object"].as_str().unwrap().contains("handleClick")
}),
"missing HANDLES_CALLBACK App->handleClick: {ir}"
);
assert!(
ir["relationships"].as_array().unwrap().iter().any(|r| {
r["predicate"] == "registers"
&& r["subject"].as_str().unwrap().contains("nextConfig")
}),
"missing next.config registration: {ir}"
);
for pred in ["exports", "annotates", "registers", "configured_by", "handles_callback"] {
assert!(has_relationship(&ir, pred), "missing {pred} relationships: {ir}");
}
let routes = run_ok(&workdir(_repo.path()), &["flows"]);
assert!(routes.contains("GET /health"), "{routes}");
let again = run_ok(&workdir(_repo.path()), &["export", "system-ir.json"]);
assert_eq!(
serde_json::from_str::<serde_json::Value>(&again).unwrap(),
ir,
"re-export must be byte-identical"
);
let verify = run_ok(&workdir(_repo.path()), &["verify"]);
assert!(verify.contains("VERIFIED"), "{verify}");
}