ek-ui 0.9.22

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! 运行时装配链(ui 工厂 / 关窗协议 / eTimer 快照回路)。
//!
//! L1+集成:`ui_kinds` 可装配、观察线程收场触发各插件 on_stop、
//! eTimer 1s 组快照真把 svctl.list 行数落进 shared。

use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};

use ek_core::main_engine::MainEngine;
use ek_core::plugin::Plugin;
use ek_event::Event as BusEvent;
use ek_station::{StationConfig, boot_with_cfg};
use ek_ui::{spawn_host_watcher, ui_kinds};

#[derive(Default)]
pub struct StopProbe {
    stops: Arc<Mutex<Vec<String>>>,
}

impl Plugin for StopProbe {
    fn name(&self) -> &str {
        "stopp"
    }

    fn on_stop(&self) {
        self.stops.lock().unwrap().push("stopp".to_string());
    }

    fn on_mail(&self, _event: &BusEvent) {}
}

fn wait_until(tag: &str, secs: u64, f: impl Fn() -> bool) {
    let deadline = Instant::now() + Duration::from_secs(secs);
    while Instant::now() < deadline {
        if f() {
            return;
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    panic!("wait_until timeout: {tag}");
}

/// 全链:装配(含 ui 工厂)→ start → eTimer 快照 → 关窗位 → 观察线程 close。
#[test]
fn t0_ui_kind_snapshot_loop_and_close_chain() {
    let engine = Arc::new(MainEngine::new(None));
    let shared = Arc::new(ek_ui::UiShared::new());
    let stops = Arc::new(Mutex::new(Vec::<String>::new()));

    engine
        .add_plugin(Arc::new(StopProbe {
            stops: Arc::clone(&stops),
        }))
        .unwrap();

    let cfg = StationConfig {
        plugins: vec!["cfg".to_string(), "dm".to_string(), "oms".to_string(), "ui".to_string()],
    };
    // 步骤 5:显式根(缺省形会落 Store::eco() = 真实档案根;复审已把显缝加回 default_root,测试仍走显式根)。
    let rig = ek_cfg::Store::with_root(std::path::PathBuf::from("target/ui-runtime-rig"));
    boot_with_cfg(&engine, &ui_kinds(&shared, true), &cfg, Some(&rig)).unwrap();
    engine.start().unwrap();

    // eTimer(1s) → ui.refresh → shared 快照:行数 ≥ 4(svctl/dm/oms/ui 皆见)。
    wait_until("snapshot fills", 4, || {
        let s = shared.snapshot();
        s.rows.len() >= 4 && s.rows.iter().any(|r| r.name == "ui")
    });
    // ui 也上过线(自身上线日志)。
    assert!(
        shared
            .logs_snapshot()
            .0
            .iter()
            .any(|l| l.contains("界面插件上线")),
        "ui plugin started log missing"
    );

    // 关窗协议:request_stop → 观察线程 close_all(全体 on_stop)→ join 回收。
    let watcher = spawn_host_watcher(&engine, &shared);
    shared.request_stop();
    watcher.join().unwrap();

    let got = stops.lock().unwrap();
    assert!(
        got.iter().any(|n| n == "stopp"),
        "stop probe must have fired on close chain: {got:?}"
    );
}

/// RunOpts 缺省钉:dm_rt 必须缺省 true(UI 宿主实时门;derive(Default) 的
/// bool=false 陷阱曾在配置面撤销时引入回归,此钉防再犯)。
#[test]
fn t1_run_opts_default_dm_rt_true() {
    assert!(ek_ui::runtime::RunOpts::default().dm_rt);
    assert!(ek_ui::runtime::RunOpts::plain().dm_rt);
    assert!(ek_ui::runtime::RunOpts::default().ready_marker.is_none());
}