#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use regex::Regex;
use serde::Deserialize;
use omni_dev::data::context::ScopeDefinition;
use omni_dev::git::parse_subject;
#[derive(Deserialize)]
struct ScopesConfig {
scopes: Vec<ScopeDefinition>,
}
fn omni_dev_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".omni-dev")
}
fn yaml_scopes() -> BTreeMap<String, String> {
let path = omni_dev_dir().join("scopes.yaml");
let raw =
fs::read_to_string(&path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let config: ScopesConfig = serde_yaml::from_str(&raw)
.unwrap_or_else(|e| panic!("cannot parse {}: {e}", path.display()));
config
.scopes
.into_iter()
.map(|scope| (scope.name, scope.description))
.collect()
}
fn commit_guidelines() -> String {
let path = omni_dev_dir().join("commit-guidelines.md");
fs::read_to_string(&path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()))
}
fn section<'a>(markdown: &'a str, heading: &str) -> &'a str {
let marker = format!("\n## {heading}\n");
let start = markdown
.find(&marker)
.unwrap_or_else(|| panic!("commit-guidelines.md has no `## {heading}` section"))
+ marker.len();
let end = markdown[start..]
.find("\n## ")
.map_or(markdown.len(), |offset| start + offset);
&markdown[start..end]
}
#[test]
fn example_scopes_are_defined_in_scopes_yaml() {
let yaml = yaml_scopes();
let guidelines = commit_guidelines();
let examples = section(&guidelines, "Examples");
let header = Regex::new(r"(?m)^([a-z]+)\(([^)]+)\)!?:").unwrap();
let mut undefined = Vec::new();
for caps in header.captures_iter(examples) {
for scope in caps[2].split(',') {
let scope = scope.trim();
if !yaml.contains_key(scope) {
undefined.push(format!("`{scope}` used in `{}`", caps[0].trim()));
}
}
}
assert!(
undefined.is_empty(),
"the `## Examples` section of .omni-dev/commit-guidelines.md uses scopes that \
are not defined in .omni-dev/scopes.yaml (STYLE-0007):\n {}",
undefined.join("\n ")
);
}
fn example_subjects(body: &str) -> Vec<&str> {
let mut subjects = Vec::new();
let mut in_block = false;
let mut expecting_subject = false;
for line in body.lines() {
if line.trim_start().starts_with("```") {
in_block = !in_block;
expecting_subject = in_block;
} else if expecting_subject {
subjects.push(line);
expecting_subject = false;
}
}
subjects
}
#[test]
fn example_subjects_are_parseable_by_parse_subject() {
let guidelines = commit_guidelines();
let subjects = example_subjects(section(&guidelines, "Examples"));
assert!(
!subjects.is_empty(),
"no fenced examples found in `## Examples` — the block scraper is broken, \
which would let this test pass vacuously"
);
assert!(
subjects.iter().any(|subject| subject.contains(")!:")),
"no `type(scope)!:` breaking-change example found in `## Examples` — that form \
is the reason this test exists (#1473), so its absence must fail loudly"
);
let unparseable: Vec<&str> = subjects
.iter()
.copied()
.filter(|subject| parse_subject(subject).and_then(|p| p.scope).is_none())
.collect();
assert!(
unparseable.is_empty(),
"`omni_dev::git::parse_subject` cannot parse a scope out of these subjects \
demonstrated in the `## Examples` section of .omni-dev/commit-guidelines.md:\n {}\n\n\
the guidelines are the source of truth — the parser must handle every scoped \
form they teach.",
unparseable.join("\n ")
);
}
#[test]
fn example_subjects_takes_the_first_line_of_each_fenced_block() {
let body = "\n### Simple\n```\nfix(git): a subject\n```\n\n\
### With body\n```\nfeat(cli)!: another subject\n\n\
BREAKING CHANGE: not a subject\n```\n";
assert_eq!(
example_subjects(body),
vec!["fix(git): a subject", "feat(cli)!: another subject"]
);
}
#[test]
fn section_stops_at_the_next_heading_but_not_subheadings() {
let markdown = "# Title\n\n## One\n\nbody\n\n### Sub\n\nmore\n\n## Two\n\nother\n";
assert_eq!(section(markdown, "One"), "\nbody\n\n### Sub\n\nmore\n");
assert_eq!(section(markdown, "Two"), "\nother\n");
}
#[test]
fn check_prompt_injects_repository_scopes_without_a_markdown_inventory() {
let guidelines = commit_guidelines();
let scope_section = section(&guidelines, "Scopes");
let inventory_bullet = Regex::new(r"(?m)^\s*- `[a-z0-9-]+` [-:] ").unwrap();
assert!(
!inventory_bullet.is_match(scope_section),
"keep the project scope inventory in scopes.yaml, not commit-guidelines.md"
);
let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let config_dir = tempfile::tempdir().unwrap();
fs::copy(
omni_dev_dir().join("scopes.yaml"),
config_dir.path().join("scopes.yaml"),
)
.unwrap();
let scopes = omni_dev::claude::context::load_project_scopes(config_dir.path(), &repo_root);
let yaml = yaml_scopes();
assert!(
!yaml.is_empty(),
"repository scope fixture must not be empty"
);
for (name, description) in &yaml {
assert!(
scopes
.iter()
.any(|scope| scope.name == *name && scope.description == *description),
"repository scope missing from resolved set: {name}"
);
}
for name in ["cargo", "core", "lib", "test"] {
assert!(scopes.iter().any(|scope| scope.name == name));
}
let prompt = omni_dev::claude::prompts::generate_check_system_prompt_with_scopes(
Some(&guidelines),
&scopes,
);
assert!(prompt.contains(&guidelines));
let header = "=== VALID SCOPES FOR THIS PROJECT ===";
assert_eq!(prompt.matches(header).count(), 1);
let inventory = prompt.split_once(header).unwrap().1;
let inventory = inventory.split_once("\n## Scope Checking Rules").unwrap().0;
assert_eq!(
inventory
.lines()
.filter(|line| line.starts_with("- `"))
.count(),
scopes.len(),
"injected inventory must contain only the resolved scopes"
);
for scope in &scopes {
let entry = format!("- `{}`: {}", scope.name, scope.description);
assert_eq!(
inventory.lines().filter(|line| *line == entry).count(),
1,
"scope entry must occur exactly once: {entry}"
);
}
}