fix(m159): r1 复审返工 —— parse.rs 拆出子模块 + 五条 P3 真问题
按独立复审 `reviewer-updategui-r1` 的 p1-1 / fix-then-merge 返工。
P1-1(挡门)
- `crates/cli/src/cli/parse.rs` 406 → **382** 行:`fn parse_update` 搬到
`crates/cli/src/cli/parse/update.rs`(与 `parse/{accounts,agent,instances}.rs` 同形状)。
全仓 `crates/**/*.rs` 最大仍是 400(`design/tests/card_neutral.rs` 等三处,未越线)。
上一轮报告里「所有改动文件都在 400 行内」那句是错的,已在 r2 报告里更正。
P3-6 `Job::apply` 不查包形态
- 新增 `self_replaceable(format)`(只有 `PortableExe` 允许)+ `ensure_self_replaceable`,
**挡在取包之前**;错误分类 `InvalidManifest`(发布侧挂错,换源/重试无用)。
比复审建议的 `needs_installer()` 更严:`portable-zip`/`appimage` 同样不能原地当 exe。
- 用例:`crates/core/src/update/tests/job_cases.rs`(msi/zip 被拒 + 目标零改动 + 暂存目录
没被建 + 便携 exe 整环成功 + Nothing 不碰目标 + 闸门的源码锚点)。
P3-7 `ULL_UPDATE_DIR` 生产可用
- `default_source` 改走 `local_dir_override()`:`if !cfg!(debug_assertions) { return None }`,
发布构建里整段被常量折叠掉(这条路径在 release 二进制里不存在)。判据与三档取舍写在
`job.rs` 的文档表里(为什么不用 CLI 参数、为什么不用运行期开关)。
P3-1「忽略这个版本」语义
- `UpdateView.dismissed: bool` → `skipped: Option<String>`(记**版本号**):忽略 0.2.3 之后
同一次运行里冒出 0.2.4 **照常提示**;手动检查清空(`begin_check(manual)`),静默检查不清。
文案保持「忽略这个版本」。presenter / app 侧视图模型 / 两处用例同步。
P3-3 CLI `--json` 失败信封缺结构化分类
- `CliError` 加 `detail`(`Option<Box<Value>>`,装箱避免 `result_large_err`)+ `with_detail`,
`to_json` 输出 `error.detail`;`commands/update.rs` 的 failure 填
`{"kind": core 分类 id, "retryable": 可重试位}`。
- 上一轮报告里引的 `{"kind":"digest_mismatch"}` 形状确实不存在(那是 CLI 的 user/internal),
r2 报告换成了实机 dump。
P3-4 update 命令没有解析测试
- `crates/cli/tests/parse_test.rs`:`update_subcommands`(check / apply / `--dest` 两种写法)
+ `update_usage_errors`(无子命令 / 未知子命令 / 多余位置参数 / 缺少取值 / 未知选项);
`help_text_covers_every_command` 的 topic 列表补 `"update"`。
P3-5 `install.rs` 模块文档与实测矛盾
- 改成「可以改名、不能覆盖/删除」,写明 M159 在 Windows 真机验过运行中的 exe 自替换,
并提醒别再照旧文档去设计「退出自身 → 另一个进程搬运」的双进程方案。
顺手(复审 P3-2/P3-8 里低成本的两条)
- 设置页补「下载中」态(`SettingsUpdateApplying` 不再是死 key,下载期间不画「下载并安装」);
渠道显示改读 `update.channel`(`stable` 走 key,其它原样);删掉从未使用的
`UpdateBannerRestartLater`(catalog + 金表同步,29 条 key 全部有引用)。
- `adopt` 只让**检查**刷新 `checked_at`(应用回报不再冒充「上次检查」);
`UpdateView::default()` 的 `current_version` 取本进程版本号(mock 下不再空白)。
门禁(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-159`):fmt 绿、clippy -D warnings 绿、
`cargo test --workspace --all-targets --no-fail-fast -j 4` **exit 0**(51 个测试二进制全 ok,
`core::addon::pack` 未被 skip)。该族用例复现了复审记的泄漏:一次全量在 `/tmp` 留 72 个
`ull-pack-*`(`/tmp` 149M → 8853M),跑完已 `rm -rf` 清回 150M。
This commit is contained in:
1 parent
1c8f6aa1b3
commit
04ac90911e
19 files changed
+521
-112
No files matched your search
@@ -10,8 +10,12 @@
|
||||
//! UI 与 CLI 都不许自己比版本号(mission:GUI 与 CLI 行为一致,不许两套逻辑);
|
||||
//! 4. 本模块不含 core 类型(D42:dto 只放扁平 IPC 类型)。
|
||||
//!
|
||||
//! 「拒绝更新」([`UpdateView::dismissed`])是**会话级**状态:用户点了「忽略这个版本」,本次运行
|
||||
//! 不再自动弹提示条;但设置页的「立即检查」不受影响(它会把这一位清掉重查)。
|
||||
//! 「忽略这个版本」([`UpdateView::skipped`])记的是**版本号**,不是「别再问我了」:
|
||||
//!
|
||||
//! * 用户点「忽略这个版本」时写下**当时那个版本号**(会话级,不落盘);
|
||||
//! * 同一次运行里如果又出现**更高**的版本(0.2.3 被忽略后冒出 0.2.4),提示条**照常弹**——
|
||||
//! 忽略的是那一个版本,文案也确实是「忽略这个版本」;
|
||||
//! * 设置页的「立即检查」会清掉它(手动检查 = 用户要看真结果),静默检查**永不**清。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -164,13 +168,18 @@ pub struct UpdateView {
|
||||
pub applying: bool,
|
||||
/// 已经就位的新版本(等重启)。
|
||||
pub ready: Option<ReadyUpdate>,
|
||||
/// 用户点了「忽略这个版本」:本次会话不再弹提示条(设置页手动检查会清掉它)。
|
||||
pub dismissed: bool,
|
||||
/// 用户点了「忽略这个版本」时记下的**版本号**(`None` = 没忽略)。
|
||||
///
|
||||
/// 只压住**这一个**版本:0.2.3 被忽略后 0.2.4 仍然提示(文案是「忽略这个版本」,
|
||||
/// 不是「别再问我」)。设置页手动检查会把它清空。
|
||||
pub skipped: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for UpdateView {
|
||||
/// 出厂状态:当前版本取本进程的版本号(**不是空串**——mock 后端/未接线的路径下,
|
||||
/// 设置页的「当前版本」那一行至少要显示自己的版本,而不是一片空白)。
|
||||
fn default() -> Self {
|
||||
Self::new(String::new(), "stable", true)
|
||||
Self::new(env!("CARGO_PKG_VERSION"), "stable", true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,7 +196,7 @@ impl UpdateView {
|
||||
silent: false,
|
||||
applying: false,
|
||||
ready: None,
|
||||
dismissed: false,
|
||||
skipped: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +216,7 @@ impl UpdateView {
|
||||
}
|
||||
self.outcome
|
||||
.candidate()
|
||||
.filter(|_| !self.dismissed)
|
||||
.filter(|candidate| self.skipped.as_deref() != Some(candidate.version.as_str()))
|
||||
.map(UpdateBanner::Available)
|
||||
}
|
||||
|
||||
@@ -258,20 +267,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dismissed_hides_the_banner_but_keeps_the_candidate() {
|
||||
fn skipping_one_version_does_not_silence_the_next_one() {
|
||||
let mut view = UpdateView {
|
||||
outcome: UpdateOutcome::Available(Box::new(candidate("0.2.3"))),
|
||||
..UpdateView::default()
|
||||
};
|
||||
assert!(matches!(view.banner(), Some(UpdateBanner::Available(_))));
|
||||
|
||||
view.dismissed = true;
|
||||
assert!(view.banner().is_none(), "拒绝之后本次不再打扰");
|
||||
view.skipped = Some("0.2.3".to_string());
|
||||
assert!(view.banner().is_none(), "忽略 0.2.3 之后它自己不再打扰");
|
||||
// 候选没被丢掉:设置页仍能显示「有新版本 0.2.3」并手动装
|
||||
assert_eq!(
|
||||
view.outcome.candidate().map(|item| item.version.as_str()),
|
||||
Some("0.2.3")
|
||||
);
|
||||
|
||||
// 同一次运行里冒出更高的版本:**照常提示**(文案是「忽略这个版本」,不是「别再问我」)。
|
||||
view.outcome = UpdateOutcome::Available(Box::new(candidate("0.2.4")));
|
||||
assert!(
|
||||
matches!(view.banner(), Some(UpdateBanner::Available(c)) if c.version == "0.2.4"),
|
||||
"0.2.3 被忽略不该压住 0.2.4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -6,60 +6,78 @@ use super::*;
|
||||
|
||||
// ------------------------------------------------------------------ 会话状态
|
||||
|
||||
/// 拒绝之后提示条消失,但候选留着;手动检查把「拒绝」清掉。
|
||||
/// 忽略**某一个版本**之后:它自己不弹了,候选留着;**更高版本照弹**;手动检查清掉忽略。
|
||||
///
|
||||
/// 这条是复审 P3-1 的守门:`skipped` 曾经是会话级 bool,于是「忽略 0.2.3」把同一次运行里
|
||||
/// 后来发布的 0.2.4 也一起压住了——而文案写的是「忽略这个版本」。
|
||||
#[test]
|
||||
fn dismissed_stops_the_banner_but_a_manual_check_revives_it() {
|
||||
fn skipping_a_version_does_not_silence_the_next_one() {
|
||||
fn available(version: &str) -> Report {
|
||||
Report::Checked(UpdateOutcome::Available(Box::new(
|
||||
crate::dto::UpdateCandidateView {
|
||||
version: version.to_string(),
|
||||
size: 12,
|
||||
notes: String::new(),
|
||||
forced: false,
|
||||
},
|
||||
)))
|
||||
}
|
||||
|
||||
let tmp = TempDir::new("dismiss");
|
||||
let mut runtime = Runtime::with_factory(&HostEnv::at_root(tmp.join("root")), factory(&tmp));
|
||||
runtime.adopt(Report::Checked(UpdateOutcome::Available(Box::new(
|
||||
crate::dto::UpdateCandidateView {
|
||||
version: "0.9.0".to_string(),
|
||||
size: 12,
|
||||
notes: String::new(),
|
||||
forced: false,
|
||||
},
|
||||
))));
|
||||
runtime.adopt(available("0.9.0"));
|
||||
assert!(matches!(
|
||||
runtime.view().banner(),
|
||||
Some(UpdateBanner::Available(_))
|
||||
));
|
||||
|
||||
runtime.dismiss();
|
||||
assert!(runtime.view().banner().is_none(), "拒绝后本次不再打扰");
|
||||
assert_eq!(
|
||||
runtime.view().skipped.as_deref(),
|
||||
Some("0.9.0"),
|
||||
"忽略要记下**那一个**版本号"
|
||||
);
|
||||
assert!(runtime.view().banner().is_none(), "被忽略的版本不再打扰");
|
||||
assert!(
|
||||
runtime.view().outcome.candidate().is_some(),
|
||||
"候选不能丢:设置页还要能手动装"
|
||||
);
|
||||
|
||||
// 静默检查**回来之后**也不许把拒绝弹回来(`adopt` 才是真正写状态的那一步)
|
||||
// 静默检查回来的是**同一个**版本:仍然压住(用户刚说过忽略它)。
|
||||
let _ = runtime.begin_check(false).expect("静默检查派发");
|
||||
runtime.adopt(Report::Checked(UpdateOutcome::Available(Box::new(
|
||||
crate::dto::UpdateCandidateView {
|
||||
version: "0.9.0".to_string(),
|
||||
size: 12,
|
||||
notes: String::new(),
|
||||
forced: false,
|
||||
},
|
||||
))));
|
||||
assert!(
|
||||
runtime.view().dismissed,
|
||||
"静默检查的结论回来时也不能清掉用户的拒绝"
|
||||
);
|
||||
runtime.adopt(available("0.9.0"));
|
||||
assert!(
|
||||
runtime.view().banner().is_none(),
|
||||
"被拒绝的版本不许因为一次静默检查又弹出来"
|
||||
"同一版本不许因为一次静默检查又弹出来"
|
||||
);
|
||||
|
||||
// 手动检查 ⇒ 清掉拒绝,重新查
|
||||
let job = runtime.begin_check(true).expect("手动检查派发");
|
||||
assert!(!runtime.view().dismissed, "手动检查要清掉拒绝标记");
|
||||
assert!(runtime.view().outcome.is_checking());
|
||||
// 静默检查(启动时)**不**清拒绝标记
|
||||
runtime.adopt(Report::Checked(UpdateOutcome::UpToDate { latest: None }));
|
||||
runtime.dismiss();
|
||||
// 静默检查回来的是**更高**版本:照弹(忽略的是 0.9.0,不是「以后别再问」)。
|
||||
let _ = runtime.begin_check(false).expect("静默检查派发");
|
||||
assert!(runtime.view().dismissed, "静默检查不许把用户的拒绝弹回来");
|
||||
drop(job);
|
||||
runtime.adopt(available("0.9.1"));
|
||||
assert!(
|
||||
matches!(runtime.view().banner(), Some(UpdateBanner::Available(c)) if c.version == "0.9.1"),
|
||||
"0.9.0 被忽略不该压住 0.9.1"
|
||||
);
|
||||
|
||||
// 手动检查 ⇒ 清掉忽略标记(用户要看真结果),重新查。
|
||||
let _ = runtime.begin_check(true).expect("手动检查派发");
|
||||
assert!(
|
||||
runtime.view().skipped.is_none(),
|
||||
"手动检查要清掉「忽略这个版本」"
|
||||
);
|
||||
assert!(runtime.view().outcome.is_checking());
|
||||
}
|
||||
|
||||
/// 没有候选时点「忽略」是空操作:不该写进一个「你别问」的状态。
|
||||
#[test]
|
||||
fn dismissing_without_a_candidate_does_nothing() {
|
||||
let tmp = TempDir::new("dismiss-none");
|
||||
let mut runtime = Runtime::with_factory(&HostEnv::at_root(tmp.join("root")), factory(&tmp));
|
||||
runtime.adopt(Report::Checked(UpdateOutcome::UpToDate {
|
||||
latest: Some("0.2.2".to_string()),
|
||||
}));
|
||||
runtime.dismiss();
|
||||
assert!(runtime.view().skipped.is_none());
|
||||
}
|
||||
|
||||
/// 关掉「启动时检查」之后:状态上是 no-op、启动路径上根本不派发。
|
||||
|
||||
@@ -106,7 +106,8 @@ impl Runtime {
|
||||
self.view.silent = !manual;
|
||||
self.view.applying = false;
|
||||
if manual {
|
||||
self.view.dismissed = false;
|
||||
// 手动检查 = 用户要看真结果,把「忽略这个版本」清掉。
|
||||
self.view.skipped = None;
|
||||
}
|
||||
Some(self.factory.job())
|
||||
}
|
||||
@@ -124,16 +125,14 @@ impl Runtime {
|
||||
/// 后台回报进快照(**唯一**的写入口:检查与应用都走这里)。
|
||||
pub fn adopt(&mut self, report: Report) {
|
||||
self.inflight = false;
|
||||
self.view.checked_at = super::update::stamp();
|
||||
match report {
|
||||
Report::Checked(outcome) => {
|
||||
// 「上次检查」只该被**检查**刷新:应用回来的那一刻不是一次检查。
|
||||
self.view.checked_at = super::update::stamp();
|
||||
// 「忽略这个版本」记的是版本号,所以**这里不需要动它**:
|
||||
// 同一版本回来仍被压住,更高版本自然就提示了(见 `UpdateView::skipped`)。
|
||||
// 手动检查那一支的清空在 `begin_check(manual)` 里。
|
||||
self.view.outcome = outcome;
|
||||
// 只有**用户主动查**才清掉「忽略这个版本」;静默检查(启动时/自动)一律不许把
|
||||
// 用户的拒绝弹回来(mission:拒绝之后本次不再打扰)。手动那一路在
|
||||
// `begin_check` 里就已经清过了,这里的条件只是让「谁清」这件事只有一条判据。
|
||||
if !self.view.silent && self.view.outcome.candidate().is_some() {
|
||||
self.view.dismissed = false;
|
||||
}
|
||||
}
|
||||
Report::Applied(ApplyReport::Installed {
|
||||
version,
|
||||
@@ -166,9 +165,13 @@ impl Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 用户拒绝这次更新:本次会话不再弹提示条(候选仍在快照里,设置页可手动装)。
|
||||
/// 用户忽略**当前这个候选版本**:记下版本号(候选仍留在快照里,设置页可手动装)。
|
||||
///
|
||||
/// 没有候选时是空操作(`skipped` 保持原样):没版本可忽略就不该写进一个「你别问」的状态。
|
||||
pub fn dismiss(&mut self) {
|
||||
self.view.dismissed = true;
|
||||
if let Some(candidate) = self.view.outcome.candidate() {
|
||||
self.view.skipped = Some(candidate.version.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
//! | [`a_higher_version_without_our_platform_is_not_up_to_date`] | 有新版但没本平台产物 → 单独的变体 |
|
||||
//! | [`sha256_mismatch_refuses_the_apply_and_leaves_no_temp_file`] | 摘要不符拒绝应用、暂存文件不残留、目标没被换掉 |
|
||||
//! | [`a_successful_apply_leaves_the_new_file_and_a_backup`] | 换上去的是新字节、旧字节留在备份里、回报是「就位」 |
|
||||
//! | [`dismissed_stops_the_banner_but_a_manual_check_revives_it`] | 拒绝后不再打扰;手动检查清掉拒绝 |
|
||||
//! | [`skipping_a_version_does_not_silence_the_next_one`] | 忽略的语义是**那一个版本**:同版本不再弹、更高版本照弹;手动检查清掉忽略 |
|
||||
//! | [`auto_check_off_means_no_request_on_start`] | 关掉开关后启动不派发检查(结构性 + 状态两层) |
|
||||
//! | [`the_gui_and_the_cli_share_one_kernel_path`] | GUI/CLI 走同一个 `Job`(源码锚点,不是口号) |
|
||||
|
||||
|
||||
Reference in new issue
Block a user