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_text =
fs::read_to_string(source.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
let mirror_text =
fs::read_to_string(mirror.join(&rel)).unwrap_or_else(|err| panic!("{rel}: {err}"));
assert_eq!(
source_text,
mirror_text,
"{} drifted from {}",
mirror.join(&rel).display(),
source.join(&rel).display()
);
}
}
fn run(cwd: &Path, args: &[&str]) -> Output {
run_with(cwd, args, None, &[])
}
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!(content.contains(
"6. Execution: one task at a time, after reading the execution Context Pack and code-intelligence handoff."
));
assert!(content.contains(
"6a. ADR: create or update Architecture Decision Records from execution evidence."
));
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("6. Execution: one task at a time.\n6a. ADR"));
}
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_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, ".devin/workflows", "md", stage);
assert_stage_command_fallback(root, ".opencode/commands", "md", stage);
assert_stage_command_fallback(root, ".trae/commands", "md", stage);
}
}
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("");
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 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/sdd.md").is_file());
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/workflows/sdd.md").is_file());
assert!(compact
.path()
.join(".devin/workflows/orchestration.md")
.is_file());
assert!(compact.path().join(".devin/workflows/prd.md").is_file());
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!(compact.path().join(".sdd/clients/README.md").is_file());
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/sdd.md").is_file());
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/workflows/sdd.md").is_file());
assert!(initialized
.path()
.join(".devin/workflows/orchestration.md")
.is_file());
assert!(initialized.path().join(".devin/workflows/prd.md").is_file());
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_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(".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(".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/sdd.md").is_file());
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/workflows/orchestration.md").is_file());
assert!(root.join(".devin/workflows/prd.md").is_file());
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(".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/sdd.md").is_file());
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/workflows/orchestration.md").is_file());
assert!(root.join(".devin/workflows/prd.md").is_file());
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 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/sdd.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 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 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 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/06-adr.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",
"06-adr.md",
"07-review.md",
"08-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("- Tokens source: estimated"));
assert!(discovery.contains("- Tempo de geração: "));
assert!(discovery.contains("- Tempo source: local"));
let discovery_html =
fs::read_to_string(root.join("docs/opencode-kimi/00-project-discovery.html")).unwrap();
assert!(discovery_html.contains("Provider: <code>opencode</code>"));
assert!(discovery_html.contains("Modelo: <code>opencode-go/kimi-k2.7</code>"));
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("- Tokens source: estimated"));
assert!(prd.contains("- Tempo de geração: "));
assert!(prd.contains("- Tempo source: local"));
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"));
}
#[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_historical_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/06-adr.md"),
"# ADR\n\nDecidimos usar snapshots determinísticos para cache operacional.\n",
)
.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/06-adr.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 discover_json_without_name_materializes_fallback_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</title>"));
assert!(html.contains("Stack Utilizada"));
assert!(html.contains("Arquitetura Inferida"));
assert!(html.contains("Manifests e Dependências"));
assert!(html.contains("Cobertura do Inventário"));
assert!(html.contains("Fluxo SDD Recomendado"));
assert!(html.contains("Capability Map"));
}
#[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 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/08-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/08-memory.md"));
}
#[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("adr-input.md"),
"# ADR\n\n## Decisão\n- Estratégia de cache: JSONL derivado\n",
)
.unwrap();
fs::write(
root.join("memory-input.md"),
"# Memória\n\n## Decisões\n- Estratégia de cache: SQLite como fonte canônica\n",
)
.unwrap();
assert_success(run(
root,
&[
"artifact",
"save",
name,
"adr",
"--file",
"adr-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("06-adr.md"));
assert!(context_pack.contains("08-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("adr-input.md"),
"# ADR\n\n## Rastreabilidade\n- teste\n\n## Status\nAprovada.\n\n## Contexto\nDecidimos usar JSONL derivado para cache operacional.\n\n## Problema\nEvitar cache opaco.\n\n## Decisão\nRegistrar a política no fluxo SDD.\n\n## Alternativas consideradas\n- SQLite.\n\n## Consequências\n- Índice reconstruível.\n\n## Impactos e riscos\n- Baixo.\n\n## Plano de adoção\n- Usar CLI.\n\n## Plano de reversão\n- Remover índice.\n\n## Evidências\n- teste\n\n## Histórico de revisão\n- inicial\n",
)
.unwrap();
fs::write(
root.join("memory-input.md"),
"# Memória\n\n## Estado final\nRegistrado.\n\n## Rastreabilidade\n- teste\n\n## Decisões\n| Decisão | Escolha |\n|---|---|\n| 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,
"adr",
"--file",
"adr-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 `cache-operacional`"));
assert!(context_pack.contains("06-adr.md"));
assert!(context_pack.contains("08-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 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 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"),
("execution", "TASK-001"),
("adr", "decisão arquitetural pós-execução"),
("review", "diff atual"),
("memory", "feature concluída"),
] {
assert_success(run(
root,
&[stage, "--name", "Minha Orquestração", "--force", input],
));
}
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",
"adr",
"review",
"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(),
],
));
assert_success(run(
root,
&[
"validate-artifact",
"adr",
"docs/minha-orquestracao/06-adr.md",
"--schema",
schema.to_str().unwrap(),
],
));
}
#[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/agentic-sdd-loop.md")).unwrap();
assert!(codex_loop.contains("agent: sdd-orchestrator"));
assert!(codex_loop.contains("sdd workflow run agentic-sdd-loop"));
assert!(codex_loop.contains("docs/AGENTIC-SDD-LOOP.md"));
let claude_loop =
fs::read_to_string(root.join(".claude/commands/agentic-sdd-loop.md")).unwrap();
assert!(claude_loop.contains("sdd workflow run agentic-sdd-loop"));
assert!(claude_loop.contains("sdd workflow status agentic-sdd-loop"));
assert!(claude_loop.contains("sdd auto run --real --unattended"));
assert!(claude_loop.contains("channel: automation"));
let devin_config = fs::read_to_string(root.join(".devin/config.json")).unwrap();
assert!(devin_config.contains("\"read_config_from\""));
assert!(devin_config.contains("\"cursor\": true"));
assert!(devin_config.contains("\"claude\": true"));
let devin_rule = fs::read_to_string(root.join(".devin/rules/sdd.md")).unwrap();
assert!(devin_rule.contains("Skills/workflows Devin"));
assert!(devin_rule.contains("Subagents Devin"));
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>\""));
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\"`"));
let devin_workflow =
fs::read_to_string(root.join(".devin/workflows/orchestration.md")).unwrap();
assert!(devin_workflow.contains("sdd orchestration \"$ARGUMENTS\""));
assert!(devin_workflow.contains("sdd init \"<nome-da-orquestracao>\""));
let devin_sdd_workflow = fs::read_to_string(root.join(".devin/workflows/sdd.md")).unwrap();
assert!(devin_sdd_workflow.contains("sdd clients doctor"));
assert!(devin_sdd_workflow.contains("sdd clients sync --targets all --force"));
let devin_prd_workflow = fs::read_to_string(root.join(".devin/workflows/prd.md")).unwrap();
assert!(devin_prd_workflow.contains("Rode `sdd prd \"$ARGUMENTS\"`"));
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 \"$ARGUMENTS\""));
assert!(cursor_command.contains("sdd init \"<nome-da-orquestracao>\""));
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);
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/agentic-sdd-loop.md")).unwrap();
assert!(opencode_loop.contains("SDD_PROVIDER=opencode sdd workflow run agentic-sdd-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 init \"<nome-da-orquestracao>\""));
assert!(trae_command.contains("sdd artifact save"));
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",
"--dry-run",
],
));
fs::remove_dir_all(root.join(".cursor")).unwrap();
fs::remove_dir_all(root.join(".devin")).unwrap();
assert_success(run(root, &["clients", "sync", "--targets", "cursor,devin"]));
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/sdd.md").is_file());
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/workflows/sdd.md").is_file());
assert!(root.join(".devin/workflows/orchestration.md").is_file());
assert!(root.join(".devin/workflows/prd.md").is_file());
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("sk-"));
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/workflows/orchestration.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",
] {
assert!(json_contains_text(&parsed, id), "missing skill id {id}");
}
assert_success(run(root, &["skills", "doctor", "--strict"]));
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 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("completed")
);
assert_eq!(deep.get("iterations").and_then(Value::as_u64), Some(0));
assert!(json_contains_text(&deep, "execution"));
assert!(json_contains_text(&deep, "eval"));
assert!(json_contains_text(&deep, "review"));
assert!(root
.join("docs/corrigir-regressao/06-execution.md")
.is_file());
assert!(root.join("docs/corrigir-regressao/07-review.md").is_file());
assert!(fs::read_to_string(root.join(".sdd/evaluations.jsonl"))
.unwrap()
.contains("eval_stage"));
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, "completed"));
}
#[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 plugin =
fs::read_to_string(root.join("plugins/orchestration/skills/orchestration/SKILL.md"))
.unwrap();
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!(plugin, agents);
assert_eq!(plugin, 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: uma task por vez, depois de ler o Context Pack de execution e o handoff de code intelligence.",
),
(
"6a. ADR: create or update Architecture Decision Records from execution evidence.",
"6a. ADR: crie ou atualize Architecture Decision Records a partir das evidências de execução.",
),
(
"After Execution, run `sdd adr` when a technical or architectural decision was made or changed.",
"Após Execution, rode `sdd adr` quando uma decisão técnica ou arquitetural tiver sido criada, confirmada ou alterada.",
),
(
"Before `/execution`, generate or read `sdd context build --name \"<orchestration>\" --stage execution --write`",
"Antes de Execution, gere ou leia `sdd context build --name \"<orquestracao>\" --stage execution --write`",
),
(
"During `/execution`, use `execution-discipline`",
"Durante Execution, use `execution-discipline`",
),
] {
assert!(
canonical.contains(canonical_anchor),
"canonical orchestration skill is missing anchor: {canonical_anchor}"
);
assert!(
plugin.contains(plugin_anchor),
"plugin orchestration skill is missing localized anchor: {plugin_anchor}"
);
}
}
#[test]
fn project_discovery_plugin_skill_mirrors_source_contract() {
let root = repo_root();
let plugin =
fs::read_to_string(root.join("plugins/orchestration/skills/project-discovery/SKILL.md"))
.unwrap();
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!(plugin, agents);
assert_eq!(plugin, claude);
assert!(plugin.contains("sdd discover --json --dry-run"));
assert!(plugin.contains("sdd context recommend"));
assert!(plugin.contains("Mapeie linguagem de domínio"));
assert!(plugin.contains("Mapeie code intelligence opcional"));
assert!(plugin.contains("Inclua capability map"));
}
#[test]
fn orchestration_plugin_skill_directories_are_complete_mirrors() {
let root = repo_root();
let source = root.join("plugins/orchestration/skills");
assert_directory_mirror(
&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("Invalid"));
assert!(doctor_text.contains(".opencode/agents/frontend-dev.md"));
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 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"),
"accent: #125c9c\n",
)
.unwrap();
assert_success(run(root, &["diagram", "doctor", "--name", "Fluxo Visual"]));
}