ralph-workflow 0.7.18

PROMPT-driven multi-agent orchestrator for git repos
Documentation
use crate::config::{CloudStateConfig, GitAuthStateMethod, GitRemoteStateConfig};
use crate::reducer::event::{CommitEvent, ProcessExecutionResult};
use crate::reducer::io_tests::create_test_state;
use crate::reducer::{reduce, PipelineEvent};

#[test]
fn test_push_failed_keeps_pending_push_commit_for_retry() {
    let mut state = create_test_state();
    state.cloud = CloudStateConfig {
        enabled: true,
        api_url: Some("https://api.example.com".to_string()),
        run_id: Some("run_1".to_string()),
        heartbeat_interval_secs: 30,
        graceful_degradation: true,
        git_remote: GitRemoteStateConfig {
            auth_method: GitAuthStateMethod::SshKey { key_path: None },
            push_branch: "main".to_string(),
            create_pr: false,
            pr_title_template: None,
            pr_body_template: None,
            pr_base_branch: None,
            force_push: false,
            remote_name: "origin".to_string(),
        },
    };
    state.pending_push_commit = Some("abc123".to_string());

    let next = reduce(
        state,
        PipelineEvent::Commit(CommitEvent::PushExecuted {
            remote: "origin".to_string(),
            branch: "main".to_string(),
            commit_sha: "abc123".to_string(),
            result: ProcessExecutionResult {
                exit_code: 1,
                stdout: String::new(),
                stderr: "boom".to_string(),
            },
        }),
    );

    assert_eq!(
        next.pending_push_commit.as_deref(),
        Some("abc123"),
        "Push failures must not clear pending push commit; reducer should allow retry"
    );
}

#[test]
fn test_push_failed_eventually_records_unpushed_commit_and_clears_pending() {
    let mut state = create_test_state();
    state.cloud = CloudStateConfig {
        enabled: true,
        api_url: Some("https://api.example.com".to_string()),
        run_id: Some("run_1".to_string()),
        heartbeat_interval_secs: 30,
        graceful_degradation: true,
        git_remote: GitRemoteStateConfig {
            auth_method: GitAuthStateMethod::SshKey { key_path: None },
            push_branch: "main".to_string(),
            create_pr: false,
            pr_title_template: None,
            pr_body_template: None,
            pr_base_branch: None,
            force_push: false,
            remote_name: "origin".to_string(),
        },
    };
    state.pending_push_commit = Some("abc123".to_string());

    for i in 0..3 {
        state = reduce(
            state,
            PipelineEvent::Commit(CommitEvent::PushExecuted {
                remote: "origin".to_string(),
                branch: "main".to_string(),
                commit_sha: "abc123".to_string(),
                result: ProcessExecutionResult {
                    exit_code: 1,
                    stdout: String::new(),
                    stderr: format!("boom-{i}"),
                },
            }),
        );
    }

    assert!(
        state.pending_push_commit.is_none(),
        "after exhausting push failure budget, reducer should clear pending push so pipeline can proceed"
    );
    assert!(
        state.unpushed_commits.iter().any(|c| c == "abc123"),
        "unpushed commits must be recorded for completion reporting"
    );
}

#[test]
fn test_push_failed_error_is_redacted_before_storing_in_state() {
    let mut state = create_test_state();
    state.cloud = CloudStateConfig {
        enabled: true,
        api_url: Some("https://api.example.com".to_string()),
        run_id: Some("run_1".to_string()),
        heartbeat_interval_secs: 30,
        graceful_degradation: true,
        git_remote: GitRemoteStateConfig {
            auth_method: GitAuthStateMethod::SshKey { key_path: None },
            push_branch: "main".to_string(),
            create_pr: false,
            pr_title_template: None,
            pr_body_template: None,
            pr_base_branch: None,
            force_push: false,
            remote_name: "origin".to_string(),
        },
    };
    state.pending_push_commit = Some("abc123".to_string());

    let next = reduce(
        state,
        PipelineEvent::Commit(CommitEvent::PushExecuted {
            remote: "origin".to_string(),
            branch: "main".to_string(),
            commit_sha: "abc123".to_string(),
            result: ProcessExecutionResult {
                exit_code: 1,
                stdout: String::new(),
                stderr: "fatal: could not read Username for 'https://token@github.com/org/repo.git': terminal prompts disabled".to_string(),
            },
        }),
    );

    let err = next.last_push_error.expect("stored error");
    assert!(
        !err.contains("token@github.com"),
        "userinfo should be redacted"
    );
    assert!(
        err.contains("<redacted>"),
        "should contain redaction marker"
    );
}