use std::env;
use std::fs;
use std::path::Path;
use std::process::{Command, Output, Stdio};
fn sdd_bin() -> &'static str {
env!("CARGO_BIN_EXE_sdd")
}
fn run_sdd(cwd: &Path, args: &[&str]) -> Output {
let mut cmd = Command::new(sdd_bin());
cmd.args(args)
.current_dir(cwd)
.env_remove("SDD_USAGE_INPUT_TOKENS")
.env_remove("SDD_USAGE_CACHED_INPUT_TOKENS")
.env_remove("SDD_USAGE_OUTPUT_TOKENS")
.env_remove("SDD_USAGE_REASONING_OUTPUT_TOKENS")
.env_remove("SDD_USAGE_TOTAL_TOKENS")
.env_remove("SDD_GENERATION_STARTED_AT")
.env_remove("SDD_GENERATION_EXECUTION_STARTED_AT")
.env_remove("SDD_GENERATION_FINISHED_AT")
.env_remove("SDD_GENERATION_DURATION_MS")
.env_remove("SDD_GENERATION_REASONING_DURATION_MS")
.env_remove("SDD_GENERATION_EXECUTION_DURATION_MS")
.env_remove("CLAUDE_PROJECT_DIR")
.env_remove("SDD_LAYER_ROOT")
.env_remove("SDD_PROJECT_ROOT")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
cmd.output().expect("falha ao executar sdd")
}
fn snapshots_dir() -> std::path::PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("snapshots")
}
fn scaffold_stage(tmpdir: &Path, stage: &str, name: &str, input: &str) -> String {
run_sdd(tmpdir, &["init", name]);
let out = run_sdd(tmpdir, &[stage, "--name", name, "--force", input]);
assert!(
out.status.success(),
"sdd {stage} falhou: {}",
String::from_utf8_lossy(&out.stderr)
);
let slug = name
.to_lowercase()
.replace(' ', "-")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-')
.collect::<String>();
let docs_dir = tmpdir.join("docs").join(&slug);
let entries = fs::read_dir(&docs_dir)
.unwrap_or_else(|e| panic!("docs/{slug} não encontrado: {e}"))
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(&format!("{stage}.md")))
.unwrap_or(false)
})
.collect::<Vec<_>>();
assert!(
!entries.is_empty(),
"nenhum arquivo {stage}.md encontrado em docs/{slug}"
);
fs::read_to_string(&entries[0])
.unwrap_or_else(|e| panic!("erro ao ler {}: {e}", entries[0].display()))
}
fn regen_enabled() -> bool {
env::var("SDD_REGEN_SNAPSHOTS")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or(false)
}
fn write_or_assert_snapshot(name: &str, content: &str) {
let path = snapshots_dir().join(format!("pre_refactor_{name}.md"));
if regen_enabled() || !path.exists() {
fs::create_dir_all(snapshots_dir()).expect("criar diretório snapshots");
fs::write(&path, content)
.unwrap_or_else(|e| panic!("erro ao escrever snapshot {name}: {e}"));
println!("snapshot gerado: {}", path.display());
} else {
let existing = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("erro ao ler snapshot {name}: {e}"));
assert!(
!existing.is_empty(),
"snapshot {name} existe mas está vazio"
);
assert!(
existing.contains("## Rastreabilidade"),
"snapshot {name} não contém '## Rastreabilidade'"
);
}
}
#[test]
fn pre_refactor_snapshot_idea() {
let tmp = tempfile::tempdir().expect("tempdir");
let content = scaffold_stage(
tmp.path(),
"idea",
"Snapshot Pre Refactor",
"baseline de caracterização",
);
write_or_assert_snapshot("idea", &content);
assert!(
content.contains("## Rastreabilidade"),
"idea: seção Rastreabilidade presente"
);
assert!(content.contains("# Idea"), "idea: título presente");
let rastreabilidade_pos = content.find("## Rastreabilidade").unwrap_or(usize::MAX);
let resumo_pos = content.find("## Resumo").unwrap_or(0);
assert!(
rastreabilidade_pos < content.len(),
"idea: posição de Rastreabilidade capturada em {rastreabilidade_pos}"
);
println!("idea: Resumo em {resumo_pos}, Rastreabilidade em {rastreabilidade_pos}");
}
#[test]
fn pre_refactor_snapshot_prd() {
let tmp = tempfile::tempdir().expect("tempdir");
let content = scaffold_stage(
tmp.path(),
"prd",
"Snapshot Pre Refactor PRD",
"baseline de caracterização de prd",
);
write_or_assert_snapshot("prd", &content);
assert!(
content.contains("## Rastreabilidade"),
"prd: seção Rastreabilidade presente"
);
assert!(content.contains("# PRD"), "prd: título presente");
let rastreabilidade_pos = content.find("## Rastreabilidade").unwrap_or(usize::MAX);
println!("prd: Rastreabilidade em {rastreabilidade_pos}");
}
#[test]
fn pre_refactor_snapshot_tasks() {
let tmp = tempfile::tempdir().expect("tempdir");
let content = scaffold_stage(
tmp.path(),
"tasks",
"Snapshot Pre Refactor Tasks",
"baseline de caracterização de tasks",
);
write_or_assert_snapshot("tasks", &content);
assert!(
content.contains("## Rastreabilidade"),
"tasks: seção Rastreabilidade presente"
);
let rastreabilidade_pos = content.find("## Rastreabilidade").unwrap_or(usize::MAX);
println!("tasks: Rastreabilidade em {rastreabilidade_pos}");
}
#[test]
fn pre_refactor_snapshot_discovery() {
let tmp = tempfile::tempdir().expect("tempdir");
let name = "Snapshot Pre Refactor Discovery";
run_sdd(tmp.path(), &["init", name]);
let out = run_sdd(
tmp.path(),
&[
"discover",
"--name",
name,
"--force",
"--offline",
"baseline de discovery",
],
);
if out.status.success() {
let slug = name
.to_lowercase()
.replace(' ', "-")
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-')
.collect::<String>();
let docs_dir = tmp.path().join("docs").join(&slug);
if let Ok(entries) = fs::read_dir(&docs_dir) {
let discovery_file = entries.filter_map(|e| e.ok()).map(|e| e.path()).find(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.contains("discovery") && n.ends_with(".md"))
.unwrap_or(false)
});
if let Some(path) = discovery_file {
if let Ok(content) = fs::read_to_string(&path) {
write_or_assert_snapshot("discovery", &content);
assert!(
content.contains("## Rastreabilidade")
|| content.contains("# Project Discovery"),
"discovery: estrutura básica presente"
);
println!("discovery snapshot: {} bytes", content.len());
}
}
}
} else {
println!("discovery snapshot: pulado (sem provider offline disponível)");
let path = snapshots_dir().join("pre_refactor_discovery.md");
if !path.exists() {
fs::create_dir_all(snapshots_dir()).expect("criar diretório snapshots");
fs::write(
&path,
"<!-- snapshot não disponível sem provider; gerado como placeholder -->\n",
)
.ok();
}
}
}
fn write_or_assert_post_refactor_snapshot(name: &str, content: &str) {
let path = snapshots_dir().join(format!("post_refactor_{name}.md"));
if regen_enabled() || !path.exists() {
fs::create_dir_all(snapshots_dir()).expect("criar diretório snapshots");
fs::write(&path, content)
.unwrap_or_else(|e| panic!("erro ao escrever snapshot post_refactor_{name}: {e}"));
println!("post_refactor snapshot gerado: {}", path.display());
} else {
let existing = fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("erro ao ler snapshot post_refactor_{name}: {e}"));
assert!(
!existing.is_empty(),
"post_refactor snapshot {name} existe mas está vazio"
);
assert!(
existing.contains("## Rastreabilidade"),
"post_refactor snapshot {name} não contém '## Rastreabilidade'"
);
}
}
#[allow(dead_code)]
fn last_pos(haystack: &str, needle: &str) -> Option<usize> {
haystack.rfind(needle)
}
fn first_h2_pos_excluding_title(content: &str) -> usize {
content
.match_indices("## ")
.map(|(pos, _)| pos)
.next()
.unwrap_or(usize::MAX)
}
#[test]
fn t05_post_refactor_snapshot_idea() {
let tmp = tempfile::tempdir().expect("tempdir");
let content = scaffold_stage(
tmp.path(),
"idea",
"Snapshot Post Refactor",
"baseline pos-refactor",
);
write_or_assert_post_refactor_snapshot("idea", &content);
assert!(
content.contains("## Rastreabilidade"),
"post_refactor idea: Rastreabilidade presente"
);
let rast_pos = content.find("## Rastreabilidade").unwrap();
let first_h2 = first_h2_pos_excluding_title(&content);
assert!(
rast_pos > first_h2,
"post_refactor idea: Rastreabilidade deve ser penúltima seção, não a primeira (rast_pos={rast_pos}, first_h2={first_h2})"
);
assert!(
!content.contains("- Provider:"),
"post_refactor idea: '- Provider:' não deve aparecer em scaffolding determinístico"
);
assert!(
!content.contains("- Tokens:"),
"post_refactor idea: '- Tokens:' não deve aparecer em scaffolding determinístico"
);
println!("post_refactor idea: Rastreabilidade em {rast_pos}, first_h2={first_h2}");
}
#[test]
fn t05_post_refactor_snapshot_prd() {
let tmp = tempfile::tempdir().expect("tempdir");
let content = scaffold_stage(
tmp.path(),
"prd",
"Snapshot Post Refactor PRD",
"baseline pos-refactor prd",
);
write_or_assert_post_refactor_snapshot("prd", &content);
assert!(
content.contains("## Rastreabilidade"),
"post_refactor prd: Rastreabilidade presente"
);
let rast_pos = content.find("## Rastreabilidade").unwrap();
let first_h2 = first_h2_pos_excluding_title(&content);
assert!(
rast_pos > first_h2,
"post_refactor prd: Rastreabilidade deve ser penúltima seção (rast_pos={rast_pos}, first_h2={first_h2})"
);
assert!(
!content.contains("- Provider:"),
"post_refactor prd: sem provider em scaffolding"
);
println!("post_refactor prd: Rastreabilidade em {rast_pos}, first_h2={first_h2}");
}
#[test]
fn t05_post_refactor_snapshot_tasks() {
let tmp = tempfile::tempdir().expect("tempdir");
let content = scaffold_stage(
tmp.path(),
"tasks",
"Snapshot Post Refactor Tasks",
"baseline pos-refactor tasks",
);
write_or_assert_post_refactor_snapshot("tasks", &content);
assert!(
content.contains("## Rastreabilidade"),
"post_refactor tasks: Rastreabilidade presente"
);
let rast_pos = content.find("## Rastreabilidade").unwrap();
let first_h2 = first_h2_pos_excluding_title(&content);
assert!(
rast_pos > first_h2,
"post_refactor tasks: Rastreabilidade deve ser penúltima seção (rast_pos={rast_pos}, first_h2={first_h2})"
);
assert!(
!content.contains("- Provider:"),
"post_refactor tasks: sem provider em scaffolding"
);
println!("post_refactor tasks: Rastreabilidade em {rast_pos}, first_h2={first_h2}");
}
#[test]
fn t05_structural_diff_rastreabilidade_moved_to_penultimate() {
let pre_path = snapshots_dir().join("pre_refactor_idea.md");
let post_path = snapshots_dir().join("post_refactor_idea.md");
if !pre_path.exists() || !post_path.exists() {
println!("t05_structural_diff: snapshots não encontrados, pulando diff (rodar com SDD_REGEN_SNAPSHOTS=1 para gerar)");
return;
}
let pre = fs::read_to_string(&pre_path).expect("ler pre_refactor_idea.md");
let post = fs::read_to_string(&post_path).expect("ler post_refactor_idea.md");
let pre_rast = pre.find("## Rastreabilidade").unwrap_or(usize::MAX);
let post_rast = post.find("## Rastreabilidade").unwrap_or(usize::MAX);
let post_first_h2 = first_h2_pos_excluding_title(&post);
println!(
"diff estrutural: pre Rastreabilidade em {pre_rast}, post Rastreabilidade em {post_rast}"
);
assert!(
post_rast > post_first_h2,
"pós-refactor: Rastreabilidade deve ser penúltima seção, não a primeira"
);
assert!(
!post.contains("- Provider:"),
"pós-refactor: sem '- Provider:' em scaffolding determinístico"
);
assert!(
!post.contains("- Tokens:"),
"pós-refactor: sem '- Tokens:' em scaffolding determinístico"
);
}