use std::fs;
use std::path::{Path, PathBuf};
use agentlint::{LintOptions, Severity};
use assert_cmd::Command;
use tempfile::tempdir;
fn fixture(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
fn rules(name: &str) -> Vec<String> {
agentlint::lint(&fixture(name), &LintOptions::default())
.expect("fixture should lint")
.diagnostics
.into_iter()
.map(|diagnostic| diagnostic.rule)
.collect()
}
#[test]
fn clean_project_has_no_diagnostics() {
let report = agentlint::lint(&fixture("clean-project"), &LintOptions::default()).expect("lint");
assert!(report.diagnostics.is_empty(), "{:?}", report.diagnostics);
assert_eq!(report.files_scanned, 1);
}
#[test]
fn validates_mcp_structure_executable_and_environment() {
let found = rules("broken-mcp");
assert!(found.contains(&"AGL101".into()));
assert!(found.contains(&"AGL102".into()));
assert!(found.contains(&"AGL104".into()));
assert!(found.contains(&"AGL105".into()));
}
#[test]
fn detects_duplicate_guidance() {
assert!(rules("duplicate-rules").contains(&"AGL302".into()));
}
#[test]
fn detects_stale_paths_and_scripts() {
let found = rules("stale-paths");
assert!(found.contains(&"AGL202".into()));
assert!(found.contains(&"AGL203".into()));
}
#[test]
fn secret_diagnostic_is_redacted() {
let report = agentlint::lint(&fixture("leaked-secret"), &LintOptions::default()).expect("lint");
let diagnostic = report
.diagnostics
.iter()
.find(|diagnostic| diagnostic.rule == "AGL401")
.expect("secret diagnostic");
assert!(
!diagnostic
.message
.contains("sk-proj-fixture123456789secret")
);
assert!(diagnostic.message.contains("..."));
}
#[test]
fn detects_package_manager_conflict() {
assert!(rules("package-manager-conflict").contains(&"AGL601".into()));
}
#[test]
fn malformed_json_reports_source_location() {
let directory = tempdir().expect("tempdir");
fs::write(
directory.path().join(".mcp.json"),
"{\n \"mcpServers\": {\n",
)
.expect("fixture");
let report = agentlint::lint(directory.path(), &LintOptions::default()).expect("lint");
let diagnostic = report
.diagnostics
.iter()
.find(|item| item.rule == "AGL001")
.expect("syntax diagnostic");
assert!(diagnostic.line.is_some());
assert_eq!(diagnostic.severity, Severity::Error);
}
#[test]
fn duplicate_json_key_is_reported() {
let directory = tempdir().expect("tempdir");
fs::write(
directory.path().join(".mcp.json"),
"{\n \"mcpServers\": {\n \"one\": {\n \"command\": \"cargo\",\n \"command\": \"git\"\n }\n }\n}\n",
)
.expect("fixture");
assert!(
agentlint::lint(directory.path(), &LintOptions::default())
.expect("lint")
.diagnostics
.iter()
.any(|item| item.rule == "AGL003")
);
}
#[test]
fn contradiction_patterns_are_conservative_and_detect_examples() {
let directory = tempdir().expect("tempdir");
fs::write(
directory.path().join("AGENTS.md"),
"Always use npm.\nNever use npm; use pnpm instead.\nDo not modify files under src/generated.\nYou may freely edit src/generated.\n",
)
.expect("fixture");
let report = agentlint::lint(directory.path(), &LintOptions::default()).expect("lint");
assert!(
report
.diagnostics
.iter()
.filter(|item| item.rule == "AGL602")
.count()
>= 2
);
}
#[test]
fn gitignore_and_agentlintignore_are_respected() {
let directory = tempdir().expect("tempdir");
fs::create_dir(directory.path().join("ignored")).expect("directory");
fs::write(directory.path().join(".agentlintignore"), "ignored/**\n").expect("ignore");
fs::write(
directory.path().join("ignored/AGENTS.md"),
"Run scripts/missing.sh\n",
)
.expect("file");
let report = agentlint::lint(directory.path(), &LintOptions::default()).expect("lint");
assert_eq!(report.files_scanned, 0);
}
#[test]
fn cli_json_output_is_valid_and_clean() {
let output = Command::cargo_bin("agentlint")
.expect("binary")
.args([
fixture("stale-paths").to_str().expect("utf8"),
"--format",
"json",
"--no-color",
])
.output()
.expect("run");
assert_eq!(output.status.code(), Some(1));
let value: serde_json::Value =
serde_json::from_slice(&output.stdout).expect("valid JSON stdout");
assert_eq!(value["version"], "0.1.0");
assert!(output.stderr.is_empty());
}
#[test]
fn cli_github_output_uses_annotations() {
Command::cargo_bin("agentlint")
.expect("binary")
.args([
fixture("stale-paths").to_str().expect("utf8"),
"--format",
"github",
])
.assert()
.failure()
.stdout(predicates::str::contains("::error file=AGENTS.md"));
}
#[test]
fn init_creates_valid_starter_config() {
let directory = tempdir().expect("tempdir");
Command::cargo_bin("agentlint")
.expect("binary")
.args(["init", directory.path().to_str().expect("utf8")])
.assert()
.success();
let config = fs::read_to_string(directory.path().join(".agentlint.yaml")).expect("config");
assert!(config.contains("AGL401: error"));
agentlint::lint(directory.path(), &LintOptions::default()).expect("starter config parses");
}
#[test]
fn safe_fix_corrects_proven_package_manager() {
let directory = tempdir().expect("tempdir");
fs::write(
directory.path().join("pnpm-lock.yaml"),
"lockfileVersion: '9.0'\n",
)
.expect("lock");
fs::write(
directory.path().join("package.json"),
r#"{"scripts":{"test":"node --test"}}"#,
)
.expect("package");
fs::write(
directory.path().join("AGENTS.md"),
"Run `npm run test`. \n",
)
.expect("instructions");
Command::cargo_bin("agentlint")
.expect("binary")
.args([
directory.path().to_str().expect("utf8"),
"--fix",
"--no-color",
])
.assert()
.success();
assert_eq!(
fs::read_to_string(directory.path().join("AGENTS.md")).expect("fixed"),
"Run `pnpm run test`.\n"
);
}
#[test]
fn skill_frontmatter_and_globs_are_validated() {
let directory = tempdir().expect("tempdir");
fs::create_dir_all(directory.path().join("skills/demo")).expect("directory");
fs::write(
directory.path().join("skills/demo/SKILL.md"),
"---\nname: demo\ndescription: Demo skill\nglobs: '[src/**'\n---\n# Demo\n",
)
.expect("skill");
let found = agentlint::lint(directory.path(), &LintOptions::default())
.expect("lint")
.diagnostics;
assert!(found.iter().any(|item| item.rule == "AGL102"));
assert!(found.iter().any(|item| item.rule == "AGL201"));
}
#[test]
fn missing_skill_frontmatter_is_an_error() {
let directory = tempdir().expect("tempdir");
fs::create_dir_all(directory.path().join("skills/demo")).expect("directory");
fs::write(directory.path().join("skills/demo/SKILL.md"), "# Demo\n").expect("skill");
let report = agentlint::lint(directory.path(), &LintOptions::default()).expect("lint");
assert!(
report
.diagnostics
.iter()
.any(|item| item.rule == "AGL002" && item.severity == Severity::Error)
);
}
#[test]
fn destructive_commands_and_stale_tooling_are_detected() {
let directory = tempdir().expect("tempdir");
fs::write(
directory.path().join("AGENTS.md"),
"Run:\n```bash\ngit clean -fdx\npytest\n```\n",
)
.expect("instructions");
let found = agentlint::lint(directory.path(), &LintOptions::default())
.expect("lint")
.diagnostics;
assert!(found.iter().any(|item| item.rule == "AGL501"));
assert!(found.iter().any(|item| item.rule == "AGL603"));
}
#[test]
fn generic_agent_configuration_syntax_is_checked() {
let directory = tempdir().expect("tempdir");
fs::create_dir(directory.path().join(".codex")).expect("directory");
fs::write(directory.path().join(".codex/config.toml"), "model = [\n").expect("config");
assert!(
agentlint::lint(directory.path(), &LintOptions::default())
.expect("lint")
.diagnostics
.iter()
.any(|item| item.rule == "AGL001")
);
}
#[test]
fn configured_rule_can_be_disabled() {
let directory = tempdir().expect("tempdir");
fs::write(
directory.path().join("AGENTS.md"),
"Read `src/missing.rs`.\n",
)
.expect("instructions");
fs::write(
directory.path().join(".agentlint.yaml"),
"rules:\n AGL202: off\n",
)
.expect("config");
let report = agentlint::lint(directory.path(), &LintOptions::default()).expect("lint");
assert!(!report.diagnostics.iter().any(|item| item.rule == "AGL202"));
}
#[test]
fn large_context_is_measured_and_reported() {
let directory = tempdir().expect("tempdir");
let content = format!("Always follow this policy.\n\n{}\n", "word ".repeat(6_100));
fs::write(directory.path().join("AGENTS.md"), content).expect("instructions");
let report = agentlint::lint(directory.path(), &LintOptions::default()).expect("lint");
assert!(report.estimated_total_tokens >= 6_000);
assert!(report.diagnostics.iter().any(|item| item.rule == "AGL301"));
}