use std::env;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::{Command, Output, Stdio};
use serde_json::Value;
use serde_yaml::Value as YamlValue;
use tempfile::tempdir;
fn sdd_bin() -> &'static str {
env!("CARGO_BIN_EXE_sdd")
}
fn repo_root() -> &'static Path {
Path::new(env!("CARGO_MANIFEST_DIR"))
}
fn collect_relative_files(base: &Path) -> Vec<String> {
fn visit(base: &Path, current: &Path, out: &mut Vec<String>) {
let mut entries = fs::read_dir(current)
.unwrap_or_else(|err| panic!("read dir {}: {err}", current.display()))
.map(|entry| entry.expect("read dir entry").path())
.collect::<Vec<_>>();
entries.sort();
for path in entries {
if path.is_dir() {
visit(base, &path, out);
} else if path.is_file() {
out.push(
path.strip_prefix(base)
.expect("relative path")
.to_string_lossy()
.replace('\\', "/"),
);
}
}
}
let mut files = Vec::new();
visit(base, base, &mut files);
files
}
fn assert_directory_mirror(source: &Path, mirror: &Path) {
let source_files = collect_relative_files(source);
let mirror_files = collect_relative_files(mirror);
assert_eq!(
source_files,
mirror_files,
"{} drifted from {}",
mirror.display(),
source.display()
);
for rel in source_files {
let source_bytes = fs::read(source.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
let mirror_bytes = fs::read(mirror.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
assert_eq!(
source_bytes,
mirror_bytes,
"{} drifted from {}",
mirror.join(&rel).display(),
source.join(&rel).display()
);
}
}
fn run(cwd: &Path, args: &[&str]) -> Output {
run_with(cwd, args, None, &[])
}
#[cfg(not(feature = "html-gen"))]
fn run_no_default_features(cwd: &Path, args: &[&str]) -> Output {
let mut command = Command::new(env!("CARGO"));
command
.arg("run")
.arg("--quiet")
.arg("--manifest-path")
.arg(repo_root().join("Cargo.toml"))
.arg("--locked")
.arg("--no-default-features")
.arg("--bin")
.arg("sdd")
.arg("--")
.args(args)
.current_dir(cwd)
.env_remove("CLAUDE_PROJECT_DIR")
.env_remove("SDD_LAYER_ROOT")
.env_remove("SDD_PROJECT_ROOT")
.env_remove("SDD_USAGE_INPUT_TOKENS")
.env_remove("SDD_USAGE_OUTPUT_TOKENS")
.env_remove("SDD_USAGE_CACHE_READ_TOKENS")
.env_remove("SDD_USAGE_CACHE_WRITE_TOKENS")
.env_remove("SDD_USAGE_OUTPUT_REASONING_TOKENS")
.env_remove("SDD_USAGE_COST_TOTAL")
.env_remove("SDD_USAGE_COST_CURRENCY")
.env_remove("SDD_GENERATION_STARTED_AT")
.env_remove("SDD_GENERATION_EXECUTION_STARTED_AT")
.env_remove("SDD_GENERATION_FINISHED_AT")
.output()
.unwrap()
}
fn run_with(cwd: &Path, args: &[&str], input: Option<&str>, envs: &[(&str, &Path)]) -> Output {
let mut command = Command::new(sdd_bin());
command
.args(args)
.current_dir(cwd)
.env_remove("CLAUDE_PROJECT_DIR")
.env_remove("SDD_LAYER_ROOT")
.env_remove("SDD_PROJECT_ROOT")
.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")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in envs {
command.env(key, value);
}
if input.is_some() {
command.stdin(Stdio::piped());
} else {
command.stdin(Stdio::null());
}
let mut child = command.spawn().expect("spawn sdd");
if let Some(input) = input {
child
.stdin
.as_mut()
.expect("stdin")
.write_all(input.as_bytes())
.expect("write stdin");
}
child.wait_with_output().expect("wait sdd")
}
fn run_with_env_values(
cwd: &Path,
args: &[&str],
input: Option<&str>,
envs: &[(&str, &str)],
) -> Output {
let mut command = Command::new(sdd_bin());
command
.args(args)
.current_dir(cwd)
.env_remove("CLAUDE_PROJECT_DIR")
.env_remove("SDD_LAYER_ROOT")
.env_remove("SDD_PROJECT_ROOT")
.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")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
for (key, value) in envs {
command.env(key, value);
}
if input.is_some() {
command.stdin(Stdio::piped());
} else {
command.stdin(Stdio::null());
}
let mut child = command.spawn().expect("spawn sdd");
if let Some(input) = input {
child
.stdin
.as_mut()
.expect("stdin")
.write_all(input.as_bytes())
.expect("write stdin");
}
child.wait_with_output().expect("wait sdd")
}
fn combined(output: &Output) -> String {
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn assert_portable_orchestration_skill_contract(content: &str) {
assert_worktree_decision_contract(content);
assert!(content.contains(
"6. Execution: one task at a time, after reading the execution Context Pack and code-intelligence handoff."
));
assert!(content.contains(
"8. QA: validate observable behaviour against acceptance criteria (not a code review repeat). Checkpoint."
));
assert!(content.contains(
"ADR is not a numbered stage — it is a utility, callable from any stage whenever a real technical decision needs to be locked"
));
assert!(content.contains(
"Before `/execution`, generate or read `sdd context build --name \"<orchestration>\" --stage execution --write`"
));
assert!(content.contains("If `sdd` is not in `PATH`"));
assert!(content.contains("cargo run --bin sdd -- ..."));
assert!(content.contains("During `/execution`, use `execution-discipline`"));
assert!(!content.contains("6a. ADR"));
}
fn assert_worktree_decision_contract(content: &str) {
let lower = content.to_lowercase();
assert!(
content.contains("orchestration.isolation"),
"isolation contract must reference the orchestration.isolation config key"
);
assert!(
content.contains("--worktree yes"),
"isolation contract must expose the explicit yes override"
);
assert!(
content.contains("--worktree no"),
"isolation contract must expose the explicit no override"
);
assert!(
lower.contains("local") && lower.contains("worktree") && lower.contains("cloud"),
"isolation contract must document local|worktree|cloud"
);
assert!(
lower.contains("default") || lower.contains("padrão") || lower.contains("padrao"),
"isolation contract must state that local is the default"
);
for stale in [
"Mandatory checkpoint: use worktrees",
"Mandatory worktree checkpoint",
"Checkpoint obrigatório: usar worktree",
"Checkpoint obrigatório de worktree",
"Pergunta obrigatória: usar worktrees",
"CHECKPOINT OBRIGATÓRIO — uso de worktrees",
"EXIGE a decisão",
"Stop and wait for the user's explicit response",
"Pare e aguarde a resposta",
"pergunte explicitamente ao usuário se deseja usar git worktree",
] {
assert!(
!content.contains(stale),
"stale mandatory-worktree-question guidance remains: {stale}"
);
}
for stale in [
"Both modes create their own worktree",
"Os dois modos criam worktree próprio",
"first call does the lazy worktree setup",
"1ª vez — faz o setup do worktree",
] {
assert!(
!content.contains(stale),
"stale automatic-worktree guidance remains: {stale}"
);
}
}
const CLAUDE_AGENT_SKILL_CONTRACTS: &[(&str, &[&str])] = &[
("adr", &[]),
("architecture-reviewer", &[]),
("backend-implementer", &[]),
("data-contract-reviewer", &[]),
("execution", &["bdd-gherkin", "traceability"]),
("frontend-implementer", &[]),
("idea", &[]),
("memory", &["memory"]),
("performance-reviewer", &[]),
("prd", &["artifact-diagrams"]),
(
"project-discovery",
&[
"project-discovery",
"repo-adapter",
"risk-classifier",
"integrations",
],
),
("qa-strategist", &["bdd-gherkin"]),
("refinement", &["checkpoints", "traceability"]),
("review", &["code-review", "traceability"]),
(
"risk-classifier",
&[
"risk-classifier",
"security-privacy",
"data-contracts",
"test-strategy",
"checkpoints",
"release-readiness",
],
),
("security-reviewer", &[]),
("tasks", &["bdd-gherkin", "traceability"]),
("techspec", &["artifact-diagrams"]),
("test-implementer", &[]),
("test-reviewer", &[]),
];
fn assert_claude_agent_skill_contract(content: &str, name: &str, preloads: &[&str]) {
let frontmatter = content
.strip_prefix("---\n")
.and_then(|rest| rest.split_once("\n---\n"))
.map(|(frontmatter, _)| frontmatter)
.unwrap_or_else(|| panic!(".claude/agents/{name}.md has invalid frontmatter"));
let tools = frontmatter
.lines()
.find_map(|line| line.strip_prefix("tools:"))
.unwrap_or_else(|| panic!(".claude/agents/{name}.md is missing tools"));
assert!(
tools.split(',').any(|tool| tool.trim() == "Skill"),
".claude/agents/{name}.md must allow the native Skill tool"
);
for skill in preloads {
assert!(
frontmatter
.lines()
.any(|line| line.trim() == format!("- {skill}")),
".claude/agents/{name}.md must preload cited skill {skill}"
);
}
}
const CORE_STAGE_SKILLS: &[&str] = &[
"idea",
"prd",
"techspec",
"tasks",
"execution",
"review",
"memory",
];
fn assert_stage_skill_contract(root: &Path, prefix: &str, stage: &str) {
let rel = format!("{prefix}/{stage}/SKILL.md");
let content = fs::read_to_string(root.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
assert!(content.starts_with("---\n"), "{rel}");
assert!(content.contains(&format!("name: {stage}\n")), "{rel}");
assert!(content.contains("description: "), "{rel}");
assert!(
content.contains("delegue estado, persistência e validação ao CLI `sdd`"),
"{rel}"
);
assert!(content.contains(&format!("sdd {stage}")), "{rel}");
assert!(content.contains("sdd init"), "{rel}");
assert!(content.contains("sdd artifact save"), "{rel}");
assert!(content.contains("docs/<slug-da-orquestracao>/"), "{rel}");
assert!(content.contains("traceability-map.yaml"), "{rel}");
assert!(
content.chars().any(|ch| ch as u32 > 0x7f),
"{rel} should preserve PT-BR accents"
);
}
fn assert_core_stage_skills(root: &Path, prefix: &str) {
for stage in CORE_STAGE_SKILLS {
assert_stage_skill_contract(root, prefix, stage);
}
}
fn assert_kiro_surface(root: &Path) {
for rel in [
".kiro/agents/sdd/orchestrator.md",
".kiro/agents/sdd/project-discovery.md",
".kiro/agents/sdd/idea.md",
".kiro/agents/sdd/execution.md",
".kiro/agents/sdd/adr.md",
".kiro/agents/sdd/review.md",
".kiro/skills/sdd/SKILL.md",
".kiro/skills/workflow/SKILL.md",
".kiro/skills/diagram/SKILL.md",
".kiro/skills/orchestration/SKILL.md",
".kiro/skills/prd/SKILL.md",
".kiro/steering/00-sdd-principles.md",
".kiro/steering/10-traceability.md",
".kiro/steering/20-output-conventions.md",
".kiro/hooks/sdd-guardrails.json",
".kiro/settings/mcp.json",
".sdd/clients/kiro.md",
".sdd/clients/kiro-power/POWER.md",
".sdd/clients/kiro-power/mcp.json",
] {
assert!(root.join(rel).is_file(), "missing Kiro surface {rel}");
}
assert_core_stage_skills(root, ".kiro/skills");
assert_kiro_native_contract(root);
}
fn assert_kiro_native_contract(root: &Path) {
let agent = fs::read_to_string(root.join(".kiro/agents/sdd/orchestrator.md")).unwrap();
assert!(agent.starts_with("---\n"));
assert!(agent.contains("tools: [\"@builtin\", \"@mcp\"]"));
assert!(agent.contains("capability: shell"));
assert!(agent.contains("sdd orchestration next"));
let hook: Value = serde_json::from_str(
&fs::read_to_string(root.join(".kiro/hooks/sdd-guardrails.json")).unwrap(),
)
.unwrap();
assert_eq!(hook["version"], "v1");
assert!(json_contains_text(&hook, "SessionStart"));
assert!(json_contains_text(&hook, "PreToolUse"));
assert!(json_contains_text(&hook, "^(write|shell)$"));
assert!(json_contains_text(&hook, "exit 2"));
assert!(json_contains_text(&hook, "exit 0"));
assert!(!json_contains_text(&hook, "|| true"));
let prd_skill = fs::read_to_string(root.join(".kiro/skills/prd/SKILL.md")).unwrap();
assert!(prd_skill.contains("subagent `sdd/prd`"));
let mcp: Value =
serde_json::from_str(&fs::read_to_string(root.join(".kiro/settings/mcp.json")).unwrap())
.unwrap();
assert_eq!(mcp["mcpServers"]["sdd"]["command"], "sdd");
assert!(json_contains_text(&mcp, "mcp"));
assert!(json_contains_text(&mcp, "serve"));
assert!(json_contains_text(&mcp, "codegraph"));
}
fn assert_kiro_catalog_skills(root: &Path) {
for rel in [
".kiro/skills/generic-project/SKILL.md",
".kiro/skills/test-strategy-local/SKILL.md",
] {
assert!(root.join(rel).is_file(), "missing Kiro catalog skill {rel}");
}
assert!(
fs::read_to_string(root.join(".kiro/skills/generic-project/SKILL.md"))
.unwrap()
.contains("\nname: generic-project\n")
);
assert!(
fs::read_to_string(root.join(".kiro/skills/test-strategy-local/SKILL.md"))
.unwrap()
.contains("\nname: test-strategy-local\n")
);
}
fn assert_stage_command_fallback(root: &Path, prefix: &str, extension: &str, stage: &str) {
let rel = format!("{prefix}/{stage}.{extension}");
let content = fs::read_to_string(root.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
assert!(
content.contains("Use a skill ou subagent desta etapa se o client a expuser"),
"{rel}"
);
assert!(
content.contains(&format!("sdd {stage}")),
"{rel} should delegate to sdd {stage}"
);
}
fn assert_stage_command_fallbacks(root: &Path) {
for stage in CORE_STAGE_SKILLS {
assert_stage_command_fallback(root, ".cursor/commands", "md", stage);
assert_stage_command_fallback(root, ".opencode/commands", "md", stage);
assert_stage_command_fallback(root, ".trae/commands", "md", stage);
}
}
fn assert_diagram_command_contract(content: &str) {
for expected in [
"Fluxo obrigatório",
"Gate de estilo",
"Coleta automática",
"Validação",
"Fallback provider-neutral",
"sdd resolve-input",
"sdd diagram doctor --name \"<nome-da-orquestracao>\"",
"--file <arquivo.html|svg|excalidraw>",
"sdd validate-artifact <tipo> <arquivo.md>",
"PNG é derivado opcional, não formato principal",
] {
assert!(
content.contains(expected),
"diagram command should contain `{expected}`"
);
}
}
fn canonical_orchestration_skill() -> &'static str {
include_str!("../templates/client-skills/orchestration.md")
}
fn assert_success(output: Output) {
assert!(
output.status.success(),
"expected success, got {:?}\n{}",
output.status.code(),
combined(&output)
);
}
fn assert_code(output: Output, expected: i32) {
assert_eq!(
output.status.code(),
Some(expected),
"unexpected exit code\n{}",
combined(&output)
);
}
fn yaml_get<'a>(mapping: &'a serde_yaml::Mapping, key: &str) -> Option<&'a YamlValue> {
mapping.get(YamlValue::String(key.to_string()))
}
fn artifact_stage_files(yaml: &str) -> Vec<(String, String)> {
let root: YamlValue = serde_yaml::from_str(yaml).expect("valid yaml");
let root = root.as_mapping().expect("root mapping");
let artifacts = yaml_get(root, "artifacts")
.and_then(YamlValue::as_mapping)
.expect("artifacts mapping");
artifacts
.iter()
.map(|(stage, metadata)| {
let stage = stage.as_str().expect("artifact stage key").to_string();
let file = metadata
.as_mapping()
.and_then(|metadata| yaml_get(metadata, "file"))
.and_then(YamlValue::as_str)
.unwrap_or("")
.to_string();
(stage, file)
})
.collect()
}
fn contract_stage_files(yaml: &str) -> Vec<(String, String)> {
let root: YamlValue = serde_yaml::from_str(yaml).expect("valid yaml");
let root = root.as_mapping().expect("root mapping");
let stages = yaml_get(root, "stages")
.and_then(YamlValue::as_sequence)
.expect("stages sequence");
stages
.iter()
.map(|stage| {
let stage = stage.as_mapping().expect("stage mapping");
let key = yaml_get(stage, "key")
.and_then(YamlValue::as_str)
.expect("stage key")
.to_string();
let file = yaml_get(stage, "filename")
.and_then(YamlValue::as_str)
.expect("stage filename")
.to_string();
(key, file)
})
.collect()
}
fn assert_no_script_runtime(path: &Path) {
fn visit(path: &Path) {
for entry in fs::read_dir(path).expect("read dir") {
let entry = entry.expect("dir entry");
let path = entry.path();
let name = path
.file_name()
.and_then(|item| item.to_str())
.unwrap_or("");
if path.is_dir() && (name == ".worktree" || name == "worktrees") {
continue;
}
assert_ne!(
name,
"__pycache__",
"cache directory leaked: {}",
path.display()
);
assert_ne!(
name,
"scripts",
"script directory leaked: {}",
path.display()
);
assert_ne!(
path.extension().and_then(|item| item.to_str()),
Some("py"),
"script file leaked: {}",
path.display()
);
assert_ne!(
path.extension().and_then(|item| item.to_str()),
Some("pyc"),
"compiled script cache leaked: {}",
path.display()
);
if path.is_dir() {
visit(&path);
}
}
}
visit(path);
}
fn json_contains_text(value: &Value, needle: &str) -> bool {
match value {
Value::String(text) => text.contains(needle),
Value::Array(items) => items.iter().any(|item| json_contains_text(item, needle)),
Value::Object(map) => map
.iter()
.any(|(key, value)| key.contains(needle) || json_contains_text(value, needle)),
_ => false,
}
}
fn install_generic_project(root: &Path) {
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
}
fn create_project_intelligence_artifacts(root: &Path) {
let name = "Project Intelligence Layer";
assert_success(run(root, &["init", name]));
for (stage, input) in [
("idea", "mapear contexto do projeto"),
("prd", "produto precisa de contexto auditavel"),
("techspec", "implementar pacote de contexto deterministico"),
] {
assert_success(run(root, &[stage, "--name", name, "--force", input]));
}
}
fn prepend_path(bin: &Path) -> String {
let current = env::var_os("PATH").unwrap_or_default();
env::join_paths(std::iter::once(bin.to_path_buf()).chain(env::split_paths(¤t)))
.expect("join PATH")
.to_string_lossy()
.to_string()
}
fn write_fake_codegraph(bin: &Path) {
fs::create_dir_all(bin).unwrap();
let executable = if cfg!(windows) {
bin.join("codegraph.cmd")
} else {
bin.join("codegraph")
};
let body = if cfg!(windows) {
"@echo off\r\necho %* > codegraph-args.txt\r\nmkdir .codegraph 2>nul\r\nexit /b 0\r\n"
} else {
"#!/bin/sh\nprintf '%s\\n' \"$*\" > codegraph-args.txt\nmkdir -p .codegraph\n"
};
fs::write(&executable, body).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut permissions = fs::metadata(&executable).unwrap().permissions();
permissions.set_mode(0o755);
fs::set_permissions(&executable, permissions).unwrap();
}
}
#[test]
fn detects_api_presets_and_installs_them() {
let tmp = tempdir().unwrap();
let project = tmp.path();
fs::write(
project.join("pyproject.toml"),
"[project]\nname = \"api\"\n",
)
.unwrap();
let output = run(project, &["init", "--dry-run"]);
assert_success(output);
assert!(combined(&run(project, &["init", "--dry-run"])).contains("Preset: python-api"));
fs::remove_file(project.join("pyproject.toml")).unwrap();
fs::write(
project.join("Cargo.toml"),
"[package]\nname = \"api\"\nversion = \"0.1.0\"\n[dependencies]\naxum = \"0.7\"\n",
)
.unwrap();
assert!(combined(&run(project, &["init", "--dry-run"])).contains("Preset: rust-api"));
fs::remove_file(project.join("Cargo.toml")).unwrap();
fs::write(
project.join("package.json"),
r#"{"dependencies":{"hono":"latest"}}"#,
)
.unwrap();
assert!(combined(&run(project, &["init", "--dry-run"])).contains("Preset: hono-api"));
for preset in ["python-api", "rust-api", "hono-api"] {
let target = tempdir().unwrap();
assert_success(run(
repo_root(),
&[
"install",
target.path().to_str().unwrap(),
"--preset",
preset,
],
));
let config = fs::read_to_string(target.path().join("sdd.config.yaml")).unwrap();
assert!(config.contains(&format!("name: {preset}-project")));
assert_success(run(target.path(), &["doctor"]));
assert_no_script_runtime(target.path());
}
}
#[test]
fn doctor_output_uses_tui_summary_and_groups_repeated_warnings() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let workflows = root.join(".github/workflows");
fs::create_dir_all(&workflows).unwrap();
fs::write(
workflows.join("ci.yml"),
"name: ci\non: [push]\njobs:\n placeholder:\n runs-on: ubuntu-latest\n steps: []\n",
)
.unwrap();
let output = run(root, &["doctor"]);
let text = combined(&output);
assert_success(output);
assert!(text.contains("╭─ SDD doctor"));
assert!(text.contains("│ ⚠ WARN"));
assert!(text.contains("failures ·"));
assert!(text.contains("◇ ✓ clients (0)"));
assert!(text.contains("◇ ⚠ doctor"));
assert!(text.contains("⚠ CI detectado, mas workflow não contém check esperado: `cargo test`"));
assert!(text.contains("⚠ CI detectado, mas workflow não contém check esperado: `cargo clippy`"));
assert!(text.contains("⚠ CI detectado, mas workflow não contém check esperado: `sdd ci`"));
assert!(!text.contains("- CI detectado"));
}
#[test]
fn install_output_uses_rail_steps_quick_start_and_relative_paths() {
let target = tempdir().unwrap();
let output = run(
repo_root(),
&[
"install",
target.path().to_str().unwrap(),
"--preset",
"generic",
"--dry-run",
],
);
let text = combined(&output);
assert_success(output);
assert!(text.contains("│ SDD install"));
assert!(text.contains("◇ Project root"));
assert!(text.contains("◆ Camada SDD sincronizada"));
assert!(text.contains("Planejadas"));
assert!(text.contains("copiar ./AGENTS.md"));
assert!(text.contains("◇ Quick start"));
assert!(text.contains("sdd doctor"));
assert!(text.contains("└ Done! SDD install complete."));
assert!(!text.contains("Timeline"));
assert!(!text.contains("Ações"));
assert!(!text.contains(&format!(
"dry-run copy {}",
target.path().join("AGENTS.md").display()
)));
}
#[test]
fn init_auto_indexes_codegraph_when_available() {
let target = tempdir().unwrap();
let bin = tempdir().unwrap();
write_fake_codegraph(bin.path());
let path = prepend_path(bin.path());
let output = run_with_env_values(
repo_root(),
&[
"init",
"--root",
target.path().to_str().unwrap(),
"--preset",
"generic",
],
None,
&[("PATH", &path)],
);
let text = combined(&output);
assert_success(output);
assert!(text.contains("CodeGraph: ran codegraph init -i ."));
assert!(target.path().join(".codegraph").is_dir());
assert!(fs::read_to_string(target.path().join(".gitignore"))
.unwrap()
.contains(".codegraph/"));
assert_eq!(
fs::read_to_string(target.path().join("codegraph-args.txt"))
.unwrap()
.trim(),
"init -i ."
);
}
#[test]
fn init_dry_run_reports_codegraph_auto_index_without_running() {
let target = tempdir().unwrap();
let bin = tempdir().unwrap();
write_fake_codegraph(bin.path());
let path = prepend_path(bin.path());
let output = run_with_env_values(
repo_root(),
&[
"init",
"--root",
target.path().to_str().unwrap(),
"--preset",
"generic",
"--dry-run",
],
None,
&[("PATH", &path)],
);
let text = combined(&output);
assert_success(output);
assert!(text.contains("CodeGraph: dry-run codegraph init -i ."));
assert!(!target.path().join(".codegraph").exists());
assert!(!target.path().join("codegraph-args.txt").exists());
}
#[test]
fn install_layouts_ci_and_cleanup_are_rust_only() {
let compact = tempdir().unwrap();
let output = run(
repo_root(),
&[
"install",
compact.path().to_str().unwrap(),
"--preset",
"generic",
"--with-ci",
],
);
assert_success(output);
assert!(compact.path().join(".sdd/tests").is_dir());
assert!(compact.path().join(".github/workflows/sdd.yml").is_file());
assert!(compact.path().join(".devin/config.json").is_file());
assert!(!compact.path().join(".devin/rules").exists());
assert!(compact
.path()
.join(".devin/agents/sdd-orchestrator/AGENT.md")
.is_file());
assert!(compact
.path()
.join(".devin/skills/orchestration/SKILL.md")
.is_file());
assert!(compact.path().join(".devin/skills/prd/SKILL.md").is_file());
assert!(compact.path().join(".devin/skills/sdd/SKILL.md").is_file());
assert!(compact
.path()
.join(".devin/skills/workflow/SKILL.md")
.is_file());
assert!(compact
.path()
.join(".devin/skills/diagram/SKILL.md")
.is_file());
assert!(!compact.path().join(".devin/workflows").exists());
assert!(compact.path().join(".cursor/model-routing.yaml").is_file());
assert!(compact.path().join(".cursor/rules/sdd.mdc").is_file());
assert!(compact.path().join(".cursor/commands/sdd.md").is_file());
assert!(compact
.path()
.join(".cursor/commands/orchestration.md")
.is_file());
assert!(compact
.path()
.join(".cursor/agents/sdd-orchestrator.md")
.is_file());
assert!(compact
.path()
.join(".cursor/skills/orchestration/SKILL.md")
.is_file());
assert!(compact
.path()
.join(".opencode/commands/orchestration.md")
.is_file());
assert!(compact
.path()
.join(".agents/skills/orchestration/SKILL.md")
.is_file());
assert!(compact
.path()
.join(".trae/commands/orchestration.md")
.is_file());
assert!(compact
.path()
.join(".trae/rules/00-sdd-principles.md")
.is_file());
assert!(compact
.path()
.join(".trae/skills/orchestration/SKILL.md")
.is_file());
assert_kiro_surface(compact.path());
assert_kiro_catalog_skills(compact.path());
assert!(compact.path().join(".claude/agents/adr.md").is_file());
assert!(compact.path().join(".codex/agents/adr.toml").is_file());
let devin_execution =
fs::read_to_string(compact.path().join(".devin/agents/execution/AGENT.md")).unwrap();
assert!(devin_execution.contains(" - write"));
assert!(devin_execution.contains(" - edit"));
assert!(compact.path().join(".sdd/clients/README.md").is_file());
assert!(!compact.path().join("plugins").exists());
assert!(!compact.path().join(".sdd/plugins").exists());
assert!(!compact.path().join("presets").exists());
assert!(!compact.path().join("scripts").exists());
assert_success(run(compact.path(), &["doctor"]));
assert_success(run(compact.path(), &["clients", "doctor"]));
assert_no_script_runtime(compact.path());
let initialized = tempdir().unwrap();
assert_success(run(initialized.path(), &["init", "--preset", "generic"]));
assert!(initialized.path().join(".devin/config.json").is_file());
assert!(!initialized.path().join(".devin/rules").exists());
assert!(initialized
.path()
.join(".devin/agents/sdd-orchestrator/AGENT.md")
.is_file());
assert!(initialized
.path()
.join(".devin/skills/orchestration/SKILL.md")
.is_file());
assert!(initialized
.path()
.join(".devin/skills/prd/SKILL.md")
.is_file());
assert!(initialized
.path()
.join(".devin/skills/workflow/SKILL.md")
.is_file());
assert!(!initialized.path().join(".devin/workflows").exists());
assert!(initialized
.path()
.join(".cursor/model-routing.yaml")
.is_file());
assert!(initialized.path().join(".cursor/rules/sdd.mdc").is_file());
assert!(initialized.path().join(".cursor/commands/sdd.md").is_file());
assert!(initialized
.path()
.join(".cursor/commands/orchestration.md")
.is_file());
assert!(initialized
.path()
.join(".cursor/agents/sdd-orchestrator.md")
.is_file());
assert!(initialized
.path()
.join(".cursor/skills/orchestration/SKILL.md")
.is_file());
assert!(initialized
.path()
.join(".trae/commands/orchestration.md")
.is_file());
assert_kiro_surface(initialized.path());
assert_kiro_catalog_skills(initialized.path());
assert_success(run(initialized.path(), &["clients", "doctor"]));
let flat = tempdir().unwrap();
assert_success(run(
repo_root(),
&[
"install",
flat.path().to_str().unwrap(),
"--preset",
"generic",
"--layout",
"flat",
],
));
assert!(flat.path().join("presets").is_dir());
assert!(!flat.path().join("plugins").exists());
assert!(!flat.path().join(".sdd").exists());
assert!(!flat.path().join("scripts").exists());
let preserved = flat.path().join("docs/minha-orquestracao");
fs::create_dir_all(&preserved).unwrap();
fs::write(
preserved.join("traceability-map.yaml"),
"orchestration: {}\n",
)
.unwrap();
assert_success(run(
repo_root(),
&[
"install",
flat.path().to_str().unwrap(),
"--preset",
"generic",
"--force",
"--cleanup-legacy",
],
));
assert!(flat.path().join(".sdd/presets").is_dir());
assert!(preserved.exists());
for rel in [
"presets",
"schemas",
"templates",
"adapters",
"clients",
"tests",
"examples",
"scripts",
] {
assert!(
!flat.path().join(rel).exists(),
"legacy path still exists: {rel}"
);
}
}
#[test]
fn install_and_update_skip_bot_bundle_when_install_is_declined() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run_with(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
Some("n\n"),
&[],
));
assert!(!root.join("bot").exists());
assert!(!root.join(".sdd/bot").exists());
assert_success(run_with(root, &["update"], Some("n\n"), &[]));
assert!(!root.join("bot").exists());
assert!(!root.join(".sdd/bot").exists());
let flat = tempdir().unwrap();
assert_success(run_with(
repo_root(),
&[
"install",
flat.path().to_str().unwrap(),
"--preset",
"generic",
"--layout",
"flat",
],
Some("n\n"),
&[],
));
assert!(!flat.path().join("bot").exists());
}
#[test]
fn update_refreshes_installed_package_without_losing_project_state() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let installed_mcp =
serde_json::from_str::<Value>(&fs::read_to_string(root.join(".mcp.json")).unwrap())
.unwrap();
let root_canonical = {
let c = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
let s = c.to_string_lossy().into_owned();
let stripped = s.strip_prefix(r"\\?\").unwrap_or(&s).to_string();
stripped.replace('\\', "/")
};
assert!(json_contains_text(&installed_mcp, &root_canonical));
assert!(root.join(".cursor/mcp.json").is_file());
let config_path = root.join("sdd.config.yaml");
let mut config = fs::read_to_string(&config_path)
.unwrap()
.replace("\r\n", "\n");
config.push_str("\ncustom_marker: keep-me\n");
config = config.replace(
"\nruntime:\n",
"\n custom:\n kind: openai-compatible\n auth_env: CUSTOM_PROVIDER_KEY\n model: custom-model\n capabilities: [chat]\n enabled: true\n\nruntime:\n",
);
fs::write(&config_path, config).unwrap();
let artifact_dir = root.join("docs/minha-orquestracao");
fs::create_dir_all(&artifact_dir).unwrap();
fs::write(
artifact_dir.join("traceability-map.yaml"),
"orchestration: {}\n",
)
.unwrap();
let custom_skill = root.join(".agents/skills/custom-stack/SKILL.md");
fs::create_dir_all(custom_skill.parent().unwrap()).unwrap();
fs::write(&custom_skill, "# Custom Stack\n").unwrap();
fs::remove_dir_all(root.join(".cursor")).unwrap();
fs::remove_dir_all(root.join(".devin")).unwrap();
fs::remove_dir_all(root.join(".kiro")).unwrap();
fs::remove_dir_all(root.join(".opencode")).unwrap();
fs::remove_dir_all(root.join(".trae")).unwrap();
fs::remove_dir_all(root.join(".agents/skills/orchestration")).unwrap();
fs::remove_dir_all(root.join(".sdd/clients")).unwrap();
fs::remove_file(root.join(".mcp.json")).unwrap();
assert_success(run(root, &["update"]));
assert!(root.join("AGENTS.md").is_file());
let updated_mcp =
serde_json::from_str::<Value>(&fs::read_to_string(root.join(".mcp.json")).unwrap())
.unwrap();
assert!(json_contains_text(&updated_mcp, &root_canonical));
assert!(root.join(".devin/config.json").is_file());
assert!(!root.join(".devin/rules").exists());
assert!(root
.join(".devin/agents/sdd-orchestrator/AGENT.md")
.is_file());
assert!(root.join(".devin/skills/orchestration/SKILL.md").is_file());
assert!(root.join(".devin/skills/workflow/SKILL.md").is_file());
assert!(!root.join(".devin/workflows").exists());
assert_kiro_surface(root);
assert_kiro_catalog_skills(root);
assert!(root.join(".cursor/model-routing.yaml").is_file());
assert!(root.join(".cursor/mcp.json").is_file());
assert!(root.join(".cursor/rules/sdd.mdc").is_file());
assert!(root.join(".cursor/rules/codegraph.mdc").is_file());
assert!(root.join(".cursor/commands/sdd.md").is_file());
assert!(root.join(".cursor/commands/orchestration.md").is_file());
assert!(root.join(".cursor/agents/sdd-orchestrator.md").is_file());
assert!(root.join(".cursor/skills/orchestration/SKILL.md").is_file());
assert!(root.join(".opencode/commands/orchestration.md").is_file());
assert!(root.join(".trae/commands/orchestration.md").is_file());
assert!(root.join(".agents/skills/orchestration/SKILL.md").is_file());
assert!(root.join(".sdd/clients/README.md").is_file());
fs::remove_dir_all(root.join(".cursor")).unwrap();
fs::remove_dir_all(root.join(".devin")).unwrap();
fs::remove_dir_all(root.join(".kiro")).unwrap();
fs::remove_dir_all(root.join(".opencode")).unwrap();
fs::remove_dir_all(root.join(".trae")).unwrap();
fs::remove_dir_all(root.join(".agents/skills/orchestration")).unwrap();
fs::remove_dir_all(root.join(".sdd/clients")).unwrap();
fs::remove_file(root.join(".mcp.json")).unwrap();
assert_success(run(
repo_root(),
&["update", "--root", root.to_str().unwrap()],
));
let preserved_config = fs::read_to_string(&config_path).unwrap();
assert!(preserved_config.contains("custom_marker: keep-me"));
assert!(preserved_config.contains("CUSTOM_PROVIDER_KEY"));
assert!(preserved_config.contains("custom-model"));
assert!(artifact_dir.join("traceability-map.yaml").is_file());
assert!(custom_skill.is_file());
let repo_update_mcp =
serde_json::from_str::<Value>(&fs::read_to_string(root.join(".mcp.json")).unwrap())
.unwrap();
assert!(json_contains_text(&repo_update_mcp, &root_canonical));
assert!(root.join(".devin/config.json").is_file());
assert!(!root.join(".devin/rules").exists());
assert!(root
.join(".devin/agents/sdd-orchestrator/AGENT.md")
.is_file());
assert!(root.join(".devin/skills/orchestration/SKILL.md").is_file());
assert!(root.join(".devin/skills/workflow/SKILL.md").is_file());
assert!(!root.join(".devin/workflows").exists());
assert_kiro_surface(root);
assert_kiro_catalog_skills(root);
assert!(root.join(".cursor/model-routing.yaml").is_file());
assert!(root.join(".cursor/mcp.json").is_file());
assert!(root.join(".cursor/rules/sdd.mdc").is_file());
assert!(root.join(".cursor/rules/codegraph.mdc").is_file());
assert!(root.join(".cursor/commands/sdd.md").is_file());
assert!(root.join(".cursor/commands/orchestration.md").is_file());
assert!(root.join(".cursor/agents/sdd-orchestrator.md").is_file());
assert!(root.join(".cursor/skills/orchestration/SKILL.md").is_file());
assert!(root.join(".opencode/commands/orchestration.md").is_file());
assert!(root.join(".trae/commands/orchestration.md").is_file());
assert!(root.join(".agents/skills/orchestration/SKILL.md").is_file());
assert!(root.join(".sdd/clients/README.md").is_file());
assert_success(run(root, &["clients", "doctor"]));
assert_success(run(
repo_root(),
&[
"update",
"--root",
root.to_str().unwrap(),
"--preset",
"backend-node",
"--update-config",
],
));
let refreshed_config = fs::read_to_string(&config_path).unwrap();
assert!(refreshed_config.contains("name: backend-node-project"));
assert!(!refreshed_config.contains("custom_marker: keep-me"));
assert_success(run(
repo_root(),
&["upgrade", "--root", root.to_str().unwrap(), "--dry-run"],
));
}
#[test]
fn update_syncs_clients_capabilities_and_skills_without_extra_commands() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let drifted_files = [
".claude/skills/orchestration/SKILL.md",
".codex/agents/adr.toml",
".cursor/rules/sdd.mdc",
".agents/skills/orchestration/SKILL.md",
];
for rel in drifted_files {
let path = root.join(rel);
assert!(path.is_file(), "esperava {rel} instalado antes do drift");
fs::write(&path, "CONTEUDO OBSOLETO — drift simulado\n").unwrap();
}
assert_success(run(
repo_root(),
&["update", "--root", root.to_str().unwrap()],
));
for rel in drifted_files {
let content = fs::read_to_string(root.join(rel)).unwrap();
assert!(
!content.contains("CONTEUDO OBSOLETO"),
"{rel} não foi resincronizado por `sdd update`"
);
}
}
#[test]
fn update_report_confirms_clients_capabilities_and_skills_sync_status() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let output = run(repo_root(), &["update", "--root", root.to_str().unwrap()]);
let text = combined(&output);
assert_success(output);
assert!(
text.contains("Clients: sincronizados") && text.contains("doctor pass"),
"output de `sdd update` deve confirmar status de sincronização de clients: {text}"
);
assert!(
text.contains("Capabilities: sincronizadas") && text.contains("doctor "),
"output de `sdd update` deve confirmar status de sincronização de capabilities: {text}"
);
assert!(
text.contains("Skills: sincronizadas") && text.contains("doctor pass"),
"output de `sdd update` deve confirmar status de sincronização de skills: {text}"
);
let dry_output = run(
repo_root(),
&["update", "--root", root.to_str().unwrap(), "--dry-run"],
);
let dry_text = combined(&dry_output);
assert_success(dry_output);
assert!(
dry_text.to_lowercase().contains("dry"),
"modo dry-run deve deixar claro que a sincronização não foi aplicada: {dry_text}"
);
}
#[test]
fn install_and_update_merge_existing_agent_docs() {
let target = tempdir().unwrap();
let root = target.path();
fs::write(
root.join("AGENTS.md"),
"# Projeto\n\nRegras locais do Codex.\n",
)
.unwrap();
fs::write(
root.join("CLAUDE.md"),
"# Projeto Claude\n\nRegras locais do Claude.\n",
)
.unwrap();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let agents = fs::read_to_string(root.join("AGENTS.md")).unwrap();
assert!(agents.contains("# Projeto"));
assert!(agents.contains("Regras locais do Codex."));
assert!(agents.contains("<!-- sdd-layer:start -->"));
assert!(agents.contains("# AGENTS.md — Camada de orquestração SDLC (Spec-Driven)"));
assert_eq!(agents.matches("<!-- sdd-layer:start -->").count(), 1);
let claude = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
assert!(claude.contains("# Projeto Claude"));
assert!(claude.contains("Regras locais do Claude."));
assert!(claude.contains("<!-- sdd-layer:start -->"));
assert!(claude.contains("# CLAUDE.md — Camada de orquestração SDLC (Spec-Driven)"));
assert_eq!(claude.matches("<!-- sdd-layer:start -->").count(), 1);
fs::write(
root.join("AGENTS.md"),
format!("{agents}\n## Regras após SDD\n\nContinue preservando esta seção.\n"),
)
.unwrap();
assert_success(run(root, &["update"]));
let agents_after_update = fs::read_to_string(root.join("AGENTS.md")).unwrap();
assert!(agents_after_update.contains("# Projeto"));
assert!(agents_after_update.contains("Regras locais do Codex."));
assert!(agents_after_update.contains("## Regras após SDD"));
assert!(agents_after_update.contains(
"<!-- sdd-layer:end -->\n\n## Regras após SDD\n\nContinue preservando esta seção."
));
assert_eq!(
agents_after_update
.matches("<!-- sdd-layer:start -->")
.count(),
1
);
let claude_after_update = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
assert!(claude_after_update.contains("# Projeto Claude"));
assert!(claude_after_update.contains("Regras locais do Claude."));
assert_eq!(
claude_after_update
.matches("<!-- sdd-layer:start -->")
.count(),
1
);
}
#[test]
fn update_ignores_parent_project_installation_as_package_source() {
let target = tempdir().unwrap();
let parent = target.path();
let root = parent.join("apps/child");
fs::create_dir_all(&root).unwrap();
fs::write(parent.join("AGENTS.md"), "# Parent SDD\n").unwrap();
fs::write(parent.join("CLAUDE.md"), "# Parent SDD\n").unwrap();
for rel in [".agents", ".claude", ".codex", ".cursor"] {
fs::create_dir_all(parent.join(rel)).unwrap();
}
fs::create_dir_all(parent.join(".sdd/presets")).unwrap();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
fs::remove_dir_all(root.join(".devin")).unwrap();
assert_success(run(&root, &["update"]));
assert!(root.join(".devin/config.json").is_file());
assert!(!root.join(".devin/rules").exists());
assert!(root.join(".devin/skills/sdd/SKILL.md").is_file());
}
#[test]
fn demand_enqueue_tick_and_human_approval_flow() {
let t = tempdir().unwrap();
let root = t.path();
fs::write(root.join("sdd.config.yaml"), "approvers:\n cli: [alan]\n").unwrap();
let r = root.to_str().unwrap();
let out = run(
root,
&[
"demand",
"--root",
r,
"enqueue",
"--id",
"DEM-1",
"--type",
"story",
"--title",
"Teste Esteira",
],
);
assert!(combined(&out).contains("enfileirada"), "{}", combined(&out));
assert_success(out);
assert_success(run(root, &["auto", "--root", r, "tick"]));
let out = run(root, &["auto", "--root", r, "tick"]);
let txt = combined(&out);
assert!(txt.contains("paused") && txt.contains("prd"), "{txt}");
assert_success(out);
let out = run(root, &["demand", "--root", r, "status", "DEM-1"]);
assert!(
combined(&out).contains("awaiting_approval"),
"{}",
combined(&out)
);
assert_success(out);
assert!(root.join("docs/teste-esteira/01-idea.md").is_file());
assert!(root.join("docs/teste-esteira/02-prd.md").is_file());
let out = run(
root,
&[
"demand", "--root", r, "approve", "DEM-1", "--gate", "prd", "--by", "mallory",
],
);
assert!(
combined(&out).contains("não autorizado"),
"{}",
combined(&out)
);
assert_code(out, 1);
let out = run(
root,
&[
"demand", "--root", r, "approve", "DEM-1", "--gate", "prd", "--by", "alan",
],
);
assert!(combined(&out).contains("aprovado"), "{}", combined(&out));
assert_success(out);
let out = run(root, &["demand", "--root", r, "status", "DEM-1"]);
assert!(combined(&out).contains("techspec"), "{}", combined(&out));
assert_success(out);
}
#[test]
fn demand_approval_can_use_slack_approver_channel() {
let t = tempdir().unwrap();
let root = t.path();
fs::write(
root.join("sdd.config.yaml"),
"approvers:\n cli: [alan]\n slack: [U123]\n",
)
.unwrap();
let r = root.to_str().unwrap();
assert_success(run(
root,
&[
"demand",
"--root",
r,
"enqueue",
"--id",
"DEM-SLACK",
"--type",
"story",
"--title",
"Aprovação Slack",
"--source",
"webhook",
],
));
assert_success(run(root, &["auto", "--root", r, "run"]));
let denied = run(
root,
&[
"demand",
"--root",
r,
"approve",
"DEM-SLACK",
"--gate",
"prd",
"--channel",
"slack",
"--by",
"alan",
],
);
assert!(
combined(&denied).contains("não autorizado"),
"{}",
combined(&denied)
);
assert_code(denied, 1);
let approved = run(
root,
&[
"demand",
"--root",
r,
"approve",
"DEM-SLACK",
"--gate",
"prd",
"--channel",
"slack",
"--by",
"U123",
],
);
assert_success(approved);
let status = run(
root,
&["demand", "--root", r, "status", "DEM-SLACK", "--json"],
);
let text = combined(&status);
assert_success(status);
let json = serde_json::from_str::<Value>(&text).unwrap();
assert_eq!(json["state"]["approvals"][0]["channel"], "slack");
assert_eq!(json["state"]["approvals"][0]["author"], "U123");
}
#[test]
fn auto_run_drives_to_gate_with_audit() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
assert_success(run(
root,
&[
"demand",
"--root",
r,
"enqueue",
"--id",
"DEM-1",
"--type",
"story",
"--title",
"Esteira Run",
],
));
let run_out = run(root, &["auto", "--root", r, "run"]);
let run_txt = combined(&run_out);
assert!(run_txt.contains("aguardando aprovação"), "{run_txt}");
assert!(run_txt.contains("sdd demand approve"), "{run_txt}");
assert_success(run_out);
let out = run(root, &["demand", "--root", r, "status", "DEM-1"]);
assert!(
combined(&out).contains("awaiting_approval"),
"{}",
combined(&out)
);
assert_success(out);
let audit = fs::read_to_string(root.join(".sdd/orchestrator.jsonl")).unwrap();
assert!(audit.contains("\"event\":\"tick\""), "{audit}");
assert!(audit.contains("\"event\":\"gate_request\""), "{audit}");
}
#[test]
fn ca_guard_cli_execution_blocks_missing_planning_html() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let output = run(
root,
&[
"execution",
"--root",
r,
"--name",
"Fluxo Planejado",
"T-01",
],
);
let text = combined(&output);
assert!(
!output.status.success(),
"execution deveria bloquear sem 05-planning.html\n{text}"
);
assert!(text.contains("Execution bloqueada"), "{text}");
assert!(text.contains("05-planning.html"), "{text}");
assert!(
!root.join("docs/fluxo-planejado/06-execution.md").exists(),
"não deve criar 06-execution.md quando o guard falha"
);
}
#[test]
fn ca_guard_cli_execution_also_blocks_missing_planning_html_in_bot_context() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let output = run_with_env_values(
root,
&[
"execution",
"--root",
r,
"--name",
"Fluxo Bot Planejado",
"T-01",
],
None,
&[("SDD_BOT_CONTEXT", "true")],
);
let text = combined(&output);
assert!(!output.status.success(), "{text}");
assert!(text.contains("Execution bloqueada"), "{text}");
assert!(text.contains("05-planning.html"), "{text}");
assert!(!root
.join("docs/fluxo-bot-planejado/06-execution.md")
.exists());
}
#[test]
fn auto_tick_real_uses_headless_runner() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
fs::write(
root.join("sdd.config.yaml"),
"providers:\n default: custom\n offline: true\napprovers:\n cli: [alan]\n",
)
.unwrap();
assert_success(run(
root,
&[
"demand",
"--root",
r,
"enqueue",
"--id",
"DEM-1",
"--type",
"story",
"--title",
"Busca Avancada",
],
));
let out = run(root, &["auto", "--root", r, "tick", "--real"]);
assert!(
combined(&out).contains("busca-avancada"),
"{}",
combined(&out)
);
assert_success(out);
let idea = fs::read_to_string(root.join("docs/busca-avancada/01-idea.md")).unwrap();
assert!(idea.contains("## Rastreabilidade"), "{idea}");
assert!(!idea.contains("FakeRunner"), "{idea}");
}
#[test]
fn eval_stage_and_quality_report_persist_evidence() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let fixture = repo_root().join("tests/fixtures/good/prd-diagrams.md");
assert_success(run(root, &["artifact", "--root", r, "init", "Eval OK"]));
assert_success(run(
root,
&[
"artifact",
"--root",
r,
"save",
"Eval OK",
"prd",
"--file",
fixture.to_str().unwrap(),
"--state",
"recorded",
],
));
let out = run(
root,
&[
"eval", "--root", r, "stage", "--name", "Eval OK", "--stage", "prd", "--json",
],
);
assert_success(out);
let event_log = fs::read_to_string(root.join(".sdd/evaluations.jsonl")).unwrap();
assert!(event_log.contains("eval_stage"), "{event_log}");
let quality = run(
root,
&[
"quality", "--root", r, "report", "--name", "Eval OK", "--json",
],
);
let text = combined(&quality);
assert_success(quality);
let parsed: Value = serde_json::from_str(&text).unwrap();
assert_eq!(parsed["status"], "pass");
assert_eq!(parsed["evaluation"]["critical_count"], 0);
let event_log = fs::read_to_string(root.join(".sdd/evaluations.jsonl")).unwrap();
assert!(event_log.contains("quality_report"), "{event_log}");
}
#[test]
fn quality_report_v2_resolves_typed_quality_config_without_breaking_legacy_fields() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let fixture = repo_root().join("tests/fixtures/good/prd-diagrams.md");
fs::write(
root.join("sdd.config.yaml"),
"quality:\n profile: critical\n mode: shadow\n",
)
.unwrap();
assert_success(run(root, &["artifact", "--root", r, "init", "Quality V2"]));
assert_success(run(
root,
&[
"artifact",
"--root",
r,
"save",
"Quality V2",
"prd",
"--file",
fixture.to_str().unwrap(),
"--state",
"recorded",
],
));
fs::write(
root.join("sdd.config.yaml"),
"quality:\n profile: critical\n mode: enforce\n",
)
.unwrap();
let output = run(
root,
&[
"quality",
"--root",
r,
"report",
"--name",
"Quality V2",
"--json",
],
);
let text = combined(&output);
assert_success(output);
let report: Value = serde_json::from_str(&text).unwrap();
assert_eq!(report["schema_version"], 2);
assert_eq!(report["profile"], "critical");
assert_eq!(report["mode"], "enforce");
assert!(report.get("status").is_some());
assert!(report.get("evaluation").is_some());
assert!(report.get("tools").is_some());
assert!(report.get("recommendations").is_some());
assert!(report.get("scores").is_some());
assert!(report.get("decision").is_some());
}
#[test]
fn quality_rubric_blocks_placeholder_in_canonical_contract() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let fixture = repo_root().join("tests/fixtures/good/prd-diagrams.md");
let candidate = root.join("prd-with-placeholder.md");
let text = fs::read_to_string(fixture).unwrap();
fs::write(
&candidate,
text.replace("Documento de exemplo.", "TODO: preencher"),
)
.unwrap();
assert_success(run(
root,
&["artifact", "--root", r, "init", "Quality Rubric"],
));
assert_success(run(
root,
&[
"artifact",
"--root",
r,
"save",
"Quality Rubric",
"prd",
"--file",
candidate.to_str().unwrap(),
"--state",
"recorded",
],
));
let output = run(
root,
&[
"eval",
"--root",
r,
"stage",
"--name",
"Quality Rubric",
"--stage",
"prd",
"--profile",
"fast",
"--json",
],
);
let text = combined(&output);
assert!(output.status.success(), "{text}");
let report = serde_json::Deserializer::from_str(&text)
.into_iter::<Value>()
.next()
.unwrap()
.unwrap();
assert_eq!(report["decision"], "allow");
assert_eq!(report["profile"], "fast");
assert!(json_contains_text(&report, "placeholders"), "{report}");
}
#[test]
fn enforce_quality_keeps_rejected_candidate_out_of_canonical_artifact() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let fixture = repo_root().join("tests/fixtures/good/prd-diagrams.md");
let rejected = root.join("rejected.md");
fs::write(
root.join("sdd.config.yaml"),
"quality:\n mode: enforce\n profile: fast\n",
)
.unwrap();
fs::write(
&rejected,
fs::read_to_string(&fixture)
.unwrap()
.replace("Documento de exemplo.", "TODO: preencher"),
)
.unwrap();
assert_success(run(
root,
&["artifact", "--root", r, "init", "Candidate Gate"],
));
assert_success(run(
root,
&[
"artifact",
"--root",
r,
"save",
"Candidate Gate",
"prd",
"--file",
fixture.to_str().unwrap(),
"--state",
"recorded",
],
));
let output = run(
root,
&[
"artifact",
"--root",
r,
"save",
"Candidate Gate",
"prd",
"--file",
rejected.to_str().unwrap(),
"--state",
"recorded",
"--force",
],
);
assert!(!output.status.success(), "{}", combined(&output));
let canonical = fs::read_to_string(root.join("docs/candidate-gate/02-prd.md")).unwrap();
assert!(canonical.contains("Documento de exemplo."), "{canonical}");
assert!(!canonical.contains("TODO: preencher"), "{canonical}");
let candidates = root.join(".sdd/quality/candidates/candidate-gate/prd");
assert!(candidates.exists(), "candidate evidence must be retained");
}
#[test]
fn quality_override_requires_actor_reason_and_evidence_and_is_audited() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let rejected = root.join("rejected.md");
fs::write(
root.join("sdd.config.yaml"),
"quality:\n mode: enforce\n profile: fast\n",
)
.unwrap();
fs::write(&rejected, "# PRD\n\nTODO: preencher").unwrap();
assert_success(run(
root,
&["artifact", "--root", r, "init", "Override Gate"],
));
let missing_fields = run(
root,
&[
"artifact",
"--root",
r,
"save",
"Override Gate",
"prd",
"--file",
rejected.to_str().unwrap(),
"--state",
"recorded",
"--override-author",
"allan",
],
);
assert!(!missing_fields.status.success());
assert!(combined(&missing_fields).contains("override exige"));
assert_success(run(
root,
&[
"artifact",
"--root",
r,
"save",
"Override Gate",
"prd",
"--file",
rejected.to_str().unwrap(),
"--state",
"recorded",
"--override-author",
"allan",
"--override-reason",
"aceite humano documentado",
"--override-evidence",
"ticket QA-42",
],
));
let canonical = fs::read_to_string(root.join("docs/override-gate/02-prd.md")).unwrap();
assert!(canonical.contains("TODO: preencher"));
let events = fs::read_to_string(root.join(".sdd/evaluations.jsonl")).unwrap();
assert!(events.contains("quality_override"), "{events}");
}
#[test]
fn quality_benchmark_reports_precision_recall_and_critical_false_negatives() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let fixture = repo_root().join("tests/fixtures/good/prd-diagrams.md");
let good = fs::read_to_string(fixture).unwrap();
let suite = root.join("quality-suite.json");
fs::write(
&suite,
serde_json::to_string(&serde_json::json!([
{
"id": "prd-good",
"stage": "prd",
"artifact": good,
"expected_decision": "allow",
"critical": false
},
{
"id": "prd-placeholder",
"stage": "prd",
"artifact": "# PRD\n\nTODO: preencher",
"expected_decision": "block",
"critical": true
}
]))
.unwrap(),
)
.unwrap();
let output = run(
root,
&[
"quality",
"--root",
r,
"benchmark",
"--suite",
suite.to_str().unwrap(),
"--json",
],
);
let text = combined(&output);
assert_success(output);
let report: Value = serde_json::from_str(&text).unwrap();
assert_eq!(report["total"], 2);
assert_eq!(report["critical_false_negatives"], 0);
assert_eq!(report["recall"], 1.0);
}
#[test]
fn eval_stage_accepts_visual_companion_diagram() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
let fixture = repo_root().join("tests/fixtures/good/prd-visual-companion.md");
assert_success(run(root, &["artifact", "--root", r, "init", "Eval Visual"]));
assert_success(run(
root,
&[
"artifact",
"--root",
r,
"save",
"Eval Visual",
"prd",
"--file",
fixture.to_str().unwrap(),
"--state",
"recorded",
],
));
assert_success(run(
root,
&[
"eval",
"--root",
r,
"stage",
"--name",
"Eval Visual",
"--stage",
"prd",
"--json",
],
));
}
#[test]
fn auto_run_through_review_materializes_post_planning_after_gates() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
fs::write(root.join("sdd.config.yaml"), "approvers:\n cli: [alan]\n").unwrap();
assert_success(run(
root,
&[
"demand",
"--root",
r,
"enqueue",
"--id",
"DEM-2",
"--type",
"story",
"--title",
"Fluxo Completo",
],
));
assert_success(run(root, &["auto", "--root", r, "run"]));
assert_success(run(
root,
&[
"demand", "--root", r, "approve", "DEM-2", "--gate", "prd", "--by", "alan",
],
));
assert_success(run(root, &["auto", "--root", r, "run"]));
assert_success(run(
root,
&[
"demand", "--root", r, "approve", "DEM-2", "--gate", "techspec", "--by", "alan",
],
));
assert_success(run(root, &["auto", "--root", r, "run"]));
assert_success(run(
root,
&[
"demand",
"--root",
r,
"approve",
"DEM-2",
"--gate",
"refinement",
"--by",
"alan",
],
));
let out = run(
root,
&["auto", "--root", r, "run", "--through", "review", "--json"],
);
let text = combined(&out);
assert_success(out);
let parsed: Value = serde_json::from_str(&text).unwrap();
assert!(parsed["post_planning"]["produced"]
.as_array()
.unwrap()
.iter()
.any(|item| item[1] == "review"));
assert!(root.join("docs/fluxo-completo/06-execution.md").is_file());
assert!(root.join("docs/fluxo-completo/07-review.md").is_file());
let status = run(root, &["demand", "--root", r, "status", "DEM-2"]);
let status_text = combined(&status);
assert_success(status);
assert!(status_text.contains("paused"), "{status_text}");
}
#[test]
fn auto_run_unattended_reaches_memory_without_human_approvers() {
let t = tempdir().unwrap();
let root = t.path();
let r = root.to_str().unwrap();
assert_success(run(
root,
&[
"demand",
"--root",
r,
"enqueue",
"--id",
"DEM-AUTO",
"--type",
"story",
"--title",
"Fluxo Sem Humano",
],
));
let out = run(
root,
&["auto", "--root", r, "run", "--unattended", "--json"],
);
let text = combined(&out);
assert_success(out);
let parsed: Value = serde_json::from_str(&text).unwrap();
assert_eq!(parsed["unattended"], true);
assert_eq!(parsed["paused"].as_array().unwrap().len(), 0);
assert!(
parsed["auto_accepted"].as_array().unwrap().len() >= 3,
"{text}"
);
for file in [
"01-idea.md",
"02-prd.md",
"03-techspec.md",
"04-tasks.md",
"05-refinement.md",
"06-execution.md",
"07-review.md",
"08-qa.md",
"09-memory.md",
] {
assert!(
root.join("docs/fluxo-sem-humano").join(file).is_file(),
"faltou {file}"
);
}
let state_text = fs::read_to_string(root.join(".sdd/state/fluxo-sem-humano.json")).unwrap();
let state: Value = serde_json::from_str(&state_text).unwrap();
assert_eq!(state["status"], "completed");
let approvals = state["approvals"].as_array().unwrap();
assert_eq!(approvals.len(), 3, "{state_text}");
assert!(approvals
.iter()
.all(|approval| approval["channel"] == "automation"));
assert!(approvals
.iter()
.all(|approval| approval["author"] == "sdd-auto"));
let audit = fs::read_to_string(root.join(".sdd/orchestrator.jsonl")).unwrap();
assert!(audit.contains("\"event\":\"gate_auto_accept\""), "{audit}");
}
#[test]
fn providers_memory_and_stage_flags_are_offline_and_redacted() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let idea_agent = root.join(".codex/agents/idea.toml");
let idea_agent_text = fs::read_to_string(&idea_agent).unwrap();
fs::write(
&idea_agent,
idea_agent_text.replace("model = \"gpt-5.4\"", "model = \"gpt-5.5\""),
)
.unwrap();
let prd_agent = root.join(".codex/agents/prd.toml");
let prd_agent_text = fs::read_to_string(&prd_agent).unwrap();
fs::write(
&prd_agent,
prd_agent_text.replace(
"model = \"gpt-5.4\"",
"model = \"not-a-real-provider-model\"",
),
)
.unwrap();
let providers = combined(&run(root, &["providers", "list", "--json"]));
assert!(providers.contains("\"id\": \"codex\""));
assert!(providers.contains("\"id\": \"antigravity\""));
assert!(!providers.contains("\"id\": \"gemini\""));
assert!(providers.contains("\"id\": \"opencode\""));
assert!(providers.contains("\"id\": \"cursor\""));
assert!(providers.contains("\"id\": \"devin\""));
assert!(providers.contains("\"id\": \"trae\""));
assert!(providers.contains("\"id\": \"rig\""));
assert!(providers.contains("\"kind\": \"rig-adapter\""));
assert!(providers.contains("\"auth_methods\""));
assert!(providers.contains("\"models\""));
assert!(providers.contains("\"efforts\""));
assert!(providers.contains("\"stage_models\""));
assert!(providers.contains("\"usage_limits\""));
assert!(providers.contains("gpt-5.5"));
assert!(providers.contains("\"idea\": \"gpt-5.5\""));
assert!(!providers.contains("not-a-real-provider-model"));
assert!(providers.contains("gemini-3.5-flash"));
assert!(providers.contains("gemini-3.1-pro"));
assert!(providers.contains("claude-opus-4.6"));
assert!(providers.contains("composer-2.5-fast"));
assert!(providers.contains("composer-2.5"));
assert!(providers.contains("swe-1-6-fast"));
assert!(providers.contains("\"sonnet\""));
assert!(providers.contains("\"opus\""));
assert!(providers.contains("trae_agent_model"));
assert!(providers.contains("opencode-go/deepseek-v4-flash"));
assert!(providers.contains("opencode-go/deepseek-v4-pro"));
assert!(providers.contains("opencode-go/kimi-k2.7"));
assert!(providers.contains("opencode-go/qwen3.7-plus"));
assert!(providers.contains("openai/gpt-5.5"));
assert!(providers.contains("anthropic/claude-opus-4-8"));
assert!(providers.contains("provider-routing"));
let doctor = combined(&run_with_env_values(
root,
&["providers", "doctor", "--json"],
None,
&[("OPENAI_API_KEY", "sk-test-secret")],
));
assert!(doctor.contains("\"id\": \"codex\""));
assert!(doctor.contains("Codex CLI"));
assert!(doctor.contains("OpenAI API"));
assert!(doctor.contains("Claude Code"));
assert!(doctor.contains("Antigravity CLI"));
assert!(doctor.contains("agy models"));
assert!(doctor.contains("Google AI API"));
assert!(!doctor.contains("Gemini CLI"));
assert!(doctor.contains("Cursor CLI"));
assert!(doctor.contains("Devin CLI"));
assert!(doctor.contains("devin auth status"));
assert!(doctor.contains("Trae Agent CLI"));
assert!(doctor.contains("trae-cli show-config"));
assert!(doctor.contains("opencode CLI"));
assert!(doctor.contains("opencode Go/Zen credential"));
assert!(doctor.contains("\"usage_limits\""));
assert!(doctor.contains("\"present\": true"));
assert!(!doctor.contains("sk-test-secret"));
let dry_run = combined(&run(
root,
&[
"orchestration",
"--provider",
"claude",
"--model",
"claude-sonnet-4-6",
"--effort",
"high",
"--dry-run",
"x",
],
));
assert!(dry_run.contains("Provider: claude"));
assert!(dry_run.contains("Model: claude-sonnet-4-6"));
assert!(dry_run.contains("Effort: high"));
assert!(dry_run.contains("Offline: true"));
assert_success(run(
root,
&[
"prd",
"--provider",
"custom",
"--model",
"local-test",
"--effort",
"medium",
"--name",
"Ciclo Provider",
"--force",
"entrada segura",
],
));
let prd = fs::read_to_string(root.join("docs/ciclo-provider/02-prd.md")).unwrap();
assert!(prd.contains("- Provider: custom"));
assert!(prd.contains("- Modelo: local-test"));
assert!(prd.contains("- Effort: medium"));
assert!(!prd.contains("sk-test-secret"));
assert_success(run(root, &["memory", "learn", "--name", "Ciclo Provider"]));
let index = fs::read_to_string(root.join(".sdd/memory/learnings.jsonl")).unwrap();
assert!(index.contains("\"source_artifact\""));
assert!(index.contains("\"source_hash\""));
assert!(index.contains("docs/ciclo-provider/02-prd.md"));
let status = combined(&run(root, &["memory", "status", "--json"]));
assert!(status.contains("\"learnings\": 1"));
assert!(status.contains("\"sources_indexed\": 1"));
assert_success(run(root, &["doctor"]));
}
#[test]
fn opencode_traceability_uses_stage_routing_and_explicit_overrides() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
assert_success(run(
root,
&[
"discover",
"--provider",
"opencode",
"--name",
"OpenCode Kimi",
"--force",
"inventario",
],
));
let discovery =
fs::read_to_string(root.join("docs/opencode-kimi/00-project-discovery.md")).unwrap();
assert!(discovery.contains("- Provider: opencode"));
assert!(discovery.contains("- Modelo: opencode-go/kimi-k2.7"));
assert!(discovery.contains("- Effort: xhigh"));
assert!(!discovery.contains("- Tokens: input ~"));
assert!(!discovery.contains("- Tempo de geração: "));
let discovery_html =
fs::read_to_string(root.join("docs/opencode-kimi/00-project-discovery.html")).unwrap();
assert!(discovery_html.contains("<td>Provider</td><td><code>opencode</code></td>"));
assert!(discovery_html.contains("<td>Modelo</td><td><code>opencode-go/kimi-k2.7</code></td>"));
assert!(!discovery_html.contains("Tokens source: <code>estimated</code>"));
assert!(!discovery_html.contains("Tempo source: <code>local</code>"));
assert_success(run(
root,
&[
"prd",
"--provider",
"opencode",
"--model",
"opencode-go/qwen3.7-max",
"--effort",
"medium",
"--name",
"OpenCode Kimi",
"--force",
"ctx",
],
));
let prd = fs::read_to_string(root.join("docs/opencode-kimi/02-prd.md")).unwrap();
assert!(prd.contains("- Provider: opencode"));
assert!(prd.contains("- Modelo: opencode-go/qwen3.7-max"));
assert!(prd.contains("- Effort: medium"));
assert!(!prd.contains("- Tokens: input ~"));
assert!(!prd.contains("- Tempo de geração: "));
assert!(!prd.contains("- Modelo: opencode-go/deepseek-v4-pro"));
}
#[test]
fn traceability_records_provider_reported_usage_and_timing_from_env() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
assert_success(run_with_env_values(
root,
&[
"idea",
"--provider",
"opencode",
"--name",
"Usage Reported",
"--force",
"ctx",
],
None,
&[
("SDD_USAGE_INPUT_TOKENS", "1000"),
("SDD_USAGE_CACHED_INPUT_TOKENS", "400"),
("SDD_USAGE_OUTPUT_TOKENS", "120"),
("SDD_USAGE_REASONING_OUTPUT_TOKENS", "30"),
("SDD_USAGE_TOTAL_TOKENS", "1120"),
("SDD_GENERATION_STARTED_AT", "2026-06-30T10:00:00Z"),
(
"SDD_GENERATION_EXECUTION_STARTED_AT",
"2026-06-30T10:00:03Z",
),
("SDD_GENERATION_FINISHED_AT", "2026-06-30T10:00:05Z"),
("SDD_GENERATION_DURATION_MS", "5000"),
("SDD_GENERATION_REASONING_DURATION_MS", "3000"),
("SDD_GENERATION_EXECUTION_DURATION_MS", "2000"),
],
));
let idea = fs::read_to_string(root.join("docs/usage-reported/01-idea.md")).unwrap();
assert!(idea
.contains("- Tokens: input ~1000 (cache ~400), output ~120 (reasoning ~30), total ~1120"));
assert!(idea.contains("- Tokens source: provider-reported"));
assert!(idea
.contains("- Tempo de geração: 2026-06-30T10:00:00Z -> 2026-06-30T10:00:05Z (~5000 ms)"));
assert!(idea.contains("- Tempo source: provider-reported"));
assert!(idea.contains("- Execução iniciada: 2026-06-30T10:00:03Z"));
assert!(idea.contains("- Tempo fases: reasoning ~3000 ms, execution ~2000 ms"));
}
#[test]
fn artifact_save_injects_provider_reported_telemetry_from_env() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let input = root.join("external.md");
fs::write(
&input,
"# Idea\n\n## Rastreabilidade\n- Origem: editor externo\n\n## Resumo\n- Conteúdo gerado fora do CLI.\n",
)
.unwrap();
assert_success(run_with_env_values(
root,
&[
"artifact",
"save",
"External Timing",
"idea",
"--file",
input.to_str().unwrap(),
"--state",
"recorded",
"--force",
],
None,
&[
("SDD_USAGE_INPUT_TOKENS", "10"),
("SDD_USAGE_OUTPUT_TOKENS", "20"),
("SDD_USAGE_TOTAL_TOKENS", "30"),
("SDD_GENERATION_STARTED_AT", "2026-06-30T11:00:00Z"),
("SDD_GENERATION_FINISHED_AT", "2026-06-30T11:00:02Z"),
("SDD_GENERATION_DURATION_MS", "2000"),
],
));
let saved = fs::read_to_string(root.join("docs/external-timing/01-idea.md")).unwrap();
assert!(saved.contains("- Tokens: input ~10, output ~20, total ~30"));
assert!(saved.contains("- Tokens source: provider-reported"));
assert!(saved
.contains("- Tempo de geração: 2026-06-30T11:00:00Z -> 2026-06-30T11:00:02Z (~2000 ms)"));
assert!(saved.contains("- Tempo source: provider-reported"));
assert!(saved.contains("- Origem: editor externo"));
}
#[test]
fn intelligence_help_lists_learn_status_and_health() {
let tmp = tempdir().unwrap();
let root = tmp.path();
let help_output = run(root, &["intelligence", "--help"]);
let help = combined(&help_output);
assert_success(help_output);
assert!(help.contains("learn"));
assert!(help.contains("status"));
assert!(help.contains("health"));
assert!(help.contains("repair"));
}
#[test]
fn context_build_write_creates_techspec_pack_with_manifesto() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(
root,
&[
"context", "build", "--name", name, "--stage", "techspec", "--write",
],
));
let context_pack_path =
root.join(".sdd/intelligence/context-packs/project-intelligence-layer/techspec.md");
let context_pack = fs::read_to_string(&context_pack_path).unwrap();
assert!(context_pack.contains("Rastreabilidade"));
assert!(context_pack.contains("Contexto essencial"));
assert!(context_pack.contains("Manifesto de contexto"));
}
#[test]
fn context_materialize_default_keeps_root_only() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("src/domain")).unwrap();
fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
let output = run(root, &["context", "materialize", "--name", name, "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
assert!(json_contains_text(&json, "src/domain"));
assert!(json_contains_text(&json, "root-agents"));
assert_eq!(json["agent_context_policy"], "root_only");
assert_eq!(json["file_budget"]["planned_local_agents"], 0);
assert!(json["context_profiles"]
.as_array()
.unwrap()
.iter()
.any(|profile| {
profile["path"].as_str() == Some("src/domain")
&& profile["materialized"].as_bool() == Some(false)
}));
assert!(!json["actions"]
.as_array()
.unwrap()
.iter()
.any(|action| { action["target"].as_str() == Some("src/domain/AGENTS.md") }));
assert!(!root
.join("docs/project-intelligence-layer/00-context-recommendations.md")
.exists());
assert!(!root.join("src/domain/AGENTS.md").exists());
}
#[test]
fn context_materialize_write_updates_managed_blocks_idempotently() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("src/domain")).unwrap();
fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
let agents = fs::read_to_string(root.join("AGENTS.md")).unwrap();
fs::write(
root.join("AGENTS.md"),
format!("{agents}\n## Manual local\n\nPreservar esta seção.\n"),
)
.unwrap();
assert_success(run(
root,
&["context", "materialize", "--name", name, "--write"],
));
assert!(root
.join("docs/project-intelligence-layer/00-context-recommendations.md")
.is_file());
let agents_after = fs::read_to_string(root.join("AGENTS.md")).unwrap();
assert!(agents_after.contains("## Manual local"));
assert!(agents_after.contains("Preservar esta seção."));
assert_eq!(
agents_after
.matches("<!-- sdd-context:start id=\"root-agents\" -->")
.count(),
1
);
let claude = fs::read_to_string(root.join("CLAUDE.md")).unwrap();
assert_eq!(
claude
.matches("<!-- sdd-context:start id=\"root-claude\" -->")
.count(),
1
);
assert!(!root.join("src/domain/AGENTS.md").exists());
assert_success(run(
root,
&["context", "materialize", "--name", name, "--write"],
));
let agents_second = fs::read_to_string(root.join("AGENTS.md")).unwrap();
assert_eq!(
agents_second
.matches("<!-- sdd-context:start id=\"root-agents\" -->")
.count(),
1
);
assert!(!root.join("src/domain/AGENTS.md").exists());
}
#[test]
fn context_materialize_local_agents_opt_in_is_capped() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("src/domain")).unwrap();
fs::create_dir_all(root.join("src/runtime")).unwrap();
fs::create_dir_all(root.join("src/tui")).unwrap();
fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
fs::write(root.join("src/runtime/mod.rs"), "pub mod mcp;\n").unwrap();
fs::write(root.join("src/tui/mod.rs"), "pub mod app;\n").unwrap();
let output = run(
root,
&[
"context",
"materialize",
"--name",
name,
"--local-agents",
"--max-boundaries",
"2",
"--write",
"--json",
],
);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
assert_eq!(json["agent_context_policy"], "scoped_opt_in");
assert_eq!(json["file_budget"]["max_boundaries"], 2);
let planned = json["local_agent_boundaries"].as_array().unwrap();
assert!(planned.len() <= 2);
assert!(planned.iter().all(|profile| {
profile["confidence"].as_str() == Some("alta") && profile["path"].as_str() != Some("src")
}));
let created = planned
.iter()
.filter_map(|profile| profile["path"].as_str())
.filter(|path| root.join(path).join("AGENTS.md").is_file())
.count();
assert_eq!(created, planned.len());
}
#[test]
fn context_materialize_excludes_client_dirs() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join(".opencode")).unwrap();
fs::write(root.join(".opencode/package.json"), "{}").unwrap();
fs::create_dir_all(root.join(".cursor")).unwrap();
fs::write(root.join(".cursor/package.json"), "{}").unwrap();
let output = run(
root,
&[
"context",
"materialize",
"--name",
name,
"--local-agents",
"--json",
],
);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
assert!(!json_contains_text(&json, ".opencode/AGENTS.md"));
assert!(!json_contains_text(&json, ".cursor/AGENTS.md"));
assert!(!json["context_profiles"]
.as_array()
.unwrap()
.iter()
.any(|profile| {
profile["path"]
.as_str()
.is_some_and(|path| path.starts_with(".opencode") || path.starts_with(".cursor"))
}));
}
#[test]
fn context_materialize_existing_config_local_agents_still_works() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("src/domain")).unwrap();
fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
let config_path = root.join("sdd.config.yaml");
let config = fs::read_to_string(&config_path).unwrap();
let enabled = config.replacen("local_agents: false", "local_agents: true", 1);
assert_ne!(
config, enabled,
"config fixture should include local_agents"
);
fs::write(&config_path, enabled).unwrap();
let output = run(
root,
&[
"context",
"materialize",
"--name",
name,
"--write",
"--json",
],
);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
assert_eq!(json["agent_context_policy"], "scoped_config");
assert!(root.join("src/domain/AGENTS.md").is_file());
}
#[test]
fn mcp_doctor_json_reports_agent_contexts() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(root, &["mcp", "doctor", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("mcp doctor output should be json");
assert!(json.get("agent_contexts").is_some());
assert_eq!(json["agent_contexts"]["policy"], "root_only");
assert_eq!(json["agent_contexts"]["file_budget"]["max_boundaries"], 4);
}
#[test]
fn context_materialize_reports_stale_learning_as_blocked_pattern() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["intelligence", "learn", "--name", name]));
fs::write(
root.join("docs/project-intelligence-layer/03-techspec.md"),
"# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
)
.unwrap();
let output = run(root, &["context", "materialize", "--name", name, "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
assert!(json_contains_text(&json, "Learning obsoleto bloqueado"));
assert!(json_contains_text(
&json,
"docs/project-intelligence-layer/03-techspec.md"
));
}
#[test]
fn context_materialize_disabled_does_not_write() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("src/domain")).unwrap();
fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
let config_path = root.join("sdd.config.yaml");
let config = fs::read_to_string(&config_path).unwrap();
let disabled = config.replacen(
"context_materialization:\n enabled: true",
"context_materialization:\n enabled: false",
1,
);
assert_ne!(
config, disabled,
"config fixture should include materialization toggle"
);
fs::write(&config_path, disabled).unwrap();
let output = run(
root,
&[
"context",
"materialize",
"--name",
name,
"--write",
"--json",
],
);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("materialize output should be json");
assert!(json_contains_text(
&json,
"context materialization disabled"
));
assert!(!root
.join("docs/project-intelligence-layer/00-context-recommendations.md")
.exists());
assert!(!fs::read_to_string(root.join("AGENTS.md"))
.unwrap()
.contains("<!-- sdd-context:start id=\"root-agents\" -->"));
assert!(!root.join("src/domain/AGENTS.md").exists());
}
#[test]
fn context_materialize_plan_includes_expected_block_preview() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("src/domain")).unwrap();
fs::write(root.join("src/domain/mod.rs"), "pub mod policy;\n").unwrap();
let output = run(root, &["context", "materialize", "--name", name]);
let text = combined(&output);
assert_success(output);
assert!(text.contains("## Preview de blocos gerenciados"));
assert!(text.contains("### `AGENTS.md` / `root-agents`"));
assert!(text.contains("<!-- sdd-context:start id=\"root-agents\" -->"));
assert!(!root
.join("docs/project-intelligence-layer/00-context-recommendations.md")
.exists());
}
#[test]
fn context_materialize_redacts_persisted_artifact() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join(".sdd/intelligence")).unwrap();
fs::write(
root.join(".sdd/intelligence/learnings.jsonl"),
r#"{"source_artifact":"docs/OPENAI_API_KEY=sk-test-secret.md","source_stage":"memory","status":"active","summary":"Registrar padrão operacional sem segredo."}"#,
)
.unwrap();
assert_success(run(
root,
&["context", "materialize", "--name", name, "--write"],
));
let artifact = fs::read_to_string(
root.join("docs/project-intelligence-layer/00-context-recommendations.md"),
)
.unwrap();
assert!(artifact.contains("OPENAI_API_KEY=[REDACTED]"));
assert!(!artifact.contains("sk-test-secret"));
}
#[test]
fn context_build_includes_explicitly_related_artifacts_from_docs_store() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["init", "Legacy Cache Decisions"]));
fs::write(
root.join("docs/legacy-cache-decisions/03-techspec.md"),
"# Tech Spec\n\nDecidimos usar snapshots determinísticos para cache operacional.\n",
)
.unwrap();
let map_path = root.join("docs/project-intelligence-layer/traceability-map.yaml");
let map = fs::read_to_string(&map_path).unwrap();
fs::write(
&map_path,
map.replace(
"related_orchestrations: []",
"related_orchestrations:\n - legacy-cache-decisions",
),
)
.unwrap();
assert_success(run(
root,
&[
"context",
"build",
"--name",
name,
"--stage",
"execution",
"--write",
],
));
let context_pack = fs::read_to_string(
root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
)
.unwrap();
assert!(context_pack.contains("docs/legacy-cache-decisions/03-techspec.md"));
assert!(context_pack.contains("snapshots determinísticos"));
}
#[test]
fn discover_json_reports_domain_code_intelligence_and_does_not_write_on_dry_run() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
fs::write(
root.join("CONTEXT.md"),
"# Glossario\n\n- Pedido: demanda.\n",
)
.unwrap();
fs::create_dir_all(root.join("docs/adr")).unwrap();
fs::write(root.join("docs/adr/0001-cache.md"), "# ADR\n").unwrap();
fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"app\"\nversion = \"0.1.0\"\n",
)
.unwrap();
let output = run(root, &["discover", "--json", "--dry-run", "mapear dominio"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("discover output should be json");
assert_eq!(
json.get("artifact_written").and_then(Value::as_bool),
Some(false)
);
assert_eq!(
json.get("html_artifact_written").and_then(Value::as_bool),
Some(false)
);
assert!(json_contains_text(&json, "CONTEXT.md"));
assert!(json_contains_text(&json, "docs/adr"));
assert!(json_contains_text(&json, "codegraph"));
assert!(json_contains_text(&json, "codegraph init -i ."));
assert!(json_contains_text(
&json,
"codegraph context \"<task>\" --path . --format markdown"
));
assert!(json_contains_text(&json, "lexa"));
assert!(json_contains_text(&json, "execution-discipline"));
assert!(!root
.join("docs/sdd-orchestration/00-project-discovery.md")
.exists());
assert!(!root
.join("docs/sdd-orchestration/00-project-discovery.html")
.exists());
}
#[test]
fn ca_01_discovery_companion_materializes_markdown_and_html() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(root, &["discover", "--json", "--force"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("discover output should be json");
assert_eq!(
json.get("artifact_written").and_then(Value::as_bool),
Some(true)
);
assert_eq!(
json.get("html_artifact_written").and_then(Value::as_bool),
Some(true)
);
assert!(json_contains_text(
&json,
"docs/sdd-orchestration/00-project-discovery.md"
));
assert!(json_contains_text(
&json,
"docs/sdd-orchestration/00-project-discovery.html"
));
assert!(root
.join("docs/sdd-orchestration/00-project-discovery.md")
.exists());
assert!(root
.join("docs/sdd-orchestration/00-project-discovery.html")
.exists());
}
#[test]
fn discover_infers_high_level_architecture_from_project_signals() {
let target = tempdir().unwrap();
let root = target.path();
fs::create_dir_all(root.join("app/api/orders")).unwrap();
fs::create_dir_all(root.join("src/components")).unwrap();
fs::create_dir_all(root.join("tests")).unwrap();
fs::create_dir_all(root.join("prisma/migrations/001_init")).unwrap();
fs::create_dir_all(root.join(".github/workflows")).unwrap();
fs::write(
root.join("package.json"),
r#"{
"name": "dropchat-frontend",
"version": "1.2.3",
"scripts": {
"dev": "next dev",
"build": "next build",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "15.0.0",
"react": "19.0.0",
"stripe": "17.0.0",
"next-auth": "5.0.0",
"@sentry/nextjs": "8.0.0",
"@prisma/client": "6.0.0",
"bullmq": "5.0.0"
},
"devDependencies": {
"vitest": "2.0.0",
"typescript": "5.0.0"
}
}"#,
)
.unwrap();
fs::write(
root.join("app/page.tsx"),
"export default function Page() { return null }\n",
)
.unwrap();
fs::write(
root.join("app/api/orders/route.ts"),
"export async function POST() { return Response.json({ ok: true }) }\n",
)
.unwrap();
fs::write(
root.join("src/components/Button.tsx"),
"export function Button() {}\n",
)
.unwrap();
fs::write(
root.join("tests/orders.test.ts"),
"import { test } from 'vitest'\n",
)
.unwrap();
fs::write(
root.join("prisma/schema.prisma"),
"datasource db { provider = \"postgresql\" url = env(\"DATABASE_URL\") }\n",
)
.unwrap();
fs::write(
root.join("prisma/migrations/001_init/migration.sql"),
"-- init\n",
)
.unwrap();
fs::write(root.join(".github/workflows/ci.yml"), "name: ci\n").unwrap();
fs::write(root.join("Dockerfile"), "FROM node:22\n").unwrap();
fs::write(root.join("vercel.json"), "{}\n").unwrap();
fs::write(
root.join(".env.example"),
"DATABASE_URL=postgres://example\nSTRIPE_SECRET_KEY=sk_test_secret\nSENTRY_DSN=https://example\n",
)
.unwrap();
let output = run(root, &["discover", "--json", "--dry-run", "onboarding"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("discover output should be json");
assert!(json_contains_text(&json, "dropchat-frontend"));
assert!(json_contains_text(&json, "app/page.tsx"));
assert!(json_contains_text(&json, "app/api"));
assert!(json_contains_text(&json, "Next.js"));
assert!(json_contains_text(&json, "Prisma"));
assert!(json_contains_text(&json, "Stripe"));
assert!(json_contains_text(&json, "Sentry"));
assert!(json_contains_text(&json, "BullMQ"));
assert!(json_contains_text(&json, "DATABASE_URL"));
assert!(json_contains_text(&json, "STRIPE_SECRET_KEY"));
assert!(!json_contains_text(&json, "sk_test_secret"));
}
#[test]
fn discover_named_artifact_contains_capability_map_and_code_intelligence() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
assert_success(run(
root,
&[
"discover",
"--name",
"Discovery Intelligence",
"--force",
"brownfield execution",
],
));
let artifact =
fs::read_to_string(root.join("docs/discovery-intelligence/00-project-discovery.md"))
.unwrap();
let html =
fs::read_to_string(root.join("docs/discovery-intelligence/00-project-discovery.html"))
.unwrap();
assert!(artifact.contains("## Arquitetura e padrões"));
assert!(artifact.contains("## Inventário técnico detalhado"));
assert!(artifact.contains("Resumo inferido"));
assert!(artifact.contains("Entrypoints reais"));
assert!(artifact.contains("## Visualizações e documentação HTML"));
assert!(artifact.contains("```mermaid"));
assert!(artifact.contains("00-project-discovery.html"));
assert!(artifact.contains("Code intelligence"));
assert!(artifact.contains("codegraph init -i ."));
assert!(artifact.contains("Comandos sugeridos"));
assert!(artifact.contains("code-intelligence"));
assert!(artifact.contains("execution-discipline"));
assert!(artifact.contains("domain-language"));
assert!(artifact.contains("architecture-deepening"));
assert!(artifact.contains("Fallback"));
assert!(html.contains("<title>Project Discovery — Discovery Intelligence · SDD</title>"));
assert!(html.contains("<span class=\"stage-badge\">project-discovery</span>"));
assert!(
!html.contains("Project Discovery registrado para"),
"Discovery default não deve usar o renderer HTML legado do monolito"
);
assert!(html.contains("Stack Utilizada"));
assert!(html.contains("Arquitetura Inferida"));
assert!(html.contains("Manifests e Dependências"));
}
#[test]
fn optimize_status_json_reports_optional_wrappers_and_ignored_paths() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(root, &["optimize", "status", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("status output should be json");
assert_eq!(json.get("enabled").and_then(Value::as_bool), Some(true));
assert!(json_contains_text(&json, "codegraph"));
assert!(json_contains_text(&json, "rtk"));
assert!(json_contains_text(&json, "caveman"));
assert!(json_contains_text(&json, "node_modules"));
assert!(json_contains_text(&json, "nova hipótese testada"));
assert!(json_contains_text(&json, "mesmo comando"));
}
#[test]
fn context_build_metadata_only_omits_content_and_returns_handoff() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
create_project_intelligence_artifacts(root);
let output = run(
root,
&[
"context",
"build",
"--name",
"Project Intelligence Layer",
"--stage",
"execution",
"--json",
"--metadata-only",
],
);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("metadata should be json");
assert!(json.get("content").is_none(), "{text}");
assert!(json.get("path").and_then(Value::as_str).is_some());
assert!(
json.get("sources_included")
.and_then(Value::as_u64)
.unwrap_or_default()
> 0
);
assert!(json.get("conflicts").and_then(Value::as_u64).is_some());
assert!(json.get("used_chars").and_then(Value::as_u64).is_some());
assert!(json
.pointer("/handoff/strategy")
.and_then(Value::as_str)
.is_some());
assert!(json
.pointer("/handoff/suggested_tests")
.and_then(Value::as_array)
.is_some());
assert!(json.get("stale").and_then(Value::as_array).is_some());
assert!(json
.get("decision_conflicts")
.and_then(Value::as_array)
.is_some());
}
#[test]
fn context_pack_execution_prioritizes_handoff_before_long_artifact_snippets() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
create_project_intelligence_artifacts(root);
fs::write(
root.join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.unwrap();
let output = run(
root,
&[
"context",
"build",
"--name",
"Project Intelligence Layer",
"--stage",
"execution",
],
);
let text = combined(&output);
assert_success(output);
let handoff_index = text.find("## Context Handoff").expect("handoff section");
let decisions_index = text
.find("## Decisões relevantes")
.expect("decisions section");
assert!(
handoff_index < decisions_index,
"handoff must appear before long snippets"
);
assert!(
text.contains("- `cargo test`") || text.contains("descobrir pelo manifesto"),
"{text}"
);
assert!(text.contains("Fallback amplo somente quando o handoff não cobrir o risco"));
}
#[test]
fn health_json_reports_pass_and_codegraph_absence_without_orchestration_name() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let config_path = root.join("sdd.config.yaml");
let config = fs::read_to_string(&config_path).unwrap();
fs::write(
&config_path,
config.replace("command: codegraph", "command: missing-codegraph"),
)
.unwrap();
let output = run(root, &["health", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("health should be json");
assert_eq!(json["status"], "pass", "{text}");
assert_eq!(json["components"]["quality"]["status"], "skipped");
assert_eq!(
json["components"]["optimization"]["codegraph"]["available"],
false
);
assert_eq!(json["components"]["workflow"]["status"], "pass");
assert_eq!(json["components"]["clients"]["status"], "pass");
assert_eq!(json["components"]["skills"]["status"], "pass");
}
#[test]
fn health_json_surfaces_missing_memory_warnings() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
assert_success(run(root, &["init", "Health Warning"]));
let output = run(root, &["health", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("health should be json");
assert_eq!(json["status"], "warn", "{text}");
assert_eq!(json["components"]["intelligence"]["status"], "warn");
assert!(json_contains_text(&json, "missing-memory"), "{text}");
}
#[test]
fn health_json_keeps_info_only_intelligence_signals_passing() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
assert_success(run(root, &["init", "Info Only"]));
fs::write(
root.join("docs/info-only/09-memory.md"),
"# Memória\n\n## Rastreabilidade\n- teste\n",
)
.unwrap();
let output = run(root, &["health", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("health should be json");
assert_eq!(json["status"], "pass", "{text}");
assert_eq!(json["components"]["intelligence"]["status"], "pass");
assert_eq!(json["components"]["intelligence"]["warning_count"], 0);
assert!(json_contains_text(&json, "missing-artifact"), "{text}");
assert!(json_contains_text(
&json,
"sdd intelligence repair --all --json"
));
}
#[test]
fn health_strict_fails_on_warning_status() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
assert_success(run(root, &["init", "Health Warning"]));
let output = run(root, &["health", "--strict", "--name", "health-warning"]);
assert!(
!output.status.success(),
"expected health strict to fail\n{}",
combined(&output)
);
}
#[test]
fn ci_strict_release_dry_run_json_reports_gate_matrix() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(
root,
&[
"ci",
"--strict-release",
"--dry-run",
"--json",
"--name",
"Release Gate",
],
);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
assert_eq!(json["dry_run"], true);
assert!(json["checks"].as_array().unwrap().iter().any(|check| {
check["command"]
.as_str()
.is_some_and(|command| command == "cargo fmt --check")
}));
assert!(json["checks"].as_array().unwrap().iter().any(|check| {
check["command"]
.as_str()
.is_some_and(|command| command == "sdd health --strict --json --name Release Gate")
}));
assert!(json["checks"]
.as_array()
.unwrap()
.iter()
.any(|check| { check["area"].as_str() == Some("advisory") && check["required"] == false }));
let output = run(root, &["ci", "--strict-release", "--dry-run", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
assert!(json["checks"].as_array().unwrap().iter().any(|check| {
check["command"]
.as_str()
.is_some_and(|command| command == "sdd health --strict --json")
}));
}
#[test]
fn ci_strict_release_markdown_is_human_friendly() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(root, &["ci", "--strict-release", "--dry-run"]);
let text = combined(&output);
assert_success(output);
assert!(text.contains("# 🚦 SDD Strict Release Gate"));
assert!(text.contains("**Status:** 🟡 **WARN**"));
assert!(text.contains("**Leitura:** dry-run"));
assert!(text.contains("## 🧱 Resumo por área"));
assert!(text.contains("## ⚠️ Ações Recomendadas"));
assert!(text.contains("> [!NOTE]"));
assert!(text.contains("**Dry-run ativo**"));
assert!(text.contains("## 🧾 Evidências"));
assert!(text.contains("<summary>Ver matriz completa de checks</summary>"));
assert!(text.contains("| Área | Obrigatórios | Opcionais | Sinal |"));
assert!(text.contains("Execute `sdd ci --strict-release` sem `--dry-run`"));
}
#[test]
fn ci_strict_release_without_name_still_uses_strict_health() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(root, &["ci", "--strict-release", "--dry-run", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
assert!(json["checks"].as_array().unwrap().iter().any(|check| {
check["command"]
.as_str()
.is_some_and(|command| command == "sdd health --strict --json")
}));
}
#[test]
fn ci_strict_release_uses_layer_source_for_core_checks_from_installed_project() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
assert!(!root.join("Cargo.toml").exists());
let output = run(root, &["ci", "--strict-release", "--dry-run", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("strict gate should be json");
let checks = json["checks"].as_array().unwrap();
let same_path = |actual: &str, expected: &Path| {
fs::canonicalize(Path::new(actual)).unwrap() == fs::canonicalize(expected).unwrap()
};
let repo = repo_root();
assert!(same_path(json["root"].as_str().unwrap(), root));
assert!(same_path(json["release_root"].as_str().unwrap(), repo));
let cargo_fmt = checks
.iter()
.find(|check| check["command"].as_str() == Some("cargo fmt --check"))
.expect("cargo fmt check");
assert!(same_path(cargo_fmt["cwd"].as_str().unwrap(), repo));
let health = checks
.iter()
.find(|check| check["command"].as_str() == Some("sdd health --strict --json"))
.expect("health check");
assert!(same_path(health["cwd"].as_str().unwrap(), root));
}
#[test]
fn maintenance_report_json_covers_expected_areas() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let output = run(root, &["maintenance", "report", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("maintenance report should be json");
let ids = json["areas"]
.as_array()
.unwrap()
.iter()
.filter_map(|area| area["id"].as_str())
.collect::<Vec<_>>();
for expected in [
"core-rust",
"docs",
"clients",
"bot",
"providers",
"intelligence",
"workflows",
"release",
"diagrams",
"advisory",
] {
assert!(ids.contains(&expected), "missing area {expected}: {text}");
}
let area_status = |id: &str| {
json["areas"]
.as_array()
.unwrap()
.iter()
.find(|area| area["id"].as_str() == Some(id))
.and_then(|area| area["status"].as_str())
.unwrap()
.to_string()
};
assert_eq!(area_status("core-rust"), "skipped");
assert_eq!(area_status("bot"), "skipped");
assert_eq!(area_status("release"), "skipped");
}
#[test]
fn maintenance_report_marks_presence_only_areas_as_skipped() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
fs::write(root.join("Cargo.toml"), "[package]\nname = \"fixture\"\n").unwrap();
fs::create_dir_all(root.join("docs-site")).unwrap();
fs::write(root.join("docs-site/package.json"), "{}").unwrap();
fs::create_dir_all(root.join("bot")).unwrap();
fs::write(root.join("bot/package.json"), "{}").unwrap();
let output = run(root, &["maintenance", "report", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("maintenance report should be json");
let area_status = |id: &str| {
json["areas"]
.as_array()
.unwrap()
.iter()
.find(|area| area["id"].as_str() == Some(id))
.and_then(|area| area["status"].as_str())
.unwrap_or("")
.to_string()
};
assert_eq!(area_status("core-rust"), "skipped");
assert_eq!(area_status("docs"), "skipped");
assert_eq!(area_status("bot"), "skipped");
assert_eq!(area_status("release"), "skipped");
}
#[test]
fn optimize_compress_dedupes_trace_and_preserves_operational_identifiers() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
fs::write(
root.join("trace.log"),
"cmd: cargo test\ncmd: cargo test\nfile: src/main.rs\nurl: https://example.com/a\nTask T-01\nhash abc123def456\n",
)
.unwrap();
let output = run(
root,
&[
"optimize",
"compress",
"--kind",
"trace",
"--file",
"trace.log",
],
);
let text = combined(&output);
assert_success(output);
assert!(text.contains("Caveman trace summary"));
assert!(text.contains("cargo test"));
assert!(text.contains("src/main.rs"));
assert!(text.contains("https://example.com/a"));
assert!(text.contains("T-01"));
assert!(text.contains("abc123def456"));
assert_eq!(text.matches("cargo test").count(), 1);
}
#[test]
fn context_build_rejects_invalid_stage_path() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
let output = run(
root,
&[
"context",
"build",
"--name",
name,
"--stage",
"../escape",
"--write",
],
);
assert_code(output, 1);
assert!(!root
.join(".sdd/intelligence/context-packs/escape.md")
.exists());
}
#[test]
fn intelligence_learn_writes_enriched_jsonl_and_memory_learn_stays_compatible() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["intelligence", "learn", "--name", name]));
let intelligence_index =
fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
let sources_index = fs::read_to_string(root.join(".sdd/intelligence/sources.index.jsonl"))
.unwrap()
.lines()
.filter(|line| !line.trim().is_empty())
.count();
let records = intelligence_index
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
.collect::<Vec<_>>();
assert!(!records.is_empty());
assert!(records.iter().any(|record| {
record
.get("source_artifact")
.and_then(Value::as_str)
.is_some_and(|path| path == "docs/project-intelligence-layer/03-techspec.md")
}));
for record in &records {
assert!(record
.get("source_artifact")
.and_then(Value::as_str)
.is_some());
assert!(record.get("source_hash").and_then(Value::as_str).is_some());
assert!(record.get("status").and_then(Value::as_str).is_some());
assert_eq!(record.get("derived").and_then(Value::as_bool), Some(true));
}
assert_success(run(root, &["intelligence", "learn", "--name", name]));
let sources_index_after_second_learn =
fs::read_to_string(root.join(".sdd/intelligence/sources.index.jsonl"))
.unwrap()
.lines()
.filter(|line| !line.trim().is_empty())
.count();
assert_eq!(sources_index_after_second_learn, sources_index);
assert_success(run(root, &["memory", "learn", "--name", name]));
let memory_index = fs::read_to_string(root.join(".sdd/memory/learnings.jsonl")).unwrap();
assert!(memory_index.contains("\"source_artifact\""));
assert!(memory_index.contains("\"source_hash\""));
assert!(memory_index.contains("docs/project-intelligence-layer/03-techspec.md"));
}
#[test]
fn intelligence_learn_indexes_memory_when_present() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::write(
root.join("docs/project-intelligence-layer/09-memory.md"),
"# Memória\n\n## Estado final\nRegistrado.\n\n## Rastreabilidade\n- teste\n\n## Decisões\n- Estratégia de cache: JSONL derivado\n\n## Padrões úteis\n- Context Pack.\n\n## Testes e comandos\n- cargo test\n\n## Pendências\n- Nenhuma.\n\n## Contexto para o próximo agente\nUse docs.\n",
)
.unwrap();
assert_success(run(root, &["intelligence", "learn", "--name", name]));
let intelligence_index =
fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
assert!(intelligence_index.contains("\"source_stage\":\"memory\""));
assert!(intelligence_index.contains("docs/project-intelligence-layer/09-memory.md"));
}
#[test]
fn intelligence_repair_writes_operational_memory_and_refreshes_stale_learning() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["intelligence", "learn", "--name", name]));
fs::write(
root.join("docs/project-intelligence-layer/03-techspec.md"),
"# Tech Spec alterada\n\n## Decisões\n- Motor de memória operacional: gerar `09-memory.md` derivado\n\n## Falhas recorrentes\n- stale-learning aparece quando o hash muda\n\n## Comandos confiáveis\n- `cargo test --locked --test cli`\n\n## Padrões por repo\n- `docs/<slug>/traceability-map.yaml` é a fonte canônica local\n\n## Rastreabilidade\n- teste\n",
)
.unwrap();
let output = run(root, &["intelligence", "repair", "--name", name, "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("repair output should be json");
assert_eq!(json["status"], "pass", "{text}");
assert!(json_contains_text(
&json,
"docs/project-intelligence-layer/09-memory.md"
));
assert_eq!(json["remaining_operational_signals"], 0);
let memory = fs::read_to_string(root.join("docs/project-intelligence-layer/09-memory.md"))
.expect("repair should write operational memory");
assert!(memory.contains("## Estado final"));
assert!(memory.contains("## Decisões"));
assert!(memory.contains("## Falhas recorrentes"));
assert!(memory.contains("## Comandos confiáveis"));
assert!(memory.contains("## Padrões úteis"));
assert!(memory.contains("## Testes e comandos"));
assert!(memory.contains("## Contexto para o próximo agente"));
assert!(memory.contains("cargo test --locked --test cli"));
assert!(memory.contains("traceability-map.yaml"));
let health_output = run(root, &["intelligence", "health", "--json"]);
let health_text = combined(&health_output);
assert_success(health_output);
let health = serde_json::from_str::<Value>(&health_text).expect("health output should be json");
assert!(
!json_contains_text(&health, "missing-memory"),
"{health_text}"
);
assert!(
!json_contains_text(&health, "stale-learning"),
"{health_text}"
);
let intelligence_index =
fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
let records = intelligence_index
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
.collect::<Vec<_>>();
assert!(records.iter().any(|record| {
record.get("source_artifact").and_then(Value::as_str)
== Some("docs/project-intelligence-layer/03-techspec.md")
&& record.get("status").and_then(Value::as_str) == Some("active")
}));
assert!(records.iter().any(|record| {
record.get("source_artifact").and_then(Value::as_str)
== Some("docs/project-intelligence-layer/09-memory.md")
&& record.get("source_stage").and_then(Value::as_str) == Some("memory")
}));
}
#[test]
fn intelligence_learn_all_indexes_every_traceable_docs_store() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
create_project_intelligence_artifacts(root);
assert_success(run(root, &["init", "Legacy Cache Decisions"]));
fs::write(
root.join("docs/legacy-cache-decisions/03-techspec.md"),
"# Tech Spec Legacy\n\n## Decisões\n- Estratégia de cache: snapshots determinísticos\n",
)
.unwrap();
let output = run(root, &["intelligence", "learn", "--all", "--json"]);
let text = combined(&output);
assert_success(output);
let json = serde_json::from_str::<Value>(&text).expect("learn output should be json");
assert_eq!(json.get("scope").and_then(Value::as_str), Some("all"));
let intelligence_index =
fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
let sources_index =
fs::read_to_string(root.join(".sdd/intelligence/sources.index.jsonl")).unwrap();
assert!(intelligence_index.contains("docs/project-intelligence-layer/03-techspec.md"));
assert!(intelligence_index.contains("docs/legacy-cache-decisions/03-techspec.md"));
assert!(sources_index.contains("docs/project-intelligence-layer/traceability-map.yaml"));
assert!(sources_index.contains("docs/legacy-cache-decisions/traceability-map.yaml"));
}
#[test]
fn intelligence_status_json_marks_changed_learning_as_stale() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["intelligence", "learn", "--name", name]));
fs::write(
root.join("docs/project-intelligence-layer/03-techspec.md"),
"# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
)
.unwrap();
let status_output = run(root, &["intelligence", "status", "--json"]);
let status_text = combined(&status_output);
assert_success(status_output);
let status =
serde_json::from_str::<Value>(&status_text).expect("status output should be valid json");
assert!(status
.get("stale_sources")
.and_then(Value::as_u64)
.is_some_and(|count| count >= 1));
let intelligence_index =
fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
let records = intelligence_index
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
.collect::<Vec<_>>();
assert!(records.iter().any(|record| {
record.get("source_artifact").and_then(Value::as_str)
== Some("docs/project-intelligence-layer/03-techspec.md")
&& record.get("status").and_then(Value::as_str) == Some("stale")
}));
}
#[test]
fn context_build_dry_run_does_not_persist_stale_status() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["intelligence", "learn", "--name", name]));
fs::write(
root.join("docs/project-intelligence-layer/03-techspec.md"),
"# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
)
.unwrap();
assert_success(run(
root,
&[
"context",
"build",
"--name",
name,
"--stage",
"execution",
"--write",
"--dry-run",
],
));
let intelligence_index =
fs::read_to_string(root.join(".sdd/intelligence/learnings.jsonl")).unwrap();
let records = intelligence_index
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| serde_json::from_str::<Value>(line).expect("valid intelligence learning jsonl"))
.collect::<Vec<_>>();
assert!(records.iter().any(|record| {
record.get("source_artifact").and_then(Value::as_str)
== Some("docs/project-intelligence-layer/03-techspec.md")
&& record.get("status").and_then(Value::as_str) == Some("active")
}));
}
#[test]
fn context_build_reports_decision_conflict_with_stage_precedence() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::write(
root.join("review-input.md"),
"# Review\n\n## Rastreabilidade\n- Origem: 06-execution.md\n\n## Escopo revisado\nCache operacional derivado.\n\n## Evidências\n- cargo test\n\n## Achados\n- Estratégia de cache revisada.\n\n## Testes\n- cargo test\n\n## Veredito\nAprovado com ressalvas.\n\n## Decisão\n- [ADR-CACHE-001] Estratégia de cache: JSONL derivado\n",
)
.unwrap();
fs::write(
root.join("memory-input.md"),
"# Memória\n\n## Estado final\nRegistrado.\n\n## Rastreabilidade\n- Origem: 07-review.md\n\n## Decisões\n- [ADR-CACHE-001] Estratégia de cache: SQLite como fonte canônica\n\n## Padrões úteis\n- Context Pack.\n\n## Testes e comandos\n- cargo test\n\n## Pendências\n- Nenhuma.\n\n## Contexto para o próximo agente\nUse docs.\n",
)
.unwrap();
assert_success(run(
root,
&[
"artifact",
"save",
name,
"review",
"--file",
"review-input.md",
"--state",
"approved",
],
));
assert_success(run(
root,
&[
"artifact",
"save",
name,
"memory",
"--file",
"memory-input.md",
"--state",
"approved",
],
));
assert_success(run(
root,
&[
"context",
"build",
"--name",
name,
"--stage",
"execution",
"--write",
],
));
let context_pack = fs::read_to_string(
root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
)
.unwrap();
assert!(context_pack.contains("Conflito de decisão"));
assert!(context_pack.contains("07-review.md"));
assert!(context_pack.contains("09-memory.md"));
assert!(context_pack.contains("tem precedência"));
}
#[test]
fn context_build_detects_prose_and_table_decision_conflicts() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::write(
root.join("review-input.md"),
"# Review\n\n## Rastreabilidade\n- Origem: 06-execution.md\n\n## Escopo revisado\nDecidimos usar JSONL derivado para cache operacional.\n\n## Evidências\n- teste\n\n## Achados\n- Cache operacional revisado.\n\n## Testes\n- teste\n\n## Veredito\nAprovado.\n\n## Decisão\n- [ADR-CACHE-002] Cache operacional: JSONL derivado\n",
)
.unwrap();
fs::write(
root.join("memory-input.md"),
"# Memória\n\n## Estado final\nRegistrado.\n\n## Rastreabilidade\n- Origem: 07-review.md\n\n## Decisões\n| ID | Chave | Escolha |\n|---|---|---|\n| ADR-CACHE-002 | Cache operacional | SQLite canônico |\n\n## Padrões úteis\n- Context Pack.\n\n## Testes e comandos\n- cargo test\n\n## Pendências\n- Nenhuma.\n\n## Contexto para o próximo agente\nUse docs.\n",
)
.unwrap();
assert_success(run(
root,
&[
"artifact",
"save",
name,
"review",
"--file",
"review-input.md",
"--state",
"approved",
],
));
assert_success(run(
root,
&[
"artifact",
"save",
name,
"memory",
"--file",
"memory-input.md",
"--state",
"approved",
],
));
assert_success(run(
root,
&[
"context",
"build",
"--name",
name,
"--stage",
"execution",
"--write",
],
));
let context_pack = fs::read_to_string(
root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
)
.unwrap();
assert!(context_pack.contains("Conflito de decisão `ADR-CACHE-002`"));
assert!(context_pack.contains("07-review.md"));
assert!(context_pack.contains("09-memory.md"));
}
#[test]
fn context_build_execution_pack_contains_code_intelligence_and_capability_handoff() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
fs::create_dir_all(root.join("node_modules/pkg")).unwrap();
fs::write(root.join("node_modules/pkg/ignored.md"), "IGNORE-ME").unwrap();
fs::create_dir_all(root.join("target/debug")).unwrap();
fs::write(root.join("target/debug/ignored.md"), "IGNORE-ME").unwrap();
assert_success(run(
root,
&[
"context",
"build",
"--name",
name,
"--stage",
"execution",
"--task",
"T-04",
"--write",
],
));
let context_pack = fs::read_to_string(
root.join(".sdd/intelligence/context-packs/project-intelligence-layer/execution.md"),
)
.unwrap();
assert!(context_pack.contains("## Context Handoff"));
assert!(context_pack.contains("Task: `T-04`"));
assert!(context_pack.contains("Fora de escopo por padrão"));
assert!(context_pack.contains("Loop produtivo"));
assert!(context_pack.contains("Optimization wrapper"));
assert!(context_pack.contains("rtk wrapper"));
assert!(context_pack.contains("caveman wrapper"));
assert!(context_pack.contains("## Code intelligence"));
assert!(context_pack.contains("codegraph"));
assert!(context_pack.contains("codegraph init -i ."));
assert!(context_pack.contains("codegraph context \"<task>\" --path . --format markdown"));
assert!(context_pack.contains("lexa"));
assert!(context_pack.contains("## Capability handoff"));
assert!(context_pack.contains("execution-discipline"));
assert!(context_pack.contains("code-intelligence"));
assert!(context_pack.contains("tracer bullet vertical"));
assert!(!context_pack.contains("IGNORE-ME"));
}
#[test]
fn intelligence_health_json_reports_missing_memory_for_incomplete_orchestration() {
let incomplete = tempdir().unwrap();
let incomplete_root = incomplete.path();
install_generic_project(incomplete_root);
assert_success(run(incomplete_root, &["init", "Incomplete Intelligence"]));
let health_output = run(incomplete_root, &["intelligence", "health", "--json"]);
let health_text = combined(&health_output);
assert_success(health_output);
let health =
serde_json::from_str::<Value>(&health_text).expect("health output should be valid json");
assert!(json_contains_text(&health, "memory"));
assert!(
json_contains_text(&health, "missing")
|| json_contains_text(&health, "ausente")
|| json_contains_text(&health, "absent"),
"expected missing memory signal in health json:\n{health_text}"
);
}
#[test]
fn intelligence_health_fail_on_threshold_returns_nonzero() {
let incomplete = tempdir().unwrap();
let incomplete_root = incomplete.path();
install_generic_project(incomplete_root);
assert_success(run(incomplete_root, &["init", "Incomplete Intelligence"]));
let output = run(
incomplete_root,
&["intelligence", "health", "--fail-on", "warning"],
);
let text = combined(&output);
assert_code(output, 1);
assert!(text.contains("missing-memory"));
assert!(text.contains("intelligence health failed"));
}
#[test]
fn intelligence_health_detects_stale_learning_without_status_first() {
let target = tempdir().unwrap();
let root = target.path();
install_generic_project(root);
let name = "Project Intelligence Layer";
create_project_intelligence_artifacts(root);
assert_success(run(root, &["intelligence", "learn", "--name", name]));
fs::write(
root.join("docs/project-intelligence-layer/03-techspec.md"),
"# Tech Spec alterada\n\n## Rastreabilidade\n- teste\n",
)
.unwrap();
let health_output = run(root, &["intelligence", "health", "--json"]);
let health_text = combined(&health_output);
assert_success(health_output);
let health =
serde_json::from_str::<Value>(&health_text).expect("health output should be valid json");
assert!(json_contains_text(&health, "stale-learning"));
assert!(json_contains_text(&health, "hash mudou"));
}
#[test]
fn creation_templates_match_cli_stage_contract() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(root, &["init", "Fluxo Completo"]));
let contract = fs::read_to_string(repo_root().join("schemas/sdd-contract.yaml"))
.expect("contract manifest");
let expected = contract_stage_files(&contract);
let generated = fs::read_to_string(root.join("docs/fluxo-completo/traceability-map.yaml"))
.expect("generated traceability map");
let generated_stages = artifact_stage_files(&generated);
assert_eq!(generated_stages, expected);
for rel in [
"templates/traceability-map.yaml",
"templates/artifact-index.yaml",
] {
let template = fs::read_to_string(repo_root().join(rel)).expect(rel);
assert_eq!(
artifact_stage_files(&template),
generated_stages,
"{rel} drifted from the CLI artifact stage contract"
);
}
}
#[test]
fn portable_orchestration_clients_are_generated_from_canonical_skill_source() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
root,
&[
"clients",
"sync",
"--targets",
"codex,antigravity,cursor,devin",
"--force",
],
));
let canonical = canonical_orchestration_skill();
assert_portable_orchestration_skill_contract(canonical);
for required_skill in [
"output-quality",
"provider-compatibility",
"humanizer",
"prompt-craft",
"execution-discipline",
"product-discovery-frameworks",
] {
assert!(
canonical.contains(required_skill),
"canonical orchestration skill must preserve {required_skill}"
);
}
for rel in [
".agents/skills/orchestration/SKILL.md",
".cursor/skills/orchestration/SKILL.md",
".devin/skills/orchestration/SKILL.md",
] {
let generated = fs::read_to_string(root.join(rel)).unwrap();
assert_eq!(
generated, canonical,
"{rel} drifted from templates/client-skills/orchestration.md"
);
}
}
#[test]
fn grill_contract_is_portable_and_auto_invocable() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
root,
&[
"skills",
"sync",
"--targets",
"codex,claude,cursor,devin,opencode,trae,antigravity",
"--force",
],
));
assert_success(run(
root,
&[
"clients",
"sync",
"--targets",
"codex,claude,cursor,devin,opencode,kiro,trae,antigravity",
"--force",
],
));
let canonical = canonical_orchestration_skill();
for anchor in [
"## Intake obrigatório: Grill antes da Idea",
"`grill-with-docs`",
"`grill-me`",
"bypass explícito",
"uma pergunta por vez",
"Tasks/Refinement",
] {
assert!(
canonical.contains(anchor),
"canonical contract missing `{anchor}`"
);
}
for stage in ["idea", "prd", "techspec"] {
let content = fs::read_to_string(root.join(format!(".agents/skills/{stage}/SKILL.md")))
.unwrap_or_else(|error| panic!("missing generated {stage} skill: {error}"));
assert!(
content.contains("grill-with-docs"),
"{stage} must use docs grill"
);
assert!(
content.contains("bypass explícito"),
"{stage} must document bypass"
);
}
let idea_agent = fs::read_to_string(root.join(".claude/agents/idea.md")).unwrap();
let prd_agent = fs::read_to_string(root.join(".claude/agents/prd.md")).unwrap();
let techspec_agent = fs::read_to_string(root.join(".claude/agents/techspec.md")).unwrap();
for (name, content) in [
("idea", idea_agent),
("prd", prd_agent),
("techspec", techspec_agent),
] {
assert!(
content.contains("grill-with-docs"),
"{name} agent missing docs grill"
);
assert!(
content.contains("uma pergunta por vez"),
"{name} agent missing interview rule"
);
}
for skill_id in ["grilling", "grill-me", "grill-with-docs", "domain-modeling"] {
let path = root.join(format!(".agents/skills/{skill_id}/SKILL.md"));
let content = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("missing generated skill {skill_id}: {error}"));
assert!(
!content.contains("disable-model-invocation: true"),
"{skill_id} must be auto-invocable"
);
}
let repository = repo_root();
for skill_id in ["grilling", "grill-me", "grill-with-docs", "domain-modeling"] {
let path = repository.join(format!(
".agents/skills/orchestration-plugin/skills/{skill_id}/SKILL.md"
));
let content = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("missing plugin skill {skill_id}: {error}"));
assert!(content.contains("grill") || skill_id == "domain-modeling");
assert!(
!content.contains("disable-model-invocation: true"),
"plugin skill {skill_id} must be auto-invocable"
);
}
}
#[test]
fn generated_orchestration_surfaces_document_config_driven_isolation() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
root,
&[
"clients",
"sync",
"--targets",
"codex,claude,cursor,opencode,devin,kiro,trae",
"--force",
],
));
assert_worktree_decision_contract(canonical_orchestration_skill());
for rel in [
".agents/agents/sdd-orchestrator/AGENT.md",
".agents/skills/orchestration/SKILL.md",
".claude/commands/orchestration.md",
".claude/skills/orchestration/SKILL.md",
".cursor/agents/sdd-orchestrator.md",
".cursor/commands/orchestration.md",
".cursor/skills/orchestration/SKILL.md",
".devin/agents/sdd-orchestrator/AGENT.md",
".devin/skills/orchestration/SKILL.md",
".kiro/agents/sdd/orchestrator.md",
".kiro/skills/orchestration/SKILL.md",
".opencode/agents/sdd-orchestrator.md",
".opencode/commands/orchestration.md",
".opencode/skills/orchestration/SKILL.md",
".trae/commands/orchestration.md",
".trae/skills/orchestration/SKILL.md",
] {
let content = fs::read_to_string(root.join(rel)).unwrap_or_else(|error| {
panic!("generated orchestration surface {rel} is missing: {error}")
});
assert_worktree_decision_contract(&content);
}
let orchestrator = fs::read_to_string(root.join(".agents/agents/sdd-orchestrator/AGENT.md"))
.expect("generated Codex orchestrator");
for required_skill in [
"output-quality",
"provider-compatibility",
"frontend-design",
"interface-design",
"visual-review",
"design-extraction",
"impeccable-design",
"systematic-debugging",
"subagent-orchestration",
"reflexion-loop",
] {
assert!(
orchestrator.contains(&format!("`{required_skill}`")),
"generated orchestrator must preserve {required_skill}"
);
}
let execution_skill =
fs::read_to_string(root.join(".agents/skills/execution/SKILL.md")).unwrap();
assert!(execution_skill.contains("execution-discipline"));
assert!(execution_skill.contains("frontend-design"));
assert!(execution_skill.contains("interface-design"));
let legacy_claude =
fs::read_to_string(repo_root().join(".claude/commands/orchestrator.md")).unwrap();
assert_worktree_decision_contract(&legacy_claude);
let plugin_command = fs::read_to_string(
repo_root().join(".agents/skills/orchestration-plugin/commands/orchestration.md"),
)
.unwrap();
assert_worktree_decision_contract(&plugin_command);
}
#[test]
fn generated_claude_agent_roster_has_skill_access_and_detects_all_mutations() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
root,
&["clients", "sync", "--targets", "claude", "--force"],
));
for (name, preloads) in CLAUDE_AGENT_SKILL_CONTRACTS {
let path = root.join(format!(".claude/agents/{name}.md"));
let content = fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("{} is missing: {error}", path.display()));
assert_claude_agent_skill_contract(&content, name, preloads);
let mutated = content.replace(", Skill", "");
assert_ne!(mutated, content, "{name} mutation must remove Skill");
fs::write(path, mutated).unwrap();
}
let doctor = run(root, &["clients", "doctor", "--strict", "--json"]);
assert_code(doctor, 1);
let report = combined(&run(root, &["clients", "doctor", "--strict", "--json"]));
for (name, _) in CLAUDE_AGENT_SKILL_CONTRACTS {
assert!(
report.contains(&format!(".claude/agents/{name}.md")),
"strict doctor did not report mutated Claude agent {name}: {report}"
);
}
}
#[test]
fn artifact_store_and_stage_pipeline_work_end_to_end() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
root,
&["artifact", "init", "Minha Orquestração", "--dry-run"],
));
assert!(!root.join("docs").exists());
assert_success(run(root, &["init", "Minha Orquestração"]));
for (stage, input) in [
("discover", "projeto brownfield"),
("risk", "exportação csv com baixo risco"),
("idea", "exportar csv"),
("prd", "visão aprovada"),
("techspec", "arquitetura aprovada"),
("tasks", "backlog técnico"),
("refinement", "grooming necessário"),
("review", "diff atual"),
("qa", "validação funcional"),
("memory", "feature concluída"),
] {
assert_success(run(
root,
&[stage, "--name", "Minha Orquestração", "--force", input],
));
}
let blocked_execution = run(
root,
&["execution", "--name", "Minha Orquestração", "TASK-001"],
);
assert_code(blocked_execution, 1);
assert!(combined(&run(
root,
&["execution", "--name", "Minha Orquestração", "TASK-001"],
))
.contains("05-planning.html ausente"));
let initial_execution = root.join("execution-initial.md");
fs::write(&initial_execution, "TASK-001\n").unwrap();
assert_success(run(
root,
&[
"artifact",
"save",
"Minha Orquestração",
"execution",
"--file",
initial_execution.to_str().unwrap(),
"--state",
"recorded",
],
));
assert_success(run(
root,
&[
"orchestration",
"--name",
"Minha Orquestração",
"--force",
"exportar csv",
],
));
assert_success(run(
root,
&[
"orchestrator",
"--name",
"Minha Orquestração",
"--force",
"exportar csv",
],
));
let status = run(root, &["artifact", "status", "Minha Orquestração"]);
assert_success(status);
let text = combined(&run(root, &["artifact", "status", "Minha Orquestração"]));
for stage in [
"project-discovery",
"risk",
"idea",
"prd",
"techspec",
"tasks",
"refinement",
"execution",
"review",
"qa",
"memory",
] {
assert!(
text.contains(&format!("{stage}: ok")),
"{stage} missing\n{text}"
);
}
let input = root.join("execution.md");
fs::write(&input, "TASK-002\n").unwrap();
assert_success(run(
root,
&[
"artifact",
"save",
"Minha Orquestração",
"execution",
"--file",
input.to_str().unwrap(),
"--state",
"recorded",
"--append",
],
));
let execution =
fs::read_to_string(root.join("docs/minha-orquestracao/06-execution.md")).unwrap();
assert!(execution.contains("TASK-001"));
assert!(execution.contains("TASK-002"));
let schema = repo_root().join("schemas/artifact-sections.json");
assert_success(run(
root,
&[
"validate-artifact",
"risk",
"docs/minha-orquestracao/00-risk-classification.md",
"--schema",
schema.to_str().unwrap(),
],
));
fs::create_dir_all(root.join("docs/minha-orquestracao/adrs")).unwrap();
fs::write(
root.join("docs/minha-orquestracao/adrs/adr-01.md"),
"# ADR - Minha Orquestração\n\n## Rastreabilidade\n- Origem: idea\n\n## Status\nAceita.\n\n## Contexto\nContexto de teste.\n\n## Problema\nProblema de teste.\n\n## Decisão\nDecisão de teste.\n\n## Alternativas consideradas\n- Alternativa A.\n\n## Consequências\n- Consequência A.\n\n## Impactos e riscos\n- Baixo.\n\n## Plano de adoção\n- Aplicar.\n\n## Plano de reversão\n- Reverter.\n\n## Evidências\n- teste\n\n## Histórico de revisão\n- inicial\n",
)
.unwrap();
assert_success(run(
root,
&[
"validate-artifact",
"adr",
"docs/minha-orquestracao/adrs/adr-01.md",
"--schema",
schema.to_str().unwrap(),
],
));
}
#[test]
fn artifact_link_registers_typed_github_external_link() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(root, &["artifact", "init", "GitHub Flow"]));
assert_success(run(
root,
&[
"artifact",
"link",
"GitHub Flow",
"github",
"--url",
"https://github.com/acme/api/issues/42",
"--kind",
"issue",
"--id",
"GH-42",
"--stage",
"idea",
],
));
let map = fs::read_to_string(root.join("docs/github-flow/traceability-map.yaml")).unwrap();
let yaml: YamlValue = serde_yaml::from_str(&map).unwrap();
let root_map = yaml.as_mapping().unwrap();
let github = yaml_get(root_map, "external_links")
.and_then(YamlValue::as_mapping)
.and_then(|links| yaml_get(links, "github"))
.and_then(YamlValue::as_sequence)
.unwrap();
let first = github[0].as_mapping().unwrap();
assert_eq!(
yaml_get(first, "url").and_then(YamlValue::as_str),
Some("https://github.com/acme/api/issues/42")
);
assert_eq!(
yaml_get(first, "kind").and_then(YamlValue::as_str),
Some("issue")
);
assert_eq!(
yaml_get(first, "id").and_then(YamlValue::as_str),
Some("GH-42")
);
assert_eq!(
yaml_get(first, "stage").and_then(YamlValue::as_str),
Some("idea")
);
}
#[test]
fn artifact_link_dry_run_does_not_create_store() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
root,
&[
"artifact",
"link",
"Dry Run Link",
"github",
"--url",
"https://github.com/acme/api/issues/42",
"--dry-run",
],
));
assert!(
!root.join("docs/dry-run-link").exists(),
"artifact link --dry-run must not create docs/<slug>"
);
}
#[test]
fn clients_and_context_commands_cover_supported_surfaces() {
let tmp = tempdir().unwrap();
let root = tmp.path();
fs::write(
root.join("package.json"),
r#"{"dependencies":{"react":"latest","vitest":"latest"}}"#,
)
.unwrap();
assert_success(run(
repo_root(),
&[
"install",
root.to_str().unwrap(),
"--preset",
"frontend-react",
],
));
let list = combined(&run(root, &["clients", "list"]));
assert!(list.contains("opencode"));
assert!(list.contains("antigravity"));
assert!(list.contains("devin"));
assert!(list.contains("trae"));
assert_success(run(root, &["clients", "doctor"]));
let codex_loop = fs::read_to_string(root.join(".codex/commands/workflow.md")).unwrap();
assert!(codex_loop.contains("sdd workflow run loop"));
assert!(codex_loop.contains("docs/WORKFLOW.md"));
let claude_loop = fs::read_to_string(root.join(".claude/commands/workflow.md")).unwrap();
assert!(claude_loop.contains("sdd workflow run loop"));
assert!(claude_loop.contains("sdd workflow status loop"));
assert!(claude_loop.contains("sdd auto run --real --unattended"));
assert!(claude_loop.contains("channel: automation"));
let devin_config: Value =
serde_json::from_str(&fs::read_to_string(root.join(".devin/config.json")).unwrap())
.unwrap();
assert_eq!(devin_config["read_config_from"]["agents_standard"], true);
assert_eq!(devin_config["read_config_from"]["cursor"], false);
assert_eq!(devin_config["read_config_from"]["claude"], false);
assert_eq!(devin_config["read_config_from"]["windsurf"], false);
assert_eq!(devin_config["mcpServers"]["sdd"]["command"], "sdd");
assert!(!root.join(".devin/rules").exists());
assert!(!root.join(".devin/workflows").exists());
let devin_agent =
fs::read_to_string(root.join(".devin/agents/sdd-orchestrator/AGENT.md")).unwrap();
assert!(devin_agent.contains("allowed-tools:"));
assert!(devin_agent.contains("sdd init \"<nome-da-orquestracao>\""));
assert!(!devin_agent.contains("Exec(sdd *)"));
assert!(devin_agent.contains("Exec(sdd orchestration next)"));
assert!(devin_agent.contains("Exec(sdd context build)"));
assert!(!devin_agent.contains("Exec(sdd artifact save)"));
let devin_denies = devin_agent.split_once(" deny:\n").unwrap().1;
assert!(devin_denies.contains("Exec(sdd install)"));
assert!(devin_denies.contains("Exec(sdd auto)"));
assert!(!devin_agent
.lines()
.filter(|line| line.trim_start().starts_with("- Exec("))
.any(|line| line.contains('*')));
for rel in collect_relative_files(&root.join(".devin/agents")) {
let generated = fs::read_to_string(root.join(".devin/agents").join(&rel)).unwrap();
assert!(
!generated
.lines()
.filter(|line| line.trim_start().starts_with("- Exec("))
.any(|line| line.contains('*')),
"Devin Exec permissions must use literal command prefixes, not globs: {rel}"
);
let deny = generated.split_once(" deny:\n").unwrap().1;
for prefix in [
"Exec(sdd orchestration approve)",
"Exec(sdd orchestration reject)",
"Exec(sdd orchestration confirm)",
"Exec(sdd install)",
"Exec(sdd update)",
"Exec(sdd upgrade)",
"Exec(sdd auto)",
] {
assert!(deny.contains(prefix), "missing Devin deny {prefix}: {rel}");
}
}
assert!(devin_agent.contains("Custom subagents do Devin não recebem `ask_user_question`"));
assert!(devin_agent.contains("orchestration.isolation"));
let devin_skill =
fs::read_to_string(root.join(".devin/skills/orchestration/SKILL.md")).unwrap();
assert!(devin_skill.contains("name: orchestration"));
assert!(devin_skill.contains("docs/<slug-da-orquestracao>/"));
assert_portable_orchestration_skill_contract(&devin_skill);
let devin_prd = fs::read_to_string(root.join(".devin/skills/prd/SKILL.md")).unwrap();
assert!(devin_prd.contains("name: prd"));
assert!(devin_prd.contains("Rode `sdd prd \"$ARGUMENTS\"`"));
assert!(!devin_prd.contains("model: claude-sonnet-4-6"));
let devin_sdd = fs::read_to_string(root.join(".devin/skills/sdd/SKILL.md")).unwrap();
assert!(devin_sdd.contains("name: sdd"));
assert!(devin_sdd.contains("sdd clients doctor"));
let devin_agentic = fs::read_to_string(root.join(".devin/skills/workflow/SKILL.md")).unwrap();
assert!(devin_agentic.contains("name: workflow"));
assert!(devin_agentic.contains("sdd workflow run loop"));
let cursor_routing = fs::read_to_string(root.join(".cursor/model-routing.yaml")).unwrap();
assert!(cursor_routing.contains("default:"));
assert!(cursor_routing.contains("stages:"));
let cursor_rule = fs::read_to_string(root.join(".cursor/rules/sdd.mdc")).unwrap();
assert!(cursor_rule.contains("sdd init \"<nome-da-orquestracao>\""));
assert!(cursor_rule.contains("docs/<slug-da-orquestracao>/"));
assert!(cursor_rule.contains("Contrato de persistência no Cursor"));
assert!(cursor_rule.contains("sdd orchestration --name \"<nome-da-orquestracao>\""));
assert!(cursor_rule.contains("não entregue o artefato final apenas na sessão do chat"));
assert!(cursor_rule.contains("Formato de saída por etapa"));
assert!(cursor_rule.contains("`prd`: resumo executivo"));
assert!(cursor_rule.contains("`Diagramas` com Mermaid/Excalidraw"));
assert!(cursor_rule.contains("`refinement`: documento único e enxuto"));
let cursor_command =
fs::read_to_string(root.join(".cursor/commands/orchestration.md")).unwrap();
assert!(cursor_command.contains("sdd orchestration next \"$ARGUMENTS\""));
assert!(cursor_command.contains("sdd orchestration done --name"));
let cursor_sdd = fs::read_to_string(root.join(".cursor/commands/sdd.md")).unwrap();
assert!(cursor_sdd.contains("sdd clients doctor"));
assert!(cursor_sdd.contains("sdd clients sync --targets all --force"));
let cursor_prd = fs::read_to_string(root.join(".cursor/commands/prd.md")).unwrap();
assert!(cursor_prd.contains("Rode `sdd prd \"$ARGUMENTS\"`"));
let cursor_agent = fs::read_to_string(root.join(".cursor/agents/sdd-orchestrator.md")).unwrap();
assert!(cursor_agent.contains("name: sdd-orchestrator"));
assert!(cursor_agent.contains("Formato de saída por etapa"));
let cursor_skill =
fs::read_to_string(root.join(".cursor/skills/orchestration/SKILL.md")).unwrap();
assert!(cursor_skill.contains("name: orchestration"));
assert_portable_orchestration_skill_contract(&cursor_skill);
for prefix in [
".agents/skills",
".claude/skills",
".cursor/skills",
".opencode/skills",
".devin/skills",
".trae/skills",
] {
assert_core_stage_skills(root, prefix);
}
assert_stage_command_fallbacks(root);
for rel in [
".claude/commands/diagram.md",
".cursor/commands/diagram.md",
".opencode/commands/diagram.md",
".trae/commands/diagram.md",
] {
let content = fs::read_to_string(root.join(rel)).unwrap();
assert_diagram_command_contract(&content);
}
let devin_diagram = fs::read_to_string(root.join(".devin/skills/diagram/SKILL.md")).unwrap();
assert!(devin_diagram.contains("name: diagram"));
assert!(devin_diagram.contains("sdd diagram doctor --json"));
let opencode_prd = fs::read_to_string(root.join(".opencode/commands/prd.md")).unwrap();
assert!(opencode_prd.contains("agent: sdd-orchestrator"));
assert!(opencode_prd.contains("SDD_PROVIDER=opencode sdd prd \"$ARGUMENTS\""));
assert!(opencode_prd.contains("Diagramas Mermaid/Excalidraw"));
assert!(opencode_prd.contains("sdd init \"<nome-da-orquestracao>\""));
let opencode_loop = fs::read_to_string(root.join(".opencode/commands/workflow.md")).unwrap();
assert!(opencode_loop.contains("SDD_PROVIDER=opencode sdd workflow run loop"));
let opencode_agent =
fs::read_to_string(root.join(".opencode/agents/sdd-orchestrator.md")).unwrap();
assert!(opencode_agent.contains("Formato de saída por etapa"));
assert!(opencode_agent.contains("`tasks`: épicos"));
assert!(opencode_agent.contains("`Prompts Agent` standalone por tarefa"));
let trae_command = fs::read_to_string(root.join(".trae/commands/orchestration.md")).unwrap();
assert!(trae_command.contains("command -v sdd"));
assert!(trae_command.contains("cargo run --bin sdd -- orchestration"));
assert!(trae_command.contains("sdd orchestration next \"$ARGUMENTS\""));
assert!(trae_command.contains("sdd orchestration done --name"));
let trae_tasks = fs::read_to_string(root.join(".trae/commands/tasks.md")).unwrap();
assert!(trae_tasks.contains("Antes de rodar comandos `sdd`, resolva o CLI uma vez"));
assert!(trae_tasks.contains("cargo run --bin sdd --"));
assert!(trae_tasks.contains("Rode `sdd tasks \"$ARGUMENTS\"`"));
assert!(trae_tasks
.to_lowercase()
.contains("critérios de aceite em gherkin"));
assert!(trae_tasks.contains("Prompts Agent standalone por tarefa"));
let trae_skill = fs::read_to_string(root.join(".trae/skills/orchestration/SKILL.md")).unwrap();
assert!(trae_skill.contains("command -v sdd"));
assert!(trae_skill.contains("cargo run --bin sdd -- orchestration"));
assert!(trae_skill.contains("sdd init \"<nome-da-orquestracao>\""));
assert!(trae_skill.contains("docs/<slug-da-orquestracao>/"));
assert!(trae_skill.contains("Formato de saída por etapa"));
assert!(trae_skill.contains("`techspec`: visão técnica"));
assert!(trae_skill.contains("`Prompts Agent`"));
let antigravity_rule = fs::read_to_string(root.join(".agents/rules/sdd.md")).unwrap();
assert!(antigravity_rule.contains("Formato de saída por etapa"));
assert!(antigravity_rule.contains("`memory`: estado atual"));
assert_success(run(
root,
&[
"clients",
"sync",
"--targets",
"cursor,trae,opencode,antigravity,devin,kiro",
"--dry-run",
],
));
fs::remove_dir_all(root.join(".cursor")).unwrap();
fs::remove_dir_all(root.join(".devin")).unwrap();
fs::remove_dir_all(root.join(".kiro")).unwrap();
assert_success(run(
root,
&["clients", "sync", "--targets", "cursor,devin,kiro"],
));
assert!(root.join(".cursor/model-routing.yaml").is_file());
assert!(root.join(".cursor/rules/sdd.mdc").is_file());
assert!(root.join(".cursor/commands/sdd.md").is_file());
assert!(root.join(".cursor/commands/orchestration.md").is_file());
assert!(root.join(".cursor/commands/prd.md").is_file());
assert!(root.join(".cursor/agents/sdd-orchestrator.md").is_file());
assert!(root.join(".cursor/skills/orchestration/SKILL.md").is_file());
assert_core_stage_skills(root, ".cursor/skills");
assert!(root.join(".devin/config.json").is_file());
assert!(!root.join(".devin/rules").exists());
assert!(root
.join(".devin/agents/sdd-orchestrator/AGENT.md")
.is_file());
assert!(root.join(".devin/skills/orchestration/SKILL.md").is_file());
assert_core_stage_skills(root, ".devin/skills");
assert!(root.join(".devin/skills/sdd/SKILL.md").is_file());
assert!(root.join(".devin/skills/workflow/SKILL.md").is_file());
assert!(!root.join(".devin/workflows").exists());
assert_kiro_surface(root);
assert_portable_orchestration_skill_contract(
&fs::read_to_string(root.join(".cursor/skills/orchestration/SKILL.md")).unwrap(),
);
assert_portable_orchestration_skill_contract(
&fs::read_to_string(root.join(".devin/skills/orchestration/SKILL.md")).unwrap(),
);
let recommendations = combined(&run(root, &["context", "recommend"]));
assert!(recommendations.contains("React/Next/Vite"));
assert!(recommendations.contains(".agents/skills/react-frontend/SKILL.md"));
assert!(recommendations.contains("Uso: trate cada linha como recomendação"));
assert!(recommendations.contains("Capability map"));
assert!(recommendations.contains("Matt Pocock Skills"));
assert!(recommendations.contains("Superpowers"));
assert!(recommendations.contains("Taste Skill"));
assert!(recommendations.contains("Code intelligence opcional"));
assert!(recommendations.contains(".agents/skills/execution-discipline/SKILL.md"));
let polyglot = tempdir().unwrap();
let polyglot_root = polyglot.path();
fs::write(
polyglot_root.join("go.mod"),
"module example.com/api\nrequire github.com/gin-gonic/gin v1.10.0\n",
)
.unwrap();
fs::create_dir_all(polyglot_root.join(".github/workflows")).unwrap();
fs::write(polyglot_root.join(".github/workflows/ci.yml"), "name: ci\n").unwrap();
fs::create_dir_all(polyglot_root.join("api")).unwrap();
fs::write(polyglot_root.join("api/openapi.yaml"), "openapi: 3.1.0\n").unwrap();
fs::write(polyglot_root.join(".env.example"), "DATABASE_URL=\n").unwrap();
let polyglot_recommendations = combined(&run(polyglot_root, &["context", "recommend"]));
assert!(polyglot_recommendations.contains("Go project conventions"));
assert!(polyglot_recommendations.contains("CI and release readiness"));
assert!(polyglot_recommendations.contains("API contract conventions"));
assert!(polyglot_recommendations.contains("Secrets and runtime config handling"));
assert_success(run(
root,
&[
"discover",
"--name",
"Frontend Discovery",
"--force",
"brownfield react app",
],
));
let discovery =
fs::read_to_string(root.join("docs/frontend-discovery/00-project-discovery.md")).unwrap();
assert!(discovery.contains("Recomendações de skills e regras"));
assert!(discovery.contains("React/Next/Vite"));
assert_success(run(
root,
&[
"context",
"recommend",
"--name",
"Frontend Discovery",
"--write",
],
));
let written =
fs::read_to_string(root.join("docs/frontend-discovery/00-context-recommendations.md"))
.unwrap();
assert!(written.contains("- Orquestração: Frontend Discovery"));
assert!(written.contains(".agents/skills/react-frontend/SKILL.md"));
}
#[test]
fn opencode_session_ergonomics_surfaces_are_generated_and_validated() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
fs::remove_dir_all(root.join(".opencode")).unwrap();
assert_success(run(
root,
&["clients", "sync", "--targets", "opencode", "--force"],
));
for rel in [
".opencode/commands/verify-changes.md",
".opencode/commands/team-review.md",
".opencode/commands/team-execution.md",
".opencode/commands/security-audit.md",
".opencode/commands/test-design.md",
".opencode/commands/fast-lane.md",
".opencode/skills/code-review/SKILL.md",
".opencode/skills/security-privacy/SKILL.md",
".opencode/skills/test-strategy/SKILL.md",
".opencode/skills/architecture-deepening/SKILL.md",
".opencode/skills/execution-discipline/SKILL.md",
".opencode/skills/release-readiness/SKILL.md",
".opencode/plugins/sdd-security-scan.ts",
".opencode/plugins/sdd-verification.ts",
".opencode/plugins/sdd-parallel-guard.ts",
] {
assert!(root.join(rel).is_file(), "{rel} should be generated");
}
let verify = fs::read_to_string(root.join(".opencode/commands/verify-changes.md")).unwrap();
assert!(verify.contains("sdd ci"));
assert!(verify.contains("sdd eval orchestration"));
assert!(verify.contains("sdd quality report"));
assert!(verify.contains("checkpoint humano"));
assert!(verify.contains("docs/<slug-da-orquestracao>/"));
let team_review = fs::read_to_string(root.join(".opencode/commands/team-review.md")).unwrap();
assert!(team_review.contains("code-review"));
assert!(team_review.contains("security-privacy"));
assert!(team_review.contains("test-strategy"));
assert!(team_review.contains("fallback sequencial"));
let fast_lane = fs::read_to_string(root.join(".opencode/commands/fast-lane.md")).unwrap();
assert!(fast_lane.contains("sdd risk"));
assert!(fast_lane.contains("baixo risco"));
assert!(fast_lane.contains("Não autoaprove"));
let code_review =
fs::read_to_string(root.join(".opencode/skills/code-review/SKILL.md")).unwrap();
assert!(code_review.contains("name: code-review"));
assert!(code_review.contains("Crítico"));
let security_plugin =
fs::read_to_string(root.join(".opencode/plugins/sdd-security-scan.ts")).unwrap();
assert!(security_plugin.contains("SENSITIVE_FILES"));
assert!(security_plugin.contains(r"\.env"));
assert!(security_plugin.contains("id_ed25519"));
assert!(security_plugin.contains("tool.execute.before"));
assert!(security_plugin.contains("async (input: any, output: any)"));
assert!(security_plugin.contains("const args = output?.args || {}"));
assert!(security_plugin.contains("args.file_path"));
assert!(!security_plugin.contains("input.args"));
assert!(!security_plugin.contains("sk-"));
if Command::new("bun")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
{
let plugin_path = root.join(".opencode/plugins/sdd-security-scan.ts");
let hook_probe = r#"
const pluginModule = await import(process.env.SDD_SECURITY_PLUGIN)
const hooks = await pluginModule.default()
const before = hooks["tool.execute.before"]
await before({ tool: "write" }, { args: { file_path: "src/main.ts" } })
await before({ tool: "read" }, { args: { file_path: ".env" } })
let blocked = false
try {
await before({ tool: "write" }, { args: { file_path: ".env" } })
} catch (error) {
blocked = String(error).includes("SDD SECURITY BLOCK")
}
if (!blocked) throw new Error("sensitive write was not blocked")
"#;
let hook_output = Command::new("bun")
.args(["-e", hook_probe])
.env("SDD_SECURITY_PLUGIN", &plugin_path)
.current_dir(root)
.output()
.unwrap();
assert!(
hook_output.status.success(),
"OpenCode security hook probe failed:\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&hook_output.stdout),
String::from_utf8_lossy(&hook_output.stderr)
);
}
let verification_plugin =
fs::read_to_string(root.join(".opencode/plugins/sdd-verification.ts")).unwrap();
assert!(verification_plugin.contains("sdd ci"));
assert!(verification_plugin.contains("/verify-changes"));
assert!(verification_plugin.contains("Não executa formatador"));
assert_success(run(
root,
&["clients", "sync", "--targets", "all", "--force"],
));
assert_success(run(root, &["clients", "doctor", "--strict"]));
assert_success(run(
root,
&["capabilities", "doctor", "--targets", "opencode"],
));
let capabilities = combined(&run(root, &["capabilities", "list"]));
assert!(capabilities.contains("opencode-verify-changes"));
assert!(capabilities.contains("opencode-fast-lane"));
assert!(capabilities.contains("opencode-parallel-guard-plugin"));
fs::remove_file(root.join(".opencode/skills/release-readiness/SKILL.md")).unwrap();
let missing_skill = run(root, &["capabilities", "doctor", "--targets", "opencode"]);
assert!(
!missing_skill.status.success(),
"expected opencode skill collection gap to fail\n{}",
combined(&missing_skill)
);
let missing_skill_text = combined(&missing_skill);
assert!(missing_skill_text.contains("opencode-selected-skills"));
assert!(missing_skill_text.contains(".opencode/skills/release-readiness/SKILL.md"));
assert_success(run(
root,
&["clients", "sync", "--targets", "opencode", "--force"],
));
fs::write(
root.join(".opencode/plugins/sdd-verification.ts"),
"/* Hook opt-in para OpenCode */\nexport default async () => ({})\n",
)
.unwrap();
let invalid_plugin = run(root, &["capabilities", "doctor", "--targets", "opencode"]);
assert!(
!invalid_plugin.status.success(),
"expected weak opencode plugin to fail\n{}",
combined(&invalid_plugin)
);
let invalid_plugin_text = combined(&invalid_plugin);
assert!(invalid_plugin_text.contains("missing OpenCode plugin hook export"));
assert!(invalid_plugin_text.contains(".opencode/plugins/sdd-verification.ts"));
}
#[test]
fn capabilities_catalog_doctor_sync_and_recommend_cover_portable_surfaces() {
let tmp = tempdir().unwrap();
let root = tmp.path();
fs::write(
root.join("package.json"),
r#"{"dependencies":{"react":"latest","vitest":"latest"}}"#,
)
.unwrap();
assert_success(run(
repo_root(),
&[
"install",
root.to_str().unwrap(),
"--preset",
"frontend-react",
],
));
assert_success(run(
root,
&["clients", "sync", "--targets", "all", "--force"],
));
let list = combined(&run(root, &["capabilities", "list"]));
assert!(list.contains("version: 2"));
assert!(list.contains("orchestration-skill"));
assert!(list.contains("providers-doctor"));
assert!(list.contains("hooks-guardrails"));
assert!(list.contains("opencode-verify-changes"));
assert!(list.contains("opencode-selected-skills"));
assert!(list.contains("opencode-verification-plugin"));
let json = combined(&run(root, &["capabilities", "list", "--json"]));
let parsed = serde_json::from_str::<Value>(&json).expect("capabilities list json");
assert_eq!(parsed.get("version").and_then(Value::as_u64), Some(2));
assert!(json_contains_text(&parsed, "orchestration-command"));
assert!(json_contains_text(
&parsed,
".opencode/commands/verify-changes.md"
));
assert!(json_contains_text(
&parsed,
".agents/skills/orchestration/SKILL.md"
));
assert_success(run(root, &["capabilities", "doctor", "--targets", "all"]));
assert_success(run(root, &["clients", "doctor", "--strict"]));
let sync = combined(&run(
root,
&[
"capabilities",
"sync",
"--targets",
"cursor,devin",
"--dry-run",
],
));
assert!(sync.contains("SDD capabilities sync"));
assert!(sync.contains(".cursor/commands/orchestration.md"));
assert!(sync.contains(".devin/skills/orchestration/SKILL.md"));
assert_success(run(root, &["capabilities", "recommend", "--write"]));
let recommendations =
fs::read_to_string(root.join(".sdd/capability-recommendations.md")).unwrap();
assert!(recommendations.contains("Resource adoption map"));
assert!(recommendations.contains("Núcleo portável"));
assert!(recommendations.contains("Codex: `.agents/skills`"));
assert!(recommendations.contains("React/Next/Vite"));
}
#[test]
fn skills_catalog_doctor_sync_and_recommend_cover_hybrid_skills() {
let tmp = tempdir().unwrap();
let root = tmp.path();
fs::write(
root.join("package.json"),
r#"{"dependencies":{"react":"latest","vitest":"latest"}}"#,
)
.unwrap();
fs::create_dir_all(root.join(".github/workflows")).unwrap();
fs::write(root.join(".github/workflows/ci.yml"), "name: ci\n").unwrap();
assert_success(run(
repo_root(),
&[
"install",
root.to_str().unwrap(),
"--preset",
"frontend-react",
],
));
let list = combined(&run(root, &["skills", "list", "--json"]));
let parsed = serde_json::from_str::<Value>(&list).expect("skills list json");
assert_eq!(parsed.get("version").and_then(Value::as_u64), Some(1));
for id in [
"diagram-design",
"react-frontend",
"test-strategy-local",
"ci-release",
"bdd-gherkin",
"code-review",
"generic-project",
"grilling",
"grill-me",
"grill-with-docs",
"domain-modeling",
] {
assert!(json_contains_text(&parsed, id), "missing skill id {id}");
}
let skills_doctor_output = run(root, &["skills", "doctor", "--strict"]);
let skills_doctor = combined(&skills_doctor_output);
assert_success(skills_doctor_output);
assert!(skills_doctor.contains("╭─ SDD skills doctor"));
assert!(skills_doctor.contains("│ ✓ PASS"));
assert!(skills_doctor.contains("◇ Missing (0)"));
assert!(skills_doctor.contains("◇ Invalid (0)"));
assert!(skills_doctor.contains("◇ Checked"));
assert!(
skills_doctor.find("◇ Missing").unwrap() < skills_doctor.find("◇ Checked").unwrap(),
"skills issues should be surfaced before checked inventory\n{skills_doctor}"
);
let sync = combined(&run(
root,
&[
"skills",
"sync",
"--targets",
"codex,claude,cursor,devin,opencode,trae,antigravity",
"--dry-run",
],
));
assert!(sync.contains("SDD skills sync"));
assert!(sync.contains(".agents/skills/react-frontend/SKILL.md"));
assert!(sync.contains(".claude/skills/react-frontend/SKILL.md"));
assert_success(run(
root,
&["skills", "sync", "--targets", "codex,claude", "--force"],
));
let synced_skill = root.join(".agents/skills/react-frontend/SKILL.md");
assert!(synced_skill.is_file());
assert!(root
.join(".claude/skills/diagram-design/assets/template.html")
.is_file());
assert!(root
.join(".claude/skills/diagram-design/references/style-guide.md")
.is_file());
assert_success(run(
root,
&[
"skills",
"validate",
synced_skill.to_str().unwrap(),
"--json",
],
));
assert_success(run(root, &["skills", "recommend", "--write"]));
let recommendations = fs::read_to_string(root.join(".sdd/skill-recommendations.md")).unwrap();
assert!(recommendations.contains("Skill Recommendations"));
assert!(recommendations.contains("react-frontend"));
let context = combined(&run(root, &["context", "recommend"]));
for id in ["react-frontend", "test-strategy-local", "ci-release"] {
assert!(json_contains_text(&parsed, id));
}
assert!(context.contains(".agents/skills/react-frontend/SKILL.md"));
assert!(context.contains(".agents/skills/local-test-strategy/SKILL.md"));
assert!(context.contains(".agents/skills/ci-release/SKILL.md"));
}
#[test]
fn skills_sync_covers_codex_skill_targets() {
let sync = combined(&run(
repo_root(),
&["skills", "sync", "--targets", "codex", "--dry-run"],
));
assert!(sync.contains("SDD skills sync"));
assert!(sync.contains(".agents/skills/architecture-deepening/SKILL.md"));
assert!(sync.contains(".agents/skills/diagram-design"));
}
#[test]
fn skills_doctor_failure_uses_tui_summary_before_inventory() {
let target = tempdir().unwrap();
let root = target.path();
fs::create_dir_all(root.join("templates")).unwrap();
fs::write(
root.join("templates/skill-catalog.yaml"),
r#"version: 1
skills:
- id: tiny-skill
title: Tiny Skill
description: Minimal failing fixture
category: test
status: available
local_path: .agents/skills/tiny-skill/SKILL.md
"#,
)
.unwrap();
let output = run(root, &["skills", "doctor"]);
assert_code(output, 1);
let text = combined(&run(root, &["skills", "doctor"]));
assert!(text.contains("╭─ SDD skills doctor"));
assert!(text.contains("│ ✕ FAIL"));
assert!(text.contains("◇ Missing (1)"));
assert!(text.contains("◇ Checked (1)"));
assert!(
text.find("◇ Missing").unwrap() < text.find("◇ Checked").unwrap(),
"missing skills should be surfaced before checked inventory\n{text}"
);
assert!(text.contains("◇ tiny-skill"));
assert!(text.contains("missing .agents/skills/tiny-skill/SKILL.md"));
assert!(!text.contains("- tiny-skill missing"));
}
#[test]
fn workflows_validate_run_and_persist_dynamic_dsl() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let list = combined(&run(root, &["workflow", "list", "--json"]));
let workflows = serde_json::from_str::<Value>(&list).expect("workflow list json");
for id in [
"standard-sdd",
"deep-execution-review",
"discovery-skill-hardening",
"bugfix-diagnostic-loop",
] {
let paths = workflows
.get(id)
.and_then(Value::as_array)
.unwrap_or_else(|| panic!("missing workflow {id}"));
assert_eq!(
paths.len(),
1,
"workflow {id} should not list duplicate paths"
);
assert_success(run(root, &["workflow", "validate", id, "--json"]));
}
let bad_dir = root.join("bad-workflows");
fs::create_dir_all(&bad_dir).unwrap();
let cases = [
(
"unknown-action.yaml",
"version: 1\nid: bad-action\ntitle: Bad Action\nnodes:\n - id: start\n action: mystery\n",
"action desconhecida",
),
(
"missing-destination.yaml",
"version: 1\nid: missing-destination\ntitle: Missing Destination\nnodes:\n - id: start\n action: command\n command: echo start\n - id: stranded\n action: eval\n - id: end\n action: notify\nedges:\n - from: start\n to: end\n",
"sem destino",
),
(
"loop-without-limit.yaml",
"version: 1\nid: loop-without-limit\ntitle: Loop Without Limit\nnodes:\n - id: start\n action: command\n command: echo start\n - id: end\n action: notify\nedges:\n - from: start\n to: end\n - from: end\n to: start\n",
"sem max_iterations",
),
(
"checkpoint-bypass.yaml",
"version: 1\nid: checkpoint-bypass\ntitle: Checkpoint Bypass\nnodes:\n - id: gate\n action: checkpoint\n stage: prd\n command: sdd demand approve DEMO --gate prd\n",
"autoaprovar",
),
];
for (name, body, expected) in cases {
let path = bad_dir.join(name);
fs::write(&path, body).unwrap();
let output = run(root, &["workflow", "validate", path.to_str().unwrap()]);
assert_code(output, 1);
assert!(
combined(&run(
root,
&["workflow", "validate", path.to_str().unwrap()]
))
.contains(expected),
"expected validation output to contain {expected}"
);
}
let standard = combined(&run(
root,
&[
"workflow",
"run",
"standard-sdd",
"--input",
"Criar busca por tags",
"--max-iterations",
"10",
"--json",
],
));
let standard = serde_json::from_str::<Value>(&standard).expect("standard workflow json");
assert_eq!(
standard.get("status").and_then(Value::as_str),
Some("awaiting_checkpoint")
);
assert!(json_contains_text(&standard, "prd"));
assert!(json_contains_text(&standard, "criar-busca-por-tags"));
let auto_root = tempdir().unwrap();
assert_success(run(
repo_root(),
&[
"install",
auto_root.path().to_str().unwrap(),
"--preset",
"generic",
],
));
assert_success(run(
auto_root.path(),
&[
"demand",
"enqueue",
"--id",
"AUTO-1",
"--type",
"story",
"--title",
"Criar busca por tags",
],
));
let auto = combined(&run(
auto_root.path(),
&["auto", "run", "--max-iterations", "10", "--json"],
));
assert!(auto.contains("\"produced\": 2"));
assert!(auto.contains("\"paused\""));
assert!(auto.contains("criar-busca-por-tags"));
let deep = combined(&run(
root,
&[
"workflow",
"run",
"deep-execution-review",
"--input",
"Corrigir regressão",
"--max-iterations",
"2",
"--json",
],
));
let deep = serde_json::from_str::<Value>(&deep).expect("deep workflow json");
assert_eq!(deep.get("status").and_then(Value::as_str), Some("error"));
assert_eq!(deep.get("iterations").and_then(Value::as_u64), Some(0));
assert!(json_contains_text(&deep, "execution"));
assert!(json_contains_text(&deep, "05-planning.html"));
assert!(!root
.join("docs/corrigir-regressao/06-execution.md")
.is_file());
assert!(!root.join("docs/corrigir-regressao/07-review.md").is_file());
assert!(root
.join(".sdd/workflows/deep-execution-review.json")
.is_file());
let status = combined(&run(
root,
&["workflow", "status", "deep-execution-review", "--json"],
));
let status = serde_json::from_str::<Value>(&status).expect("workflow status json");
assert_eq!(status.get("status").and_then(Value::as_str), Some("pass"));
assert!(json_contains_text(&status, "05-planning.html"));
}
#[test]
fn capabilities_doctor_rejects_semantic_drift_missing_sources_and_ascii_only_human_content() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let cursor_command = root.join(".cursor/commands/orchestration.md");
let cursor_text = fs::read_to_string(&cursor_command).unwrap();
fs::write(
&cursor_command,
cursor_text.replace("sdd orchestration", "sdd orchestrator"),
)
.unwrap();
let drift = run(root, &["capabilities", "doctor", "--targets", "cursor"]);
assert_code(drift, 1);
let drift_text = combined(&run(
root,
&["capabilities", "doctor", "--targets", "cursor"],
));
assert!(drift_text.contains("canonical `sdd orchestration`") || drift_text.contains("drifted"));
assert_success(run(
repo_root(),
&[
"update",
"--root",
root.to_str().unwrap(),
"--preset",
"generic",
"--dry-run",
],
));
fs::write(
root.join(".agents/skills/orchestration/SKILL.md"),
"---\nname: orchestration\ndescription: Portable orchestration.\n---\nRun `sdd orchestration \"$ARGUMENTS\"`. Treat orchestrator as legacy alias. Use checkpoints and `sdd init \"<name>\"`.\n",
)
.unwrap();
let ascii = run(root, &["capabilities", "doctor", "--targets", "codex"]);
assert_code(ascii, 1);
let ascii_text = combined(&run(
root,
&["capabilities", "doctor", "--targets", "codex"],
));
assert!(ascii_text.contains("PT-BR UTF-8 accents"));
let catalog_path = root.join(".sdd/templates/capability-catalog.yaml");
let mut catalog = fs::read_to_string(&catalog_path).unwrap();
catalog.push_str(
"\n - id: broken-source\n category: command\n title: Fonte ausente\n stages: [review]\n trigger: Testar fonte ausente.\n canonical_source: missing/canonical.md\n adapters:\n cli:\n command: sdd missing\n kind: cli\n fallback: Registrar falha.\n validation: [adapter-command]\n docs: []\n",
);
fs::write(&catalog_path, catalog).unwrap();
let missing = run(root, &["capabilities", "doctor", "--targets", "cli"]);
assert_code(missing, 1);
let missing_text = combined(&run(root, &["capabilities", "doctor", "--targets", "cli"]));
assert!(missing_text.contains("missing canonical_source missing/canonical.md"));
}
#[test]
fn orchestration_plugin_skill_mirrors_include_execution_and_adr_contract() {
let root = repo_root();
let canonical = canonical_orchestration_skill();
let agents = fs::read_to_string(
root.join(".agents/skills/orchestration-plugin/skills/orchestration/SKILL.md"),
)
.unwrap();
let claude = fs::read_to_string(
root.join(".claude/skills/orchestration-plugin/skills/orchestration/SKILL.md"),
)
.unwrap();
assert_eq!(agents, claude);
for (canonical_anchor, plugin_anchor) in [
(
"6. Execution: one task at a time, after reading the execution Context Pack and code-intelligence handoff.",
"6. Execution: one task at a time, after reading the execution Context Pack and code-intelligence handoff.",
),
(
"8. QA: validate observable behaviour against acceptance criteria (not a code review repeat). Checkpoint.",
"8. QA: validate observable behaviour against acceptance criteria (not a code review repeat). Checkpoint.",
),
(
"Before `/execution`, generate or read `sdd context build --name \"<orchestration>\" --stage execution --write`",
"Before `/execution`, generate or read `sdd context build --name \"<orchestration>\" --stage execution --write`",
),
(
"During `/execution`, use `execution-discipline`",
"During `/execution`, use `execution-discipline`",
),
] {
assert!(
canonical.contains(canonical_anchor),
"canonical orchestration skill is missing anchor: {canonical_anchor}"
);
assert!(
agents.contains(plugin_anchor),
"plugin orchestration skill is missing anchor: {plugin_anchor}"
);
}
}
#[test]
fn project_discovery_plugin_skill_mirrors_source_contract() {
let root = repo_root();
let agents = fs::read_to_string(
root.join(".agents/skills/orchestration-plugin/skills/project-discovery/SKILL.md"),
)
.unwrap();
let claude = fs::read_to_string(
root.join(".claude/skills/orchestration-plugin/skills/project-discovery/SKILL.md"),
)
.unwrap();
assert_eq!(agents, claude);
assert!(agents.contains("sdd discover --json --dry-run"));
assert!(agents.contains("sdd context recommend"));
assert!(agents.contains("Mapeie linguagem de domínio"));
assert!(agents.contains("Mapeie code intelligence opcional"));
assert!(agents.contains("Inclua capability map"));
}
#[test]
fn orchestration_plugin_skill_directories_are_complete_mirrors() {
let root = repo_root();
let source = root.join(".agents/skills/orchestration-plugin/skills");
assert_directory_mirror(
&source,
&root.join(".claude/skills/orchestration-plugin/skills"),
);
}
#[test]
fn default_client_hooks_do_not_block_automatic_loops() {
let root = repo_root();
for rel in [
".claude/settings.json",
".claude/skills/orchestration-plugin/hooks/hooks.json",
".agents/skills/orchestration-plugin/hooks/hooks.json",
] {
let text = fs::read_to_string(root.join(rel)).unwrap();
let value = serde_json::from_str::<Value>(&text).unwrap();
let hooks = value
.get("hooks")
.and_then(Value::as_object)
.unwrap_or_else(|| panic!("{rel} missing hooks object"));
for event in [
"PreToolUse",
"PostToolUse",
"SubagentStop",
"TaskCreated",
"TaskCompleted",
"TeammateIdle",
"Notification",
] {
assert_eq!(
hooks.get(event).and_then(Value::as_array).map(Vec::len),
Some(0),
"{rel} must keep {event} empty by default"
);
}
}
}
#[test]
fn opencode_prompt_file_refs_are_repaired_and_validated() {
let target = tempdir().unwrap();
let root = target.path();
fs::write(
root.join("opencode.json"),
r##"{
"$schema": "https://opencode.ai/config.json",
"agent": {
"frontend-dev": {
"description": "Frontend development specialist",
"mode": "subagent",
"prompt": "{file:.opencode/agents/frontend-dev.md}",
"color": "#3b82f6"
},
"api-dev": {
"description": "API specialist",
"mode": "subagent",
"prompt": "{$file:.opencode/agents/api-dev.md}",
"color": "#f59e0b"
}
}
}"##,
)
.unwrap();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
let frontend_prompt = root.join(".opencode/agents/frontend-dev.md");
let api_prompt = root.join(".opencode/agents/api-dev.md");
assert!(frontend_prompt.is_file());
assert!(api_prompt.is_file());
let frontend_content = fs::read_to_string(&frontend_prompt).unwrap();
assert!(frontend_content.contains("opencode.json"));
assert!(frontend_content.contains("AGENTS.md"));
assert_success(run(root, &["clients", "doctor"]));
fs::remove_file(&frontend_prompt).unwrap();
let doctor = run(root, &["clients", "doctor"]);
assert_code(doctor, 1);
let doctor_text = combined(&run(root, &["clients", "doctor"]));
assert!(doctor_text.contains("╭─ SDD clients doctor"));
assert!(doctor_text.contains("│ ✕ FAIL"));
assert!(doctor_text.contains("Invalid"));
assert!(doctor_text.contains("◇ Covered"));
assert!(
doctor_text.find("◇ Invalid").unwrap() < doctor_text.find("◇ Covered").unwrap(),
"invalid client issues should be surfaced before covered inventory\n{doctor_text}"
);
assert!(doctor_text.contains(".opencode/agents/frontend-dev.md"));
assert!(!doctor_text.contains("- opencode:"));
assert_success(run(root, &["clients", "sync", "--targets", "opencode"]));
assert!(frontend_prompt.is_file());
assert_success(run(root, &["clients", "doctor"]));
let init_target = tempdir().unwrap();
let init_root = init_target.path();
fs::write(
init_root.join("opencode.json"),
r#"{"agent":{"frontend-dev":{"prompt":"{file:.opencode/agents/frontend-dev.md}"}}}"#,
)
.unwrap();
assert_success(run(init_root, &["init", "--preset", "generic"]));
assert!(init_root.join(".opencode/agents/frontend-dev.md").is_file());
assert_success(run(init_root, &["clients", "doctor"]));
fs::write(
root.join("opencode.json"),
r#"{"agent":{"docs":{"prompt":"{file:.opencode/prompts/docs.md}"}}}"#,
)
.unwrap();
let unrepaired = run(root, &["clients", "doctor"]);
assert_code(unrepaired, 1);
assert!(combined(&run(root, &["clients", "doctor"])).contains(".opencode/prompts/docs.md"));
}
#[test]
fn strict_client_doctor_detects_and_repairs_generated_kiro_drift() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
repo_root(),
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
assert_success(run(root, &["clients", "doctor", "--strict", "--json"]));
let agent = root.join(".kiro/agents/sdd/orchestrator.md");
let mut content = fs::read_to_string(&agent).unwrap();
content.push_str("\n<!-- local drift -->\n");
fs::write(&agent, content).unwrap();
assert_success(run(root, &["clients", "doctor", "--json"]));
let strict = run(root, &["clients", "doctor", "--strict", "--json"]);
assert_code(strict, 1);
let strict_text = combined(&run(root, &["clients", "doctor", "--strict", "--json"]));
assert!(strict_text.contains("generated kiro adapter"));
assert!(strict_text.contains(".kiro/agents/sdd/orchestrator.md"));
assert_success(run(
root,
&["clients", "sync", "--targets", "kiro", "--force"],
));
assert_success(run(root, &["clients", "doctor", "--strict", "--json"]));
}
#[test]
fn fresh_install_sync_sequence_converges_on_opencode_and_trae_stage_skills() {
let target = tempdir().unwrap();
let root = target.path();
assert_success(run(
root,
&["install", root.to_str().unwrap(), "--preset", "generic"],
));
assert_success(run(root, &["clients", "sync", "--force"]));
assert_success(run(root, &["capabilities", "sync", "--force"]));
assert_success(run(root, &["skills", "sync", "--force"]));
let strict = run(root, &["clients", "doctor", "--strict", "--json"]);
let strict_text = combined(&strict);
assert_code(strict, 0);
assert!(
strict_text.contains("\"status\": \"pass\""),
"clients doctor --strict should report status pass: {strict_text}"
);
assert!(
strict_text.contains("\"invalid\": {}"),
"clients doctor --strict should report zero invalid entries \
(previously .opencode/skills/memory/SKILL.md, .trae/skills/memory/SKILL.md, \
and .trae/skills/orchestration/SKILL.md drifted here): {strict_text}"
);
let cap_doctor = run(root, &["capabilities", "doctor", "--json"]);
let cap_doctor_text = combined(&cap_doctor);
assert_code(cap_doctor, 0);
assert!(
!cap_doctor_text.contains("drifted from Rust generator"),
"capability doctor should report zero drift: {cap_doctor_text}"
);
}
#[test]
fn devin_force_sync_removes_only_known_legacy_rules_and_workflows() {
let target = tempdir().unwrap();
let root = target.path();
for rel in [
".devin/rules/sdd.md",
".devin/workflows/orchestration.md",
".devin/workflows/prd.md",
] {
let path = root.join(rel);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(path, "legacy SDD adapter\n").unwrap();
}
let custom = root.join(".devin/workflows/custom-user-playbook.md");
fs::write(&custom, "keep me\n").unwrap();
let dry_run = combined(&run(
root,
&[
"clients",
"sync",
"--targets",
"devin",
"--force",
"--dry-run",
],
));
assert!(dry_run.contains("dry-run remove"));
assert!(root.join(".devin/rules/sdd.md").is_file());
assert_success(run(
root,
&["clients", "sync", "--targets", "devin", "--force"],
));
assert!(!root.join(".devin/rules/sdd.md").exists());
assert!(!root.join(".devin/workflows/orchestration.md").exists());
assert!(!root.join(".devin/workflows/prd.md").exists());
assert!(custom.is_file());
}
#[test]
fn validators_risk_and_hooks_cover_old_script_behavior() {
let root = repo_root();
assert_success(run(
root,
&[
"validate-artifact",
"checkpoint",
"tests/fixtures/good/checkpoint.md",
],
));
assert_success(run(
root,
&[
"validate-artifact",
"project-discovery",
"tests/fixtures/good/project-discovery.md",
],
));
assert_code(
run(
root,
&[
"validate-artifact",
"project-discovery",
"tests/fixtures/bad/project-discovery-missing-capability-map.md",
],
),
1,
);
assert_code(
run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/bad/prd-missing-sections.md",
],
),
1,
);
assert_success(run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/good/prd-diagrams.md",
],
));
assert_success(run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/good/prd-diagrams-subheading.md",
],
));
assert_success(run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/good/prd-visual-companion.md",
],
));
assert_success(run(
root,
&[
"validate-artifact",
"techspec",
"tests/fixtures/good/techspec-visual-companion.md",
],
));
let invalid_prd = run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/bad/prd-invalid-diagrams.md",
],
);
assert_code(invalid_prd, 1);
assert!(combined(&run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/bad/prd-invalid-diagrams.md",
],
))
.contains("Diagramas"));
assert_code(
run(
root,
&[
"validate-artifact",
"prd",
"tests/fixtures/bad/prd-visual-companion-outside-assets.md",
],
),
1,
);
assert_success(run(
root,
&[
"validate-artifact",
"tasks",
"tests/fixtures/good/tasks-prompts-agent.md",
],
));
assert_success(run(
root,
&[
"validate-artifact",
"tasks",
"tests/fixtures/good/tasks-prompts-agent-subheading.md",
],
));
assert_code(
run(
root,
&[
"validate-artifact",
"tasks",
"tests/fixtures/bad/tasks-prompts-agent-missing-task.md",
],
),
1,
);
assert_code(
run(
root,
&[
"validate-artifact",
"checkpoint",
"tests/fixtures/bad/checkpoint-missing-decision-action.md",
],
),
1,
);
let risk = run(root, &["risk", "login SSO com permissao e dados pessoais"]);
assert_success(risk);
let risk_text = combined(&run(
root,
&["risk", "login SSO com permissao e dados pessoais"],
));
assert!(risk_text.contains("risk_level: high"));
assert!(risk_text.contains("data_sensitive"));
let hook_project = tempdir().unwrap();
let bad_task = r#"{"hook_event_name":"TaskCreated","task":{"description":"fazer tela"}}"#;
assert_code(
run_with(
root,
&["hook", "task-gate"],
Some(bad_task),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
),
2,
);
let good_task = r#"{"hook_event_name":"TaskCreated","task":{"description":"Origem PRD-1. Dado um usuario, Quando salva, Entao persiste. Definition of Done com testes."}}"#;
assert_success(run_with(
root,
&["hook", "task-gate"],
Some(good_task),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
));
assert_code(
run_with(
root,
&["hook", "bash-guard"],
Some(r#"{"tool_input":{"command":"git reset --hard HEAD"}}"#),
&[],
),
2,
);
assert_success(run_with(
root,
&["hook", "bash-guard"],
Some(r#"{"tool_input":{"command":"git push origin feature-x"}}"#),
&[],
));
assert_code(
run_with(
root,
&["hook", "bash-guard"],
Some(r#"{"tool_input":{"command":"git push --force-with-lease origin feature-x"}}"#),
&[],
),
2,
);
assert_success(run_with_env_values(
root,
&["hook", "bash-guard"],
Some(r#"{"tool_input":{"command":"git push --force-with-lease origin feature-x"}}"#),
&[("SDD_GUARD_APPROVED", "1")],
));
assert_code(
run_with(
root,
&["hook", "write-guard"],
Some(r#"{"tool_name":"Write","agent_type":"prd"}"#),
&[],
),
2,
);
fs::create_dir_all(hook_project.path().join("schemas")).unwrap();
fs::copy(
root.join("schemas/artifact-sections.json"),
hook_project.path().join("schemas/artifact-sections.json"),
)
.unwrap();
let bad_prd = hook_project.path().join("02-prd.md");
fs::write(&bad_prd, "# PRD\n\n## Resumo\nIncompleto.\n").unwrap();
let bad_prd_json = bad_prd.display().to_string().replace('\\', "/");
let payload = format!(
r#"{{"hook_event_name":"PostToolUse","tool_name":"Write","tool_input":{{"file_path":"{}"}}}}"#,
bad_prd_json
);
assert_code(
run_with(
root,
&["hook", "artifact-gate"],
Some(&payload),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
),
2,
);
let changelog = hook_project.path().join("CHANGELOG.md");
fs::write(
&changelog,
"# Changelog\n\n## 0.0.1\n\n- Feature: discovery e tasks de exemplo.\n",
)
.unwrap();
let changelog_json = changelog.display().to_string().replace('\\', "/");
let changelog_payload = format!(
r#"{{"hook_event_name":"PostToolUse","tool_name":"Edit","tool_input":{{"file_path":"{}"}}}}"#,
changelog_json
);
assert_success(run_with(
root,
&["hook", "artifact-gate"],
Some(&changelog_payload),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
));
assert_success(run_with(
root,
&["hook", "trace-log"],
Some(
r#"{"hook_event_name":"PostToolUse","tool_name":"Bash","tool_input":{"command":"cargo test"}}"#,
),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
));
assert_success(run_with(
root,
&["hook", "notify"],
Some(r#"{"hook_event_name":"Notification","matcher":"approval"}"#),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
));
assert_success(run_with(
root,
&["hook", "subagent-audit"],
Some(
r#"{"hook_event_name":"SubagentStop","agent_type":"review","transcript_path":"/tmp/transcript"}"#,
),
&[("CLAUDE_PROJECT_DIR", hook_project.path())],
));
assert!(hook_project.path().join(".sdd/events.jsonl").is_file());
assert!(hook_project
.path()
.join(".sdd/notifications.jsonl")
.is_file());
assert!(hook_project.path().join(".sdd/subagents.jsonl").is_file());
}
#[test]
fn diagram_attach_copies_visual_asset_and_updates_stage_artifact() {
let tmp = tempdir().unwrap();
let root = tmp.path();
assert_success(run(root, &["init", "Fluxo Visual"]));
assert_success(run(
root,
&[
"idea",
"--name",
"Fluxo Visual",
"--force",
"mapear jornada visual",
],
));
let idea = root.join("docs/fluxo-visual/01-idea.md");
let initial = fs::read_to_string(&idea).unwrap();
assert!(initial.contains("## Diagramas"));
let source = root.join("visual.html");
fs::write(&source, "<!doctype html><svg></svg>\n").unwrap();
assert_success(run(
root,
&[
"diagram",
"attach",
"--name",
"Fluxo Visual",
"--stage",
"idea",
"--file",
"visual.html",
"--title",
"Jornada principal",
"--source",
"01-idea.md",
],
));
let updated = fs::read_to_string(&idea).unwrap();
assert!(updated.contains("<!-- sdd-diagram:jornada-principal -->"));
assert!(updated.contains("assets/diagrams/idea-jornada-principal.html"));
assert!(root
.join("docs/fluxo-visual/assets/diagrams/idea-jornada-principal.html")
.is_file());
let installed_skill = root.join(".agents/skills/diagram-design");
fs::create_dir_all(installed_skill.join("references")).unwrap();
fs::write(
installed_skill.join("SKILL.md"),
"---\nname: diagram-design\ndescription: test bundle\n---\n",
)
.unwrap();
fs::write(
installed_skill.join("references/style-guide.md"),
"paper: #f5f5f5\nink: #2d3142\naccent: #eb6c36\nmuted: #4f5d75\n",
)
.unwrap();
let default_doctor = combined(&run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]));
assert!(default_doctor.contains("Style guide: default"));
fs::write(
installed_skill.join("references/style-guide.md"),
"accent: #125c9c\n",
)
.unwrap();
let custom_doctor = run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]);
assert_success(custom_doctor);
let custom_doctor = combined(&run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]));
assert!(custom_doctor.contains("Style guide: custom"));
}
#[test]
fn t03_rastreabilidade_is_last_required_section_in_generated_stage_artifact() {
let tmp = tempdir().unwrap();
let root = tmp.path();
install_generic_project(root);
assert_success(run(root, &["init", "Teste Ordem"]));
let output = run(
root,
&[
"idea",
"--name",
"Teste Ordem",
"--force",
"feature de teste",
],
);
assert_success(output);
let artifact = fs::read_to_string(root.join("docs/teste-ordem/01-idea.md")).unwrap();
let lines: Vec<&str> = artifact.lines().collect();
let rast_pos = lines
.iter()
.position(|l| l.trim() == "## Rastreabilidade")
.expect("## Rastreabilidade deve existir no artefato");
let headers_before_rast = lines[..rast_pos]
.iter()
.filter(|l| l.starts_with("## "))
.count();
assert!(
headers_before_rast >= 3,
"## Rastreabilidade deve aparecer após pelo menos 3 seções de conteúdo; \
encontrados {headers_before_rast} cabeçalhos ## antes dela"
);
let first_h2_pos = lines
.iter()
.position(|l| l.starts_with("## "))
.expect("deve haver pelo menos um cabeçalho ##");
assert!(
rast_pos > first_h2_pos,
"## Rastreabilidade não deve ser a primeira seção do artefato"
);
}
#[test]
fn t04_no_provider_fields_in_artifact_without_real_telemetry() {
let tmp = tempdir().unwrap();
let root = tmp.path();
install_generic_project(root);
assert_success(run(root, &["init", "Teste Sem Telemetria"]));
let output = run(
root,
&[
"prd",
"--name",
"Teste Sem Telemetria",
"--force",
"feature sem telemetria",
],
);
assert_success(output);
let artifact = fs::read_to_string(root.join("docs/teste-sem-telemetria/02-prd.md")).unwrap();
assert!(
!artifact.contains("- Provider:"),
"sem telemetria real, '- Provider:' não deve aparecer no artefato"
);
assert!(
!artifact.contains("- Modelo:"),
"sem telemetria real, '- Modelo:' não deve aparecer no artefato"
);
assert!(
!artifact.contains("- Effort:"),
"sem telemetria real, '- Effort:' não deve aparecer no artefato"
);
assert!(
!artifact.contains("- Offline:"),
"sem telemetria real, '- Offline:' não deve aparecer no artefato"
);
}
#[test]
fn t04_provider_fields_present_when_real_telemetry_env_set() {
let tmp = tempdir().unwrap();
let root = tmp.path();
install_generic_project(root);
assert_success(run(root, &["init", "Teste Com Telemetria"]));
let output = run_with_env_values(
root,
&[
"idea",
"--name",
"Teste Com Telemetria",
"--force",
"feature com telemetria",
],
None,
&[
("SDD_USAGE_INPUT_TOKENS", "100"),
("SDD_USAGE_OUTPUT_TOKENS", "200"),
("SDD_USAGE_TOTAL_TOKENS", "300"),
],
);
assert_success(output);
let artifact = fs::read_to_string(root.join("docs/teste-com-telemetria/01-idea.md")).unwrap();
assert!(
artifact.contains("- Provider:"),
"com telemetria real, '- Provider:' deve aparecer no artefato"
);
assert!(
artifact.contains("- Tokens:"),
"com telemetria real, '- Tokens:' deve aparecer no artefato"
);
}
#[test]
fn t06_format_flag_listed_in_stage_help() {
let tmp = tempdir().unwrap();
let help = run(tmp.path(), &["prd", "--help"]);
let out = combined(&help);
assert!(
out.contains("--format"),
"sdd prd --help deve listar --format, mas obteve:\n{out}"
);
assert!(
out.contains("md") || out.contains("html"),
"sdd prd --help deve listar pelo menos 'md' e 'html' como valores de --format, mas obteve:\n{out}"
);
}
#[test]
fn t06_no_telemetry_flag_listed_in_stage_help() {
let tmp = tempdir().unwrap();
let help = run(tmp.path(), &["idea", "--help"]);
let out = combined(&help);
assert!(
out.contains("--no-telemetry"),
"sdd idea --help deve listar --no-telemetry, mas obteve:\n{out}"
);
}
#[test]
fn t07_no_telemetry_suppresses_provider_fields_even_with_env_set() {
let tmp = tempdir().unwrap();
let root = tmp.path();
install_generic_project(root);
assert_success(run(root, &["init", "Teste No Telemetry"]));
let output = run_with_env_values(
root,
&[
"prd",
"--name",
"Teste No Telemetry",
"--force",
"--no-telemetry",
"feature com --no-telemetry",
],
None,
&[
("SDD_USAGE_INPUT_TOKENS", "500"),
("SDD_USAGE_OUTPUT_TOKENS", "100"),
("SDD_USAGE_TOTAL_TOKENS", "600"),
],
);
assert_success(output);
let artifact = fs::read_to_string(root.join("docs/teste-no-telemetry/02-prd.md")).unwrap();
assert!(
!artifact.contains("- Provider:"),
"--no-telemetry: '- Provider:' não deve aparecer mesmo com SDD_USAGE_* setado"
);
assert!(
!artifact.contains("- Tokens:"),
"--no-telemetry: '- Tokens:' não deve aparecer mesmo com SDD_USAGE_* setado"
);
assert!(
!artifact.contains("- Modelo:"),
"--no-telemetry: '- Modelo:' não deve aparecer mesmo com SDD_USAGE_* setado"
);
}
#[test]
fn t07_without_no_telemetry_flag_env_telemetry_still_emitted() {
let tmp = tempdir().unwrap();
let root = tmp.path();
install_generic_project(root);
assert_success(run(root, &["init", "Teste Com Env Telemetry"]));
let output = run_with_env_values(
root,
&[
"prd",
"--name",
"Teste Com Env Telemetry",
"--force",
"feature sem supressao",
],
None,
&[
("SDD_USAGE_INPUT_TOKENS", "500"),
("SDD_USAGE_OUTPUT_TOKENS", "100"),
("SDD_USAGE_TOTAL_TOKENS", "600"),
],
);
assert_success(output);
let artifact = fs::read_to_string(root.join("docs/teste-com-env-telemetry/02-prd.md")).unwrap();
assert!(
artifact.contains("- Provider:"),
"sem --no-telemetry e com SDD_USAGE_* setado, '- Provider:' deve aparecer"
);
assert!(
artifact.contains("- Tokens:"),
"sem --no-telemetry e com SDD_USAGE_* setado, '- Tokens:' deve aparecer"
);
}
#[test]
fn default_features_include_html_gen() {
let manifest = fs::read_to_string(repo_root().join("Cargo.toml")).unwrap();
assert!(
manifest.contains("default = [\"html-gen\"]"),
"Cargo.toml deve incluir html-gen no build default"
);
}
#[test]
#[cfg(not(feature = "html-gen"))]
fn ca_10_no_default_features_html_required_flow_fails() {
let dir = tempdir().unwrap();
let root = dir.path();
let output = run_no_default_features(
root,
&[
"discover",
"--name",
"HTML Obrigatório Sem Feature",
"--format",
"html",
"mapear projeto",
],
);
assert!(
!output.status.success(),
"esperado falha quando Project Discovery exige HTML sem feature html-gen"
);
let err = combined(&output);
assert!(
err.contains("html-gen"),
"mensagem de erro deve mencionar 'html-gen', mas foi: {err}"
);
assert!(
err.contains("--no-default-features") || err.contains("default-features"),
"mensagem de erro deve orientar sobre build reduzido/default-features, mas foi: {err}"
);
let html_path = root.join("docs/html-obrigatorio-sem-feature/00-project-discovery.html");
assert!(
!html_path.exists(),
"arquivo HTML obrigatório não deve ser gerado sem feature html-gen"
);
let md_path = root.join("docs/html-obrigatorio-sem-feature/00-project-discovery.md");
assert!(
!md_path.exists(),
"não deve haver downgrade silencioso para Markdown quando HTML obrigatório foi pedido"
);
}
#[test]
#[cfg(not(feature = "html-gen"))]
fn ca_10_no_default_features_discovery_companion_fails_without_html_gen() {
let dir = tempdir().unwrap();
let root = dir.path();
let output = run_no_default_features(
root,
&[
"discover",
"--name",
"Discovery Default Sem Feature",
"mapear projeto",
],
);
assert!(
!output.status.success(),
"esperado falha no Discovery default sem feature html-gen porque companion HTML é obrigatório"
);
let err = combined(&output);
assert!(
err.contains("html-gen"),
"mensagem de erro deve mencionar 'html-gen', mas foi: {err}"
);
assert!(
err.contains("--no-default-features") || err.contains("default-features"),
"mensagem de erro deve orientar sobre build reduzido/default-features, mas foi: {err}"
);
let doc_dir = root.join("docs/discovery-default-sem-feature");
assert!(
!doc_dir.join("00-project-discovery.html").exists(),
"companion HTML não deve ser gerado sem html-gen"
);
assert!(
!doc_dir.join("00-project-discovery.md").exists(),
"Discovery não deve gravar Markdown antes de falhar por ausência de HTML obrigatório"
);
}
#[test]
#[cfg(feature = "html-gen")]
fn format_html_generates_html_file_for_stages() {
let dir = tempdir().unwrap();
let root = dir.path();
assert_success(run(root, &["init", "Teste HTML Stage"]));
let output = run(
root,
&[
"idea",
"--name",
"Teste HTML Stage",
"--format",
"html",
"--force",
"uma ideia de teste",
],
);
assert_success(output);
let html_path = root.join("docs/teste-html-stage/01-idea.html");
assert!(
html_path.exists(),
"esperado arquivo .html em {}, mas não existe",
html_path.display()
);
let md_path = root.join("docs/teste-html-stage/01-idea.md");
assert!(
!md_path.exists(),
"arquivo .md não deveria ser gerado quando --format html, mas existe: {}",
md_path.display()
);
let content = fs::read_to_string(&html_path).unwrap();
assert!(
content.contains("<!doctype html") || content.contains("<html"),
"arquivo .html deve conter marcação HTML, mas conteúdo começa com: {}",
&content[..content.len().min(200)]
);
}
#[test]
#[cfg(not(feature = "html-gen"))]
fn format_html_without_html_gen_feature_fails_with_clear_error() {
let dir = tempdir().unwrap();
let root = dir.path();
assert_success(run(root, &["init", "Teste HTML Sem Feature"]));
let output = run(
root,
&[
"idea",
"--name",
"Teste HTML Sem Feature",
"--format",
"html",
"uma ideia",
],
);
assert!(
!output.status.success(),
"esperado falha quando --format html sem feature html-gen, mas saiu com sucesso"
);
let err = combined(&output);
assert!(
err.contains("html-gen"),
"mensagem de erro deve mencionar 'html-gen', mas foi: {err}"
);
let html_path = root.join("docs/teste-html-sem-feature/01-idea.html");
assert!(
!html_path.exists(),
"arquivo .html não deve existir quando a feature está ausente"
);
let md_path = root.join("docs/teste-html-sem-feature/01-idea.md");
assert!(
!md_path.exists(),
"arquivo .md não deve ser gerado quando --format html foi pedido (degradação silenciosa)"
);
}
#[test]
fn version_flag_prints_binary_version() {
let root = tempdir().unwrap();
let output = run(root.path(), &["--version"]);
assert!(output.status.success());
let text = combined(&output);
assert!(
text.trim() == format!("sdd {}", env!("CARGO_PKG_VERSION")),
"saída de --version inesperada: {text:?}"
);
let output_short = run(root.path(), &["-V"]);
assert!(output_short.status.success());
assert_eq!(combined(&output_short).trim(), combined(&output).trim());
}