ek-ui 0.9.46

ek 生态常驻主界面(egui/eframe):插件即界面——观测页 + 日志页,车头与 UI 同插件
//! **D1/D2:同一账户的两条 configure 路径会不会落到同一个 `CtpConfig`。**
//!
//! D1 = 两条入口产出逐字段比:
//!   - 档案路径:`UiCmd::Start` → 宿主 boot 载入段 → `Configurable::apply_config`
//!     (**不碰密库**,密码只登引用)
//!   - 表单路径:界面填表 → `{gw}.configure` → `CtpPlugin::configure`
//!
//! D2 = 档案段**每个键**都要有归宿(adopted 或 deferred),不许静默吞。
//!
//! 现场档案是加密的、只有本机有,所以 `#[ignore]`,需要 `EK_CFG_FALLBACK_ROOT`
//! 指向一份可读档案根(`<root>/cfg/cfg.db` + `<root>/master.key`)。
//!
//! 本测试**只读**档案:`apply_config` 按契约禁调 `secret::get`,`dump_config`
//! 在明文为空时也不会写库;为稳妥仍先复制副本,母本不碰。
//!
//! 跑法:
//! ```powershell
//! $env:CARGO_PROFILE_DEV_DEBUG="0"
//! $env:EK_CFG_FALLBACK_ROOT="C:\dev\fuke\ek-ui\target\arch-real\cfg"
//! cd C:\dev\fuke\ek-ui
//! cargo test --features ctp-native --test cfg_paths -- --ignored --nocapture
//! ```

#![cfg(feature = "ctp-native")]

use ek_core::section::Configurable;
use std::path::PathBuf;

fn manifest() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}

fn esc(s: &str) -> String {
    s.replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', " ")
}

/// 报表收集:先记账,最后一次性打印 + 失败汇总(同 B 档口径)。
struct Rec {
    lines: Vec<String>,
    fails: usize,
}

impl Rec {
    fn new() -> Self {
        Self {
            lines: Vec::new(),
            fails: 0,
        }
    }
    fn add(&mut self, id: &str, ok: bool, detail: &str) {
        let v = if ok { "PASS" } else { "FAIL" };
        if !ok {
            self.fails += 1;
        }
        println!("[{v}] {id} :: {detail}");
        self.lines.push(format!(
            r#"{{"step":"{}","observed":"{}","verdict":"{v}"}}"#,
            esc(id),
            esc(detail)
        ));
    }
    fn info(&mut self, id: &str, detail: &str) {
        println!("[INFO] {id} :: {detail}");
        self.lines.push(format!(
            r#"{{"step":"{}","observed":"{}","verdict":"INFO"}}"#,
            esc(id),
            esc(detail)
        ));
    }
    fn flush(&self) {
        let dir = manifest().join("target").join("b-mode");
        let _ = std::fs::create_dir_all(&dir);
        let p = dir.join("D1-cfg-paths.jsonl");
        let _ = std::fs::write(&p, self.lines.join("\n") + "\n");
        println!("[报表] {} 行 → {}", self.lines.len(), p.display());
    }
}

/// 表单路径的**忠实复原**:与 `restore_saved_accounts_from` 的取字段口径一字不差
/// (`md_user`/`md_password` 故意留空 —— 复原行永远走 Start,不走表单路)。
fn form_like_restore(map: &ek_core::section::SectionMap) -> ek_ui::CtpForm {
    let mut form = ek_ui::CtpForm {
        broker_id: map.get_plain("broker_id").unwrap_or_default().to_owned(),
        td_addr: map.get_plain("td_addr").unwrap_or_default().to_owned(),
        td_user: map.get_plain("td_user").unwrap_or_default().to_owned(),
        td_password: String::new(),
        td_auth_code: map.get_plain("td_auth_code").unwrap_or_default().to_owned(),
        td_app_id: map.get_plain("td_app_id").unwrap_or_default().to_owned(),
        md_addr: map.get_plain("md_addr").unwrap_or_default().to_owned(),
        md_user: String::new(),
        md_password: String::new(),
        symbols: String::new(),
        accept_futures: false,
        accept_options: false,
    };
    for c in map
        .get_plain("product_classes")
        .unwrap_or_default()
        .split(',')
    {
        if c.trim() == "1" {
            form.accept_futures = true;
        }
    }
    form
}

fn classes(v: &str) -> Vec<String> {
    let raw: Vec<String> = v
        .split(',')
        .map(str::trim)
        .filter(|c| !c.is_empty())
        .map(str::to_owned)
        .collect();
    ek_ctp::map::ProductClasses(raw).normalize()
}

#[test]
#[ignore = "需要现场档案根(EK_CFG_FALLBACK_ROOT)"]
fn d1_d2_archive_and_form_paths() {
    let src_root = std::env::var("EK_CFG_FALLBACK_ROOT").expect("需要 EK_CFG_FALLBACK_ROOT");
    let src_root = PathBuf::from(src_root);
    // 复制一份副本(母本只读)。
    let run = manifest().join("target").join("arch-d12");
    let _ = std::fs::remove_dir_all(&run);
    std::fs::create_dir_all(run.join("cfg")).expect("建副本目录");
    std::fs::copy(
        src_root
            .parent()
            .expect("档案根应有父目录")
            .join("master.key"),
        run.join("master.key"),
    )
    .expect("拷 master.key");
    std::fs::copy(src_root.join("cfg.db"), run.join("cfg").join("cfg.db")).expect("拷 cfg.db");
    // SAFETY: 本测试是独立进程里唯一访问档案根的地方,且在任何 Store 打开之前写。
    unsafe {
        std::env::set_var("EK_CFG_FALLBACK_ROOT", run.join("cfg"));
    }
    let store = ek_cfg::Store::eco().expect("档案根可用");

    let mut rec = Rec::new();
    let mut namespaces: Vec<String> = store
        .list_namespaces()
        .expect("档案清点")
        .into_iter()
        .filter(|ns| ns.starts_with("ctp."))
        .collect();
    namespaces.sort();
    assert!(!namespaces.is_empty(), "档案里没有 ctp.* 段");

    for ns in &namespaces {
        let map = match store.load(ns) {
            Ok(ek_cfg::LoadOutcome::Loaded { map, warnings }) => {
                if !warnings.is_empty() {
                    rec.info(&format!("{ns} 载入告警"), &warnings.join(" | "));
                }
                map
            }
            Ok(ek_cfg::LoadOutcome::Fresh) => {
                rec.info(&format!("{ns} 段"), "Fresh(空段,跳过)");
                continue;
            }
            Err(e) => {
                rec.add(&format!("{ns} 读段"), false, &format!("{e:?}"));
                continue;
            }
        };

        // ── 路径 A:档案(apply_config,不碰密库) ──────────────────────
        let pa = ek_ctp::CtpPlugin::new(ek_ctp::CtpConfig::default());
        let rep = match pa.apply_config(&map) {
            Ok(r) => r,
            Err(e) => {
                rec.add(&format!("{ns} apply_config"), false, &format!("{e}"));
                continue;
            }
        };
        let da = pa.dump_config();

        // ── 路径 B:表单(configure;密码给探针值,只为过校验) ─────────
        let form = form_like_restore(&map);
        let mut req = form.to_configure_req();
        req.td_password = ek_cfg::SecretString::from("probe-not-a-real-password".to_string());
        let pb = ek_ctp::CtpPlugin::new(ek_ctp::CtpConfig::default());
        let reply = pb.configure(&req);
        if !reply.ok {
            rec.add(&format!("{ns} configure(表单)"), false, &reply.msg);
            continue;
        }
        let db = pb.dump_config();

        // ── D1:非秘字段逐项比 ─────────────────────────────────────────
        // 注意 `md_user` 单列:按 BD-UI-62(用户裁定「MD 的账号密码不要单独列」),
        // **表单根本不设 md 账号位**,留空 = 回落 td 凭据。所以这一格两路天然不等,
        // 判据要比**有效值**(空 → td_user),并把这条投影差当 INFO 显形。
        let keys = [
            "broker_id",
            "td_addr",
            "td_user",
            "td_auth_code",
            "td_app_id",
            "md_addr",
        ];
        let mut diffs: Vec<String> = Vec::new();
        let mut detail: Vec<String> = Vec::new();
        for k in keys {
            let a = da.get_plain(k).unwrap_or("");
            let b = db.get_plain(k).unwrap_or("");
            detail.push(format!("{k}={a:?}/{b:?}"));
            if a != b {
                diffs.push(format!("{k}: 档案={a:?} 表单={b:?}"));
            }
        }
        let md_a = da.get_plain("md_user").unwrap_or("");
        let md_b = db.get_plain("md_user").unwrap_or("");
        let td_b = db.get_plain("td_user").unwrap_or("");
        let md_eff_b = if md_b.is_empty() { td_b } else { md_b };
        detail.push(format!("md_user={md_a:?}/{md_b:?}(回落 {md_eff_b:?})"));
        if md_a != md_b {
            if md_a == md_eff_b {
                rec.info(
                    &format!("D1 {ns} md_user 投影差"),
                    &format!(
                        "档案={md_a:?} 表单=空 → 回落 td_user={td_b:?},**有效值相等**;\
                         表单不设 md 账号位是 BD-UI-62 的裁定,不是漏映射"
                    ),
                );
            } else {
                diffs.push(format!(
                    "md_user: 档案={md_a:?} 表单={md_b:?}(回落 {md_eff_b:?}) —— 有效值也不等"
                ));
            }
        }
        // product_classes 比**语义**(空 = 回落期货 '1'),存储形态差异不算错。
        let ca = classes(da.get_plain("product_classes").unwrap_or(""));
        let cb = classes(db.get_plain("product_classes").unwrap_or(""));
        detail.push(format!("product_classes={ca:?}/{cb:?}"));
        if ca != cb {
            diffs.push(format!(
                "product_classes: 档案={ca:?}(原始 {:?}) 表单={cb:?}(原始 {:?})",
                da.get_plain("product_classes"),
                db.get_plain("product_classes")
            ));
        }
        rec.add(
            &format!("D1 {ns} 两路字段一致"),
            diffs.is_empty(),
            &format!(
                "{};{}",
                detail.join(" "),
                if diffs.is_empty() {
                    "无差异".to_string()
                } else {
                    format!("差异 → {}", diffs.join(" ;; "))
                }
            ),
        );

        // ── D1-秘面:两路都必须是**引用形态**,明文一个都不许出现 ──────
        let sa = da.get_secret_key("td_password");
        let sb = db.get_secret_key("td_password");
        let plain_a = da.get_plain("td_password");
        let plain_b = db.get_plain("td_password");
        rec.add(
            &format!("D1 {ns} 密码只出引用"),
            sa.is_some() && sb.is_some() && plain_a.is_none() && plain_b.is_none(),
            &format!("档案引用={sa:?} 表单引用={sb:?};明文(档案/表单)=({plain_a:?},{plain_b:?})"),
        );

        // ── D2:段键归宿(adopted / deferred),不许静默吞 ─────────────
        let mut silent: Vec<String> = Vec::new();
        for (k, _f) in map.iter() {
            let adopted = rep.adopted.iter().any(|x| x == k);
            let deferred = rep.deferred.iter().any(|x| x == k);
            if !adopted && !deferred {
                silent.push(k.to_string());
            }
        }
        // 反向:dump 出来的非秘键必须能在原段里找到(不凭空造字段/不丢字段)。
        let mut extra: Vec<String> = Vec::new();
        let original_plain = map
            .iter()
            .filter(|(_, f)| f.is_plain())
            .map(|(k, _)| k.to_string())
            .collect::<Vec<_>>();
        for (k, f) in da.iter() {
            if f.is_plain() && !original_plain.iter().any(|o| o == k) {
                extra.push(k.to_string());
            }
        }
        rec.add(
            &format!("D2 {ns} 段键全覆盖"),
            silent.is_empty() && extra.is_empty(),
            &format!(
                "段键 {} 个;adopted {} 个;deferred {:?};静默吞 {:?};dump 多出 {:?}",
                map.len(),
                rep.adopted.len(),
                rep.deferred,
                silent,
                extra
            ),
        );
    }

    rec.flush();
    assert_eq!(rec.fails, 0, "D1/D2 有 {} 条 FAIL(见上)", rec.fails);
}