ek-core 0.2.0

Business-agnostic plugin host and actor-over-bus runtime on top of ek-event: capability index with runtime service discovery, per-plugin actor workers, dynamic add/remove with lifecycle broadcasts, one-shot ask/ask_service with contract gates, panic quarantine, five-step close.
Documentation
//! host_lifecycle:注册校验 / 拓扑 start / 逆序五步 close / 关停静默 / 不回放 / start 后拒绝 add

mod common;

use common::{records, wait_until};
use ek_core::{MainEngine, MailStats, PluginError, PluginState};
use ek_event::Event;
use std::sync::Arc;
use std::time::Duration;

// HL-1 注册期校验:名字规则/重名/deps 存在性/自依赖环/start 后拒绝
#[test]
fn hl1_register_validation() {
    let eng = MainEngine::new(None);
    // 非法名:空/数字开头/含空白/超长
    for bad in ["", "9abc", "bad name", "日本語", &"a".repeat(33)] {
        match eng.add_plugin(common::test_plugin(bad).build()) {
            Err(PluginError::IllegalName(n)) => assert_eq!(n, bad),
            other => panic!("expected IllegalName for {bad:?}, got {other:?}"),
        }
    }
    eng.add_plugin(common::test_plugin("oms").build()).unwrap();
    // 重名
    match eng.add_plugin(common::test_plugin("oms").build()) {
        Err(PluginError::Duplicate(n)) => assert_eq!(n, "oms"),
        other => panic!("expected Duplicate, got {other:?}"),
    }
    // deps 指向未注册目标:新 D1 注册自由 —— add 期放行,start 期把关
    eng.add_plugin(common::test_plugin("x").deps(&["ghost"]).build()).unwrap();
    // 自依赖 = 环
    match eng.add_plugin(common::test_plugin("y").deps(&["y"]).build()) {
        Err(PluginError::DepCycle(_)) => {}
        other => panic!("expected DepCycle, got {other:?}"),
    }
    // 合法注册成功 + 查询
    assert_eq!(eng.plugin_names(), vec!["oms".to_string(), "x".to_string()]);
    assert_eq!(eng.get_state("oms"), Some(PluginState::Ready));
    let info = eng.get_plugin("oms").unwrap();
    assert_eq!(info.deps, Vec::<String>::new());

    // start 期把关:x 的 deps [ghost] 不存在 → Err(UnknownDep)
    {
        let e3 = MainEngine::new(None);
        e3.add_plugin(common::test_plugin("x2").deps(&["ghost"]).build()).unwrap();
        match e3.start() {
            Err(PluginError::UnknownDep(d)) => assert_eq!(d, "ghost"),
            other => panic!("expected UnknownDep at start, got {other:?}"),
        }
        e3.close_all(Duration::from_secs(1));
    }
    eng.close_all(Duration::from_secs(1));
}

// HL-2 start 拓扑序 + close 逆序(D1/D4)
#[test]
fn hl2_start_close_order() {
    let eng = MainEngine::new(None);
    let seq = records::<String>();

    let mk = |name: &'static str, deps: &'static [&'static str]| {
        let seq_start = Arc::clone(&seq);
        let seq_stop = Arc::clone(&seq);
        common::test_plugin(name)
            .deps(deps)
            .start_hook(move || common::push_record(&seq_start, format!("start:{name}")))
            .stop_hook(move || common::push_record(&seq_stop, format!("stop:{name}")))
            .build()
    };
    // A deps B,B 无 deps,C deps A → 拓扑:B A C;close 反向:C B A
    // (deps 在 add 期校验存在,故按依赖顺序注册:B → A → C)
    eng.add_plugin(mk("b", &[])).unwrap();
    eng.add_plugin(mk("a", &["b"])).unwrap();
    eng.add_plugin(mk("c", &["a"])).unwrap();

    eng.start().unwrap();
    eng.close(Duration::from_secs(2));

    let got = common::snapshot(&seq);
    assert_eq!(
        got,
        vec![
            "start:b".to_string(),
            "start:a".to_string(),
            "start:c".to_string(),
            "stop:c".to_string(),
            "stop:a".to_string(),
            "stop:b".to_string(),
        ]
    );
    // close 后插件表已清空
    assert!(eng.plugin_names().is_empty());
    assert_eq!(eng.get_state("b"), None);
}

// HL-3 close 五步第①③步的可见效果:关停后不再收到任何邮件
#[test]
fn hl3_silence_after_close() {
    let eng = MainEngine::new(None);
    let ticks = common::counter();
    let t = Arc::clone(&ticks);
    eng.add_plugin(
        common::test_plugin("p")
            .routes_str(&["eTick."])
            .mail(move |_e: &Event| {
                common::bump(&t);
            })
            .build(),
    )
    .unwrap();
    eng.start().unwrap();
    for i in 0..3u64 {
        eng.event_engine()
            .put(Event::with_data("eTick.", Arc::new(i)))
            .unwrap();
    }
    assert!(
        wait_until(Duration::from_secs(2), || common::load(&ticks) == 3),
        "ticks before close"
    );

    eng.close(Duration::from_secs(2));
    for i in 0..5u64 {
        // 总线仍在(未 close_all):事件可 put,但无人收
        let _ = eng.event_engine().put(Event::new("eTick."));
        let _ = i;
    }
    std::thread::sleep(Duration::from_millis(300));
    assert_eq!(common::load(&ticks), 3, "no mails after close");
}

// HL-4 g3 等效:启动前流出的事件不回放;start 前只允许装配车道
#[test]
fn hl4_no_replay_then_live() {
    let eng = MainEngine::new(None);
    let bus = Arc::clone(eng.event_engine());
    bus.start(); // 先于 main.start() 手工拉起总线

    // 启动前放置 2 个事件:分发时刻无 forwarder → 无人收,也不回放
    bus.put(Event::new("eTick.")).unwrap();
    bus.put(Event::new("eTick.")).unwrap();
    assert!(wait_until(Duration::from_secs(1), || bus.pending_events() == 0));
    // 给最后一条事件的处理留裕量(处理为微秒级,200ms 足够消除竞态)
    std::thread::sleep(Duration::from_millis(200));

    let ticks = common::counter();
    let t = Arc::clone(&ticks);
    eng.add_plugin(
        common::test_plugin("p")
            .routes_str(&["eTick."])
            .mail(move |_e: &Event| {
                common::bump(&t);
            })
            .build(),
    )
    .unwrap();
    eng.start().unwrap();
    assert_eq!(common::load(&ticks), 0, "no replay of pre-start events");

    for i in 0..3u64 {
        bus.put(Event::with_data("eTick.", Arc::new(i))).unwrap();
    }
    assert!(wait_until(Duration::from_secs(2), || common::load(&ticks) == 3));
    eng.close_all(Duration::from_secs(1));
}

// HL-5 统计快照可用(get_state/get_plugin/mail_stats)
#[test]
fn hl5_info_and_stats() {
    let eng = MainEngine::new(None);
    eng.add_plugin(
        common::test_plugin("m")
            .routes_str(&["eTick."])
            .mail(|_e: &Event| {})
            .build(),
    )
    .unwrap();
    eng.start().unwrap();
    assert_eq!(eng.get_state("m"), Some(PluginState::Running));
    assert_eq!(
        eng.mail_stats("m"),
        Some(MailStats { spilled: 0, panics: 0 })
    );
    // 事件流转一次
    eng.event_engine().put(Event::new("eTick.")).unwrap();
    assert!(wait_until(Duration::from_millis(500), || {
        eng.mail_stats("m").map(|s| s.panics).unwrap_or(9) == 0
    }));
    eng.close_all(Duration::from_secs(1));
}