ralph-workflow 0.7.18

PROMPT-driven multi-agent orchestrator for git repos
Documentation
mod tests {
    use super::*;
    use crate::phases::commit::diff_truncation::{
        truncate_diff_if_large, truncate_lines_to_fit, CLAUDE_MAX_PROMPT_SIZE, GLM_MAX_PROMPT_SIZE,
        MAX_SAFE_PROMPT_SIZE,
    };
    use crate::prompts::SubstitutionLog;
    use crate::reducer::event::{CommitEvent, PipelineEvent};

    #[test]
    fn test_truncate_diff_if_large() {
        let _cloud = crate::config::types::CloudConfig::disabled();
        let large_diff = "diff --git a/src/main.rs b/src/main.rs\n".repeat(1000);
        let truncated = truncate_diff_if_large(&large_diff, 10_000);

        assert!(truncated.len() <= 10_000 + 200);
        assert!(truncated.contains("[Truncated:"));
    }

    #[test]
    fn test_truncate_diff_no_truncation_needed() {
        let _cloud = crate::config::types::CloudConfig::disabled();
        let small_diff = "diff --git a/src/main.rs b/src/main.rs\n+change\n";
        let truncated = truncate_diff_if_large(small_diff, 10_000);

        assert_eq!(truncated, small_diff);
    }

    #[test]
    fn test_truncate_diff_preserves_structure() {
        let _cloud = crate::config::types::CloudConfig::disabled();
        let diff = "diff --git a/src/main.rs b/src/main.rs\n+change1\n\
            diff --git a/src/lib.rs b/src/lib.rs\n+change2\n";
        let truncated = truncate_diff_if_large(diff, 10_000);

        assert!(truncated.contains("diff --git a/src/main.rs"));
        assert!(truncated.contains("diff --git a/src/lib.rs"));
    }

    #[test]
    fn test_truncate_diff_very_small_limit() {
        let _cloud = crate::config::types::CloudConfig::disabled();
        // Diff must exceed the limit (80) to trigger truncation.
        let diff = "diff --git a/src/main.rs b/src/main.rs\n+change with enough content here to exceed\n";
        let truncated = truncate_diff_if_large(diff, 80);

        assert!(truncated.len() <= 80 + 200);
        assert!(truncated.contains("[Truncated:"));
    }

    #[test]
    fn test_truncate_lines_to_fit() {
        let lines = vec![
            "line1".to_string(),
            "line2".to_string(),
            "line3".to_string(),
        ];
        let max_size = 12;

        let truncated = truncate_lines_to_fit(&lines, max_size);

        assert!(truncated.join("\n").len() <= max_size);
    }

    #[test]
    fn test_truncate_lines_to_fit_no_truncation() {
        let lines = vec!["a".to_string(), "b".to_string()];
        let max_size = 100;

        let truncated = truncate_lines_to_fit(&lines, max_size);

        assert_eq!(truncated.len(), 2);
    }

    #[test]
    fn test_effective_model_budget_bytes_single_agent() {
        let agents = vec!["claude".to_string()];
        assert_eq!(
            effective_model_budget_bytes(&agents),
            CLAUDE_MAX_PROMPT_SIZE
        );
    }

    #[test]
    fn test_effective_model_budget_bytes_multiple_agents() {
        let agents = vec!["claude".to_string(), "glm".to_string()];
        assert_eq!(effective_model_budget_bytes(&agents), GLM_MAX_PROMPT_SIZE);
    }

    #[test]
    fn test_effective_model_budget_bytes_no_agents() {
        let agents: Vec<String> = vec![];
        assert_eq!(effective_model_budget_bytes(&agents), MAX_SAFE_PROMPT_SIZE);
    }

    #[test]
    fn test_commit_prompt_content_id_includes_residual_files() {
        let residual = vec!["src/lib.rs".to_string(), "Cargo.toml".to_string()];
        let with_residual = commit_prompt_content_id("diff123", "consumer456", &residual);
        let without_residual = commit_prompt_content_id("diff123", "consumer456", &[]);

        assert_ne!(with_residual, without_residual);
    }

    #[test]
    fn test_commit_xsd_retry_prompt_content_id_changes_with_error() {
        let first = commit_xsd_retry_prompt_content_id("diff123", "xsd-a", "consumer456");
        let second = commit_xsd_retry_prompt_content_id("diff123", "xsd-b", "consumer456");

        assert_ne!(first, second);
    }

    #[test]
    fn test_prepend_residual_files_context_formats_note() {
        let base_prompt = "Base prompt";
        let residual = vec!["src/lib.rs".to_string(), "Cargo.toml".to_string()];

        let updated = prepend_residual_files_context(base_prompt, &residual);

        assert!(updated.contains("must be accounted for in this commit run"));
        assert!(updated.contains("- src/lib.rs"));
        assert!(updated.contains("- Cargo.toml"));
        assert!(updated.ends_with(base_prompt));
    }

    #[test]
    fn test_diff_unavailable_investigation_instructions_contains_error() {
        let message = diff_unavailable_investigation_instructions("boom");

        assert!(message.contains("git diff"));
        assert!(message.contains("boom"));
        assert!(message.contains("<ralph-commit>"));
    }

    #[test]
    fn test_commit_outcome_event_prefers_message() {
        let event = commit_outcome_event_from_validated(
            Some("feat: add parser".to_string()),
            Some("should be ignored".to_string()),
            7,
        );

        assert!(matches!(
            event,
            PipelineEvent::Commit(CommitEvent::MessageGenerated {
                message,
                attempt: 7
            }) if message == "feat: add parser"
        ));
    }

    #[test]
    fn test_commit_outcome_event_uses_reason_without_message() {
        let event = commit_outcome_event_from_validated(None, Some("invalid xml".to_string()), 3);

        assert!(matches!(
            event,
            PipelineEvent::Commit(CommitEvent::MessageValidationFailed {
                reason,
                attempt: 3
            }) if reason == "invalid xml"
        ));
    }

    #[test]
    fn test_commit_outcome_event_reports_missing_message_and_reason() {
        let event = commit_outcome_event_from_validated(None, None, 1);

        assert!(matches!(
            event,
            PipelineEvent::Commit(CommitEvent::GenerationFailed { reason })
                if reason == "Commit validation outcome missing message and reason"
        ));
    }

    #[test]
    fn test_parse_commit_xml_document_detects_skip() {
        let xml = "<ralph-commit><ralph-skip>No changes to commit</ralph-skip></ralph-commit>";

        let parsed = parse_commit_xml_document(xml);

        assert!(matches!(
            parsed,
            ParsedCommitXmlOutcome::Skipped(reason) if reason == "No changes to commit"
        ));
    }

    #[test]
    fn test_commit_representation_and_reason_prefers_model_budget_exceeded() {
        let (representation, reason) = commit_representation_and_reason(
            123,
            100,
            true,
            std::path::Path::new(".agent/tmp/commit_diff.model_safe.txt"),
        );

        assert!(matches!(
            representation,
            crate::reducer::state::PromptInputRepresentation::FileReference { .. }
        ));
        assert_eq!(
            reason,
            crate::reducer::state::PromptMaterializationReason::ModelBudgetExceeded
        );
    }

    #[test]
    fn test_commit_representation_and_reason_selects_inline_within_budgets() {
        let (representation, reason) = commit_representation_and_reason(
            64,
            100,
            false,
            std::path::Path::new(".agent/tmp/commit_diff.model_safe.txt"),
        );

        assert_eq!(
            representation,
            crate::reducer::state::PromptInputRepresentation::Inline
        );
        assert_eq!(
            reason,
            crate::reducer::state::PromptMaterializationReason::WithinBudgets
        );
    }

    #[test]
    fn test_base_prompt_for_same_agent_retry_strips_existing_retry_header() {
        use crate::reducer::state::{ContinuationState, SameAgentRetryReason};
        let continuation = ContinuationState {
            same_agent_retry_count: 1,
            same_agent_retry_reason: Some(SameAgentRetryReason::Timeout),
            ..ContinuationState::default()
        };
        let preamble =
            crate::reducer::boundary::retry_guidance::same_agent_retry_preamble(&continuation);
        let original = "Original base prompt";
        let previous_prompt = format!("{preamble}\n\n{original}");
        let generated = "Freshly generated prompt";

        let (base_prompt, should_validate) =
            base_prompt_for_same_agent_retry(Some(&previous_prompt), generated);

        assert_eq!(base_prompt, "Original base prompt");
        assert!(!should_validate);
    }

    #[test]
    fn test_prompt_captured_event_returns_none_when_prompt_was_replayed() {
        let event = prompt_captured_event("scope-key", "prompt body", "content-id", true);

        assert!(event.is_none());
    }

    #[test]
    fn test_prompt_captured_event_returns_prompt_input_when_fresh() {
        let event = prompt_captured_event("scope-key", "prompt body", "content-id", false)
            .expect("fresh prompts must emit PromptCaptured event");

        assert!(matches!(
            event,
            PipelineEvent::PromptInput(crate::reducer::event::PromptInputEvent::PromptCaptured {
                key,
                content,
                content_id: Some(id)
            }) if key == "scope-key" && content == "prompt body" && id == "content-id"
        ));
    }

    #[test]
    fn test_commit_prompt_prepared_result_adds_template_rendered_event_when_present() {
        let rendered_log = SubstitutionLog {
            template_name: "commit_message_xml".to_string(),
            substituted: Vec::new(),
            unsubstituted: Vec::new(),
        };

        let result = commit_prompt_prepared_result(
            2,
            crate::reducer::event::PipelinePhase::Planning,
            "scope-key".to_string(),
            false,
            None,
            Some(rendered_log),
            "commit_message_xml",
        );

        assert!(matches!(
            result.event,
            PipelineEvent::Commit(CommitEvent::PromptPrepared { attempt: 2 })
        ));
        assert!(matches!(
            result.ui_events.as_slice(),
            [
                crate::reducer::ui_event::UIEvent::PhaseTransition {
                    to: crate::reducer::event::PipelinePhase::CommitMessage,
                    ..
                },
                crate::reducer::ui_event::UIEvent::PromptReplayHit { key, was_replayed: false }
            ] if key == "scope-key"
        ));
        assert!(matches!(
            result.additional_events.as_slice(),
            [PipelineEvent::PromptInput(
                crate::reducer::event::PromptInputEvent::TemplateRendered {
                    phase: crate::reducer::event::PipelinePhase::CommitMessage,
                    template_name,
                    ..
                }
            )] if template_name == "commit_message_xml"
        ));
    }
}