refactor(presenter): 两处纯搬移腾出 400 行余量(events 用例 / AI 栏事件投递)
events.rs 399 行、state.rs 400 行(AGENTS §3.1 与 §3.9 的零余量贴线文件)。M223 要往 AppCommand/CoreEvent 各加一条「导入已有版本」的变体、往 AppState 加一份事件态,都得先腾地方。 纯搬移、一行未改: - events.rs 尾部 #[cfg(test)] mod tests(80 行)→ dto/events/tests.rs(399 → 321); - state.rs 的 apply_agent + agent_rail_visible(28 行)→ state/agent.rs(400 → 376)。 方法路径不变(impl 写在子模块、pub use 无需新增),断言一条未动,搬移 diff 可逐行对。
This commit is contained in:
1 parent
13a317d35c
commit
9ab909d01c
4 files changed
+129
-105
No files matched your search
@@ -318,82 +318,4 @@ pub trait Backend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests;
|
||||||
use super::*;
|
|
||||||
use crate::dto::{AccountKind, DeviceLoginPhase, InstallStage};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn events_are_serializable_ipc_contracts() {
|
|
||||||
let event = AppEvent::Core(CoreEvent::InstallProgress(Some(InstallProgressView {
|
|
||||||
stage: InstallStage::Downloading,
|
|
||||||
files_done: 3,
|
|
||||||
files_total: 9,
|
|
||||||
..InstallProgressView::new()
|
|
||||||
})));
|
|
||||||
let text = serde_json::to_string(&event).expect("序列化");
|
|
||||||
let back: AppEvent = serde_json::from_str(&text).expect("反序列化");
|
|
||||||
assert_eq!(back, event);
|
|
||||||
|
|
||||||
let account = AppEvent::Core(CoreEvent::AccountsChanged(vec![AccountView {
|
|
||||||
id: "account:1".to_string(),
|
|
||||||
name: "Steve".to_string(),
|
|
||||||
kind: AccountKind::Offline,
|
|
||||||
profile_id: "0f8f".to_string(),
|
|
||||||
login_name: String::new(),
|
|
||||||
created: "2026-10-31 20:12".to_string(),
|
|
||||||
active: true,
|
|
||||||
server_url: String::new(),
|
|
||||||
skin: None,
|
|
||||||
}]));
|
|
||||||
let text = serde_json::to_string(&account).expect("序列化");
|
|
||||||
let back: AppEvent = serde_json::from_str(&text).expect("反序列化");
|
|
||||||
assert_eq!(back, account);
|
|
||||||
// 凭据永不跨界:序列化文本里没有 token 类字段
|
|
||||||
assert!(!text.to_lowercase().contains("token"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn commands_round_trip() {
|
|
||||||
let command = AppCommand::CreateInstance(CreateRequest {
|
|
||||||
version: "1.20.4".to_string(),
|
|
||||||
loader: crate::dto::LoaderKind::Fabric,
|
|
||||||
loader_version: "0.16.5".to_string(),
|
|
||||||
name: "生存".to_string(),
|
|
||||||
});
|
|
||||||
let text = serde_json::to_string(&command).expect("序列化");
|
|
||||||
let back: AppCommand = serde_json::from_str(&text).expect("反序列化");
|
|
||||||
assert_eq!(back, command);
|
|
||||||
|
|
||||||
let login = AppCommand::StartDeviceLogin;
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::from_str::<AppCommand>(&serde_json::to_string(&login).expect("序列化"))
|
|
||||||
.expect("反序列化"),
|
|
||||||
login
|
|
||||||
);
|
|
||||||
assert!(DeviceLoginPhase::Waiting.busy());
|
|
||||||
|
|
||||||
// 第三方轨(M114):请求进、状态出,两边都不带 token 字段
|
|
||||||
let authlib = AppCommand::AddAuthlibAccount(AuthlibRequest::new(
|
|
||||||
"https://littleskin.cn/api/yggdrasil/",
|
|
||||||
"me@example.com",
|
|
||||||
"hunter2",
|
|
||||||
));
|
|
||||||
let text = serde_json::to_string(&authlib).expect("序列化");
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::from_str::<AppCommand>(&text).expect("反序列化"),
|
|
||||||
authlib
|
|
||||||
);
|
|
||||||
|
|
||||||
let view = AppEvent::Core(CoreEvent::AuthlibLoginChanged(AuthlibLoginView::reachable(
|
|
||||||
"https://littleskin.cn/api/yggdrasil/",
|
|
||||||
"LittleSkin",
|
|
||||||
)));
|
|
||||||
let text = serde_json::to_string(&view).expect("序列化");
|
|
||||||
let back: AppEvent = serde_json::from_str(&text).expect("反序列化");
|
|
||||||
assert_eq!(back, view);
|
|
||||||
assert!(
|
|
||||||
!text.to_lowercase().contains("token"),
|
|
||||||
"回程视图里不该有 token"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
//! `dto::events` 的 IPC 契约用例(M223 从 `events.rs` 尾部**纯搬移**,一行未改)。
|
||||||
|
//!
|
||||||
|
//! 为什么搬:M223 要给 `AppCommand` / `CoreEvent` 各加一条变体(「导入已有版本」的指令与事件),
|
||||||
|
//! 而 `events.rs` 当时正好 399 行——加两行变体 + 两行 import 就顶破 400 行硬线(AGENTS §3.1)。
|
||||||
|
//! 拆分口径与仓库其它拆分一致:**纯搬移、一行不改**,`mod tests;` 只是把位置挪到
|
||||||
|
//! `dto/events/tests.rs`(`events.rs` 是非 `mod.rs` 文件,子模块目录就是 `events/`)。
|
||||||
|
//!
|
||||||
|
//! 搬移后 `events.rs` 只剩契约本体(枚举 + 构造器 + `Backend` trait),本文件的断言一条没动,
|
||||||
|
//! 所以「搬移零行为变化」可以用逐行 diff 对。
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::dto::{AccountKind, DeviceLoginPhase, InstallStage};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn events_are_serializable_ipc_contracts() {
|
||||||
|
let event = AppEvent::Core(CoreEvent::InstallProgress(Some(InstallProgressView {
|
||||||
|
stage: InstallStage::Downloading,
|
||||||
|
files_done: 3,
|
||||||
|
files_total: 9,
|
||||||
|
..InstallProgressView::new()
|
||||||
|
})));
|
||||||
|
let text = serde_json::to_string(&event).expect("序列化");
|
||||||
|
let back: AppEvent = serde_json::from_str(&text).expect("反序列化");
|
||||||
|
assert_eq!(back, event);
|
||||||
|
|
||||||
|
let account = AppEvent::Core(CoreEvent::AccountsChanged(vec![AccountView {
|
||||||
|
id: "account:1".to_string(),
|
||||||
|
name: "Steve".to_string(),
|
||||||
|
kind: AccountKind::Offline,
|
||||||
|
profile_id: "0f8f".to_string(),
|
||||||
|
login_name: String::new(),
|
||||||
|
created: "2026-10-31 20:12".to_string(),
|
||||||
|
active: true,
|
||||||
|
server_url: String::new(),
|
||||||
|
skin: None,
|
||||||
|
}]));
|
||||||
|
let text = serde_json::to_string(&account).expect("序列化");
|
||||||
|
let back: AppEvent = serde_json::from_str(&text).expect("反序列化");
|
||||||
|
assert_eq!(back, account);
|
||||||
|
// 凭据永不跨界:序列化文本里没有 token 类字段
|
||||||
|
assert!(!text.to_lowercase().contains("token"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn commands_round_trip() {
|
||||||
|
let command = AppCommand::CreateInstance(CreateRequest {
|
||||||
|
version: "1.20.4".to_string(),
|
||||||
|
loader: crate::dto::LoaderKind::Fabric,
|
||||||
|
loader_version: "0.16.5".to_string(),
|
||||||
|
name: "生存".to_string(),
|
||||||
|
});
|
||||||
|
let text = serde_json::to_string(&command).expect("序列化");
|
||||||
|
let back: AppCommand = serde_json::from_str(&text).expect("反序列化");
|
||||||
|
assert_eq!(back, command);
|
||||||
|
|
||||||
|
let login = AppCommand::StartDeviceLogin;
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<AppCommand>(&serde_json::to_string(&login).expect("序列化"))
|
||||||
|
.expect("反序列化"),
|
||||||
|
login
|
||||||
|
);
|
||||||
|
assert!(DeviceLoginPhase::Waiting.busy());
|
||||||
|
|
||||||
|
// 第三方轨(M114):请求进、状态出,两边都不带 token 字段
|
||||||
|
let authlib = AppCommand::AddAuthlibAccount(AuthlibRequest::new(
|
||||||
|
"https://littleskin.cn/api/yggdrasil/",
|
||||||
|
"me@example.com",
|
||||||
|
"hunter2",
|
||||||
|
));
|
||||||
|
let text = serde_json::to_string(&authlib).expect("序列化");
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_str::<AppCommand>(&text).expect("反序列化"),
|
||||||
|
authlib
|
||||||
|
);
|
||||||
|
|
||||||
|
let view = AppEvent::Core(CoreEvent::AuthlibLoginChanged(AuthlibLoginView::reachable(
|
||||||
|
"https://littleskin.cn/api/yggdrasil/",
|
||||||
|
"LittleSkin",
|
||||||
|
)));
|
||||||
|
let text = serde_json::to_string(&view).expect("序列化");
|
||||||
|
let back: AppEvent = serde_json::from_str(&text).expect("反序列化");
|
||||||
|
assert_eq!(back, view);
|
||||||
|
assert!(
|
||||||
|
!text.to_lowercase().contains("token"),
|
||||||
|
"回程视图里不该有 token"
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ use crate::dialogs::DialogQueue;
|
|||||||
use crate::dto::{
|
use crate::dto::{
|
||||||
// 主线(M105 的 `LoaderKind` / M113 的 `PackState`)与 M114 的 `AuthlibLoginView` 并集。
|
// 主线(M105 的 `LoaderKind` / M113 的 `PackState`)与 M114 的 `AuthlibLoginView` 并集。
|
||||||
AccountView,
|
AccountView,
|
||||||
AgentEvent,
|
|
||||||
AppEvent,
|
AppEvent,
|
||||||
AuthlibLoginView,
|
AuthlibLoginView,
|
||||||
CoreEvent,
|
CoreEvent,
|
||||||
@@ -42,6 +41,8 @@ use crate::wizard::WizardState;
|
|||||||
// 所以这里没有 `mod ai;` / `pub use ai::AiSession;`——M114 r1 曾把旧结构搬到 `state/ai.rs`,
|
// 所以这里没有 `mod ai;` / `pub use ai::AiSession;`——M114 r1 曾把旧结构搬到 `state/ai.rs`,
|
||||||
// 该文件已随 rebase 删除,**以 main 的 agent 契约为准**。
|
// 该文件已随 rebase 删除,**以 main 的 agent 契约为准**。
|
||||||
mod accounts;
|
mod accounts;
|
||||||
|
// M223:AI 栏的事件投递与可见性(纯搬移,守 400 行硬线)
|
||||||
|
mod agent;
|
||||||
mod live;
|
mod live;
|
||||||
mod tiles;
|
mod tiles;
|
||||||
|
|
||||||
@@ -304,31 +305,6 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// agent 事件 → AI 栏视图模型(投影规则见 [`crate::agent::rail`])。
|
|
||||||
///
|
|
||||||
/// 未读规则:栏不可见时,助手侧新内容(助手/工具消息、错误、待确认请求)逐条累加 `unread`;
|
|
||||||
/// 用户自己说的话与纯增量不算。栏可见时到达的内容直接算已读(红点不该在眼前亮着)。
|
|
||||||
pub fn apply_agent(&mut self, event: AgentEvent) {
|
|
||||||
let visible = self.agent_rail_visible();
|
|
||||||
let fresh = self.ai.ingest(&event);
|
|
||||||
if fresh {
|
|
||||||
if visible {
|
|
||||||
self.ai.mark_read();
|
|
||||||
} else {
|
|
||||||
self.ai.unread = self.ai.unread.saturating_add(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// AI 栏此刻是否「在用户眼前」。
|
|
||||||
///
|
|
||||||
/// 独立页形态(栈顶是 `Route::Agent`)必然可见;停靠形态下只有收成 icon 轨才算离开——
|
|
||||||
/// 右侧常驻栏在任何一级页面都挂着,用户扫一眼就能看到。
|
|
||||||
#[must_use]
|
|
||||||
pub fn agent_rail_visible(&self) -> bool {
|
|
||||||
self.ai_mode() == AiPanelMode::Page || !self.prefs.ai_collapsed
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 按搜索词过滤后的磁贴网格(顺序稳定 = 实例列表顺序)。
|
/// 按搜索词过滤后的磁贴网格(顺序稳定 = 实例列表顺序)。
|
||||||
pub fn tiles(&self) -> Vec<TileCell<'_>> {
|
pub fn tiles(&self) -> Vec<TileCell<'_>> {
|
||||||
let query = self.instance_query.trim().to_lowercase();
|
let query = self.instance_query.trim().to_lowercase();
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//! AI 栏的事件投递与可见性判定(M223 从 `state.rs` **纯搬移**,一行未改)。
|
||||||
|
//!
|
||||||
|
//! 为什么搬:M223 要往 `AppState` 加「导入已有版本」的状态字段与一条 `CoreEvent::Adopt` 分支,
|
||||||
|
//! 而 `state.rs` 当时正好 400 行(零余量,AGENTS §3.9)。拆分口径与 [`super::tiles`] 一致:
|
||||||
|
//! **纯搬移、一行不改**;`impl AppState` 写在子模块里,方法路径
|
||||||
|
//! `AppState::apply_agent` / `AppState::agent_rail_visible` 对调用方完全不变
|
||||||
|
//! (子模块看得见父模块的私有字段,所以这里直接用 `self.ai` / `self.prefs`)。
|
||||||
|
|
||||||
|
use crate::dto::AgentEvent;
|
||||||
|
use crate::router::AiPanelMode;
|
||||||
|
|
||||||
|
use super::AppState;
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
/// agent 事件 → AI 栏视图模型(投影规则见 [`crate::agent::rail`])。
|
||||||
|
///
|
||||||
|
/// 未读规则:栏不可见时,助手侧新内容(助手/工具消息、错误、待确认请求)逐条累加 `unread`;
|
||||||
|
/// 用户自己说的话与纯增量不算。栏可见时到达的内容直接算已读(红点不该在眼前亮着)。
|
||||||
|
pub fn apply_agent(&mut self, event: AgentEvent) {
|
||||||
|
let visible = self.agent_rail_visible();
|
||||||
|
let fresh = self.ai.ingest(&event);
|
||||||
|
if fresh {
|
||||||
|
if visible {
|
||||||
|
self.ai.mark_read();
|
||||||
|
} else {
|
||||||
|
self.ai.unread = self.ai.unread.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AI 栏此刻是否「在用户眼前」。
|
||||||
|
///
|
||||||
|
/// 独立页形态(栈顶是 `Route::Agent`)必然可见;停靠形态下只有收成 icon 轨才算离开——
|
||||||
|
/// 右侧常驻栏在任何一级页面都挂着,用户扫一眼就能看到。
|
||||||
|
#[must_use]
|
||||||
|
pub fn agent_rail_visible(&self) -> bool {
|
||||||
|
self.ai_mode() == AiPanelMode::Page || !self.prefs.ai_collapsed
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user