use serde::Serialize;
use std::collections::BTreeSet;
use std::fs;
use std::io;
use std::path::{Component, Path, PathBuf};
use std::process::Command;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::domain::providers::OptimizationConfig;
use crate::runtime::redaction::redact_text;
pub const DEFAULT_IGNORED_PATHS: &[&str] = &[
".git",
".codegraph",
".sdd",
".sdd-execution",
".sdd-parallel",
"target",
"node_modules",
"dist",
"build",
".next",
".cache",
"coverage",
"tmp",
"temp",
"vendor",
];
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct OptimizationStatus {
pub enabled: bool,
pub codegraph: ToolStatus,
pub rtk: ToolStatus,
pub caveman: ToolStatus,
pub ignored_paths: Vec<String>,
pub loop_policy: ProductiveLoopPolicy,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct ToolStatus {
pub id: String,
pub command: String,
pub available: bool,
pub version: Option<String>,
pub indexed: Option<bool>,
pub index_marker: Option<String>,
pub mode: String,
pub capabilities: Vec<String>,
pub fallback: String,
pub recommended_action: String,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct CodeGraphIndexResult {
pub status: String,
pub command: String,
pub message: String,
}
pub struct CodeGraphAdapter<'a> {
root: &'a Path,
config: &'a OptimizationConfig,
}
impl<'a> CodeGraphAdapter<'a> {
pub fn new(root: &'a Path, config: &'a OptimizationConfig) -> Self {
Self { root, config }
}
pub fn status(&self) -> ToolStatus {
codegraph_status(self.root, self.config)
}
pub fn context_command(&self, task: &str) -> String {
let task = task.replace('"', "\\\"");
format!(
"{} context \"{}\" --path . --format markdown",
self.config.codegraph.command, task
)
}
pub fn affected_command(&self) -> String {
format!(
"git diff --name-only | {} affected --path . --stdin --quiet",
self.config.codegraph.command
)
}
pub fn auto_index(&self, dry_run: bool) -> CodeGraphIndexResult {
auto_index_codegraph(self.root, self.config, dry_run)
}
}
pub struct RtkAdapter<'a> {
root: &'a Path,
config: &'a OptimizationConfig,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct OptimizedCommandOutput {
pub command: String,
pub used_rtk: bool,
pub status_code: Option<i32>,
pub stdout: String,
pub stderr: String,
pub full_output_path: Option<String>,
}
impl<'a> RtkAdapter<'a> {
pub fn new(root: &'a Path, config: &'a OptimizationConfig) -> Self {
Self { root, config }
}
pub fn status(&self) -> ToolStatus {
rtk_status(self.config)
}
pub fn run(&self, args: &[&str]) -> io::Result<OptimizedCommandOutput> {
if args.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"optimized command requires at least one argument",
));
}
let use_rtk = tool_mode_enabled(&self.config.rtk.enabled)
&& command_available(&self.config.rtk.command);
let mut command = if use_rtk {
let mut command = Command::new(&self.config.rtk.command);
command.args(args);
command
} else {
let exe = crate::runtime::platform::find_executable(args[0])
.unwrap_or_else(|| PathBuf::from(args[0]));
let mut command = Command::new(exe);
command.args(&args[1..]);
command
};
let output = command.current_dir(self.root).output()?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let rendered_command = if use_rtk {
format!("{} {}", self.config.rtk.command, args.join(" "))
} else {
args.join(" ")
};
let full_output_path = if output.status.success() {
None
} else {
persist_failed_command_output(self.root, &rendered_command, &stdout, &stderr)?
.map(|path| path.to_string_lossy().replace('\\', "/"))
};
Ok(OptimizedCommandOutput {
command: rendered_command,
used_rtk: use_rtk,
status_code: output.status.code(),
stdout,
stderr,
full_output_path,
})
}
}
pub struct CavemanAdapter<'a> {
config: &'a OptimizationConfig,
}
impl<'a> CavemanAdapter<'a> {
pub fn new(config: &'a OptimizationConfig) -> Self {
Self { config }
}
pub fn status(&self) -> ToolStatus {
caveman_status(self.config)
}
pub fn compress(&self, kind: CompressionKind, input: &str) -> String {
compress_operational_text(kind, input)
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct ProductiveLoopPolicy {
pub required_progress: Vec<String>,
pub stop_when_repeated: Vec<String>,
}
#[derive(Clone, Debug, Default, Serialize, PartialEq, Eq)]
pub struct LoopIterationEvidence {
pub hypothesis: Option<String>,
pub source: Option<String>,
pub scope: Option<String>,
pub evidence: Option<String>,
pub error_signature: Option<String>,
pub diff_signature: Option<String>,
pub command: Option<String>,
pub search: Option<String>,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct LoopDecision {
pub continue_loop: bool,
pub reason: String,
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
pub struct ContextHandoff {
pub stage: String,
pub task: Option<String>,
pub strategy: String,
pub artifact_sources: Vec<String>,
pub probable_files: Vec<String>,
pub suggested_tests: Vec<String>,
pub fallback_used: String,
pub out_of_scope_paths: Vec<String>,
pub productive_loop_policy: ProductiveLoopPolicy,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CompressionKind {
Trace,
Handoff,
Memory,
}
impl CompressionKind {
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"trace" => Some(Self::Trace),
"handoff" => Some(Self::Handoff),
"memory" | "memory-derived" | "memoria" | "memória" => Some(Self::Memory),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Trace => "trace",
Self::Handoff => "handoff",
Self::Memory => "memory-derived",
}
}
}
pub fn status(root: &Path, config: &OptimizationConfig) -> OptimizationStatus {
let ignored_paths = if config.ignored_paths.is_empty() {
ignored_path_list()
} else {
config.ignored_paths.clone()
};
OptimizationStatus {
enabled: config.enabled,
codegraph: codegraph_status(root, config),
rtk: rtk_status(config),
caveman: caveman_status(config),
ignored_paths,
loop_policy: productive_loop_policy(),
}
}
pub fn ignored_path_list() -> Vec<String> {
DEFAULT_IGNORED_PATHS
.iter()
.map(|item| (*item).to_string())
.collect()
}
pub fn path_has_ignored_component(path: &Path) -> bool {
path.components().any(|component| {
matches!(
component,
Component::Normal(name)
if DEFAULT_IGNORED_PATHS.contains(&name.to_string_lossy().as_ref())
)
})
}
pub fn build_context_handoff(
root: &Path,
stage: &str,
task: Option<&str>,
artifact_sources: Vec<String>,
status: &OptimizationStatus,
) -> ContextHandoff {
let probable_files = git_changed_files(root);
let suggested_tests = suggested_tests(root, &probable_files, status);
let strategy = if status.codegraph.available && status.codegraph.indexed == Some(true) {
"codegraph-indexed".to_string()
} else if !probable_files.is_empty() {
"git-diff-focused".to_string()
} else {
"rg-focused-fallback".to_string()
};
let fallback_used = if strategy == "codegraph-indexed" {
"CodeGraph indexado disponÃvel; valide impacto com leitura/testes reais.".to_string()
} else {
"CodeGraph ausente ou não indexado; use git diff/status, manifests e rg delimitado."
.to_string()
};
ContextHandoff {
stage: stage.to_string(),
task: task.map(str::to_string),
strategy,
artifact_sources,
probable_files,
suggested_tests,
fallback_used,
out_of_scope_paths: status.ignored_paths.clone(),
productive_loop_policy: status.loop_policy.clone(),
}
}
pub fn auto_index_codegraph(
root: &Path,
config: &OptimizationConfig,
dry_run: bool,
) -> CodeGraphIndexResult {
let command = config.codegraph.command.clone();
let command_line = format!("{command} init -i .");
if !config.enabled {
return codegraph_index_result(
"disabled",
command_line,
"optimization disabled; skipping CodeGraph auto-index.",
);
}
if !config.codegraph.auto_index {
return codegraph_index_result(
"disabled",
command_line,
"optimization.codegraph.auto_index=false; skipping CodeGraph auto-index.",
);
}
if root.join(&config.codegraph.index_marker).exists() {
return codegraph_index_result(
"already-indexed",
command_line,
"CodeGraph index already exists; skipping `codegraph init -i .`.",
);
}
let Some(executable) = crate::runtime::platform::find_executable(&command) else {
return codegraph_index_result(
"missing",
command_line,
"CodeGraph not found in PATH; SDD init continues with rg/git fallback.",
);
};
if dry_run {
return codegraph_index_result("dry-run", command_line, "dry-run codegraph init -i .");
}
match Command::new(executable)
.current_dir(root)
.args(["init", "-i", "."])
.output()
{
Ok(output) if output.status.success() => {
codegraph_index_result("indexed", command_line, "ran codegraph init -i .")
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let detail = first_non_empty_line(&stderr)
.or_else(|| first_non_empty_line(&stdout))
.unwrap_or_else(|| "unknown CodeGraph error".to_string());
codegraph_index_result(
"failed",
command_line,
format!("codegraph init -i . failed; SDD init continues. {detail}"),
)
}
Err(err) => codegraph_index_result(
"failed",
command_line,
format!("codegraph init -i . failed to start; SDD init continues. {err}"),
),
}
}
pub fn render_context_handoff(handoff: &ContextHandoff) -> String {
let mut out = String::new();
out.push_str("## Context Handoff\n\n");
out.push_str(&format!("- Stage: `{}`\n", handoff.stage));
if let Some(task) = handoff.task.as_deref() {
out.push_str(&format!("- Task: `{}`\n", task));
}
out.push_str(&format!("- Estratégia: `{}`\n", handoff.strategy));
out.push_str(&format!("- Fallback usado: {}\n", handoff.fallback_used));
push_list(
&mut out,
"Artefatos base",
&handoff.artifact_sources,
"nenhum",
);
push_list(
&mut out,
"Arquivos prováveis",
&handoff.probable_files,
"nenhum arquivo alterado detectado por Git",
);
push_list(
&mut out,
"Testes sugeridos",
&handoff.suggested_tests,
"descobrir pelo manifesto do projeto",
);
push_list(
&mut out,
"Fora de escopo por padrão",
&handoff.out_of_scope_paths,
"nenhum",
);
out.push_str("- Loop produtivo: continue somente com nova hipótese, nova fonte, escopo menor ou evidência nova.\n");
out.push_str(
"- Parar se repetir o mesmo erro, diff, comando ou busca sem informação nova.\n\n",
);
out
}
pub fn evaluate_productive_loop(
history: &[LoopIterationEvidence],
current: &LoopIterationEvidence,
) -> LoopDecision {
if has_new_progress(history, current) {
return LoopDecision {
continue_loop: true,
reason: "nova hipótese, fonte, escopo ou evidência registrada".to_string(),
};
}
if repeated_signature(history, current, |item| item.error_signature.as_deref()) {
return stop_loop("mesmo erro repetido sem evidência nova");
}
if repeated_signature(history, current, |item| item.diff_signature.as_deref()) {
return stop_loop("mesmo diff repetido sem evidência nova");
}
if repeated_signature(history, current, |item| item.command.as_deref()) {
return stop_loop("mesmo comando repetido sem evidência nova");
}
if repeated_signature(history, current, |item| item.search.as_deref()) {
return stop_loop("mesma busca repetida sem evidência nova");
}
stop_loop("iteração sem progresso observável")
}
pub fn compress_operational_text(kind: CompressionKind, input: &str) -> String {
let mut seen = BTreeSet::new();
let mut kept = Vec::new();
let max_lines = match kind {
CompressionKind::Trace => 24,
CompressionKind::Handoff => 36,
CompressionKind::Memory => 48,
};
for line in input.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let redacted = redact_text(trimmed);
let normalized = normalize_operational_line(&redacted);
if seen.insert(normalized) {
kept.push(redacted);
}
if kept.len() >= max_lines {
break;
}
}
if kept.is_empty() {
return String::new();
}
let mut out = format!("# Caveman {} summary\n\n", kind.as_str());
for line in kept {
out.push_str("- ");
out.push_str(&line);
out.push('\n');
}
out
}
fn has_new_progress(history: &[LoopIterationEvidence], current: &LoopIterationEvidence) -> bool {
[
current.hypothesis.as_deref(),
current.source.as_deref(),
current.scope.as_deref(),
current.evidence.as_deref(),
]
.into_iter()
.flatten()
.any(|candidate| {
let candidate = candidate.trim();
!candidate.is_empty()
&& !history.iter().any(|item| {
[
item.hypothesis.as_deref(),
item.source.as_deref(),
item.scope.as_deref(),
item.evidence.as_deref(),
]
.into_iter()
.flatten()
.any(|seen| seen.trim() == candidate)
})
})
}
fn repeated_signature<F>(
history: &[LoopIterationEvidence],
current: &LoopIterationEvidence,
get: F,
) -> bool
where
F: Fn(&LoopIterationEvidence) -> Option<&str>,
{
let Some(value) = get(current)
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return false;
};
history
.iter()
.any(|item| get(item).map(str::trim) == Some(value))
}
fn stop_loop(reason: &str) -> LoopDecision {
LoopDecision {
continue_loop: false,
reason: reason.to_string(),
}
}
fn codegraph_index_result(
status: impl Into<String>,
command: impl Into<String>,
message: impl Into<String>,
) -> CodeGraphIndexResult {
CodeGraphIndexResult {
status: status.into(),
command: command.into(),
message: message.into(),
}
}
fn tool_mode_enabled(value: &str) -> bool {
!matches!(
value.trim().to_ascii_lowercase().as_str(),
"false" | "off" | "disabled" | "never"
)
}
fn persist_failed_command_output(
root: &Path,
command: &str,
stdout: &str,
stderr: &str,
) -> io::Result<Option<PathBuf>> {
let rel_dir = PathBuf::from(".sdd/optimization/rtk-failures");
let dir = root.join(&rel_dir);
fs::create_dir_all(&dir)?;
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let slug = command
.chars()
.map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '-' })
.collect::<String>()
.trim_matches('-')
.chars()
.take(48)
.collect::<String>();
let filename = if slug.is_empty() {
format!("{timestamp}-command.log")
} else {
format!("{timestamp}-{slug}.log")
};
let rel_path = rel_dir.join(filename);
let path = root.join(&rel_path);
let mut body = String::new();
body.push_str(&format!("$ {command}\n\n"));
body.push_str("## stdout\n\n");
body.push_str(stdout);
if !stdout.ends_with('\n') {
body.push('\n');
}
body.push_str("\n## stderr\n\n");
body.push_str(stderr);
if !stderr.ends_with('\n') {
body.push('\n');
}
fs::write(&path, body)?;
Ok(Some(rel_path))
}
fn codegraph_status(root: &Path, config: &OptimizationConfig) -> ToolStatus {
let command = config.codegraph.command.as_str();
let available = command_available(command);
let version = if available {
command_stdout(command, &["--version"])
} else {
None
};
let help = if available {
command_stdout(command, &["--help"]).unwrap_or_default()
} else {
String::new()
};
let capabilities = codegraph_capabilities(&help);
let marker = config.codegraph.index_marker.clone();
let indexed = Some(root.join(&marker).exists());
let recommended_action = if !available {
"Instale CodeGraph para acelerar contexto; fallback rg/git ativo.".to_string()
} else if indexed == Some(false) {
"Rode `sdd init` ou `codegraph init -i .` para habilitar Ãndice.".to_string()
} else {
"Use `codegraph context`, `query` e `affected` antes de leitura ampla.".to_string()
};
ToolStatus {
id: "codegraph".to_string(),
command: command.to_string(),
available,
version,
indexed,
index_marker: Some(marker),
mode: config.codegraph.mode.clone(),
capabilities,
fallback: "git diff/status, manifests, rg --files e rg delimitado".to_string(),
recommended_action,
}
}
fn rtk_status(config: &OptimizationConfig) -> ToolStatus {
let command = config.rtk.command.as_str();
let available = command_available(command);
let version = if available {
command_stdout(command, &["--version"])
} else {
None
};
ToolStatus {
id: "rtk".to_string(),
command: command.to_string(),
available,
version,
indexed: None,
index_marker: None,
mode: config.rtk.enabled.clone(),
capabilities: vec!["command-proxy".to_string(), "token-savings".to_string()],
fallback: "executar comando bruto e limitar stdout/stderr no caller".to_string(),
recommended_action: if available {
"Use RTK para comandos longos/ruidosos; use `rtk proxy` para shell complexo."
.to_string()
} else {
"RTK ausente; execute comandos diretos com limites de output.".to_string()
},
}
}
fn caveman_status(config: &OptimizationConfig) -> ToolStatus {
let command = config.caveman.command.as_str();
let available = command_available(command);
let version = if available {
command_stdout(command, &["--version"]).or_else(|| command_stdout(command, &["--help"]))
} else {
None
};
ToolStatus {
id: "caveman".to_string(),
command: command.to_string(),
available,
version: version.map(|value| first_line(&value)),
indexed: None,
index_marker: None,
mode: config.caveman.enabled.clone(),
capabilities: vec![
"operational-compression".to_string(),
"trace-handoff-memory".to_string(),
],
fallback: "compressão determinÃstica local preservando paths, comandos, IDs e hashes"
.to_string(),
recommended_action: if available {
"Use Caveman apenas para traces, handoffs e memória derivada.".to_string()
} else {
"Caveman ausente; usando compactação operacional embutida.".to_string()
},
}
}
fn codegraph_capabilities(help: &str) -> Vec<String> {
let mut out = Vec::new();
for capability in [
"init", "index", "sync", "status", "query", "files", "context", "affected",
] {
if help.contains(&format!("{capability} ")) || help.contains(&format!(" {capability}")) {
out.push(capability.to_string());
}
}
out
}
fn git_changed_files(root: &Path) -> Vec<String> {
if !root.join(".git").exists() {
return Vec::new();
}
let mut files = BTreeSet::new();
if let Some(output) = command_stdout_in(root, "git", &["diff", "--name-only"]) {
for line in output.lines() {
push_probable_file(&mut files, line);
}
}
if let Some(output) = command_stdout_in(root, "git", &["status", "--short"]) {
for line in output.lines() {
let path = line.get(3..).unwrap_or(line).trim();
let path = path.split(" -> ").last().unwrap_or(path);
push_probable_file(&mut files, path);
}
}
files.into_iter().take(30).collect()
}
fn suggested_tests(root: &Path, files: &[String], status: &OptimizationStatus) -> Vec<String> {
if !files.is_empty() && status.codegraph.available && status.codegraph.indexed == Some(true) {
return vec!["git diff --name-only | codegraph affected --stdin --quiet".to_string()];
}
let mut tests = Vec::new();
if root.join("Cargo.toml").exists() {
tests.push("cargo test".to_string());
}
if root.join("package.json").exists() {
tests.push("npm test".to_string());
}
if root.join("pyproject.toml").exists() || root.join("requirements.txt").exists() {
tests.push("pytest".to_string());
}
if tests.is_empty() {
tests.push("descobrir comando de teste via manifests/docs".to_string());
}
tests
}
fn productive_loop_policy() -> ProductiveLoopPolicy {
ProductiveLoopPolicy {
required_progress: vec![
"nova hipótese testada".to_string(),
"nova fonte lida".to_string(),
"escopo reduzido".to_string(),
"evidência nova produzida".to_string(),
],
stop_when_repeated: vec![
"mesmo erro".to_string(),
"mesmo diff".to_string(),
"mesmo comando".to_string(),
"mesma busca sem informação nova".to_string(),
],
}
}
fn push_list(out: &mut String, label: &str, items: &[String], empty: &str) {
if items.is_empty() {
out.push_str(&format!("- {label}: {empty}\n"));
} else {
out.push_str(&format!("- {label}: {}\n", items.join(", ")));
}
}
fn push_probable_file(files: &mut BTreeSet<String>, value: &str) {
let value = value.trim();
if value.is_empty() {
return;
}
let path = Path::new(value);
if path_has_ignored_component(path) {
return;
}
files.insert(value.replace('\\', "/"));
}
fn normalize_operational_line(input: &str) -> String {
input
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase()
}
fn command_available(command: &str) -> bool {
crate::runtime::platform::find_executable(command).is_some()
}
fn command_stdout(command: &str, args: &[&str]) -> Option<String> {
let exe = crate::runtime::platform::find_executable(command)
.unwrap_or_else(|| PathBuf::from(command));
let output = Command::new(exe).args(args).output().ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if stderr.is_empty() {
None
} else {
Some(stderr)
}
}
}
fn command_stdout_in(root: &Path, command: &str, args: &[&str]) -> Option<String> {
let exe = crate::runtime::platform::find_executable(command)
.unwrap_or_else(|| PathBuf::from(command));
let output = Command::new(exe)
.current_dir(root)
.args(args)
.output()
.ok()?;
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).trim().to_string())
} else {
None
}
}
fn first_line(input: &str) -> String {
input.lines().next().unwrap_or(input).trim().to_string()
}
fn first_non_empty_line(input: &str) -> Option<String> {
input
.lines()
.map(str::trim)
.find(|line| !line.is_empty())
.map(str::to_string)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn codegraph_help_detection_tracks_actual_commands() {
let help =
"Commands:\n init\n index\n status\n query\n files\n context\n affected\n";
let caps = codegraph_capabilities(help);
assert!(caps.contains(&"context".to_string()));
assert!(caps.contains(&"affected".to_string()));
assert!(!caps.contains(&"impact".to_string()));
}
#[test]
fn ignored_path_components_are_centralized() {
assert!(path_has_ignored_component(Path::new("target/debug/app")));
assert!(path_has_ignored_component(Path::new(
"node_modules/pkg/index.js"
)));
assert!(!path_has_ignored_component(Path::new("src/main.rs")));
}
#[test]
fn caveman_compression_preserves_operational_identifiers() {
let input = "cmd: cargo test\ncmd: cargo test\nfile: src/main.rs\nTask T-01 OK\nhash abc123def456\n";
let out = compress_operational_text(CompressionKind::Trace, input);
assert!(out.contains("cargo test"));
assert!(out.contains("src/main.rs"));
assert!(out.contains("T-01"));
assert!(out.contains("abc123def456"));
assert_eq!(out.matches("cargo test").count(), 1);
}
#[test]
fn productive_loop_continues_only_with_new_evidence() {
let first = LoopIterationEvidence {
hypothesis: Some("falha vem do parser".to_string()),
evidence: Some("teste parse_invalid reproduz".to_string()),
command: Some("cargo test parse_invalid".to_string()),
error_signature: Some("E_PARSE".to_string()),
..LoopIterationEvidence::default()
};
let decision = evaluate_productive_loop(&[], &first);
assert!(decision.continue_loop);
let repeated = LoopIterationEvidence {
command: Some("cargo test parse_invalid".to_string()),
error_signature: Some("E_PARSE".to_string()),
..LoopIterationEvidence::default()
};
let decision = evaluate_productive_loop(&[first], &repeated);
assert!(!decision.continue_loop);
assert!(decision.reason.contains("mesmo erro"));
}
#[test]
fn rtk_adapter_persists_full_output_path_on_failure() {
let dir = tempdir().unwrap();
let root = dir.path();
let mut config = OptimizationConfig::default();
config.rtk.enabled = "disabled".to_string();
let adapter = RtkAdapter::new(root, &config);
let output = adapter
.run(&["sh", "-c", "printf 'raw-out'; printf 'raw-err' >&2; exit 7"])
.unwrap();
assert_eq!(output.status_code, Some(7));
assert!(!output.used_rtk);
let path = output.full_output_path.expect("failure log path");
let body = std::fs::read_to_string(root.join(path)).unwrap();
assert!(body.contains("raw-out"));
assert!(body.contains("raw-err"));
}
#[test]
fn context_handoff_uses_git_changes_without_wide_search() {
let dir = tempdir().unwrap();
let root = dir.path();
std::fs::write(
root.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
)
.unwrap();
std::fs::create_dir_all(root.join("src")).unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn a() {}\n").unwrap();
Command::new("git")
.args(["init"])
.current_dir(root)
.output()
.unwrap();
Command::new("git")
.args(["add", "."])
.current_dir(root)
.output()
.unwrap();
Command::new("git")
.args(["commit", "-m", "init"])
.current_dir(root)
.env("GIT_AUTHOR_NAME", "t")
.env("GIT_AUTHOR_EMAIL", "t@example.com")
.env("GIT_COMMITTER_NAME", "t")
.env("GIT_COMMITTER_EMAIL", "t@example.com")
.output()
.unwrap();
std::fs::write(root.join("src/lib.rs"), "pub fn a() { let _x = 1; }\n").unwrap();
let status = OptimizationStatus {
enabled: true,
codegraph: ToolStatus {
id: "codegraph".to_string(),
command: "codegraph".to_string(),
available: false,
version: None,
indexed: Some(false),
index_marker: Some(".codegraph/".to_string()),
mode: "adaptive".to_string(),
capabilities: Vec::new(),
fallback: "rg".to_string(),
recommended_action: "fallback".to_string(),
},
rtk: rtk_status(&OptimizationConfig::default()),
caveman: caveman_status(&OptimizationConfig::default()),
ignored_paths: ignored_path_list(),
loop_policy: productive_loop_policy(),
};
let handoff = build_context_handoff(
root,
"execution",
Some("T-01"),
vec!["docs/x/04-tasks.md".to_string()],
&status,
);
assert_eq!(handoff.strategy, "git-diff-focused");
assert!(handoff.probable_files.contains(&"src/lib.rs".to_string()));
assert!(handoff.suggested_tests.contains(&"cargo test".to_string()));
}
}