fix(m174): 复审 r1 五项 —— 失败带 target、密钥消息掩码、请求体断言、桥写前重读、结构化分类
P2-1(假绿截图):「测试连接」失败类也带上被请求的 target。`ProbeOutcome` 的五个失败变体
改成 `{ target, detail }`,`ProbeResult::from_outcome` 不再把端点/模型填成空串;view 另加一条
「目标为空就整行不画」的保险。**截图种子改走 `from_outcome`**(新增
`ProbeResult::screenshot_seed`):种子只伪造「结论是哪一档」,字段形状由产品路径决定——上一版
手填字段,于是「失败态目标为空」只在产品上出现、截图永远看不见。
P2-2(明文进日志):`Message::EditKey` 的载荷从 `String` 换成 `ai::Secret`(`Debug` 掩码)。
用例 `the_key_message_never_prints_the_key` 同时钉住裸消息与整条 `pages::Message` 漏斗。
P2-3(模型没被证上线):假服务器按 `Content-Length` 读请求正文,新用例
`the_request_carries_the_configured_model` 断言正文里 `"model":"gpt-test"`。变异实测暴露出
一处真相:`ChatRequest.model` 被 `RigProvider::build_request` 静默忽略,线上模型来自
`ProviderConfig.model`(已另开 finding,并写进 probe 的注释)。
P2-4(数据丢失):`SettingsBridge::persist` 写前 `store.reload()`。`SettingStore::save` 是整份
文档落盘,不重读就会拿 ≤1 秒前的旧快照盖掉刚写的 `ai.*`。两条回归用例:
`presenter::settings_bridge::persist_keeps_keys_another_writer_just_saved` 与 app-iced 侧的
`ai::store::appearance_write_back_keeps_the_ai_keys`(保存 AI → 改主题 → 值还在)。
P2-5(子串匹配):删掉 `classify()`。分类改由 `brain::rig::map_error` 从 rig 0.45 的结构化
报告(`ErrorReport` 的 `kind` / `http_status` / `code`)翻译成新的 `ProviderFailure`
(`brain/provider/failure.rs`),`ProviderError` 因此带上 `kind`。`closed_port_is_unreachable`
不再依赖错误文本:连接被拒恒为 `ErrorKind::Http` ⇒ unreachable。防退化用例
`factory_error_carries_the_structured_classification` 断言「纯文本 401 不许被猜成 auth」。
顺带(400 行硬线):`provider.rs` / `rig.rs` 的用例各自拆到 `provider/tests.rs` /
`rig/tests.rs`(纯搬移)。
This commit is contained in:
1 parent
9cb605f316
commit
93d02ab12b
18 files changed
+840
-412
No files matched your search
@@ -27,7 +27,7 @@ use crate::app::components::inputs::{text_field, Field, FieldVariant};
|
||||
use crate::app::components::selection::{chip, switch, Chip, Switch};
|
||||
use crate::pages::frame::{card, divider, text_role_owned};
|
||||
use crate::pages::settings::ai::outcome::ProbeKind;
|
||||
use crate::pages::settings::ai::state::{Probe, ProviderId, SaveNotice, State, Status};
|
||||
use crate::pages::settings::ai::state::{Probe, ProviderId, SaveNotice, Secret, State, Status};
|
||||
use crate::pages::settings::{ai_about, GroupMessage, States};
|
||||
use crate::pages::Message;
|
||||
use crate::theme::Ui;
|
||||
@@ -64,20 +64,24 @@ fn body_of<'a>(ui: &'a Ui, state: &'a State, lang: Lang) -> Element<'a, Message>
|
||||
}
|
||||
if let Probe::Done(result) = &state.probe {
|
||||
stacked = stacked.push(probe_line(ui, catalog, result.kind));
|
||||
stacked = stacked.push(text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.render(
|
||||
Key::SettingsAiTestTarget,
|
||||
&[
|
||||
("base_url", result.base_url.as_str()),
|
||||
("model", result.model.as_str()),
|
||||
("ms", result.elapsed_ms.to_string().as_str()),
|
||||
],
|
||||
),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
// 目标行只在真有值时才画(r1 复审 P2-1 的第二道保险):失败类现在**都会**带上 target,
|
||||
// 但「没发请求」与将来的新档可能没有;空着画出来就是「目标: · 模型 · N 毫秒」。
|
||||
if !(result.base_url.is_empty() && result.model.is_empty()) {
|
||||
stacked = stacked.push(text_role_owned(
|
||||
ui,
|
||||
TypeRole::BodySmall,
|
||||
false,
|
||||
catalog.render(
|
||||
Key::SettingsAiTestTarget,
|
||||
&[
|
||||
("base_url", result.base_url.as_str()),
|
||||
("model", result.model.as_str()),
|
||||
("ms", result.elapsed_ms.to_string().as_str()),
|
||||
],
|
||||
),
|
||||
ui.color(Role::OnSurfaceVariant),
|
||||
));
|
||||
}
|
||||
if !result.detail.trim().is_empty() {
|
||||
stacked = stacked.push(text_role_owned(
|
||||
ui,
|
||||
@@ -240,7 +244,7 @@ fn key_field<'a>(ui: &'a Ui, catalog: Catalog, state: &'a State) -> Element<'a,
|
||||
),
|
||||
);
|
||||
let field = container(text_field(ui, field, |value| {
|
||||
msg(ai_about::Message::EditKey(value))
|
||||
msg(ai_about::Message::EditKey(Secret::new(value)))
|
||||
}))
|
||||
.width(Length::Fill);
|
||||
column![
|
||||
|
||||
Reference in new issue
Block a user