use crate::wiz::global_config::global_config_path;
use crate::wiz::types::{WizState, WrittenGlobal, WrittenHarness};
use std::io::Write;
use std::path::Path;
use tempfile::NamedTempFile;
pub fn write_harness_config(project_root: &str, harness: &WrittenHarness) -> Result<(), String> {
let dir = Path::new(project_root).join(".7");
std::fs::create_dir_all(&dir).map_err(|e| format!("Failed to create .7 dir: {}", e))?;
let path = dir.join("harnesses.toml");
let mut lines = vec![
format!("[harness.{}]", harness.name),
format!("command = {:?}", harness.command),
r#"args_mapping = "flags""#.to_string(),
];
if let Some(ref slot) = harness.prompt_slot {
lines.push(format!("prompt_slot = {:?}", slot));
}
let content = lines.join("\n") + "\n";
let mut tmp = NamedTempFile::new_in(&dir)
.map_err(|e| format!("Failed to create temp file for harness config: {}", e))?;
tmp.write_all(content.as_bytes())
.map_err(|e| format!("Failed to write harness config: {}", e))?;
tmp.persist(&path)
.map_err(|e| format!("Failed to persist harness config: {}", e))?;
Ok(())
}
pub fn write_global_config(global: &WrittenGlobal) -> Result<(), String> {
let path = global_config_path();
let dir = path.parent().ok_or("Global config path has no parent")?;
std::fs::create_dir_all(dir).map_err(|e| format!("Failed to create config dir: {}", e))?;
let mut lines: Vec<String> = Vec::new();
if let Some(ref root) = global.default_project_root {
lines.push(format!("default_project_root = {:?}", root));
}
if let Some(ref name) = global.default_harness {
lines.push(format!("default_harness = {:?}", name));
}
let content = lines.join("\n") + if lines.is_empty() { "" } else { "\n" };
let mut tmp = NamedTempFile::new_in(dir)
.map_err(|e| format!("Failed to create temp file for global config: {}", e))?;
tmp.write_all(content.as_bytes())
.map_err(|e| format!("Failed to write global config: {}", e))?;
tmp.persist(&path)
.map_err(|e| format!("Failed to persist global config: {}", e))?;
Ok(())
}
pub fn resolve_written_config(state: &WizState) -> (WrittenHarness, WrittenGlobal) {
let name = state
.answers
.get("harness-name")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "default".to_string());
let command = state
.answers
.get("harness-command")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "echo".to_string());
let prompt_slot = state
.answers
.get("harness-prompt-slot")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let default_project_root = state
.answers
.get("global-default-project-root")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let default_harness = state
.answers
.get("global-default-harness")
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
(
WrittenHarness {
name,
command,
prompt_slot,
},
WrittenGlobal {
default_project_root,
default_harness,
},
)
}
pub fn perform_write(state: &WizState, project_root: &str) -> Result<(), String> {
let (harness, global) = resolve_written_config(state);
write_harness_config(project_root, &harness)?;
write_global_config(&global)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn make_state_with_answers(answers: Vec<(&str, &str)>) -> WizState {
use crate::wiz::types::{WizPhase, WizQuestion, WizSection};
let mut map = std::collections::HashMap::new();
for (k, v) in answers {
map.insert(k.to_string(), v.to_string());
}
WizState {
questions: vec![WizQuestion {
id: "harness-name".to_string(),
prompt: "test".to_string(),
section: WizSection::Harness,
prefill: "".to_string(),
}],
answers: map,
selected_tab: 0,
show_review: false,
text_buffer: String::new(),
text_cursor: 0,
phase: WizPhase::Writing,
error: None,
}
}
#[test]
fn test_write_harness_config_creates_toml() {
let dir = TempDir::new().unwrap();
let harness = WrittenHarness {
name: "claude".to_string(),
command: "claude".to_string(),
prompt_slot: Some("--prompt".to_string()),
};
write_harness_config(dir.path().to_str().unwrap(), &harness).unwrap();
let content = fs::read_to_string(dir.path().join(".7").join("harnesses.toml")).unwrap();
assert!(content.contains("[harness.claude]"));
assert!(content.contains("command = \"claude\""));
assert!(content.contains("args_mapping = \"flags\""));
assert!(content.contains("prompt_slot = \"--prompt\""));
}
#[test]
fn test_write_harness_config_omits_prompt_slot_when_none() {
let dir = TempDir::new().unwrap();
let harness = WrittenHarness {
name: "mybot".to_string(),
command: "mybot".to_string(),
prompt_slot: None,
};
write_harness_config(dir.path().to_str().unwrap(), &harness).unwrap();
let content = fs::read_to_string(dir.path().join(".7").join("harnesses.toml")).unwrap();
assert!(!content.contains("prompt_slot"));
}
#[test]
fn test_no_tmp_files_remain_after_harness_write() {
let dir = TempDir::new().unwrap();
let harness = WrittenHarness {
name: "x".to_string(),
command: "x".to_string(),
prompt_slot: None,
};
write_harness_config(dir.path().to_str().unwrap(), &harness).unwrap();
let files: Vec<_> = fs::read_dir(dir.path().join(".7"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect();
assert!(!files.iter().any(|f| f.contains(".tmp")));
}
#[test]
fn test_write_global_config_creates_toml() {
let dir = TempDir::new().unwrap();
unsafe {
std::env::set_var("HOME", dir.path());
}
let global = WrittenGlobal {
default_project_root: Some("/tmp/proj".to_string()),
default_harness: Some("claude".to_string()),
};
write_global_config(&global).unwrap();
let content =
fs::read_to_string(dir.path().join(".config").join("o7").join("config.toml")).unwrap();
assert!(content.contains("default_project_root = \"/tmp/proj\""));
assert!(content.contains("default_harness = \"claude\""));
}
#[test]
fn test_write_global_config_omits_none_fields() {
let dir = TempDir::new().unwrap();
unsafe {
std::env::set_var("HOME", dir.path());
}
let global = WrittenGlobal {
default_project_root: None,
default_harness: None,
};
write_global_config(&global).unwrap();
let content =
fs::read_to_string(dir.path().join(".config").join("o7").join("config.toml")).unwrap();
assert!(!content.contains("default_project_root"));
assert!(!content.contains("default_harness"));
}
#[test]
fn test_resolve_written_config_uses_fallbacks_for_empty_answers() {
let state = make_state_with_answers(vec![]);
let (harness, _global) = resolve_written_config(&state);
assert_eq!(harness.name, "default");
assert_eq!(harness.command, "echo");
}
#[test]
fn test_resolve_written_config_extracts_all_fields() {
let state = make_state_with_answers(vec![
("harness-name", "claude"),
("harness-command", "claude"),
("harness-prompt-slot", "--prompt"),
("global-default-project-root", "/tmp"),
("global-default-harness", "claude"),
]);
let (harness, global) = resolve_written_config(&state);
assert_eq!(harness.name, "claude");
assert_eq!(harness.prompt_slot.as_deref(), Some("--prompt"));
assert_eq!(global.default_project_root.as_deref(), Some("/tmp"));
assert_eq!(global.default_harness.as_deref(), Some("claude"));
}
}