复审 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)。
97 lines
4.1 KiB
Rust
97 lines
4.1 KiB
Rust
//! 「这是模拟助手」的四处文案必须**一起**变(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)
|
||
);
|
||
}
|