ryo-app 0.1.0

[preview] Application layer for RYO - Project management, Intent handling, API
Documentation
//! Duplicate Integration Tests
//!
//! Tests code duplication operations.

use std::path::PathBuf;

use rstest::{fixture, rstest};
use ryo_app::{AnalysisContext, BlueprintExecutor, Goal, ParallelBlueprint, Planner};
use ryo_test::harness::{source_diff, sources_equal};

// =============================================================================
// Fixtures
// =============================================================================

#[fixture]
fn dup_source() -> &'static str {
    include_str!("fixtures/duplicate_test.rs")
}

#[fixture]
fn dup_ctx(dup_source: &'static str) -> DupTestContext {
    DupTestContext::new(dup_source)
}

// =============================================================================
// Test Context
// =============================================================================

pub struct DupTestContext {
    _temp_dir: tempfile::TempDir,
    workspace_root: PathBuf,
    source: String,
}

impl DupTestContext {
    pub fn new(source: &str) -> Self {
        let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
        let src_dir = temp_dir.path().join("src");
        std::fs::create_dir_all(&src_dir).expect("Failed to create src dir");

        let lib_rs = src_dir.join("lib.rs");
        std::fs::write(&lib_rs, source).expect("Failed to write lib.rs");

        let cargo_toml = temp_dir.path().join("Cargo.toml");
        std::fs::write(
            &cargo_toml,
            r#"[package]
name = "test_crate"
version = "0.1.0"
edition = "2021"
"#,
        )
        .expect("Failed to write Cargo.toml");

        Self {
            workspace_root: temp_dir.path().to_path_buf(),
            _temp_dir: temp_dir,
            source: source.to_string(),
        }
    }

    pub fn execute(&self, goal: &Goal) -> Result<String, String> {
        // Create AnalysisContext first to get registry for planning
        let mut ctx = AnalysisContext::from_workspace_root(&self.workspace_root)
            .map_err(|e| format!("Context creation error: {e}"))?;

        let specs = Planner::plan(goal, Some(&ctx.registry))
            .map_err(|e| format!("Planning failed: {}", e))?;

        if specs.is_empty() {
            return Ok(self.source.clone());
        }

        let blueprint = ParallelBlueprint::from_mutations(specs);
        if blueprint.needs_escalation() {
            return Err("Blueprint has conflicts".to_string());
        }

        let executor = BlueprintExecutor::default();
        let result = executor.execute_v2(&blueprint, &mut ctx);

        if !result.success {
            return Err(result.error.unwrap_or_else(|| "Unknown error".to_string()));
        }

        // Sync files after execute_v2 (required for ctx.files() to see changes)
        BlueprintExecutor::sync_files_and_rebuild(&result, &mut ctx).map_err(|e| e.to_string())?;

        ctx.files()
            .iter()
            .next()
            .map(|(_, file)| file.to_source().map_err(|e| e.to_string()))
            .ok_or_else(|| "No files".to_string())?
    }

    pub fn assert_output(&self, goal: &Goal, expected: &str) {
        let result = self.execute(goal).expect("Transformation failed");
        if !sources_equal(expected, &result) {
            let diff = source_diff(expected, &result);
            eprintln!("=== ACTUAL OUTPUT ===\n{}\n=== END ===", result);
            panic!("Mismatch!\n\nDiff:\n{}", diff);
        }
    }
}

// =============================================================================
// Tests
// =============================================================================

#[rstest]
fn test_duplicate_function(dup_ctx: DupTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "DuplicateFunction",
            "target_fn": "handle_request",
            "to": "handle_admin_request"
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/duplicate-function.snap");
    dup_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_duplicate_struct(dup_ctx: DupTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "DuplicateStruct",
            "target_struct": "Config",
            "to": "AdminConfig",
            "include_impls": true
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/duplicate-struct.snap");
    dup_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_duplicate_enum(dup_ctx: DupTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "DuplicateEnum",
            "target_enum": "Status",
            "to": "AdminStatus",
            "include_impls": false
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/duplicate-enum.snap");
    dup_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_duplicate_struct_cascade(dup_ctx: DupTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "DuplicateStruct",
            "target_struct": "Config",
            "to": "AdminConfig",
            "include_impls": true
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/duplicate-struct-cascade.snap");
    dup_ctx.assert_output(&goal, expected);
}

#[rstest]
fn test_duplicate_enum_cascade(dup_ctx: DupTestContext) {
    let goal: Goal = serde_json::from_str(
        r#"{
        "intents": [{
            "type": "DuplicateEnum",
            "target_enum": "Status",
            "to": "AdminStatus",
            "include_impls": true
        }]
    }"#,
    )
    .unwrap();
    let expected = include_str!("snapshots/duplicate-enum-cascade.snap");
    dup_ctx.assert_output(&goal, expected);
}