Files
Unlucky-Launcher/crates/app-iced/src/pages/agent/tests/disclaimer.rs
T
AskaEth 0e48ea15b8 fix(app-iced): M119 r2 —— 四处「模拟」文案全由 simulated 驱动;确认条不再替用户 arm 键盘
复审 r2 的两条 p2 都是「上一轮各差一步」:字段/焦点都真的做了,但**另有三处没接上**、
**键盘闸是代码自己打开的**。

## P2-1:四处文案只认一个开关

r1 只有标题胶囊由 `simulated` 驱动;副标题 / 输入框占位符 / 空态正文是**无条件取词**,
而它们的译文里写着「模拟 / 没接真实模型」——接上真 transport 之后这三句会**反向**留着
(`simulated == false` 时界面还在声明「没接真实模型」)。而且没有一条测试钉得住那个声明。

现在:

- 新增 `pages/agent::Disclaimer { simulated, subtitle, placeholder, empty_body }`,
  `Disclaimer::of(catalog, simulated)` 一次算好;`Conversation.simulated: bool` 换成
  `Conversation.note: Disclaimer`,header / composer / transcript 三处各自从它取词——
  **视图里没有第二种取词路径**;
- i18n 配成对:模拟态用既有的 `AgentSubtitle` / `AgentPlaceholder` / `AgentEmptyBody`,
  真实态新增 `AgentSubtitleReal` / `AgentPlaceholderReal` / `AgentEmptyBodyReal`(中性话)。
  (没有把三句改成中性话:那句告知本身有价值,接真模型后它才该消失。)
- 证据:`crates/app-iced/src/pages/agent/tests/disclaimer.rs`
  —— `all_four_disclaimers_appear_when_the_backend_is_simulated`(四处都提模拟)、
  `no_disclaimer_survives_when_a_real_backend_is_wired`(四处都不提,且不许用空串糊)、
  `the_two_states_use_different_keys`(两态不能共用同一个 key)。

**golden.rs 的 400 行边界**:+3 条会破线。按「按功能拆、不压注释」处理——把这张表的
**说明文档**(35 行)原样搬到 `catalog.rs` 里 `mod golden;` 的文档上(一个字的说明都没删),
`golden.rs` 只留表数据 + 3 行指针。现在 `golden.rs` 374 行、`catalog.rs` 328 行。

## P2-2:键盘闸不许由代码替用户打开

r1 在 `sync_confirm_focus`(`on_agent` 的无条件尾巴,`Events` 又由心跳驱动)里顺手
`keyboard_mode = true`——于是**只要冒出待确认,全局键盘闸就开了**;用户发完那句话接着打下一句,
第一个空格就成了「批准写操作」。复审用临时用例复现过(`rail.confirm` 变 `None`)。

现在:

- `sync_confirm_focus` **只挪焦点**到「允许」,不再碰 `keyboard_mode`(字段文档里写清了为什么);
- 键盘用户的表态是 `Tab`:`AgentUi::step_focus` 加了一条 **arming 规则**——待确认、焦点已在
  确认条上、但键盘闸还关着时,这一次 `Tab` **只点亮焦点环、不移动焦点**(否则第一次 Tab 会把
  「允许」挪到「拒绝」,用户按 Enter 就是反着裁决);之后再 Tab 才逐格走;
- 反向回归:`a_stray_space_or_enter_never_approves_a_pending_write`(打字 → 空格 / 回车都不得裁决,
  审计也不许留);`a_blocking_confirm_can_be_decided_with_the_keyboard_alone` 改成
  「确认到达时未 armed → Tab 点亮 → Enter 允许;第二轮 Tab·Tab 到拒绝 → Space」。

## 出图

`DevDocs/artifacts/m119/` 18 张全部重出(新增取证夹具 `ULL_AGENT_FIXTURE=armed`:
跑一轮真提交后再按一次 Tab,专供「焦点环可见」那张图):

- `08-agent-simulated-{light,dark}`:`simulated = true` 下四处「模拟」同时可见;
- `09-agent-confirm-focus-{light,dark}`:**按过 Tab 之后**的确认条(焦点环在「允许」上);
- `05-agent-confirm-{light,dark}`:确认条刚出现、**还没 arm**(无焦点环)——与 09 成对,
  正好把 r2 的行为差异拍下来。

## 门禁(真实退出码,最终树)

| 命令 | 退出码 |
|---|---|
| `cargo fmt --all --check` | 0 |
| `cargo clippy -p unlucky-app-iced -p unlucky-presenter -p unlucky-agent -p unlucky-i18n --all-targets -- -D warnings` | 0(0 warning)|
| `cargo test -p unlucky-app-iced --lib` | 0(605 passed / 0 failed / 2 ignored)|
| `cargo test --workspace --exclude unlucky-app` | 0(58 套 2281 passed / 0 failed / 11 ignored)|

全树受检文件无 >400(`golden.rs` 374、`catalog.rs` 328、`pages/agent/mod.rs` 371)。
2026-11-05 10:43:22 +08:00

97 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 「这是模拟助手」的四处文案必须**一起**变(M119 r2 / 复审 P2-1 的验收标准)。
//!
//! ## 为什么这个文件存在
//!
//! 复审 r2 的 P2-1 抓得很准:`simulated` 是真字段,但 r1 只有**标题胶囊**一个消费者,另外三处
//! (副标题 / 输入框占位符 / 空态正文)是**无条件取词**,而它们的译文里写着「模拟 / 没接真实模型」。
//! 于是「接上真 transport 之后四处一起消失」只是一句声明——没有测试钉住它。
//!
//! 现在四处都从 [`Disclaimer`] 取词(`Disclaimer::of(catalog, simulated)`),本文件就是那句声明的
//! 证据:`simulated == true` 四处都提模拟;`simulated == false` 四处**都不提**。
//!
//! 抓得住:谁把某一处改回无条件取词(那就会出现「字段说真、界面说模拟」的反向假话)。
//! 抓不住:文案本身翻得好不好读(那要人看),也不管视图有没有真的用这几个字段
//! (那是 `tests.rs::the_spec_borrows_the_presenter_rail_verbatim` 与出图的活)。
use unlucky_i18n::{Catalog, Key, Lang};
use super::super::Disclaimer;
/// 这句话里有没有「模拟 / 没接真实模型」的意思(中英各一套写法)。
fn mentions_simulated(text: &str) -> bool {
let lower = text.to_lowercase();
lower.contains("simulated") || lower.contains("no real model") || text.contains("模拟")
}
/// **四处一起出现**(`simulated == true`):副标题 / 占位符 / 空态正文 / 标题胶囊。
#[test]
fn all_four_disclaimers_appear_when_the_backend_is_simulated() {
for lang in [Lang::En, Lang::ZhCn] {
let catalog = Catalog::of(lang);
let note = Disclaimer::of(catalog, true);
assert!(note.simulated, "{}:开关要传下去", lang.tag());
for (place, text) in [
("副标题", note.subtitle),
("输入框占位符", note.placeholder),
("空态正文", note.empty_body),
// 第四处是标题胶囊:只在 `simulated` 时渲染,文案来自这条 key
("标题胶囊", catalog.text(Key::AgentSimulated)),
] {
assert!(
mentions_simulated(text),
"{} 的{place}没有说清「这是模拟」:{text:?}",
lang.tag()
);
}
}
}
/// **四处一起消失**(`simulated == false`):三处文案都不许再提模拟,胶囊也不该被画。
///
/// 这条就是复审要的那一句验收:接上真 transport 那天,界面不会再反向地声明「没接真实模型」。
#[test]
fn no_disclaimer_survives_when_a_real_backend_is_wired() {
for lang in [Lang::En, Lang::ZhCn] {
let catalog = Catalog::of(lang);
let note = Disclaimer::of(catalog, false);
assert!(!note.simulated, "{}:开关要传下去", lang.tag());
for (place, text) in [
("副标题", note.subtitle),
("输入框占位符", note.placeholder),
("空态正文", note.empty_body),
] {
assert!(
!mentions_simulated(text),
"{} 的{place}在真实态还在说模拟:{text:?}",
lang.tag()
);
}
// 真实态的三句也得是**有内容的话**,不能靠空串糊过去
for (place, text) in [
("副标题", note.subtitle),
("输入框占位符", note.placeholder),
("空态正文", note.empty_body),
] {
assert!(!text.trim().is_empty(), "{} 的{place}是空的", lang.tag());
}
}
}
/// 两态必须是**两套不同的文案**(同一个 key 两边都用就退化成「永远说模拟」或「永远不说」)。
#[test]
fn the_two_states_use_different_keys() {
let catalog = Catalog::of(Lang::En);
assert_ne!(
catalog.text(Key::AgentSubtitle),
catalog.text(Key::AgentSubtitleReal)
);
assert_ne!(
catalog.text(Key::AgentPlaceholder),
catalog.text(Key::AgentPlaceholderReal)
);
assert_ne!(
catalog.text(Key::AgentEmptyBody),
catalog.text(Key::AgentEmptyBodyReal)
);
}