use super::*;
use crate::config::{AccessConfig, RoleConfig};
fn bead_map(entries: &[(&str, &[&str], i64)]) -> BeadFileMap {
let mut m = BeadFileMap::new();
for (bead, files, ts) in entries {
m.insert(
bead.to_string(),
(files.iter().map(|s| s.to_string()).collect(), *ts),
);
}
m
}
#[test]
fn pairs_shared_bead_across_two_repos() {
let repos = vec![
(
"api".to_string(),
bead_map(&[("bo-1", &["contract.rs", "other.rs"], 100)]),
),
(
"web".to_string(),
bead_map(&[("bo-1", &["client.ts"], 200)]),
),
];
let pairs = pair_cross_repo(&repos, 0.7, 30.0, 1000);
assert_eq!(pairs.len(), 2);
for p in &pairs {
assert_ne!(p.repo_a, p.repo_b, "no same-repo pairs");
assert_eq!(p.co_changes, 1);
assert_eq!(p.last_co_change, 200, "max timestamp across the two sides");
}
assert!(pairs.iter().all(|p| p.repo_a == "api" && p.repo_b == "web"));
}
#[test]
fn no_edge_for_single_repo_bead_or_repo_outside_set() {
let repos = vec![
(
"api".to_string(),
bead_map(&[("bo-solo", &["only.rs"], 50), ("bo-shared", &["a.rs"], 60)]),
),
("web".to_string(), bead_map(&[("bo-shared", &["b.ts"], 70)])),
];
let pairs = pair_cross_repo(&repos, 0.7, 30.0, 1000);
assert_eq!(pairs.len(), 1, "only the shared bead couples");
assert_eq!(pairs[0].path_a, "a.rs");
assert_eq!(pairs[0].path_b, "b.ts");
assert!(pairs
.iter()
.all(|p| p.path_a != "only.rs" && p.path_b != "only.rs"));
}
#[test]
fn three_repo_bead_pairs_each_distinct_repo_pair() {
let repos = vec![
("a".to_string(), bead_map(&[("bo-x", &["fa.rs"], 10)])),
("b".to_string(), bead_map(&[("bo-x", &["fb.rs"], 20)])),
("c".to_string(), bead_map(&[("bo-x", &["fc.rs"], 30)])),
];
let pairs = pair_cross_repo(&repos, 0.7, 30.0, 1000);
assert_eq!(pairs.len(), 3);
}
fn access(default_allow: bool, roles: Vec<RoleConfig>) -> AccessConfig {
AccessConfig {
default_allow,
roles,
}
}
fn deny_role(name: &str, deny: &[&str]) -> RoleConfig {
RoleConfig {
name: name.to_string(),
allow: vec![],
deny: deny.iter().map(|s| s.to_string()).collect(),
deny_paths: vec![],
}
}
#[test]
fn access_filter_blocks_denied_repo_on_cross_repo_edge() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("meta.db");
let store = MetadataStore::open(&db).unwrap();
store
.upsert_cross_repo_coupling(&CrossRepoCoupling {
repo_a: "aegis".to_string(),
path_a: "src/seed.rs".to_string(),
repo_b: "pixelsrc".to_string(),
path_b: "src/secret.rs".to_string(),
score: 0.9,
co_changes: 3,
last_co_change: 1000,
})
.unwrap();
let permissive = RepoFilter::allow_all();
let allowed =
related_cross_repo(&store, Some("aegis"), "src/seed.rs", 10, 0.0, &permissive).unwrap();
assert_eq!(allowed.len(), 1);
assert_eq!(allowed[0].repo, "pixelsrc");
assert_eq!(allowed[0].path, "src/secret.rs");
let cfg = access(true, vec![deny_role("aegis", &["pixelsrc"])]);
let denying = RepoFilter::from_config(&cfg, "aegis");
let denied =
related_cross_repo(&store, Some("aegis"), "src/seed.rs", 10, 0.0, &denying).unwrap();
assert!(
denied.is_empty(),
"denied repo must not leak via coupling edge"
);
}
#[test]
fn empty_repo_edge_never_surfaces_even_to_allow_all() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("meta.db");
let store = MetadataStore::open(&db).unwrap();
store
.upsert_cross_repo_coupling(&CrossRepoCoupling {
repo_a: "aegis".to_string(),
path_a: "src/seed.rs".to_string(),
repo_b: "".to_string(),
path_b: "src/secret.rs".to_string(),
score: 0.9,
co_changes: 3,
last_co_change: 1000,
})
.unwrap();
let permissive = RepoFilter::allow_all();
let out =
related_cross_repo(&store, Some("aegis"), "src/seed.rs", 10, 0.0, &permissive).unwrap();
assert!(
out.is_empty(),
"empty-repo edge must never surface (fail-closed), even to allow-all"
);
}
#[test]
fn integration_two_temp_repos_share_bead_trailer() {
use std::process::Command;
fn init_repo(dir: &Path) {
Command::new("git")
.args(["init"])
.current_dir(dir)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.email", "t@t.com"])
.current_dir(dir)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "T"])
.current_dir(dir)
.output()
.unwrap();
}
fn commit(dir: &Path, file: &str, body: &str, msg: &str) {
std::fs::write(dir.join(file), body).unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(dir)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", msg])
.current_dir(dir)
.output()
.unwrap();
}
let api_dir = tempfile::tempdir().unwrap();
let web_dir = tempfile::tempdir().unwrap();
init_repo(api_dir.path());
init_repo(web_dir.path());
commit(
api_dir.path(),
"contract.rs",
"v1",
"add contract\n\nBead: bo-share",
);
commit(
web_dir.path(),
"client.ts",
"v1",
"consume contract\n\nBead: bo-share",
);
let store_dir = tempfile::tempdir().unwrap();
let db = store_dir.path().join("meta.db");
let store = MetadataStore::open(&db).unwrap();
store
.set_meta("repo_source:api", &api_dir.path().to_string_lossy())
.unwrap();
store
.set_meta("repo_source:web", &web_dir.path().to_string_lossy())
.unwrap();
let mut config = Config::default();
config.groups = vec![crate::config::GroupConfig {
name: "svc".to_string(),
repos: vec!["api".to_string(), "web".to_string()],
}];
let stored = compute_and_store_cross_repo(&store, &config).unwrap();
assert_eq!(stored, 1, "one cross-repo edge from the shared bead");
let rel = related_cross_repo(
&store,
Some("api"),
"contract.rs",
10,
0.0,
&RepoFilter::allow_all(),
)
.unwrap();
assert_eq!(rel.len(), 1);
assert_eq!(rel[0].repo, "web");
assert_eq!(rel[0].path, "client.ts");
}
#[test]
fn integration_no_cross_group_edge() {
use std::process::Command;
fn init_repo(dir: &Path) {
Command::new("git")
.args(["init"])
.current_dir(dir)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.email", "t@t.com"])
.current_dir(dir)
.output()
.unwrap();
Command::new("git")
.args(["config", "user.name", "T"])
.current_dir(dir)
.output()
.unwrap();
}
fn commit(dir: &Path, file: &str, msg: &str) {
std::fs::write(dir.join(file), "v1").unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(dir)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", msg])
.current_dir(dir)
.output()
.unwrap();
}
let g1 = tempfile::tempdir().unwrap();
let g2 = tempfile::tempdir().unwrap();
init_repo(g1.path());
init_repo(g2.path());
commit(g1.path(), "one.rs", "g1 work\n\nBead: bo-span");
commit(g2.path(), "two.rs", "g2 work\n\nBead: bo-span");
let sd = tempfile::tempdir().unwrap();
let store = MetadataStore::open(&sd.path().join("m.db")).unwrap();
store
.set_meta("repo_source:r1", &g1.path().to_string_lossy())
.unwrap();
store
.set_meta("repo_source:r2", &g2.path().to_string_lossy())
.unwrap();
let mut config = Config::default();
config.groups = vec![
crate::config::GroupConfig {
name: "group1".into(),
repos: vec!["r1".into()],
},
crate::config::GroupConfig {
name: "group2".into(),
repos: vec!["r2".into()],
},
];
let stored = compute_and_store_cross_repo(&store, &config).unwrap();
assert_eq!(
stored, 0,
"no cross-group edge: r1 and r2 are in different groups"
);
}