首尾的控制字符不再被 trim 静默吞掉,统一按控制字符报错

- optional_text 原来先 trim 再查 is_control:`"prompt\n"`、`"\tprompt"` 这类落在首尾的控制字符被静默裁掉、当成合法值,而同一个字符出现在中间却会报错
- 改成先按原串查控制字符,再用 trim 做空值与长度判定:控制字符在哪个位置都报 has_control_characters
- 纯空白值("\n\t"、"   ")仍然算空值、报原来的 empty 错误;首尾的普通空格仍按原样裁掉
- 测试补三条断言:prompt 末尾带 \n 必须被拒、纯空白 brief 仍报空、brief 首尾普通空格仍会被裁剪

验证:cargo test --bin genarrative-ai-game-creator-shell -- agent:: 953 passed / 0 failed
This commit is contained in:
2026-10-01 15:18:02 +08:00
parent 66e4ece692
commit 3f70c69119
2 changed files with 40 additions and 6 deletions
@@ -4860,6 +4860,38 @@ mod tests {
#[test]
fn non_text_field_values_report_a_type_error_instead_of_an_empty_value() {
let trailing_newline = match resource_generation_input(&json!({
"kind": "video",
"mode": "create",
"prompt": "生成过场\n",
"assetName": "开场"
})) {
Ok(_) => panic!("落在首尾的控制字符不能被 trim 静默吞掉"),
Err(error) => error.to_user_msg(),
};
assert!(
trailing_newline.contains("prompt 含有控制字符"),
"{trailing_newline}"
);
let whitespace_only =
required_text(&json!({ "brief": "\n\t" }), PrepareGameArtTextField::Brief)
.expect_err("纯空白仍然按空值处理")
.to_user_msg();
assert!(
whitespace_only.contains("brief 不能为空"),
"{whitespace_only}"
);
assert_eq!(
required_text(
&json!({ "brief": " 美术包 " }),
PrepareGameArtTextField::Brief
)
.expect("首尾普通空格仍正常裁剪"),
"美术包"
);
let prompt_not_text = match resource_generation_input(&json!({
"kind": "video",
"mode": "create",
@@ -37,17 +37,19 @@ pub(crate) fn optional_text<F: ToolTextField<E>, E>(
let Some(value) = value.as_str() else {
return Err(field.not_text(value.to_string()));
};
let value = value.trim();
if value.is_empty() {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(field.empty());
}
if value.chars().count() > field.max_chars() {
return Err(field.too_long(value.chars().count(), field.max_chars()));
}
// 控制字符必须按原串判定:否则落在首尾的 `\n`/`\t` 会被 trim 静默吞掉,
// 同一个字符在中间报错、在边上通过。
if value.chars().any(char::is_control) {
return Err(field.has_control_characters());
}
Ok(Some(value.to_string()))
if trimmed.chars().count() > field.max_chars() {
return Err(field.too_long(trimmed.chars().count(), field.max_chars()));
}
Ok(Some(trimmed.to_string()))
}
/// 一页列表结果:`offset` 是起始下标,`limit` 是这一次最多返回多少项。