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")
.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")
.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("During `/execution`, use `execution-discipline`"));
assert!(!content.contains("6. Execution: one task at a time.\n6a. ADR"));
}
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();
assert!(json_contains_text(&installed_mcp, root.to_str().unwrap()));
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.to_str().unwrap()));
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.to_str().unwrap()));
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 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("\"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("opencode-go/deepseek-v4-flash"));
assert!(providers.contains("opencode-go/deepseek-v4-pro"));
assert!(providers.contains("opencode-go/kimi-k2.6"));
assert!(providers.contains("opencode-go/qwen3.7-plus"));
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("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 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_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!(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());
}
#[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();
assert!(artifact.contains("## Arquitetura e padrões"));
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"));
}
#[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 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 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);
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("Rode `sdd prd \"$ARGUMENTS\"`"));
assert!(opencode_prd.contains("Diagramas Mermaid/Excalidraw"));
assert!(opencode_prd.contains("sdd init \"<nome-da-orquestracao>\""));
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("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("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("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!(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/skills/prd/SKILL.md").is_file());
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 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",
],
));
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"));
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,
".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 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 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",
],
));
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_success(run(
root,
&[
"validate-artifact",
"tasks",
"tests/fixtures/good/tasks-prompts-agent.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_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());
}