#![cfg(feature = "recording")]
use ralph_core::testing::{SmokeRunner, SmokeTestConfig, TerminationReason, list_fixtures};
use std::path::PathBuf;
fn fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
#[test]
fn test_fixtures_directory_exists() {
let dir = fixtures_dir();
assert!(dir.exists(), "Fixtures directory should exist at {:?}", dir);
}
#[test]
fn test_basic_session_fixture_exists() {
let fixture = fixtures_dir().join("basic_session.jsonl");
assert!(
fixture.exists(),
"Basic session fixture should exist at {:?}",
fixture
);
}
#[test]
fn test_complex_session_fixture_exists() {
let fixture = fixtures_dir().join("claude_complex_session.jsonl");
assert!(
fixture.exists(),
"Complex session fixture should exist at {:?}",
fixture
);
}
#[test]
fn test_full_replay_flow_with_basic_session() {
let fixture = fixtures_dir().join("basic_session.jsonl");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run fixture successfully");
assert!(
result.completed_successfully(),
"Basic session should complete successfully"
);
assert_eq!(
*result.termination_reason(),
TerminationReason::Completed,
"Should terminate with Completed (LOOP_COMPLETE detected)"
);
assert!(
result.iterations_run() >= 2,
"Should process at least 2 chunks (completion found in 3rd)"
);
assert!(
result.event_count() >= 2,
"Should parse at least 2 events from fixture, got {}",
result.event_count()
);
assert!(
result.output_bytes() > 0,
"Should have processed some output bytes"
);
}
#[test]
fn test_full_replay_flow_with_complex_session() {
let fixture = fixtures_dir().join("claude_complex_session.jsonl");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run fixture successfully");
assert!(
result.completed_successfully(),
"Complex session should complete successfully"
);
assert_eq!(
*result.termination_reason(),
TerminationReason::Completed,
"Should terminate with Completed (LOOP_COMPLETE detected)"
);
assert!(
result.iterations_run() >= 3,
"Should process at least 3 chunks, got {}",
result.iterations_run()
);
assert!(
result.event_count() >= 5,
"Should parse at least 5 events from complex fixture, got {}",
result.event_count()
);
assert!(
result.output_bytes() > 500,
"Complex session should have substantial output, got {} bytes",
result.output_bytes()
);
}
#[test]
fn test_fixture_discovery() {
let fixtures = list_fixtures(fixtures_dir()).expect("Should list fixtures");
assert!(
!fixtures.is_empty(),
"Should find at least one fixture in {:?}",
fixtures_dir()
);
let fixture_names: Vec<_> = fixtures
.iter()
.filter_map(|p| p.file_name())
.map(|n| n.to_string_lossy().to_string())
.collect();
assert!(
fixture_names.contains(&"basic_session.jsonl".to_string()),
"Should discover basic_session.jsonl, found: {:?}",
fixture_names
);
}
#[test]
fn test_all_discovered_fixtures_are_valid() {
let fixtures = list_fixtures(fixtures_dir()).expect("Should list fixtures");
for fixture_path in fixtures {
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config);
assert!(
result.is_ok(),
"Fixture {:?} should be valid and runnable: {:?}",
fixture_path,
result.err()
);
}
}
mod regression_detection {
use super::*;
use ralph_core::Record;
use ralph_core::testing::{ReplayBackend, SmokeTestError};
use ralph_proto::TerminalWrite;
use std::io::Write;
use std::time::Duration;
use tempfile::TempDir;
fn create_fixture(dir: &std::path::Path, name: &str, content: &str) -> PathBuf {
let path = dir.join(name);
let mut file = std::fs::File::create(&path).unwrap();
file.write_all(content.as_bytes()).unwrap();
path
}
fn make_write_line(text: &str, offset_ms: u64) -> String {
let write = TerminalWrite::new(text.as_bytes(), true, offset_ms);
let record = Record {
ts: 1000 + offset_ms,
event: "ux.terminal.write".to_string(),
data: serde_json::to_value(&write).unwrap(),
};
serde_json::to_string(&record).unwrap()
}
#[test]
fn test_catches_malformed_jsonl_fixture() {
let temp_dir = TempDir::new().unwrap();
let fixture_path = create_fixture(
temp_dir.path(),
"malformed.jsonl",
"this is not valid json\nalso not valid",
);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config);
assert!(
result.is_err(),
"Malformed JSONL should cause an error, but got: {:?}",
result
);
let err = result.unwrap_err();
assert!(
matches!(err, SmokeTestError::Io(_)),
"Should be IO error from JSON parsing, got: {:?}",
err
);
}
#[test]
fn test_catches_invalid_base64_in_terminal_write() {
let temp_dir = TempDir::new().unwrap();
let invalid_fixture = r#"{"ts":1000,"event":"ux.terminal.write","data":{"bytes":"!!!INVALID_BASE64!!!","stdout":true,"offset_ms":0}}"#;
let fixture_path = create_fixture(temp_dir.path(), "bad_base64.jsonl", invalid_fixture);
let backend = ReplayBackend::from_file(&fixture_path);
assert!(
backend.is_ok(),
"Should load file, handling bad data gracefully"
);
let mut backend = backend.unwrap();
let output = backend.next_output();
assert!(
output.is_none(),
"Invalid base64 should be skipped, not crash"
);
}
#[test]
fn test_catches_missing_data_field() {
let temp_dir = TempDir::new().unwrap();
let incomplete = r#"{"ts":1000,"event":"ux.terminal.write"}"#;
let fixture_path = create_fixture(temp_dir.path(), "missing_data.jsonl", incomplete);
let backend = ReplayBackend::from_file(&fixture_path);
assert!(
backend.is_err(),
"REGRESSION: Missing required 'data' field should cause an error"
);
}
#[test]
fn test_event_parser_counts_detected_events() {
let temp_dir = TempDir::new().unwrap();
let output_with_events = r#"Starting task
<event topic="build.task">Task 1</event>
Working on implementation...
<event topic="build.done">
tests: pass
lint: pass
typecheck: pass
audit: pass
coverage: pass
</event>
Finishing up"#;
let line = make_write_line(output_with_events, 0);
let fixture_path = create_fixture(temp_dir.path(), "with_events.jsonl", &line);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should process fixture");
assert_eq!(
result.event_count(),
2,
"REGRESSION: EventParser should find exactly 2 events (build.task, build.done)"
);
}
#[test]
fn test_completion_promise_detected() {
let temp_dir = TempDir::new().unwrap();
let line1 = make_write_line("Working on task...", 0);
let line2 = make_write_line(r#"<event topic="LOOP_COMPLETE">done</event>"#, 100);
let content = format!("{}\n{}\n", line1, line2);
let fixture_path = create_fixture(temp_dir.path(), "with_completion.jsonl", &content);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should run");
assert_eq!(
*result.termination_reason(),
TerminationReason::Completed,
"REGRESSION: LOOP_COMPLETE should trigger Completed termination"
);
}
#[test]
fn test_no_completion_results_in_fixture_exhausted() {
let temp_dir = TempDir::new().unwrap();
let line1 = make_write_line("Working on task...", 0);
let line2 = make_write_line("Done but no completion promise", 100);
let content = format!("{}\n{}\n", line1, line2);
let fixture_path = create_fixture(temp_dir.path(), "no_completion.jsonl", &content);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should run");
assert_eq!(
*result.termination_reason(),
TerminationReason::FixtureExhausted,
"REGRESSION: Missing LOOP_COMPLETE should result in FixtureExhausted"
);
}
#[test]
fn test_promise_inside_event_tag_does_not_complete() {
let temp_dir = TempDir::new().unwrap();
let output = r#"Working on task...
<event topic="build.task">Fix LOOP_COMPLETE detection bug</event>
Still working..."#;
let line = make_write_line(output, 0);
let fixture_path = create_fixture(temp_dir.path(), "promise_in_tag.jsonl", &line);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should run");
assert_eq!(
*result.termination_reason(),
TerminationReason::FixtureExhausted,
"REGRESSION: LOOP_COMPLETE inside event tag should NOT trigger completion"
);
}
#[test]
fn test_output_bytes_counted_correctly() {
let temp_dir = TempDir::new().unwrap();
let text1 = "Hello"; let text2 = "World"; let line1 = make_write_line(text1, 0);
let line2 = make_write_line(text2, 100);
let content = format!("{}\n{}\n", line1, line2);
let fixture_path = create_fixture(temp_dir.path(), "byte_count.jsonl", &content);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should run");
assert_eq!(
result.output_bytes(),
10,
"REGRESSION: Output bytes should be exactly 10 (5 + 5)"
);
}
#[test]
fn test_iteration_count_matches_chunks() {
let temp_dir = TempDir::new().unwrap();
let lines: Vec<String> = (0..5)
.map(|i| make_write_line(&format!("Chunk {}", i), i * 100))
.collect();
let content = lines.join("\n") + "\n";
let fixture_path = create_fixture(temp_dir.path(), "five_chunks.jsonl", &content);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should run");
assert_eq!(
result.iterations_run(),
5,
"REGRESSION: Should have exactly 5 iterations for 5 chunks"
);
}
#[test]
fn test_missing_fixture_produces_clear_error() {
let config = SmokeTestConfig::new("/definitely/does/not/exist/fixture.jsonl");
let result = SmokeRunner::run(&config);
assert!(result.is_err(), "Missing fixture should error");
let err = result.unwrap_err();
match err {
SmokeTestError::FixtureNotFound(path) => {
assert!(
path.to_string_lossy().contains("fixture.jsonl"),
"Error should contain the missing filename"
);
}
_ => panic!(
"REGRESSION: Should be FixtureNotFound error, got: {:?}",
err
),
}
}
#[test]
fn test_non_terminal_events_filtered_out() {
let temp_dir = TempDir::new().unwrap();
let terminal = make_write_line("Hello", 0);
let meta = r#"{"ts":1050,"event":"_meta.iteration","data":{"n":1,"elapsed_ms":50,"hat":"default"}}"#;
let bus = r#"{"ts":1100,"event":"bus.publish","data":{"topic":"test","payload":"data"}}"#;
let content = format!("{}\n{}\n{}\n", terminal, meta, bus);
let fixture_path = create_fixture(temp_dir.path(), "mixed_events.jsonl", &content);
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Should run");
assert_eq!(
result.iterations_run(),
1,
"REGRESSION: Should only count terminal write events"
);
assert_eq!(
result.output_bytes(),
5,
"REGRESSION: Only terminal write bytes should be counted"
);
}
#[test]
fn test_empty_fixture_handled_gracefully() {
let temp_dir = TempDir::new().unwrap();
let fixture_path = create_fixture(temp_dir.path(), "empty.jsonl", "");
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config).expect("Empty fixture should not error");
assert_eq!(result.iterations_run(), 0);
assert_eq!(result.event_count(), 0);
assert_eq!(result.output_bytes(), 0);
assert_eq!(
*result.termination_reason(),
TerminationReason::FixtureExhausted
);
}
#[test]
fn test_replay_preserves_order() {
let temp_dir = TempDir::new().unwrap();
let line1 = make_write_line("First", 0);
let line2 = make_write_line("Second", 100);
let line3 = make_write_line("Third", 200);
let content = format!("{}\n{}\n{}\n", line1, line2, line3);
let fixture_path = create_fixture(temp_dir.path(), "ordered.jsonl", &content);
let mut backend = ReplayBackend::from_file(&fixture_path).expect("Should load");
assert_eq!(
backend.next_output().unwrap(),
b"First",
"REGRESSION: First chunk should be 'First'"
);
assert_eq!(
backend.next_output().unwrap(),
b"Second",
"REGRESSION: Second chunk should be 'Second'"
);
assert_eq!(
backend.next_output().unwrap(),
b"Third",
"REGRESSION: Third chunk should be 'Third'"
);
assert!(
backend.next_output().is_none(),
"Should be exhausted after 3 chunks"
);
}
#[test]
fn test_basic_session_fixture_contract() {
let fixture = fixtures_dir().join("basic_session.jsonl");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Basic session should run");
assert!(
result.completed_successfully(),
"REGRESSION: Basic session fixture must complete successfully"
);
assert_eq!(
*result.termination_reason(),
TerminationReason::Completed,
"REGRESSION: Basic session must detect LOOP_COMPLETE"
);
assert!(
result.event_count() >= 2,
"REGRESSION: Basic session must contain at least 2 parseable events, got {}",
result.event_count()
);
assert!(
result.output_bytes() > 100,
"REGRESSION: Basic session should have substantial output, got {} bytes",
result.output_bytes()
);
}
#[test]
fn test_timeout_configuration_respected() {
let temp_dir = TempDir::new().unwrap();
let line = make_write_line("Quick output", 0);
let fixture_path = create_fixture(temp_dir.path(), "quick.jsonl", &line);
let config = SmokeTestConfig::new(&fixture_path).with_timeout(Duration::from_secs(60));
assert_eq!(config.timeout, Duration::from_secs(60));
let result = SmokeRunner::run(&config).expect("Should complete within 60s");
assert!(result.completed_successfully());
}
#[test]
fn test_replay_backend_reset() {
let temp_dir = TempDir::new().unwrap();
let line = make_write_line("ReplayMe", 0);
let fixture_path = create_fixture(temp_dir.path(), "replay.jsonl", &line);
let mut backend = ReplayBackend::from_file(&fixture_path).expect("Should load");
assert_eq!(backend.next_output().unwrap(), b"ReplayMe");
assert!(backend.is_exhausted());
backend.reset();
assert!(!backend.is_exhausted());
assert_eq!(
backend.next_output().unwrap(),
b"ReplayMe",
"REGRESSION: Reset should allow replaying from start"
);
}
}
mod kiro_smoke_tests {
use super::*;
fn kiro_fixtures_dir() -> PathBuf {
fixtures_dir().join("kiro")
}
#[test]
fn test_kiro_fixtures_directory_exists() {
let dir = kiro_fixtures_dir();
assert!(
dir.exists(),
"Kiro fixtures directory should exist at {:?}",
dir
);
}
#[test]
fn test_kiro_has_at_least_two_fixtures() {
let fixtures = list_fixtures(kiro_fixtures_dir()).expect("Should list Kiro fixtures");
assert!(
fixtures.len() >= 2,
"Kiro should have at least 2 fixtures, found {}",
fixtures.len()
);
}
#[test]
fn test_kiro_readme_exists() {
let readme = kiro_fixtures_dir().join("README.md");
assert!(
readme.exists(),
"Kiro fixtures README should exist at {:?}",
readme
);
}
#[test]
fn test_kiro_basic_session_fixture_loads() {
let fixture = kiro_fixtures_dir().join("basic_kiro_session.jsonl");
assert!(fixture.exists(), "basic_kiro_session.jsonl should exist");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should load and run Kiro fixture");
assert!(
result.completed_successfully(),
"Kiro basic session should complete successfully"
);
}
#[test]
fn test_kiro_autonomous_mode_fixture() {
let fixture = kiro_fixtures_dir().join("kiro_autonomous.jsonl");
assert!(fixture.exists(), "kiro_autonomous.jsonl should exist");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run autonomous mode fixture");
assert_eq!(
*result.termination_reason(),
TerminationReason::Completed,
"Autonomous mode fixture should complete with LOOP_COMPLETE"
);
}
#[test]
fn test_kiro_tool_use_events_parsed() {
let fixture = kiro_fixtures_dir().join("kiro_tool_use.jsonl");
assert!(fixture.exists(), "kiro_tool_use.jsonl should exist");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run tool use fixture");
assert!(
result.event_count() >= 2,
"Tool use fixture should have at least 2 events, got {}",
result.event_count()
);
}
#[test]
fn test_cross_backend_compatibility() {
let claude_fixture = fixtures_dir().join("basic_session.jsonl");
let claude_config = SmokeTestConfig::new(&claude_fixture);
let claude_result = SmokeRunner::run(&claude_config).expect("Claude fixture should run");
let kiro_fixture = kiro_fixtures_dir().join("basic_kiro_session.jsonl");
let kiro_config = SmokeTestConfig::new(&kiro_fixture);
let kiro_result = SmokeRunner::run(&kiro_config).expect("Kiro fixture should run");
assert!(
claude_result.completed_successfully(),
"Claude fixture should complete"
);
assert!(
kiro_result.completed_successfully(),
"Kiro fixture should complete"
);
assert!(
claude_result.event_count() >= 2,
"Claude fixture should parse events"
);
assert!(
kiro_result.event_count() >= 2,
"Kiro fixture should parse events"
);
}
#[test]
fn test_kiro_full_replay_flow() {
let fixture = kiro_fixtures_dir().join("basic_kiro_session.jsonl");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run Kiro fixture");
assert!(
result.iterations_run() >= 2,
"Should process at least 2 chunks"
);
assert!(result.output_bytes() > 0, "Should process output bytes");
assert!(result.event_count() >= 2, "Should parse events");
assert_eq!(
*result.termination_reason(),
TerminationReason::Completed,
"Should detect LOOP_COMPLETE"
);
}
#[test]
fn test_all_kiro_fixtures_are_valid() {
let fixtures = list_fixtures(kiro_fixtures_dir()).expect("Should list Kiro fixtures");
for fixture_path in fixtures {
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config);
assert!(
result.is_ok(),
"Kiro fixture {:?} should be valid and runnable: {:?}",
fixture_path,
result.err()
);
}
}
}
mod kiro_acp_smoke_tests {
use super::*;
fn kiro_acp_fixtures_dir() -> PathBuf {
fixtures_dir().join("kiro-acp")
}
#[test]
fn test_kiro_acp_fixtures_directory_exists() {
assert!(kiro_acp_fixtures_dir().exists());
}
#[test]
fn test_kiro_acp_has_at_least_two_fixtures() {
let fixtures =
list_fixtures(kiro_acp_fixtures_dir()).expect("Should list kiro-acp fixtures");
assert!(
fixtures.len() >= 2,
"Expected >= 2 fixtures, got {}",
fixtures.len()
);
}
#[test]
fn test_kiro_acp_readme_exists() {
assert!(kiro_acp_fixtures_dir().join("README.md").exists());
}
#[test]
fn test_kiro_acp_basic_session_fixture_loads() {
let fixture = kiro_acp_fixtures_dir().join("basic_kiro_acp_session.jsonl");
assert!(fixture.exists());
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run kiro-acp fixture");
assert!(result.completed_successfully());
}
#[test]
fn test_kiro_acp_tool_use_events_parsed() {
let fixture = kiro_acp_fixtures_dir().join("kiro_acp_tool_use.jsonl");
assert!(fixture.exists());
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run tool use fixture");
assert!(
result.event_count() >= 2,
"Expected >= 2 events, got {}",
result.event_count()
);
}
#[test]
fn test_kiro_acp_cross_backend_compatibility() {
let claude_fixture = fixtures_dir().join("basic_session.jsonl");
let claude_config = SmokeTestConfig::new(&claude_fixture);
let claude_result = SmokeRunner::run(&claude_config).expect("Claude fixture should run");
let kiro_acp_fixture = kiro_acp_fixtures_dir().join("basic_kiro_acp_session.jsonl");
let kiro_acp_config = SmokeTestConfig::new(&kiro_acp_fixture);
let kiro_acp_result =
SmokeRunner::run(&kiro_acp_config).expect("kiro-acp fixture should run");
assert!(claude_result.completed_successfully());
assert!(kiro_acp_result.completed_successfully());
assert!(claude_result.event_count() >= 2);
assert!(kiro_acp_result.event_count() >= 2);
}
#[test]
fn test_kiro_acp_full_replay_flow() {
let fixture = kiro_acp_fixtures_dir().join("basic_kiro_acp_session.jsonl");
let config = SmokeTestConfig::new(&fixture);
let result = SmokeRunner::run(&config).expect("Should run kiro-acp fixture");
assert!(result.iterations_run() >= 2);
assert!(result.output_bytes() > 0);
assert!(result.event_count() >= 2);
assert_eq!(*result.termination_reason(), TerminationReason::Completed);
}
#[test]
fn test_all_kiro_acp_fixtures_are_valid() {
let fixtures =
list_fixtures(kiro_acp_fixtures_dir()).expect("Should list kiro-acp fixtures");
for fixture_path in fixtures {
let config = SmokeTestConfig::new(&fixture_path);
let result = SmokeRunner::run(&config);
assert!(
result.is_ok(),
"kiro-acp fixture {:?} should be valid: {:?}",
fixture_path,
result.err()
);
}
}
}
mod skills_smoke_tests {
use ralph_core::{
HatRegistry, HatlessRalph, RalphConfig, SkillOverride, SkillRegistry, SkillsConfig,
};
use std::collections::HashMap;
use std::path::PathBuf;
fn skills_fixtures_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/skills")
}
#[test]
fn test_skills_fixtures_directory_exists() {
let dir = skills_fixtures_dir();
assert!(
dir.exists(),
"Skills fixtures directory should exist at {:?}",
dir
);
}
#[test]
fn test_builtin_skills_present_in_registry() {
let config = SkillsConfig::default();
let registry = SkillRegistry::from_config(&config, std::path::Path::new("."), None)
.expect("Should build registry with defaults");
assert!(
registry.get("ralph-tools").is_some(),
"Built-in ralph-tools skill should be registered"
);
}
#[test]
fn test_registry_discovers_fixture_skills() {
let config = SkillsConfig {
enabled: true,
dirs: vec![skills_fixtures_dir()],
overrides: HashMap::new(),
};
let registry = SkillRegistry::from_config(&config, std::path::Path::new("."), None)
.expect("Should build registry with skills dir");
let test_skill = registry.get("test-skill");
assert!(
test_skill.is_some(),
"Should discover test-skill.md from fixtures"
);
let test_skill = test_skill.unwrap();
assert_eq!(
test_skill.description,
"A test skill for smoke testing the skills system"
);
assert!(test_skill.content.contains("# Test Skill"));
let complex_skill = registry.get("complex-test-skill");
assert!(
complex_skill.is_some(),
"Should discover complex-test-skill/SKILL.md from fixtures"
);
let complex_skill = complex_skill.unwrap();
assert_eq!(
complex_skill.description,
"A directory-style test skill for smoke testing"
);
assert_eq!(complex_skill.hats, vec!["builder"]);
}
#[test]
fn test_skill_index_lists_all_visible_skills() {
let config = SkillsConfig {
enabled: true,
dirs: vec![skills_fixtures_dir()],
overrides: HashMap::new(),
};
let registry =
SkillRegistry::from_config(&config, std::path::Path::new("."), None).unwrap();
let index = registry.build_index(None);
assert!(
index.contains("## SKILLS"),
"Index should contain ## SKILLS header"
);
assert!(
index.contains("ralph-tools"),
"Index should list the ralph-tools skill"
);
assert!(
index.contains("test-skill"),
"Index should list the test-skill from fixtures"
);
assert!(
index.contains("complex-test-skill"),
"Index should list the complex-test-skill from fixtures"
);
assert!(
index.contains("`ralph tools skill load test-skill`"),
"Index should contain load command for test-skill"
);
}
#[test]
fn test_skill_index_hat_filtering() {
let config = SkillsConfig {
enabled: true,
dirs: vec![skills_fixtures_dir()],
overrides: HashMap::new(),
};
let registry =
SkillRegistry::from_config(&config, std::path::Path::new("."), None).unwrap();
let builder_index = registry.build_index(Some("builder"));
assert!(
builder_index.contains("complex-test-skill"),
"Builder should see complex-test-skill"
);
let reviewer_index = registry.build_index(Some("reviewer"));
assert!(
!reviewer_index.contains("complex-test-skill"),
"Reviewer should NOT see complex-test-skill (restricted to builder)"
);
assert!(
builder_index.contains("test-skill"),
"Builder should see unrestricted test-skill"
);
assert!(
reviewer_index.contains("test-skill"),
"Reviewer should see unrestricted test-skill"
);
}
#[test]
fn test_skill_index_injected_into_prompt() {
let config = SkillsConfig {
enabled: true,
dirs: vec![skills_fixtures_dir()],
overrides: HashMap::new(),
};
let registry =
SkillRegistry::from_config(&config, std::path::Path::new("."), None).unwrap();
let skill_index = registry.build_index(None);
let ralph_config = RalphConfig::default();
let hat_registry = HatRegistry::new();
let ralph = HatlessRalph::new(
"LOOP_COMPLETE",
ralph_config.core.clone(),
&hat_registry,
None,
)
.with_skill_index(skill_index);
let prompt = ralph.build_prompt("", &[]);
assert!(
prompt.contains("## SKILLS"),
"Assembled prompt should contain ## SKILLS section"
);
assert!(
prompt.contains("ralph-tools"),
"Assembled prompt should list ralph-tools skill"
);
assert!(
prompt.contains("test-skill"),
"Assembled prompt should list test-skill"
);
let guardrails_pos = prompt.find("GUARDRAILS");
let skills_pos = prompt.find("## SKILLS");
assert!(
guardrails_pos.is_some() && skills_pos.is_some(),
"Both GUARDRAILS and SKILLS sections should exist"
);
if let (Some(g), Some(s)) = (guardrails_pos, skills_pos) {
assert!(
g < s,
"SKILLS section should appear after GUARDRAILS (guardrails at {}, skills at {})",
g,
s
);
}
}
#[test]
fn test_backwards_compat_no_skills_in_config() {
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();
assert!(config.skills.enabled, "Skills should be enabled by default");
assert!(
config.skills.dirs.is_empty(),
"Skills dirs should default to empty"
);
assert!(
config.skills.overrides.is_empty(),
"Skills overrides should default to empty"
);
let registry =
SkillRegistry::from_config(&config.skills, std::path::Path::new("."), Some("claude"))
.unwrap();
assert!(registry.get("ralph-tools").is_some());
let index = registry.build_index(None);
assert!(index.contains("## SKILLS"));
}
#[test]
fn test_skills_config_yaml_parsing() {
let yaml = r#"
skills:
enabled: true
dirs:
- ".claude/skills"
- "/path/to/shared/skills"
overrides:
pdd:
enabled: false
memories:
auto_inject: true
hats: ["ralph"]
"#;
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
assert!(config.skills.enabled);
assert_eq!(config.skills.dirs.len(), 2);
assert_eq!(config.skills.dirs[0], PathBuf::from(".claude/skills"));
let pdd = config.skills.overrides.get("pdd").expect("pdd override");
assert_eq!(pdd.enabled, Some(false));
let memories = config
.skills
.overrides
.get("memories")
.expect("memories override");
assert_eq!(memories.auto_inject, Some(true));
assert_eq!(memories.hats, vec!["ralph"]);
}
#[test]
fn test_skills_disabled_empty_index() {
let yaml = r"
skills:
enabled: false
";
let config: RalphConfig = serde_yaml::from_str(yaml).unwrap();
assert!(!config.skills.enabled);
let ralph_config = RalphConfig::default();
let hat_registry = HatRegistry::new();
let ralph = HatlessRalph::new(
"LOOP_COMPLETE",
ralph_config.core.clone(),
&hat_registry,
None,
)
.with_skill_index(String::new());
let prompt = ralph.build_prompt("", &[]);
assert!(
!prompt.contains("## SKILLS"),
"Disabled skills should not produce ## SKILLS section"
);
}
#[test]
fn test_override_disables_skill_in_index() {
let mut overrides = HashMap::new();
overrides.insert(
"test-skill".to_string(),
SkillOverride {
enabled: Some(false),
..Default::default()
},
);
let config = SkillsConfig {
enabled: true,
dirs: vec![skills_fixtures_dir()],
overrides,
};
let registry =
SkillRegistry::from_config(&config, std::path::Path::new("."), None).unwrap();
assert!(
registry.get("test-skill").is_none(),
"test-skill should be disabled by override"
);
assert!(registry.get("ralph-tools").is_some());
assert!(registry.get("complex-test-skill").is_some());
let index = registry.build_index(None);
assert!(
!index.contains("| test-skill |"),
"Disabled test-skill should not appear in index"
);
}
#[test]
fn test_existing_smoke_tests_backwards_compat() {
use ralph_core::testing::{SmokeRunner, SmokeTestConfig};
let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
let basic = fixtures_dir.join("basic_session.jsonl");
let config = SmokeTestConfig::new(&basic);
let result = SmokeRunner::run(&config).expect("Basic session should still run");
assert!(
result.completed_successfully(),
"BACKWARDS COMPAT: Basic session fixture should still complete successfully"
);
let complex = fixtures_dir.join("claude_complex_session.jsonl");
let config = SmokeTestConfig::new(&complex);
let result = SmokeRunner::run(&config).expect("Complex session should still run");
assert!(
result.completed_successfully(),
"BACKWARDS COMPAT: Complex session fixture should still complete successfully"
);
}
#[test]
fn test_load_skill_xml_wrapping() {
let config = SkillsConfig {
enabled: true,
dirs: vec![skills_fixtures_dir()],
overrides: HashMap::new(),
};
let registry =
SkillRegistry::from_config(&config, std::path::Path::new("."), None).unwrap();
let loaded = registry
.load_skill("test-skill")
.expect("Should load test-skill");
assert!(
loaded.starts_with("<test-skill-skill>"),
"Loaded skill should start with XML open tag"
);
assert!(
loaded.ends_with("</test-skill-skill>"),
"Loaded skill should end with XML close tag"
);
assert!(
loaded.contains("# Test Skill"),
"Loaded skill should contain body content"
);
assert!(
!loaded.contains("name: test-skill"),
"Loaded skill should NOT contain frontmatter"
);
}
}