|
5 | 5 | //! 失败 kind/message 访问。 |
6 | 6 |
|
7 | 7 | 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}; |
9 | 9 |
|
10 | 10 | #[test] |
11 | 11 | fn short_note_single_slice() { |
@@ -84,3 +84,63 @@ fn failure_mapping_from_client_error() { |
84 | 84 | assert!(!f.message().is_empty()); |
85 | 85 | } |
86 | 86 | } |
| 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