复审 r1 的 P1-1:「用浏览器登录」这颗按钮在生产二进制里是哑的——`start_browser_login` 用的是
共享的 `MicrosoftAuth::from_env()`(`browser_first=false`),于是 `browser_then_device_login`
里整段预检 / 回退 / 说明编排不执行,按钮静默退化成设备码,任务③(authcode 可选加速路)
实际未交付。
- P1-1:入口改成 `msa_auth(env, MsaEntry::{DeviceCode,Browser})`,浏览器入口走
`.with_browser_first(true)`。守卫钉两段:①调用点(按 `LoginJob` 分桶记下生产入口实际请求的
模式,避开同进程并发用例串台)②模式真的落到 `MicrosoftAuth::browser_first`。变异实测:
把浏览器入口改回 `DeviceCode` ⇒ 该用例红。核心侧另有报文级证据:浏览器优先路必然对
`/authorize` 发一次预检(`unregistered_redirect_falls_back_to_the_device_code` 断言 GET 计数 == 1)。
- P2-1:`listeners_opened` 改成**绑定现场**计数——`CallbackServer::bind_counting` 在
`TcpListener::bind` 前 +1,`login_counting` 把 per-login 的 `AtomicUsize` 传下去;
删掉「进了授权码分支就记 1」的代理指标。复做变异(预检之前偷绑一个监听口)⇒ 3 条
msalogin 用例红,包括两条 `listeners_opened == 0`。两处 stale 文档(引用不存在的
`callback::bind_attempts()`)改成引用 `listeners_opened` / `bind_counting`。
- P2-2:预检加整体 deadline(10s,`PREFLIGHT_TIMEOUT`),超时归 `PreflightUnavailable` 并
回退设备码;新增「接了连接却不出响应」的假服务器单测(200ms deadline 内必须收场)。
(下载引擎的 HTTP 客户端只设了 connect_timeout,这条兜住「header 之后停住」。)
- P2-3:client_id 守卫加严到三种拼法(`concat!` / 跨行两段 / `format!` 逐段拼),做法是
「先收字符串字面量再拼」+「连续纯 hex 字面量累计 ≥32 且 ≥2 段」,并跳过注释(否则本模块
文档里的示例会自指假红);每条一个自检向量,且不误杀 `session.rs` 的 profile UUID。
- P2-4:页面级断言补中文(`Lang::ZhCn` 走 `Page::build`),并把回退说明的文案出口收敛成
视图自己的 `login::browser_note_text`(视图与取样共用,不再测试另写一份映射)。
- P2-5:主按钮文案改成「使用 Microsoft 账户登录」/「Sign in with your Microsoft account」,
下面补一行「浏览器打不开就用这个:它会给一串用户码,在任何设备上输入即可。」。
门禁:`cargo fmt --check`、`cargo clippy --workspace --all-targets -- -D warnings`、
`cargo test --workspace --all-targets`(51 个 test 目标全绿)通过;`client_id.rs` 的判据拆到
`client_id/tests.rs` 守 400 行红线。截图三张(入口 / 设备码 / 回退)已重出。
109 lines
4.9 KiB
Rust
109 lines
4.9 KiB
Rust
//! 「**模型这一层会给出什么**」的取样器:视图模型字段 + **按码做的 i18n 查表**,拼成一串。
|
||
//!
|
||
//! ## 边界(复审 r2 P2-2 订正,别把这条读成「视图覆盖」)
|
||
//!
|
||
//! 这是**平行实现**:它自己调 `catalog` 查表,**不调用** `login::panel` / `forms::status_line`,
|
||
//! 也不遍历 iced 的元素树。所以它证明的是「模型 + i18n 表里没有中文」,**不证明**「视图真的照它画」。
|
||
//!
|
||
//! 视图层靠两道别的闸:
|
||
//! 1. **类型**:`DevicePanel` / `AuthlibPanel` 里**没有** `message`/`detail` 字段(r3 删掉了),
|
||
//! 想画后端原文是编译错误,不是测试问题;
|
||
//! 2. **出图**:`DevDocs/artifacts/m114/after/*` 由人(tower/复审)逐张看。
|
||
//!
|
||
//! 为什么单独一个文件:`pages/account/tests.rs` 贴着 400 行硬线,而这段是**纯取样**
|
||
//! (不给任何断言)——断言都在 `tests.rs` 里。
|
||
|
||
use unlucky_i18n::{Key, Lang};
|
||
use unlucky_presenter::dto::AccountNoticeCode;
|
||
|
||
use super::login::{browser_note_text, failure_body_key, failure_title_key};
|
||
use super::model::{AuthlibPanel, DevicePanel, Hint, Page};
|
||
|
||
/// 页面**会画什么**:模型里的数据 + 视图层对码的 i18n 查表结果。
|
||
pub(super) fn on_screen(page: &Page) -> String {
|
||
let catalog = Lang::En.catalog();
|
||
let mut out = String::new();
|
||
for row in &page.rows {
|
||
out.push_str(&row.name);
|
||
out.push_str(&row.monogram);
|
||
out.push_str(&row.kind_label);
|
||
out.push_str(&row.added);
|
||
out.push_str(&row.detail);
|
||
}
|
||
out.push_str(&page.form.offline_name);
|
||
out.push_str(&page.form.server);
|
||
if let Some(notice) = &page.notice {
|
||
// 与 `widgets::banner` 同源:码 → i18n(主题名拼进去)
|
||
out.push_str(match notice.code {
|
||
AccountNoticeCode::OfflineNameInvalid => {
|
||
catalog.text(Key::AccountNoticeOfflineNameInvalid)
|
||
}
|
||
AccountNoticeCode::AccountExists => catalog.text(Key::AccountNoticeAccountExists),
|
||
AccountNoticeCode::StoreFailed => catalog.text(Key::AccountNoticeStoreFailed),
|
||
AccountNoticeCode::NotFound => catalog.text(Key::AccountNoticeNotFound),
|
||
AccountNoticeCode::Other => catalog.text(Key::AccountNoticeOther),
|
||
AccountNoticeCode::Added => catalog.text(Key::AccountNoticeAdded),
|
||
AccountNoticeCode::Removed => catalog.text(Key::AccountNoticeRemoved),
|
||
AccountNoticeCode::Switched => catalog.text(Key::AccountNoticeSwitched),
|
||
});
|
||
out.push_str(¬ice.subject);
|
||
}
|
||
if let Some(hint) = page.hint {
|
||
out.push_str(match hint {
|
||
Hint::AuthlibReauth => catalog.text(Key::AccountHintAuthlibReauth),
|
||
Hint::OfflineNoRelogin => catalog.text(Key::AccountHintOfflineNoRelogin),
|
||
});
|
||
}
|
||
match &page.device {
|
||
DevicePanel::Hidden => {}
|
||
DevicePanel::Requesting => out.push_str(catalog.text(Key::AccountDeviceRequesting)),
|
||
DevicePanel::Code {
|
||
code,
|
||
link,
|
||
countdown,
|
||
note,
|
||
..
|
||
} => {
|
||
out.push_str(code);
|
||
out.push_str(link);
|
||
// 倒计时文案由模型拼好(M178:owned 文字路径画不出中文)。
|
||
out.push_str(countdown);
|
||
// M178:回退说明走**视图自己的**文案出口(`login::browser_note_text`),
|
||
// 不是测试另写一份映射——否则视图哪天不画这一行,取样仍会绿。
|
||
if let Some(note) = note {
|
||
out.push_str(browser_note_text(*note, catalog));
|
||
}
|
||
}
|
||
DevicePanel::Browser {
|
||
link, countdown, ..
|
||
} => {
|
||
out.push_str(link);
|
||
out.push_str(countdown);
|
||
}
|
||
DevicePanel::Failed { failure, note } => {
|
||
out.push_str(catalog.text(failure_title_key(*failure)));
|
||
out.push_str(catalog.text(failure_body_key(*failure)));
|
||
out.push_str(&catalog.render(Key::AccountFailureCode, &[("code", failure.code())]));
|
||
if let Some(note) = note {
|
||
out.push_str(browser_note_text(*note, catalog));
|
||
}
|
||
}
|
||
DevicePanel::Succeeded { name } => {
|
||
out.push_str(&catalog.render(Key::AccountLoginSucceeded, &[("name", name)]));
|
||
}
|
||
}
|
||
match &page.authlib {
|
||
AuthlibPanel::Idle => {}
|
||
AuthlibPanel::Running => out.push_str(catalog.text(Key::AccountServerChecking)),
|
||
AuthlibPanel::Ready { server_name } => out.push_str(server_name),
|
||
AuthlibPanel::Failed { failure } => out.push_str(failure.code()),
|
||
AuthlibPanel::Succeeded { name } => out.push_str(name),
|
||
}
|
||
out
|
||
}
|
||
|
||
/// 有没有中日韩统一表意文字(语言泄漏的最直接判据)。
|
||
pub(super) fn has_cjk(text: &str) -> bool {
|
||
text.chars().any(|c| matches!(c, '\u{3400}'..='\u{4DBF}' | '\u{4E00}'..='\u{9FFF}' | '\u{3000}'..='\u{303F}' | '\u{FF00}'..='\u{FFEF}'))
|
||
}
|