use ralph_core::{EventLoop, RalphConfig};
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard, OnceLock};
use tempfile::TempDir;
fn test_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|err| err.into_inner())
}
fn safe_current_dir() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| {
let fallback = std::env::temp_dir();
std::env::set_current_dir(&fallback).expect("set fallback cwd");
fallback
})
}
struct CwdGuard {
_lock: MutexGuard<'static, ()>,
original: PathBuf,
}
impl CwdGuard {
fn set(path: &Path) -> Self {
let lock = test_lock();
let original = safe_current_dir();
std::env::set_current_dir(path).expect("set current dir");
Self {
_lock: lock,
original,
}
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
#[test]
fn test_orphaned_event_falls_to_ralph() {
let temp_dir = TempDir::new().unwrap();
let ralph_dir = temp_dir.path().join(".ralph");
fs::create_dir_all(&ralph_dir).unwrap();
let events_file = ralph_dir.join("events.jsonl");
fs::write(
&events_file,
r#"{"topic":"orphan.event","payload":"This event has no subscriber","ts":"2026-01-14T12:00:00Z"}
"#,
)
.unwrap();
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
guardrails:
- "Fresh context each iteration"
- "Backpressure is law"
event_loop:
completion_promise: "LOOP_COMPLETE"
max_iterations: 10
max_runtime_seconds: 300
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let mut event_loop = EventLoop::new(config);
let _cwd = CwdGuard::set(temp_dir.path());
let result = event_loop.process_events_from_jsonl().unwrap();
assert!(
result.has_orphans,
"Expected orphaned event to trigger Ralph"
);
}
#[test]
fn test_repeated_task_complete_does_not_trigger_loop_stale() {
let temp_dir = TempDir::new().unwrap();
let ralph_dir = temp_dir.path().join(".ralph");
fs::create_dir_all(&ralph_dir).unwrap();
let events_file = ralph_dir.join("events.jsonl");
fs::write(
&events_file,
r#"{"topic":"task.complete","payload":"task 1 complete","ts":"2026-03-08T06:54:01Z"}
{"topic":"task.complete","payload":"task 2 complete","ts":"2026-03-08T06:54:02Z"}
{"topic":"task.complete","payload":"task 3 complete","ts":"2026-03-08T06:54:03Z"}
"#,
)
.unwrap();
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
event_loop:
completion_promise: "LOOP_COMPLETE"
max_iterations: 10
max_runtime_seconds: 300
"#;
let mut config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
config.core.workspace_root = temp_dir.path().to_path_buf();
let mut event_loop = EventLoop::new(config);
let _cwd = CwdGuard::set(temp_dir.path());
event_loop.process_events_from_jsonl().unwrap();
let termination = event_loop.check_termination();
assert!(termination.is_none());
assert_eq!(
event_loop
.state()
.last_emitted_signature
.as_ref()
.map(|sig| sig.topic.as_str()),
Some("task.complete")
);
assert_eq!(event_loop.state().consecutive_same_signature, 0);
}
#[test]
fn test_ralph_completion_only_from_ralph() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
event_loop:
completion_promise: "LOOP_COMPLETE"
max_iterations: 10
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let ralph_output = r#"<event topic="LOOP_COMPLETE">All tasks complete.</event>"#;
assert!(
event_loop.check_ralph_completion(ralph_output),
"Ralph should be able to trigger completion"
);
let output_with_promise = "Some work done\nLOOP_COMPLETE\nMore text";
assert!(
!event_loop.check_ralph_completion(output_with_promise),
"Completion requires emitted event, not plain text"
);
let output_without_promise = "Some work done\nNo completion here";
assert!(
!event_loop.check_ralph_completion(output_without_promise),
"Output without LOOP_COMPLETE should not trigger completion"
);
}
#[test]
fn test_ralph_prompt_includes_ghuntley_style() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
guardrails:
- "Fresh context each iteration"
- "Backpressure is law"
event_loop:
completion_promise: "LOOP_COMPLETE"
memories:
enabled: false
tasks:
enabled: false
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let prompt = event_loop.build_ralph_prompt("Test context");
assert!(
prompt.contains("You are Ralph"),
"Prompt should identify Ralph with RFC2119 style"
);
assert!(
prompt.contains("You have fresh context each iteration"),
"Prompt should include RFC2119 identity"
);
assert!(
prompt.contains("### 0a. ORIENTATION"),
"Prompt should include orientation phase"
);
assert!(
prompt.contains("### 0b. SCRATCHPAD"),
"Prompt should include scratchpad section"
);
assert!(
prompt.contains("## WORKFLOW"),
"Prompt should include workflow section"
);
assert!(
prompt.contains("### GUARDRAILS"),
"Prompt should include guardrails section"
);
assert!(
prompt.contains("LOOP_COMPLETE"),
"Prompt should include completion event"
);
}
#[test]
fn test_ralph_prompt_solo_mode_structure() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
event_loop:
completion_promise: "LOOP_COMPLETE"
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let prompt = event_loop.build_ralph_prompt("");
assert!(prompt.contains("## WORKFLOW"), "Workflow should be present");
assert!(
prompt.contains("## EVENT WRITING"),
"Event writing section should be present"
);
assert!(
!prompt.contains("## HATS"),
"HATS section should not be present in solo mode"
);
}
#[test]
fn test_ralph_prompt_multi_hat_mode_structure() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
hats:
planner:
name: "Planner"
triggers: ["task.start"]
publishes: ["build.task"]
builder:
name: "Builder"
triggers: ["build.task"]
publishes: ["build.done"]
event_loop:
completion_promise: "LOOP_COMPLETE"
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let prompt = event_loop.build_ralph_prompt("");
assert!(
prompt.contains("## HATS"),
"HATS section should be present in multi-hat mode"
);
assert!(
prompt.contains("Delegate via events"),
"Delegation instruction should be present"
);
assert!(prompt.contains("Planner"), "Planner hat should be listed");
assert!(prompt.contains("Builder"), "Builder hat should be listed");
assert!(
prompt.contains("| Hat | Triggers On | Publishes |"),
"Hat table header should be present"
);
}
#[test]
fn test_solo_mode_memories_task_verification_requirements() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
event_loop:
completion_promise: "LOOP_COMPLETE"
memories:
enabled: true
tasks:
enabled: true
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let prompt = event_loop.build_ralph_prompt("");
assert!(
prompt.contains("### 0b. SCRATCHPAD"),
"Prompt should include SCRATCHPAD section"
);
assert!(
!prompt.contains("### 0c. TASKS"),
"Tasks section should NOT be in core_prompt — injected via skills pipeline"
);
assert!(
prompt.contains("ralph tools task"),
"Prompt should reference task CLI in workflow/done sections"
);
assert!(
prompt.contains("### 4. VERIFY & COMMIT"),
"Workflow should have VERIFY & COMMIT step"
);
assert!(
prompt.contains("AFTER commit"),
"Workflow should emphasize closing only after commit"
);
}
#[test]
fn test_multihat_mode_has_workflow_section() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
event_loop:
completion_promise: "LOOP_COMPLETE"
hats:
builder:
name: "Builder"
description: "Implements code changes"
triggers: ["build.task"]
publishes: ["build.done", "build.blocked"]
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let prompt = event_loop.build_ralph_prompt("");
assert!(
prompt.contains("### 2. DELEGATE"),
"Multi-hat mode should have DELEGATE step"
);
assert!(
!prompt.contains("### 3. IMPLEMENT"),
"Multi-hat mode should NOT have IMPLEMENT step (Ralph delegates, doesn't implement)"
);
assert!(
prompt.contains("## HATS"),
"Multi-hat mode should have HATS section"
);
assert!(
prompt.contains("Builder"),
"Multi-hat mode should list the Builder hat"
);
}
#[test]
fn test_scratchpad_mode_no_task_verification() {
let yaml = r#"
core:
scratchpad: ".ralph/agent/scratchpad.md"
specs_dir: "./specs"
event_loop:
completion_promise: "LOOP_COMPLETE"
memories:
enabled: false
tasks:
enabled: false
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
let event_loop = EventLoop::new(config);
let prompt = event_loop.build_ralph_prompt("");
assert!(
!prompt.contains("CRITICAL: Task Closure Requirements"),
"Scratchpad mode should not have detailed task closure requirements"
);
assert!(
prompt.contains("### 4. COMMIT"),
"Scratchpad mode should have COMMIT step"
);
assert!(
prompt.contains("mark the task `[x]`"),
"Scratchpad mode should use markdown task markers"
);
}
#[test]
fn test_reads_actual_events_jsonl_with_object_payloads() {
use ralph_core::EventHistory;
let history = EventHistory::new(".ralph/events.jsonl");
if !history.exists() {
return;
}
let records = history.read_all().expect("Should read events.jsonl");
assert!(!records.is_empty(), "events.jsonl should have records");
println!(
"\n✓ Successfully parsed {} records from .ralph/events.jsonl:\n",
records.len()
);
for (i, record) in records.iter().enumerate() {
let payload_preview = if record.payload.len() > 50 {
format!("{}...", &record.payload[..50])
} else {
record.payload.clone()
};
let payload_type = if record.payload.starts_with('{') {
"object→string"
} else {
"string"
};
println!(
" [{}] topic={:<25} type={:<14} payload={}",
i + 1,
record.topic,
payload_type,
payload_preview
);
if record.payload.starts_with('{') {
let _: serde_json::Value = serde_json::from_str(&record.payload)
.expect("Object payload should be valid JSON string");
}
}
println!();
}