Skip to content

Commit 1845462

Browse files
committed
fix(ai): AiTaskState/AiTaskFailure 契约对齐前端——修复精修结果永不使用的根因 (P0)
1 parent 919b544 commit 1845462

2 files changed

Lines changed: 76 additions & 2 deletions

File tree

‎app/src-tauri/src/ai_task.rs‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,14 @@ use serde::{Deserialize, Serialize};
1515
pub const SLICE_MAX_CHARS: usize = 8000;
1616

1717
/// 任务状态(前端任务卡片:进行中(按片进度)/成功/失败(原因))。
18+
///
19+
/// @ai-context: 契约修复(2026-08-21 真机"调用有记录但结果未使用"根因):
20+
/// 曾带 rename_all="camelCase" 把变体名小写化("succeeded"/{"running"}),
21+
/// 前端按 PascalCase 比较("Succeeded"/"Running" in st)全部失配——
22+
/// 任务后台真实成功、前端永久"排队中"、结果永不取回。
23+
/// 现去掉 rename_all:变体名=默认 PascalCase,字段=snake_case,
24+
/// 与前端 types.ts 逐字匹配(契约快照测试见 ai_task_tests.rs)。
1825
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
19-
#[serde(rename_all = "camelCase")]
2026
pub enum AiTaskState {
2127
Pending,
2228
Running {
@@ -30,6 +36,12 @@ pub enum AiTaskState {
3036
}
3137

3238
/// 任务失败原因(降级链可见——不静默降级;前端按类引导出口)。
39+
///
40+
/// @ai-context: 契约修复(2026-08-21):camelCase 下 InsufficientBalance →
41+
/// "insufficientBalance"、InvalidResponse → "invalidResponse",
42+
/// 前端期待 "balance"/"invalid"(四类出口)——显式 rename 对齐;
43+
/// 其余变体 camelCase 转换后本就与前端匹配(unauthorized/network/
44+
/// quota/server/other)。
3345
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3446
#[serde(rename_all = "camelCase")]
3547
pub enum AiTaskFailure {
@@ -38,12 +50,14 @@ pub enum AiTaskFailure {
3850
/// 网络错误(引导重试)
3951
Network(String),
4052
/// 余额不足(引导充值/切免费档)
53+
#[serde(rename = "balance")]
4154
InsufficientBalance(String),
4255
/// 配额受限(引导明日再试)
4356
Quota(String),
4457
/// 服务端错误(引导稍后重试)
4558
Server(String),
4659
/// 响应非法(已丢弃回退——本地版保留,不丢不假)
60+
#[serde(rename = "invalid")]
4761
InvalidResponse(String),
4862
/// 其他
4963
Other(String),

‎app/src-tauri/src/ai_task_tests.rs‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//! 失败 kind/message 访问。
66
77
use crate::ai_client::AiClientError;
8-
use crate::ai_task::{slice_note, AiTaskFailure, SLICE_MAX_CHARS};
8+
use crate::ai_task::{slice_note, AiTaskFailure, AiTaskState, SLICE_MAX_CHARS};
99

1010
#[test]
1111
fn short_note_single_slice() {
@@ -84,3 +84,63 @@ fn failure_mapping_from_client_error() {
8484
assert!(!f.message().is_empty());
8585
}
8686
}
87+
88+
/// 契约快照测试(2026-08-21 真机"调用有记录但结果未使用"根因的回归护栏):
89+
///
90+
/// @ai-context: 前端 types.ts 按 PascalCase 变体名 + snake_case 字段消费
91+
/// AiTaskState("Pending"/{"Running":{finished_slices,...}}/
92+
/// "Succeeded"/{"Failed":{reason}}),失败原因按四类小写标签
93+
/// (unauthorized/network/balance/quota/server/invalid/other)。
94+
/// 本测试断言 serde 输出 = 前端期望的 JSON 字面量——任何一端
95+
/// 契约漂移立即红,杜绝"后端成功前端永久排队中"类事故复发。
96+
#[test]
97+
fn serde_contract_snapshot_matches_frontend() {
98+
// AiTaskState:变体名 PascalCase(unit variant = 字符串字面量)
99+
assert_eq!(serde_json::to_string(&AiTaskState::Pending).unwrap(), r#""Pending""#);
100+
assert_eq!(serde_json::to_string(&AiTaskState::Succeeded).unwrap(), r#""Succeeded""#);
101+
// struct variant:外部标签 PascalCase + 字段 snake_case
102+
let running = AiTaskState::Running { finished_slices: 1, total_slices: 3 };
103+
assert_eq!(
104+
serde_json::to_string(&running).unwrap(),
105+
r#"{"Running":{"finished_slices":1,"total_slices":3}}"#
106+
);
107+
// 失败嵌套 reason
108+
let failed = AiTaskState::Failed { reason: AiTaskFailure::Unauthorized("x".into()) };
109+
assert_eq!(
110+
serde_json::to_string(&failed).unwrap(),
111+
r#"{"Failed":{"reason":{"unauthorized":"x"}}}"#
112+
);
113+
// AiTaskFailure:四类出口标签(camelCase + 两个显式 rename)
114+
assert_eq!(
115+
serde_json::to_string(&AiTaskFailure::Unauthorized("a".into())).unwrap(),
116+
r#"{"unauthorized":"a"}"#
117+
);
118+
assert_eq!(
119+
serde_json::to_string(&AiTaskFailure::Network("n".into())).unwrap(),
120+
r#"{"network":"n"}"#
121+
);
122+
assert_eq!(
123+
serde_json::to_string(&AiTaskFailure::InsufficientBalance("b".into())).unwrap(),
124+
r#"{"balance":"b"}"#
125+
);
126+
assert_eq!(
127+
serde_json::to_string(&AiTaskFailure::Quota("q".into())).unwrap(),
128+
r#"{"quota":"q"}"#
129+
);
130+
assert_eq!(
131+
serde_json::to_string(&AiTaskFailure::Server("s".into())).unwrap(),
132+
r#"{"server":"s"}"#
133+
);
134+
assert_eq!(
135+
serde_json::to_string(&AiTaskFailure::InvalidResponse("i".into())).unwrap(),
136+
r#"{"invalid":"i"}"#
137+
);
138+
assert_eq!(
139+
serde_json::to_string(&AiTaskFailure::Other("o".into())).unwrap(),
140+
r#"{"other":"o"}"#
141+
);
142+
// 反序列化回环(任务注册表存 JSON——恢复路径依赖)
143+
let v = serde_json::to_value(&running).unwrap();
144+
let back: AiTaskState = serde_json::from_value(v).unwrap();
145+
assert_eq!(back, running);
146+
}

0 commit comments

Comments
 (0)