KeiSeiKit-1.0/_primitives/_rust/kei-refactor-engine/tests/integration.rs
Parfii-bot 19ee220e0a feat(primitives): 4 Rust crates for deep-sleep — conflict-scan, refactor-engine, graph-check, store
- kei-conflict-scan: rules/hooks/blocks/orphans/CP detection (6 tests)
- kei-refactor-engine: plan-mode + advisory patch format, zero-conflict guarantee (5 tests)
- kei-graph-check: wikilinks/handoffs/block-refs validator (4 tests)
- kei-store: trait + 5 backends (filesystem/github/forgejo/gitea prod, s3 stub) (8 tests)

1916 LOC Rust total; all files <200 LOC; 23/23 tests pass.
2026-04-22 08:28:22 +08:00

125 lines
4.1 KiB
Rust

//! Integration tests for kei-refactor-engine.
use std::fs;
use std::path::PathBuf;
use tempfile::TempDir;
fn bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_kei-refactor-engine"))
}
fn sample_json(extra_manual: bool) -> String {
let mut items = vec![serde_json::json!({
"category": "blocks",
"severity": "medium",
"files": ["_blocks/a.md", "_blocks/b.md"],
"evidence": "shingle-Jaccard 72% overlap",
"suggested_fix": "keep better-cited",
"auto_resolvable": true
})];
if extra_manual {
items.push(serde_json::json!({
"category": "rules",
"severity": "high",
"files": ["rules/x.md", "rules/y.md"],
"evidence": "contradictory directive on 'push'",
"suggested_fix": "review both",
"auto_resolvable": false
}));
}
serde_json::json!({ "hit_count": items.len(), "conflicts": items }).to_string()
}
#[test]
fn plan_only_prints_markdown() {
let tmp = TempDir::new().unwrap();
let input = tmp.path().join("c.json");
fs::write(&input, sample_json(false)).unwrap();
let out = std::process::Command::new(bin())
.args(["--input"])
.arg(&input)
.output()
.unwrap();
assert!(out.status.success());
let md = String::from_utf8(out.stdout).unwrap();
assert!(md.contains("# Deep-sleep refactor plan"));
assert!(md.contains("Auto-apply"));
}
#[test]
fn manual_items_listed_but_not_in_patch() {
let tmp = TempDir::new().unwrap();
let input = tmp.path().join("c.json");
let plan_out = tmp.path().join("plan.md");
let patch_out = tmp.path().join("p.patch");
fs::write(&input, sample_json(true)).unwrap();
let out = std::process::Command::new(bin())
.args(["--input"])
.arg(&input)
.args(["--apply-to-branch", "deep-sleep/test", "--plan-out"])
.arg(&plan_out)
.args(["--patch-out"])
.arg(&patch_out)
.output()
.unwrap();
assert!(out.status.success(), "stderr: {}", String::from_utf8_lossy(&out.stderr));
let md = fs::read_to_string(&plan_out).unwrap();
assert!(md.contains("Requires human decision"));
let patch = fs::read_to_string(&patch_out).unwrap();
// patch must NOT reference rules/x.md from the manual item
assert!(!patch.contains("rules/x.md"), "patch leaked manual item: {}", patch);
assert!(patch.contains("_blocks/a.md"));
}
#[test]
fn empty_conflicts_produce_valid_plan() {
let tmp = TempDir::new().unwrap();
let input = tmp.path().join("c.json");
fs::write(&input, r#"{"hit_count": 0, "conflicts": []}"#).unwrap();
let out = std::process::Command::new(bin())
.args(["--input"])
.arg(&input)
.output()
.unwrap();
assert!(out.status.success());
let md = String::from_utf8(out.stdout).unwrap();
assert!(md.contains("Total conflicts: **0**"));
}
#[test]
fn stdin_input_works() {
let tmp = TempDir::new().unwrap();
let _ = tmp; // kept for parity
let mut child = std::process::Command::new(bin())
.args(["--input", "-"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
{
use std::io::Write;
let stdin = child.stdin.as_mut().unwrap();
stdin.write_all(sample_json(false).as_bytes()).unwrap();
}
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
assert!(String::from_utf8(out.stdout).unwrap().contains("refactor plan"));
}
#[test]
fn patch_header_shows_counts() {
let tmp = TempDir::new().unwrap();
let input = tmp.path().join("c.json");
let patch_out = tmp.path().join("p.patch");
fs::write(&input, sample_json(true)).unwrap();
std::process::Command::new(bin())
.args(["--input"])
.arg(&input)
.args(["--apply-to-branch", "deep-sleep/a", "--patch-out"])
.arg(&patch_out)
.output()
.unwrap();
let patch = fs::read_to_string(&patch_out).unwrap();
assert!(patch.contains("Auto-apply items: 1"));
assert!(patch.contains("Human-decision items"));
}