swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! Learn Auto Integration Tests
//!
//! learn auto コマンドの 3-step ワークフロー(Bootstrap → Offline Learning → Release)を検証。
//!
//! # テスト種別
//!
//! - **Unit tests**: LLM 不要、常に実行
//! - **Integration tests**: LLM サーバー必要、`#[ignore]` 付き
//!
//! # Integration tests の実行方法
//!
//! ```bash
//! # llama-server を起動
//! swarm llama start -m ~/.cache/huggingface/hub/models--LiquidAI--LFM2.5-1.2B-Instruct-GGUF/snapshots/*/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
//!
//! # Integration tests を実行
//! cargo test --package swarm-engine-core learn_auto -- --ignored
//! ```

use std::sync::Arc;

use swarm_engine_core::events::{InMemoryTraceSubscriber, NoOpTraceSubscriber, TraceSubscriber};
use swarm_engine_core::learn::{LearningPhase, SessionGroup, SessionGroupId, SessionGroupMetadata};

// ============================================================================
// Unit Tests - LLM 不要
// ============================================================================

mod unit {
    use super::*;

    #[test]
    fn test_learning_phase_serialization() {
        let bootstrap = LearningPhase::Bootstrap;
        let release = LearningPhase::Release;
        let validate = LearningPhase::Validate;

        // Display
        assert_eq!(bootstrap.to_string(), "bootstrap");
        assert_eq!(release.to_string(), "release");
        assert_eq!(validate.to_string(), "validate");

        // FromStr
        assert_eq!(
            "bootstrap".parse::<LearningPhase>().unwrap(),
            LearningPhase::Bootstrap
        );
        assert_eq!(
            "RELEASE".parse::<LearningPhase>().unwrap(),
            LearningPhase::Release
        );

        // JSON serialization
        let json = serde_json::to_string(&bootstrap).unwrap();
        assert_eq!(json, "\"bootstrap\"");

        let deserialized: LearningPhase = serde_json::from_str(&json).unwrap();
        assert_eq!(deserialized, LearningPhase::Bootstrap);
    }

    #[test]
    fn test_session_group_lifecycle() {
        // Create a bootstrap group
        let mut group = SessionGroup::new(LearningPhase::Bootstrap, "test_scenario", 5);
        assert_eq!(group.phase, LearningPhase::Bootstrap);
        assert_eq!(group.metadata.scenario, "test_scenario");
        assert_eq!(group.metadata.target_runs, 5);
        assert_eq!(group.metadata.success_count, 0);
        assert_eq!(group.metadata.failure_count, 0);
        assert!(!group.is_target_reached());

        // Add sessions
        group.add_session(swarm_engine_core::learn::SessionId("1".to_string()), true);
        group.add_session(swarm_engine_core::learn::SessionId("2".to_string()), true);
        group.add_session(swarm_engine_core::learn::SessionId("3".to_string()), false);

        assert_eq!(group.metadata.success_count, 2);
        assert_eq!(group.metadata.failure_count, 1);
        assert_eq!(group.session_ids.len(), 3);
        assert!(!group.is_target_reached());

        // Add more to reach target
        group.add_session(swarm_engine_core::learn::SessionId("4".to_string()), true);
        group.add_session(swarm_engine_core::learn::SessionId("5".to_string()), true);

        assert!(group.is_target_reached());
        assert_eq!(group.success_rate(), 0.8); // 4/5

        // Mark completed
        group.mark_completed();
        assert!(group.metadata.completed_at.is_some());
    }

    #[test]
    fn test_session_group_with_variant() {
        let group =
            SessionGroup::new(LearningPhase::Bootstrap, "test", 10).with_variant("with_graph");

        assert_eq!(group.metadata.variant, Some("with_graph".to_string()));
    }

    #[test]
    fn test_session_group_id_uniqueness() {
        let id1 = SessionGroupId::new();
        let id2 = SessionGroupId::new();

        // Different IDs (based on timestamp, so might be equal in fast execution)
        // Just verify format
        assert!(id1.0.starts_with('g'));
        assert!(id2.0.starts_with('g'));
    }

    #[test]
    fn test_session_group_metadata_success_rate() {
        let mut meta = SessionGroupMetadata::new("test", 10);

        // Initial state
        assert_eq!(meta.success_rate(), 0.0);
        assert_eq!(meta.completed_runs(), 0);

        // Record some results
        meta.record_success();
        meta.record_success();
        meta.record_failure();

        assert_eq!(meta.success_rate(), 2.0 / 3.0);
        assert_eq!(meta.completed_runs(), 3);
    }

    #[test]
    fn test_noop_trace_subscriber() {
        let subscriber = NoOpTraceSubscriber::new();

        // Should not panic
        let event = make_test_action_event(1, "TestAction", true);
        subscriber.on_event(&event);
        subscriber.finish();
    }

    #[test]
    fn test_in_memory_trace_subscriber() {
        let subscriber = InMemoryTraceSubscriber::new();
        assert!(subscriber.is_empty());

        // Add events
        subscriber.on_event(&make_test_action_event(1, "Search", true));
        subscriber.on_event(&make_test_action_event(2, "Read", true));
        subscriber.on_event(&make_test_action_event(3, "Submit", false));

        assert_eq!(subscriber.len(), 3);

        // Check events
        let events = subscriber.events();
        assert_eq!(events[0].tick, 1);
        assert_eq!(events[0].action, "Search");
        assert!(events[0].success);
        assert_eq!(events[2].action, "Submit");
        assert!(!events[2].success);

        // Clear
        subscriber.clear();
        assert!(subscriber.is_empty());
    }

    #[test]
    fn test_in_memory_trace_subscriber_dump_to_file() {
        let subscriber = InMemoryTraceSubscriber::new();

        subscriber.on_event(&make_test_action_event(1, "Search", true));
        subscriber.on_event(&make_test_action_event(2, "Read", true));

        // Dump to temp file
        let temp_dir = std::env::temp_dir();
        let path = temp_dir.join(format!("test_trace_dump_{}.jsonl", std::process::id()));

        subscriber.dump_to_file(&path).unwrap();

        // Verify content
        let content = std::fs::read_to_string(&path).unwrap();
        let lines: Vec<&str> = content.lines().collect();
        assert_eq!(lines.len(), 2);

        // Parse first line
        let first: swarm_engine_core::events::TraceEvent = serde_json::from_str(lines[0]).unwrap();
        assert_eq!(first.tick, 1);
        assert_eq!(first.action, "Search");

        // Cleanup
        std::fs::remove_file(&path).ok();
    }

    #[test]
    fn test_trace_subscriber_arc_wrapper() {
        let subscriber = Arc::new(InMemoryTraceSubscriber::new());

        // Arc<T> should implement TraceSubscriber
        let subscriber_trait: Arc<dyn TraceSubscriber> = subscriber.clone();

        subscriber_trait.on_event(&make_test_action_event(1, "Test", true));
        subscriber_trait.finish();

        assert_eq!(subscriber.len(), 1);
    }

    // Helper function to create test ActionEvent
    fn make_test_action_event(
        tick: u64,
        action: &str,
        success: bool,
    ) -> swarm_engine_core::events::ActionEvent {
        use std::time::Duration;
        use swarm_engine_core::events::{ActionEventBuilder, ActionEventResult};
        use swarm_engine_core::types::WorkerId;

        let result = if success {
            ActionEventResult::success_with_output("ok")
        } else {
            ActionEventResult::failure("error")
        };

        ActionEventBuilder::new(tick, WorkerId(0), action)
            .result(result)
            .duration(Duration::from_millis(50))
            .build()
    }
}

// ============================================================================
// Integration Tests - LLM サーバー必要
// ============================================================================

/// LLM サーバーが必要な統合テスト。
///
/// 実行前に llama-server を起動すること:
/// ```bash
/// swarm llama start -m <model.gguf>
/// ```
///
/// テスト実行:
/// ```bash
/// cargo test --package swarm-engine-core --features llm-tests --test learn_auto_integration
/// ```
#[cfg(feature = "llm-tests")]
mod integration {
    // Note: これらのテストは CLI コマンドの統合テストとして
    // tests/cli_integration.rs または examples/ に配置する方が適切。
    // ここでは構造のみ示す。

    /// Bootstrap フェーズの動作確認(LLM 必要)
    #[test]
    fn test_bootstrap_phase() {
        // TODO: CLI 経由または EvalRunner 経由でテスト
        // swarm learn auto scenarios/deep_search.toml --bootstrap-runs 3 --stop-at bootstrap
    }

    /// 3-step フロー全体の動作確認(LLM 必要)
    #[test]
    fn test_full_3step_workflow() {
        // TODO: CLI 経由でテスト
        // swarm learn auto scenarios/deep_search.toml --bootstrap-runs 3 --release-runs 3
    }
}