mod common;
use std::fs::{create_dir, create_dir_all, write};
use std::path::{Path, PathBuf};
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::tempdir;
use common::slopguard;
const RULESETS_OFF: &str = "[rulesets]\nslop = false\nsecurity = false\ncorrectness = false\n";
fn slopguard_in(project: &Path, home: &Path) -> Command {
let mut cmd = slopguard();
cmd.current_dir(project)
.env("HOME", home)
.env("XDG_CONFIG_HOME", home.join(".config"))
.env_remove("SLOPGUARD_CACHE_DIR")
.env_remove("ANTHROPIC_API_KEY")
.env_remove("OPENAI_API_KEY")
.env_remove("TYPESAFE_API_KEY")
.env_remove("OPENROUTER_API_KEY");
cmd
}
fn ai_project(project: &Path) {
write(
project.join("main.rs"),
"fn main() {\n // SAFETY: trust me\n let _x = 1;\n}\n",
)
.unwrap();
let rules_dir = project.join("custom-rules");
create_dir(&rules_dir).unwrap();
write(
rules_dir.join("ai.yml"),
r#"
id: ai-safety-demo
language: rust
severity: warning
category: slop
message: "AI: SAFETY comment may be meaningless"
rule:
kind: line_comment
regex: 'SAFETY'
ai_check:
prompt: "Is this SAFETY comment meaningful?\n{{code}}"
"#,
)
.unwrap();
write(
project.join("slopguard.toml"),
format!(
"{RULESETS_OFF}\n[rules]\ncustom_dirs = [\"./custom-rules\"]\n\n\
[ai]\nenabled = true\nprovider = \"api\"\nvendor = \"anthropic\"\n\
api_key = \"repo-secret-key\"\n"
),
)
.unwrap();
}
fn outside_dir_project(parent: &Path) -> PathBuf {
let project = parent.join("proj");
create_dir_all(&project).unwrap();
create_dir_all(parent.join("outside")).unwrap();
write(project.join("main.rs"), "fn main() {}\n").unwrap();
write(
project.join("slopguard.toml"),
format!("{RULESETS_OFF}\n[rules]\ncustom_dirs = [\"../outside\"]\n"),
)
.unwrap();
project
}
#[test]
fn repo_config_ai_keys_are_ignored_with_a_warning() {
let project = tempdir().unwrap();
let home = tempdir().unwrap();
ai_project(project.path());
slopguard_in(project.path(), home.path())
.args(["scan", "--no-cache", "--no-colors", "."])
.assert()
.success()
.stdout(predicate::str::contains("0 errors, 0 warnings"))
.stderr(predicate::str::contains(
"warning: ignored 'ai.api_key', 'ai.enabled', 'ai.provider', 'ai.vendor' in slopguard.toml",
))
.stderr(predicate::str::contains("AI is disabled"))
.stderr(predicate::str::contains("ANTHROPIC_API_KEY").not())
.stderr(predicate::str::contains("repo-secret-key").not());
}
#[test]
fn trust_repo_config_honors_repo_ai_keys() {
let project = tempdir().unwrap();
let home = tempdir().unwrap();
ai_project(project.path());
slopguard_in(project.path(), home.path())
.args([
"scan",
"--no-ai",
"--no-cache",
"--no-colors",
"--trust-repo-config",
".",
])
.assert()
.success()
.stderr(predicate::str::contains("ignored").not());
}
#[test]
fn repo_custom_dir_outside_repo_fails_with_config_error() {
let parent = tempdir().unwrap();
let home = tempdir().unwrap();
let project = outside_dir_project(parent.path());
slopguard_in(&project, home.path())
.args(["scan", "--no-cache", "--no-colors", "."])
.assert()
.code(2)
.stderr(predicate::str::contains("rules.custom_dirs"))
.stderr(predicate::str::contains("outside the repository root"));
}
#[test]
fn trust_repo_config_accepts_custom_dir_outside_repo() {
let parent = tempdir().unwrap();
let home = tempdir().unwrap();
let project = outside_dir_project(parent.path());
slopguard_in(&project, home.path())
.args([
"scan",
"--no-cache",
"--no-colors",
"--trust-repo-config",
".",
])
.assert()
.success()
.stderr(predicate::str::contains("outside the repository root").not())
.stderr(predicate::str::contains("ignored").not());
}
#[cfg(target_os = "linux")]
#[test]
fn global_config_zero_concurrency_fails_with_config_error() {
let project = tempdir().unwrap();
let home = tempdir().unwrap();
write(project.path().join("main.rs"), "fn main() {}\n").unwrap();
let global = home.path().join(".config").join("slopguard");
create_dir_all(&global).unwrap();
write(global.join("config.toml"), "[ai]\nconcurrency = 0\n").unwrap();
slopguard_in(project.path(), home.path())
.args(["scan", "--no-cache", "--no-colors", "."])
.assert()
.code(2)
.stderr(predicate::str::contains(
"[ai].concurrency must be between 1 and 64, got 0",
));
}