use aiscope::cmd::PipelineOptions;
use aiscope::model::{ConflictKind, Severity, Subsystem, Tool};
use aiscope::reason::ReasonMode;
use std::fs;
use std::path::Path;
fn copy_tree(src: &Path, dst: &Path) {
if src.is_dir() {
fs::create_dir_all(dst).unwrap();
for e in fs::read_dir(src).unwrap().flatten() {
copy_tree(&e.path(), &dst.join(e.file_name()));
}
} else {
fs::create_dir_all(dst.parent().unwrap()).unwrap();
fs::copy(src, dst).unwrap();
}
}
#[test]
fn copilot_only_discovers_all_subsystems() {
let tmp = tempfile::tempdir().unwrap();
copy_tree(Path::new("tests/fixtures/copilot-only"), tmp.path());
let bundle = aiscope::build_bundle(tmp.path(), PipelineOptions::default());
assert!(bundle.sources.iter().all(|s| s.tool == Tool::Copilot));
let subs: std::collections::HashSet<_> = bundle.sources.iter().map(|s| s.subsystem).collect();
assert!(
subs.contains(&Subsystem::Instructions),
"instructions missing"
);
assert!(subs.contains(&Subsystem::Prompts), "prompts missing");
assert!(subs.contains(&Subsystem::Agents), "agents missing");
let agents_md = bundle
.sources
.iter()
.find(|s| s.label.ends_with("AGENTS.md"))
.expect("apps/web/AGENTS.md not found");
assert_eq!(agents_md.subsystem, Subsystem::Agents);
assert!(
agents_md
.scope
.path_prefix
.as_deref()
.unwrap_or("")
.contains("apps/web"),
"path_prefix should be derived from file location, got {:?}",
agents_md.scope.path_prefix
);
let py = bundle
.sources
.iter()
.find(|s| s.label.ends_with("python.instructions.md"))
.expect("python.instructions.md not found");
assert!(py.scope.globs.iter().any(|g| g.contains("py")));
let ts = bundle
.sources
.iter()
.find(|s| s.label.ends_with("typescript.instructions.md"))
.expect("typescript.instructions.md not found");
assert!(ts.scope.globs.iter().any(|g| g.contains("ts")));
let py_ts_conflict = bundle.conflicts.iter().any(|c| {
c.severity == Severity::Low
&& matches!(c.kind, ConflictKind::Clash | ConflictKind::PolarityConflict)
});
let _ = py_ts_conflict;
assert!(
bundle
.high_severity_conflicts()
.any(|c| matches!(c.kind, ConflictKind::Clash | ConflictKind::PolarityConflict)),
"expected a HIGH-severity Copilot-only naming clash. Got: {:#?}",
bundle.conflicts
);
}
#[test]
fn copilot_only_specific_mode_silences_prompt_vs_instructions() {
let tmp = tempfile::tempdir().unwrap();
copy_tree(Path::new("tests/fixtures/copilot-only"), tmp.path());
let bundle = aiscope::build_bundle(
tmp.path(),
PipelineOptions {
mode: ReasonMode::Specific,
include_user: false,
},
);
for c in &bundle.conflicts {
if let (Some(l), Some(r)) = (
bundle.statements.get(c.left),
bundle.statements.get(c.right),
) {
let ls = bundle.sources.get(l.source_index).map(|s| s.subsystem);
let rs = bundle.sources.get(r.source_index).map(|s| s.subsystem);
let pair = (
ls.unwrap_or(Subsystem::Instructions),
rs.unwrap_or(Subsystem::Instructions),
);
assert!(
!matches!(
pair,
(Subsystem::Prompts, Subsystem::Instructions)
| (Subsystem::Instructions, Subsystem::Prompts)
),
"specific-mode should suppress prompt↔instruction clashes: {:?}",
c
);
}
}
let mismatches: Vec<_> = bundle
.conflicts
.iter()
.filter(|c| matches!(c.kind, ConflictKind::AgentToolMismatch))
.collect();
assert!(
!mismatches.is_empty(),
"expected AgentToolMismatch (agent.tools excludes bash; instruction says use bash tool). Got: {:#?}",
bundle.conflicts
);
}