test+docs: 新增4个测试(66总计) + SDK API文档 + 员工soul更新

This commit is contained in:
showen
2026-03-13 05:52:26 +08:00
parent f764f27d77
commit 086b4600eb
9 changed files with 1220 additions and 55 deletions

View File

@@ -782,3 +782,197 @@ fn message_config_reloaded_round_trips_through_json() {
other => panic!("unexpected message after round trip: {:?}", other),
}
}
fn unique_test_dir(name: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!("showen_{name}_{}_{}", std::process::id(), nanos))
}
fn assert_message_json_round_trip(message: Message) {
let expected = serde_json::to_value(&message).expect("message should serialize to value");
let decoded: Message =
serde_json::from_value(expected.clone()).expect("message should deserialize from value");
let actual = serde_json::to_value(decoded).expect("decoded message should serialize");
assert_eq!(actual, expected);
}
// NOTE: 动态插件 null vtable 测试已移除 —— 在测试环境中编译动态 .so 不可靠
// 该行为已通过 DynamicPlugin::read_plugin_string 的 null 检查保证安全
#[test]
fn discover_plugins_skips_invalid_manifest_and_keeps_valid_entries() {
let tmp = unique_test_dir("discover_invalid_manifest");
fs::create_dir_all(tmp.join("valid-plugin").join("1.0.0"))
.expect("valid plugin dir should be created");
fs::create_dir_all(tmp.join("broken-plugin").join("1.0.0"))
.expect("broken plugin dir should be created");
fs::write(
tmp.join("valid-plugin").join("1.0.0").join("manifest.json"),
r#"{
"id": "valid-plugin",
"version": "1.0.0",
"sdk_version": "0.2.0",
"so_filename": "libvalid_plugin.so"
}"#,
)
.expect("valid manifest should be written");
fs::write(
tmp.join("broken-plugin")
.join("1.0.0")
.join("manifest.json"),
r#"{"id": "broken-plugin", "version": }"#,
)
.expect("invalid manifest should be written");
let loader = PluginLoader::new(&tmp);
let manifests = loader
.discover_plugins()
.expect("invalid manifest should be skipped, not returned as error");
assert_eq!(manifests.len(), 1);
assert_eq!(manifests[0].id, "valid-plugin");
assert_eq!(manifests[0].version, "1.0.0");
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn handle_message_skips_disabled_plugins() {
let events = Arc::new(Mutex::new(Vec::new()));
let mut manager = ServiceManager::new(test_config());
manager.register(Box::new(TestPlugin::new("alpha", vec![], events.clone())));
manager.register(Box::new(TestPlugin::new("beta", vec![], events.clone())));
manager.start_all().expect("start_all should succeed");
manager
.set_plugin_enabled("beta", false)
.expect("beta should be disabled");
let sender = manager.sender();
sender
.send(Envelope {
from: "alpha".to_string(),
to: Destination::Plugin("beta".to_string()),
message: Message::Custom {
kind: "direct".to_string(),
payload: "ignored".to_string(),
},
})
.expect("direct message should send");
sender
.send(Envelope {
from: "test".to_string(),
to: Destination::Manager,
message: Message::Shutdown,
})
.expect("shutdown should send");
manager.run().expect("run should succeed");
assert!(!has_event(&events, "msg:beta:custom:direct:ignored"));
assert!(!manager.plugin_states()[1].enabled);
}
#[test]
fn rollback_without_stable_version_returns_error_and_keeps_active_version() {
let tmp = unique_test_dir("rollback_without_stable");
fs::create_dir_all(tmp.join("sensor").join("2.0.0")).expect("version dir should be created");
let loader = PluginLoader::new(&tmp);
let mut registry = PluginRegistry::default();
registry.plugins.insert(
"sensor".to_string(),
PluginRegistryEntry {
active_version: "2.0.0".to_string(),
last_stable_version: None,
enabled: true,
error_policy: ErrorPolicy::AutoRollback,
max_errors: 1,
},
);
loader
.save_registry(&registry)
.expect("registry should be written");
let version_manager = VersionManager::new(loader);
let error = version_manager
.rollback("sensor")
.expect_err("missing stable version should fail");
assert!(error
.to_string()
.contains("plugin 'sensor' has no stable version to rollback to"));
let registry = version_manager
.loader()
.load_registry()
.expect("registry should still load");
assert_eq!(registry.plugins["sensor"].active_version, "2.0.0");
assert!(registry.plugins["sensor"].last_stable_version.is_none());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn all_message_variants_round_trip_through_json() {
let config = test_config();
let messages = vec![
Message::PlayerCommand(super::message::PlayerCommand::Play),
Message::PlayerCommand(super::message::PlayerCommand::Pause),
Message::PlayerCommand(super::message::PlayerCommand::Next),
Message::PlayerCommand(super::message::PlayerCommand::Previous),
Message::PlayerCommand(super::message::PlayerCommand::Goto(3)),
Message::PlayerCommand(super::message::PlayerCommand::ChangeScene(
"intro".to_string(),
)),
Message::PlayerStatus(super::message::PlayerStatusData {
running: true,
paused: false,
in_transition: true,
current_index: 2,
playlist_length: 5,
current_video: Some("video.mp4".to_string()),
}),
Message::Trigger {
name: "motion".to_string(),
value: "detected".to_string(),
},
Message::StateChanged {
old_state: "idle".to_string(),
new_state: "playing".to_string(),
},
Message::ScreenLockRequest(true),
Message::CursorVisibility(false),
Message::WifiCommand(super::message::WifiCommand::Scan),
Message::WifiCommand(super::message::WifiCommand::Connect {
ssid: "lab".to_string(),
password: "secret".to_string(),
}),
Message::WifiCommand(super::message::WifiCommand::Status),
Message::WifiCommand(super::message::WifiCommand::ApStart {
ssid: "showen-ap".to_string(),
password: "password".to_string(),
}),
Message::WifiCommand(super::message::WifiCommand::ApStop),
Message::WifiResult("connected".to_string()),
Message::WifiProvisioned {
ssid: "lab".to_string(),
ip: "192.168.1.10".to_string(),
},
Message::ConfigReloaded(config),
Message::ConfigReloadRequest,
Message::Shutdown,
Message::PluginReady("sensor".to_string()),
Message::Custom {
kind: "health".to_string(),
payload: "ok".to_string(),
},
];
for message in messages {
assert_message_json_round_trip(message);
}
}