use std::io::Write;
use std::process::{Command, Stdio};
fn bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_ai-slop"))
}
fn run_stdin(args: &[&str], stdin: &[u8]) -> (i32, String, String) {
let mut child = bin()
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
let _ = child.stdin.as_mut().unwrap().write_all(stdin);
let out = child.wait_with_output().unwrap();
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stdout).to_string(),
String::from_utf8_lossy(&out.stderr).to_string(),
)
}
#[test]
fn missing_profile_is_usage_error() {
let (code, stdout, stderr) = run_stdin(&["check", "-"], b"text");
assert_eq!(code, 2);
assert!(stdout.is_empty(), "usage errors must not write stdout");
assert!(stderr.contains("--profile"));
}
#[test]
fn clean_doc_exits_zero_with_pure_json_stdout() {
let (code, stdout, _) = run_stdin(
&["check", "--profile", "internal-doc", "-"],
b"A plain note about the build.\n",
);
assert_eq!(code, 0);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout is JSON");
assert_eq!(v["result_state"], "no_findings");
assert_eq!(stdout.trim().lines().count(), 1);
}
#[test]
fn public_comment_profile_is_accepted() {
let (code, stdout, _) = run_stdin(
&["check", "--profile", "public-comment", "-"],
b"The retry loop reads the deadline before the first attempt.\n",
);
assert_eq!(code, 0, "stdout: {stdout}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).expect("stdout is JSON");
assert_eq!(v["result_state"], "no_findings");
assert_eq!(v["artifact"]["profile"], "public-comment");
}
#[test]
fn violation_exits_10_candidate_exits_20() {
let (code, stdout, _) = run_stdin(
&["check", "--profile", "internal-doc", "-"],
b"We delve into it.\n",
);
assert_eq!(code, 10);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v["result_state"], "violations_present");
let (code, stdout, _) = run_stdin(
&["check", "--profile", "internal-doc", "-"],
b"It fails rather than recovering.\n",
);
assert_eq!(code, 20, "stdout: {stdout}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v["result_state"], "candidates_present");
}
#[test]
fn invalid_utf8_exits_40() {
let (code, stdout, stderr) = run_stdin(
&["check", "--profile", "internal-doc", "-"],
&[0x66, 0x6F, 0xFF, 0xFE],
);
assert_eq!(code, 40);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v["result_state"], "unsupported_input");
assert!(stderr.contains("unsupported_input"));
}
#[test]
fn over_limit_exits_40() {
let (code, _, _) = run_stdin(
&[
"check",
"--profile",
"internal-doc",
"--max-bytes",
"8",
"-",
],
b"this is over eight bytes",
);
assert_eq!(code, 40);
}
#[test]
fn verify_mismatch_exits_10() {
let dir = std::env::temp_dir().join("ai-slop-cli-test");
std::fs::create_dir_all(&dir).unwrap();
let approval_path = dir.join("approval.json");
let clean = b"Reads a file and returns its bytes.";
let approval = serde_json::json!({
"artifact_sha256": ai_slop::input::sha256_hex(clean),
"policy_digest": ai_slop::policy_digest(),
"profile": "internal-doc",
"waivers": [],
});
std::fs::write(&approval_path, approval.to_string()).unwrap();
let (code, stdout, _) = run_stdin(
&["verify", "--approval", approval_path.to_str().unwrap(), "-"],
clean,
);
assert_eq!(code, 0, "stdout: {stdout}");
let (code, stdout, stderr) = run_stdin(
&["verify", "--approval", approval_path.to_str().unwrap(), "-"],
b"a mutated payload",
);
assert_eq!(code, 10);
assert!(stdout.contains("\"verified\":false"));
assert!(stderr.contains("hash mismatch"));
}
#[test]
fn incomplete_waiver_file_is_usage_error_complete_waiver_still_works() {
let dir = std::env::temp_dir().join("ai-slop-cli-test-waivers");
std::fs::create_dir_all(&dir).unwrap();
let payload = b"We delve into this.";
let incomplete_path = dir.join("incomplete.json");
std::fs::write(
&incomplete_path,
serde_json::json!({ "waivers": [{ "rule_id": "SLOP-A001" }] }).to_string(),
)
.unwrap();
let (code, stdout, stderr) = run_stdin(
&[
"check",
"--profile",
"internal-doc",
"--waivers",
incomplete_path.to_str().unwrap(),
"-",
],
payload,
);
assert_eq!(code, 2, "stdout: {stdout} stderr: {stderr}");
assert!(stdout.is_empty(), "usage errors must not write stdout");
assert!(stderr.contains("span-bound"), "stderr: {stderr}");
let complete_path = dir.join("complete.json");
std::fs::write(
&complete_path,
serde_json::json!({ "waivers": [{
"rule_id": "SLOP-A001",
"span": { "start": 3, "end": 8 },
"reason": "cited verbatim from the upstream title",
"signer_kind": "human",
"expires": "2999-01-01T00:00:00Z",
}] })
.to_string(),
)
.unwrap();
let (code, stdout, stderr) = run_stdin(
&[
"check",
"--profile",
"internal-doc",
"--waivers",
complete_path.to_str().unwrap(),
"-",
],
payload,
);
assert_eq!(code, 0, "stdout: {stdout} stderr: {stderr}");
assert!(
stdout.contains("\"waived\":true"),
"waived finding must still be reported: {stdout}"
);
}
#[test]
fn policy_digest_prints_the_embedded_digest() {
let out = bin().args(["policy", "digest"]).output().unwrap();
assert_eq!(out.status.code(), Some(0));
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
ai_slop::policy_digest()
);
}
#[test]
fn unsupported_format_for_profile_is_usage_error() {
let (code, _, _) = run_stdin(
&[
"check",
"--profile",
"commit-message",
"--format",
"markdown",
"-",
],
b"feat: x\n",
);
assert_eq!(code, 2);
}
#[test]
fn suggest_only_annotates_mechanical_rules() {
let (_, stdout, _) = run_stdin(
&["check", "--profile", "internal-doc", "--suggest", "-"],
b"A dash \xE2\x80\x94 here. We delve too.\n",
);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
let findings = v["findings"].as_array().unwrap();
let m001 = findings
.iter()
.find(|f| f["rule_id"] == "SLOP-M001")
.unwrap();
assert!(m001["suggestion"].is_object());
let a001 = findings
.iter()
.find(|f| f["rule_id"] == "SLOP-A001")
.unwrap();
assert!(a001.get("suggestion").is_none() || a001["suggestion"].is_null());
assert!(v["note"].as_str().unwrap().contains("artifact hash"));
}
#[test]
fn rust_source_path_under_prose_profile_exits_40() {
let dir = std::env::temp_dir().join("ai-slop-cli-test-rs");
std::fs::create_dir_all(&dir).unwrap();
let rs_path = dir.join("lib.rs");
std::fs::write(&rs_path, "Plain prose that happens to sit in a .rs file.\n").unwrap();
let out = bin()
.args(["check", "--profile", "api-docs", rs_path.to_str().unwrap()])
.output()
.unwrap();
assert_eq!(out.status.code(), Some(40));
let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v["result_state"], "unsupported_input");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("extract the rustdoc and gate the extract"),
"remedy missing from stderr: {stderr}"
);
}
#[test]
fn rust_shaped_stdin_under_prose_profile_exits_40() {
let rust = "\
//! Crate docs.\n\
use std::io::Read;\n\
pub struct Parser {\n\
buf: Vec<u8>,\n\
}\n\
impl Parser {\n\
pub fn new() -> Parser {\n\
Parser { buf: Vec::new() }\n\
}\n\
}\n\
fn helper() -> usize {\n\
0\n\
}\n";
let (code, stdout, stderr) =
run_stdin(&["check", "--profile", "api-docs", "-"], rust.as_bytes());
assert_eq!(code, 40, "stdout: {stdout}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_eq!(v["result_state"], "unsupported_input");
assert!(stderr.contains("extract the rustdoc and gate the extract"));
}
#[test]
fn readme_with_fenced_rust_is_not_flagged_as_rust_source() {
let readme = "\
# demo\n\n\
Parses framed records from a byte stream.\n\n\
```rust\n\
use demo::Parser;\n\
pub fn main() {\n\
let p = Parser::new();\n\
p.parse(b\"input\");\n\
}\n\
impl Parser {\n\
fn reset(&mut self) {}\n\
}\n\
struct Frame;\n\
enum Kind { A, B }\n\
const MAX: usize = 16;\n\
```\n\n\
The parser stops at the first malformed frame.\n";
let (code, stdout, _) = run_stdin(&["check", "--profile", "readme", "-"], readme.as_bytes());
assert_ne!(code, 40, "fenced Rust must stay prose: {stdout}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_ne!(v["result_state"], "unsupported_input");
}
#[test]
fn bug_report_with_indented_reproducer_is_not_flagged_as_rust_source() {
let report = [
"# Parser drops the final row",
"",
"## Reproducer",
"",
"Run this against the crate:",
"",
" use demo::Parser;",
" fn main() {",
" let p = Parser::new();",
" p.parse(b\"input\");",
" }",
" impl Parser {",
" fn reset(&mut self) {}",
" }",
" struct Frame;",
" enum Kind { A, B }",
" const MAX: usize = 16;",
"",
"## Observed",
"",
"The second row is missing from the output.",
"",
"## Expected",
"",
"Both rows appear in the output.",
"",
"## Root cause",
"",
"The loop stops one index early.",
"",
]
.join("\n");
let (code, stdout, _) = run_stdin(
&["check", "--profile", "public-bug-report", "-"],
report.as_bytes(),
);
assert_ne!(code, 40, "indented reproducer tripped the guard: {stdout}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_ne!(v["result_state"], "unsupported_input");
}
#[test]
fn readme_with_tilde_fenced_rust_is_not_flagged_as_rust_source() {
let readme = "\
# demo\n\n\
Parses framed records from a byte stream.\n\n\
~~~rust\n\
use demo::Parser;\n\
pub fn main() {\n\
let p = Parser::new();\n\
p.parse(b\"input\");\n\
}\n\
impl Parser {\n\
fn reset(&mut self) {}\n\
}\n\
struct Frame;\n\
enum Kind { A, B }\n\
const MAX: usize = 16;\n\
~~~\n\n\
The parser stops at the first malformed frame.\n";
let (code, stdout, _) = run_stdin(&["check", "--profile", "readme", "-"], readme.as_bytes());
assert_ne!(code, 40, "tilde-fenced Rust must stay prose: {stdout}");
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert_ne!(v["result_state"], "unsupported_input");
}
#[test]
fn real_rust_sources_still_exit_40() {
for rel in ["src/lib.rs", "src/input.rs", "src/engine.rs"] {
let path = format!("{}/{rel}", env!("CARGO_MANIFEST_DIR"));
let bytes = std::fs::read(&path).unwrap();
let (code, stdout, stderr) = run_stdin(&["check", "--profile", "api-docs", "-"], &bytes);
assert_eq!(code, 40, "{rel} passed the rust guard: {stdout}");
assert!(stderr.contains("extract the rustdoc and gate the extract"));
}
}
#[test]
fn repo_markdown_surfaces_pass_the_rust_guard() {
for (rel, profile) in [
("README.md", "readme"),
("skills/ai-slop/SKILL.md", "internal-doc"),
("skills/ai-slop/references/rules.md", "internal-doc"),
(
"fixtures/golden/lightningcss-unbounded-parser-recursion-stack-overflow.md",
"public-bug-report",
),
] {
let path = format!("{}/{rel}", env!("CARGO_MANIFEST_DIR"));
let bytes = std::fs::read(&path).unwrap();
let (code, _, stderr) = run_stdin(&["check", "--profile", profile, "-"], &bytes);
assert_ne!(code, 40, "{rel} tripped the rust guard: {stderr}");
}
}
#[test]
fn commit_format_is_outside_the_rust_guard() {
let commit = "\
fix: reset the parser between frames\n\n\
The reset path clears the buffer. Context lines from the diff:\n\
use std::io::Read;\n\
pub struct Parser {\n\
impl Parser {\n\
pub fn new() -> Parser {\n\
fn helper() -> usize {\n\
struct Frame;\n\
enum Kind {\n\
const MAX: usize = 16;\n";
let (code, stdout, _) = run_stdin(
&["check", "--profile", "commit-message", "-"],
commit.as_bytes(),
);
assert_ne!(code, 40, "commit format must skip the guard: {stdout}");
}