ek-ui 0.9.9

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
Documentation
//! 「插件即 App」挂载波判据(ek-ui 0.9.2, kittest)。
//!
//! U-W1 挂载表只列装配且有配置面的插件(ui 插件无 `as_configurable` = 不出现);
//! U-W2 派发出面:dm/oms/tq/ctp 各自的面可渲染(帧不炸 + 控件在场);
//! U-W3 保存 = 写临时根 + 即时喂回受理面(dm 新增周期即时在册);
//! U-W4 档案包导出 = 段文件整批复制数一致;导入 = 逐段收编 + 受理显形;
//! U-W5 未注入重建器的 oms:风控键记账照做、闸面保持装配期形态(与 O-R5 同口径)。
//!
//! 铁纪律:**只打临时根**(`target/tmp/...`),真实档案零触碰。

use egui_kittest::kittest::Queryable;
use ek_core::Configurable;
use ek_core::cfg_face::CfgHost;
use ek_core::main_engine::MainEngine;
use ek_core::plugin::Plugin;
use ek_core::section::{Field, SectionMap};
use ek_ui::faces::{self, CfgBridge, FaceStates};
use ek_ui::pluginwin::{self, WinState};
use ek_ui::state::UiShared;
use std::sync::Arc;

/// 临时根桥(判据专用;真实 %APPDATA% 不碰)。
fn temp_host(tag: &str) -> (Arc<UiShared>, ek_cfg::Store, std::path::PathBuf) {
    let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("target")
        .join("tmp")
        .join(format!("u-{}", tag));
    let _ = std::fs::remove_dir_all(&root);
    let cfg = ek_cfg::Store::with_root(root.clone());
    let shared = Arc::new(UiShared::new());
    (shared, cfg, root)
}

struct Bridge {
    cfg: ek_cfg::Store,
    shared: Arc<UiShared>,
}

impl CfgHost for Bridge {
    fn cfg_save(&self, ns: &str, map: &SectionMap) -> Result<(), String> {
        self.cfg.store(ns, map).map_err(|e| e.to_string())
    }
    fn cfg_load(&self, ns: &str) -> Result<Option<SectionMap>, String> {
        match self.cfg.load(ns) {
            Ok(ek_cfg::LoadOutcome::Loaded { map, .. }) => Ok(Some(map)),
            Ok(ek_cfg::LoadOutcome::Fresh) => Ok(None),
            Err(e) => Err(e.to_string()),
        }
    }
    fn log(&self, line: &str) {
        self.shared.log_line(line);
    }
}

fn demo_engine(tag: &str) -> Arc<MainEngine> {
    let _ = tag;
    let engine = Arc::new(MainEngine::new(None));
    engine
        .add_plugin(Arc::new(ek_dm::plugin::DmPlugin::new(
            ek_dm::plugin::DmConfig::minute("dm"),
        )))
        .expect("add dm");
    engine
        .add_plugin(Arc::new(ek_oms::plugin::OmsPlugin::new()))
        .expect("add oms");
    engine
}

#[test]
fn u_w1_mount_table_lists_only_configurable_plugins() {
    let engine = demo_engine("w1");
    let mut m = faces::mounted(&engine);
    m.sort();
    let names: Vec<&str> = m.iter().map(|(n, _)| n.as_str()).collect();
    assert_eq!(names, vec!["dm", "oms"], "只列装配且有面的插件:{:?}", m);
}

#[test]
fn u_w2_faces_dispatch_renders_without_panic() {
    let engine = demo_engine("w2");
    let shared = Arc::new(UiShared::new());
    let mut harness = egui_kittest::Harness::new_ui_state(
        move |ui, ()| {
            let mut fs = FaceStates::new();
            let host = CfgBridge {
                cfg: None,
                shared: Arc::clone(&shared),
            };
            for (name, _title) in faces::mounted(&engine) {
                faces::render_face(ui, &engine, &name, &host, &mut fs);
            }
        },
        (),
    );
    let _frames = harness.run();
    // 判据 = 两面控件同帧在场且帧不炸。取两面**独有**标签做唯一性钉
    // (dm 面「符号窗盘点」、oms 面「刷新管线读数」;两面保存钮同名会撞查询唯一性)。
    let dm_face = harness.query_by_label_contains("符号窗盘点").is_some();
    let oms_face = harness.query_by_label_contains("刷新管线读数").is_some();
    assert!(
        dm_face && oms_face,
        "面控件缺席:dm={dm_face} oms={oms_face}"
    );
}

#[test]
fn u_w3_save_writes_temp_root_and_applies_live() {
    let (shared, cfg, root) = temp_host("w3");
    let engine = demo_engine("w3c");
    let dm = engine
        .get_typed::<ek_dm::plugin::DmPlugin>("dm")
        .expect("dm shell");
    let host = Bridge {
        cfg,
        shared: Arc::clone(&shared),
    };
    // 走插件面保存路径(受理面即时生效):直接喂一段新周期
    let map = SectionMap::new(1).set("intervals", Field::plain("60,300"));
    host.cfg_save("dm", &map).expect("save temp root");
    let rep = dm.apply_config(&map).expect("apply");
    assert!(
        rep.adopted.iter().any(|k| k == "intervals"),
        "{:?}",
        rep.adopted
    );
    assert_eq!(dm.intervals_snapshot(), vec![60, 300], "即时在册");
    assert!(root.join("dm.ini").exists(), "临时根落了档案");
    let _ = std::fs::remove_dir_all(&root);
}

#[test]
fn u_w4_bundle_export_import_roundtrip() {
    let (_shared, cfg, root) = temp_host("w4");
    let map = SectionMap::new(1).set("rate_limit", Field::plain("5"));
    cfg.store("oms", &map).expect("seed oms section");
    let m2 = SectionMap::new(1).set("theme", Field::plain("dark"));
    cfg.store("ui", &m2).expect("seed ui section");
    let out = root.join("bundle");
    let (copied, skipped) = pluginwin::export_bundle(&root, &out).expect("export");
    assert_eq!(copied, 2, "两段档案要整批走(跳过 {skipped})");
    // 导入到另一台"机器":换根后逐段收编 + 受理显形
    let (shared2, cfg2, root2) = temp_host("w4b");
    let engine = demo_engine("w4c");
    let host2 = Bridge {
        cfg: cfg2,
        shared: Arc::clone(&shared2),
    };
    let lines = pluginwin::import_bundle(&host2, &out, &|ns, sec| {
        let line = match ns {
            "oms" => {
                let p = engine.get_typed::<ek_oms::plugin::OmsPlugin>(ns)?;
                p.as_configurable()?.apply_config(sec).ok()?
            }
            _ => return None,
        };
        Some(format!(
            "受理 采纳{}/待重启{}",
            line.adopted.len(),
            line.deferred.len()
        ))
    });
    let joined = lines.join(" | ");
    assert!(joined.contains("oms:收编 1 字段"), "{joined}");
    assert!(joined.contains("受理 采纳1"), "oms 段要过受理面:{joined}");
    assert!(
        joined.contains("ui:收编 1 字段"),
        "ui 段照收编(宿主自持):{joined}"
    );
    assert!(
        joined.contains("ekapp add-account"),
        "口令引导行必须在:{joined}"
    );
    let _ = std::fs::remove_dir_all(&root);
    let _ = std::fs::remove_dir_all(&root2);
}

#[test]
fn u_w5_generic_window_rows_from_view_when_no_schema() {
    // tq 段自持键(空 schema):通用窗口按 config_view 出行,秘面只读
    let (shared, cfg, root) = temp_host("w5");
    let spec = ek_tq::plugin::TqPluginSpec::new(ek_tq::bridge::BridgeConfig::default());
    let tq = ek_tq::plugin::TqPlugin::new(spec);
    let map = SectionMap::new(1)
        .set("url", Field::plain("ws://127.0.0.1:7709"))
        .set("user", Field::secret("secret://tq/user"));
    tq.apply_config(&map).expect("seed tq section");
    let host = Bridge {
        cfg,
        shared: Arc::clone(&shared),
    };
    host.cfg_save("tq", &map).expect("save tq root");
    let mut harness = egui_kittest::Harness::new_ui_state(
        move |ui, ()| {
            let mut st = WinState::new();
            pluginwin::render_window(ui, &tq, &host, &mut st);
        },
        (),
    );
    let _h = harness.run();
    let _ = std::fs::remove_dir_all(&root);
}