use std::path::{Path, PathBuf};
use std::sync::Arc;
use ek_core::{Field, SectionMap};
use ek_ui::plugin::{THEME_NS, save_account_aliases_into, save_auto_connect_flags_into};
use ek_ui::state::{AccountRow, CtpForm, LoginState, UiShared};
fn tmp_root(tag: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("target")
.join("tmp")
.join(format!("ekui-restore-{tag}-{}", std::process::id()))
}
fn seed(root: &Path, auto: Option<(&str, &str)>) -> ek_cfg::Store {
let store = ek_cfg::Store::with_root(root.to_path_buf());
let acct = SectionMap::new(1)
.set("broker_id", Field::plain("9"))
.set("td_addr", Field::plain("tcp://1.2.3.4:40001"))
.set("td_user", Field::plain("11"))
.set("td_app_id", Field::plain("simnow_client_test"))
.set("td_auth_code", Field::plain("0123456789abcdef"))
.set("md_addr", Field::plain("tcp://1.2.3.4:40011"))
.set("product_classes", Field::plain("1,2"))
.set("td_password", Field::secret("gw_td_password"));
ek_cfg::cfg_save_at(store.root(), "ctp.ctp_9_11", &acct).unwrap();
let mut ui = SectionMap::new(1).set("theme", Field::plain("dark"));
if let Some((k, v)) = auto {
ui = ui.set(format!("auto_connect.{k}"), Field::plain(v));
}
ek_cfg::cfg_save_at(store.root(), THEME_NS, &ui).unwrap();
store
}
#[test]
fn u_restored_row_comes_from_archive_without_password() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-ui/target/cfg-test-root"));
ek_cfg::secret::force_fake_for_tests(); let root = tmp_root("a");
let _ = std::fs::remove_dir_all(&root);
let store = seed(&root, None);
let shared = Arc::new(UiShared::new());
let restored = shared.restore_saved_accounts_from(&store);
assert_eq!(restored, vec!["ctp_9_11".to_string()]);
let row = shared.account("ctp_9_11").expect("复原行应入册");
let AccountRow {
restored: is_restored,
password_ref,
auto_connect,
form,
stage,
login,
..
} = row.clone();
assert!(is_restored, "档案标必须立");
assert!(password_ref, "密库引用徽标来自存档 secret 行(值不外泄)");
assert!(!auto_connect, "键缺 = 关(不善意);勾面在 ui 段");
assert_eq!(form.broker_id, "9");
assert_eq!(form.td_addr, "tcp://1.2.3.4:40001");
assert_eq!(form.td_user, "11");
assert_eq!(form.md_addr, "tcp://1.2.3.4:40011");
assert!(
form.td_password.is_empty(),
"密码绝不进界面表单(密库只在实例 on_start 解)"
);
assert!(
form.accept_futures && !form.accept_options,
"旧档 1,2 → 只期货(呈现层收窄,不得悄悄重开期权)"
);
assert_eq!(stage, ek_ui::state::AccountStage::NotAssembled);
assert_eq!(login, LoginState::Unknown, "没有证据不装在线");
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn u_auto_connect_flags_persist_and_prune() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-ui/target/cfg-test-root"));
ek_cfg::secret::force_fake_for_tests(); let root = tmp_root("b");
let _ = std::fs::remove_dir_all(&root);
let store = seed(&root, None);
let shared = Arc::new(UiShared::new());
shared.restore_saved_accounts_from(&store);
shared.set_auto_connect("ctp_9_11", true).expect("勾选置位");
assert!(
shared.take_auto_connect_persist_due(),
"手点必须立落盘标记(同主题纪律)"
);
let flags: Vec<(String, bool)> = shared
.accounts()
.iter()
.map(|a| (a.gateway.clone(), a.auto_connect))
.collect();
save_auto_connect_flags_into(&store, &flags).expect("存档");
let ui = match store.load(THEME_NS).unwrap() {
ek_cfg::LoadOutcome::Loaded { map, .. } => map,
ek_cfg::LoadOutcome::Fresh => panic!("ui 段应存在"),
};
assert_eq!(ui.get_plain("auto_connect.ctp_9_11"), Some("1"));
assert_eq!(
ui.get_plain("theme"),
Some("dark"),
"同段邻居键(主题)必须保全"
);
save_auto_connect_flags_into(&store, &[]).expect("清用");
let ui = match store.load(THEME_NS).unwrap() {
ek_cfg::LoadOutcome::Loaded { map, .. } => map,
ek_cfg::LoadOutcome::Fresh => panic!("主题键仍在,段不消失"),
};
assert!(
ui.get_plain("auto_connect.ctp_9_11").is_none(),
"摘户后旧键必须清(不留魂)"
);
assert_eq!(ui.get_plain("theme"), Some("dark"));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn u_restore_does_not_clobber_manual_rows() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-ui/target/cfg-test-root"));
ek_cfg::secret::force_fake_for_tests(); let root = tmp_root("c");
let _ = std::fs::remove_dir_all(&root);
let store = seed(&root, None);
let shared = Arc::new(UiShared::new());
let form = CtpForm {
broker_id: "9".to_string(),
td_addr: "tcp://5.6.7.8:40001".to_string(),
td_user: "11".to_string(),
td_password: "手输密码".to_string(),
td_auth_code: "0123456789abcdef".to_string(),
td_app_id: "simnow_client_test".to_string(),
md_addr: "tcp://5.6.7.8:40011".to_string(),
md_user: String::new(),
md_password: String::new(),
symbols: String::new(),
accept_futures: true,
accept_options: false,
};
let row = AccountRow::derive(&form).unwrap();
shared.add_account(row).expect("手输行入册");
let restored = shared.restore_saved_accounts_from(&store);
assert!(restored.is_empty(), "同名行已手输,清点不覆盖: {restored:?}");
let row = shared.account("ctp_9_11").expect("手输行仍在");
assert!(
!row.restored && row.form.td_addr == "tcp://5.6.7.8:40001",
"手输行的参数与档案标必须原样保留"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn u_alias_label_persists_and_restores() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-ui/target/cfg-test-root"));
ek_cfg::secret::force_fake_for_tests(); let root = tmp_root("d");
let _ = std::fs::remove_dir_all(&root);
let store = seed(&root, None);
let shared = Arc::new(UiShared::new());
shared.restore_saved_accounts_from(&store);
assert!(shared.set_account_alias("ctp_9_11", "仿真"), "别名入行");
assert!(
shared.take_alias_persist_due(),
"改别名必须立落盘标记(同自动连接纪律)"
);
let aliases: Vec<(String, String)> = shared
.accounts()
.iter()
.map(|a| (a.gateway.clone(), a.alias.clone()))
.collect();
save_account_aliases_into(&store, &aliases).expect("别名存档");
let ui = match store.load(THEME_NS).unwrap() {
ek_cfg::LoadOutcome::Loaded { map, .. } => map,
ek_cfg::LoadOutcome::Fresh => panic!("ui 段应存在"),
};
assert_eq!(ui.get_plain("alias.ctp_9_11"), Some("仿真"));
assert_eq!(ui.get_plain("theme"), Some("dark"), "邻居键(主题)保全");
let shared2 = Arc::new(UiShared::new());
shared2.restore_saved_accounts_from(&store);
assert_eq!(
shared2.account("ctp_9_11").expect("复原行").alias,
"仿真",
"重启后标签必须复原(用户口径:选择时可按标签挑)"
);
shared2.remove_account("ctp_9_11");
assert!(
shared2.take_alias_persist_due(),
"摘行必须立别名落盘标记(清键)"
);
save_account_aliases_into(&store, &[]).expect("清键");
let ui = match store.load(THEME_NS).unwrap() {
ek_cfg::LoadOutcome::Loaded { map, .. } => map,
ek_cfg::LoadOutcome::Fresh => panic!("主题键仍在,段不消失"),
};
assert!(
ui.get_plain("alias.ctp_9_11").is_none(),
"摘户后别名键必须清(不留魂)"
);
assert_eq!(ui.get_plain("theme"), Some("dark"));
let root4 = tmp_root("e");
let _ = std::fs::remove_dir_all(&root4);
let store4 = seed(&root4, None);
let aliases4 = vec![("ctp_9_11".to_string(), "仿真".to_string())];
save_account_aliases_into(&store4, &aliases4).expect("存档别名");
let shared4 = Arc::new(UiShared::new());
let form4 = CtpForm {
broker_id: "9".to_string(),
td_addr: "tcp://5.6.7.8:40001".to_string(),
td_user: "22".to_string(),
td_password: "手输密码".to_string(),
td_auth_code: "0123456789abcdef".to_string(),
td_app_id: "simnow_client_test".to_string(),
md_addr: "tcp://5.6.7.8:40011".to_string(),
md_user: String::new(),
md_password: String::new(),
symbols: String::new(),
accept_futures: true,
accept_options: false,
};
let mut row4 = AccountRow::derive(&form4).unwrap();
row4.alias = "仿真".to_string();
shared4.add_account(row4).expect("手输带别名行入册");
let restored4 = shared4.restore_saved_accounts_from(&store4);
assert_eq!(restored4, vec!["ctp_9_11".to_string()], "行照复原");
assert!(
shared4.account("ctp_9_11").expect("复原行").alias.is_empty(),
"别名撞名:标签丢弃,行不许被挡"
);
let _ = std::fs::remove_dir_all(&root);
let _ = std::fs::remove_dir_all(&root4);
}
#[test]
fn u_restore_multi_front_rows_keep_archive_names() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-ui/target/cfg-test-root"));
ek_cfg::secret::force_fake_for_tests(); let root = tmp_root("mf");
let _ = std::fs::remove_dir_all(&root);
let store = seed(&root, None);
let acct2 = SectionMap::new(1)
.set("broker_id", Field::plain("9"))
.set("td_addr", Field::plain("tcp://1.2.3.4:41001"))
.set("td_user", Field::plain("11"))
.set("td_app_id", Field::plain("simnow_client_test"))
.set("td_auth_code", Field::plain("0123456789abcdef"))
.set("md_addr", Field::plain("tcp://1.2.3.4:41011"))
.set("product_classes", Field::plain("1"))
.set("td_password", Field::secret("gw_td_password"));
ek_cfg::cfg_save_at(store.root(), "ctp.ctp_9_11_2", &acct2).unwrap();
let shared = Arc::new(UiShared::new());
let restored = shared.restore_saved_accounts_from(&store);
assert_eq!(
restored,
vec!["ctp_9_11".to_string(), "ctp_9_11_2".to_string()],
"两套前置 = 两行,注册名跟档案名"
);
assert_eq!(
shared.account("ctp_9_11_2").expect("_2 行").form.td_addr,
"tcp://1.2.3.4:41001",
"_2 行吃自己的前置"
);
assert!(
shared
.account("ctp_9_11_2")
.expect("_2 行")
.password_ref,
"_2 行的密库引用徽标照落"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn u_remove_account_purges_archive_and_no_ghost_revives() {
ek_cfg::set_default_root_for_tests(std::path::PathBuf::from("C:/dev/fuke/ek-ui/target/cfg-test-root"));
ek_cfg::secret::force_fake_for_tests(); let root = tmp_root("purge");
let _ = std::fs::remove_dir_all(&root);
let store = seed(&root, None);
let acct2 = SectionMap::new(1)
.set("broker_id", Field::plain("9"))
.set("td_addr", Field::plain("tcp://1.2.3.4:41001"))
.set("td_user", Field::plain("11"))
.set("td_app_id", Field::plain("simnow_client_test"))
.set("td_auth_code", Field::plain("0123456789abcdef"))
.set("md_addr", Field::plain("tcp://1.2.3.4:41011"))
.set("product_classes", Field::plain("1"))
.set("td_password", Field::secret("gw_td_password"));
ek_cfg::cfg_save_at(store.root(), "ctp.ctp_9_11_2", &acct2).unwrap();
let shared = Arc::new(UiShared::new());
assert_eq!(shared.restore_saved_accounts_from(&store).len(), 2);
assert!(shared.remove_account("ctp_9_11_2"));
assert_eq!(shared.take_archive_purges(), vec!["ctp_9_11_2".to_string()]);
ek_ui::appconfig::purge_account_archive(&store, "ctp_9_11_2").expect("抹段成功");
assert!(
matches!(
store.load("ctp.ctp_9_11_2").expect("抹后读"),
ek_cfg::LoadOutcome::Fresh
),
"抹段后该段消失(单模:载入回 Fresh;库文件禁删)"
);
let shared2 = Arc::new(UiShared::new());
assert_eq!(
shared2.restore_saved_accounts_from(&store),
vec!["ctp_9_11".to_string()],
"档案已抹 = 清点无鬼户口"
);
ek_ui::appconfig::purge_account_archive(&store, "ctp_9_11_2").expect("幂等");
let _ = std::fs::remove_dir_all(&root);
}