fix(app-iced): M100 r4 P2-1 —— 终态判据加「取证模式」前提,交互预览关窗不再 exit 1

复审在 `d298691` 的导出上实测:交互预览模式(不给 `--screenshot`)关窗会被新判据判成失败,
exit 1 + 一句「取证结束但没有任何图落盘(窗口提前消失 / 落盘失败)」的**假消息**。
那是把「从未要求出图」判成「取证失败」,退出码因此失去区分力
(真崩溃 / 真取证失败 / 正常关个预览窗全是 1)。

- `no_evidence(capture_mode, written) = capture_mode && written == 0`;调用点传
  `request.screenshot.is_some()`。判据里写明:交互预览模式下 `subscription` 是 `None`、
  永远不会有 `Captured`,所以「没写盘」在那里不是失败。
- 注释补一条「为什么用计数器而不是 `path.exists()`」:旧 PNG 会让 exists 骗过判据。
- 断言改名 `finishing_without_a_single_write_is_a_failure_only_while_capturing`,
  三条:`(false,0)=false`(这次的红线)/ `(true,0)=true` / `(true,1)=false`。

顺手 p3:
- p3-1 `CAPTURE_GIVE_UP_FRAMES` 注释删掉「约 3~4 秒(~120ms/帧)」这个没测准的数字,
  改成写明帧间隔量级差异(复审实测 2~3ms/帧 vs 本机 `[perf]` 的 ≈200~300ms/帧),
  「要调按第 3 帧派发→第 4~5 帧落盘的结构关系调,别按秒」。
- p3-2 旧字段名 `writes` 的注释改成「这个计数器(旧字段名叫 `writes`)」;顺带把
  `capture_stalled` 的形参 `writes` 改名 `written`,与字段对齐。
- p3-3 `examples/pages_demo.rs` 的 usage 漏 `--appearance`/`--passthrough`:该文件**不在本
  mission scope**(scope 只有 `pages/harness*`),不扩 scope,欠账仍记在已开的 finding 里。

实测:
- 交互预览(WM_CLOSE 关窗,取真实退出码):最终二进制 5/5 有效跑 exit=0、`没有任何图落盘` 行 0;
  另 2 次 exit=0xC000013A(STATUS_CONTROL_C_EXIT,控制台 Ctrl 事件打断,日志停在半途)不计。
  修前同场景 4/4 exit=1。
- M3 变异(第 3 帧 `window::screenshot` → `window::close`)3/3 exit=1 + 具名行(没被误伤)。
- M4 变异(`CAPTURE_GIVE_UP_FRAMES` 30→0)2/2 exit=1 走「采样超时」(没被误伤)。
- 正常取证 `--appearance=real` ×6:exit=0 / png=yes / `落盘次数=1` / `passthrough_ext=on` /
  错误行 0 / sha256 `414a71fcb77e9b38…`(与 r1~r4 基线逐字节相同,像素没动)。
This commit is contained in:
AskaEth committed 2026-11-03 19:20:50 +08:00
1 parent d298691c17
commit f67b24cc7b
3 files changed
+45 -20

No files matched your search

+23 -4
View File
@@ -22,13 +22,26 @@ use crate::theme::Ui;
use super::{demo_model, Harness, Message, Request, CAPTURE_POSITION};
/// **终态判据**:取证跑完了却一张图都没落盘 ⇒ 失败。
/// **终态判据**:取证运行跑完了却一张图都没落盘 ⇒ 失败。
///
/// 单独拎成纯函数是为了让它能被断言(`run()` 要真窗口才能跑)。它守的是 M100 r3 P2-1 那个
/// 真实缺陷:窗口在第 3 帧消失时,`window::frames()` 不再产消息 ⇒ 帧超时兜底**求值不到**,
/// 而 iced_winit 对「最后一个窗口 Destroyed」直接退出 ⇒ 曾经是 **exit 0 + 没有 PNG**。
pub(super) fn no_evidence(written: u32) -> bool {
written == 0
///
/// ## 为什么必须有 `capture_mode` 前提(M100 r4 P2-1)
///
/// 不给 `--screenshot` 是文档明确支持的**交互预览模式**(「没给 `--screenshot` 就只是开窗看」):
/// 该模式 `subscription()` 直接 `Subscription::none()`,**永远不会有 `Captured`** ⇒ `written` 恒 0。
/// 没有这个前提,用户**正常关掉预览窗**会拿到 exit 1 和一句「取证结束但没有任何图落盘」的
/// 假消息——那是把"从未要求出图"判成"取证失败",通道的退出码就此失去区分力
/// (真崩溃 / 真取证失败 / 正常关个预览窗全是 1)。
///
/// ## 为什么用计数器而不是 `path.exists()`
///
/// `written` 是**本进程**成功落盘的次数,不会被执行前就存在的旧 PNG 骗过;换成「目标文件存在」
/// 会在脏路径下给出「exit 0 + 上一次的旧图」。这一点写在这里,免得后来人「顺手改成 exists」。
pub(super) fn no_evidence(capture_mode: bool, written: u32) -> bool {
capture_mode && written == 0
}
/// 启动(阻塞到出图或关窗)。
@@ -209,7 +222,13 @@ pub fn run(request: Request) -> Result<(), String> {
//
// 所以判据从「超时」升级成「终态」:**跑完了没有图就是失败**。它比超时强,因为覆盖
// 「帧不再产生」这一类;帧超时那条保留,管「有帧但采样回不来」。
if no_evidence(written.load(Ordering::Relaxed)) {
//
// **前提是这次真的是取证运行**(M100 r4 P2-1):不给 `--screenshot` 的交互预览模式本来
// 就不出图,判成失败会打出假消息、并让退出码失去区分力。前提写在判据里,不写在这里。
if no_evidence(
request.screenshot.is_some(),
written.load(Ordering::Relaxed),
) {
eprintln!("[harness] 取证结束但没有任何图落盘(窗口提前消失 / 落盘失败),按失败退出");
std::process::exit(1);
}