修复+重构: 全库走查真bug+架构+P1/P2 后端 crate
- df-nodes: schema required 对齐 + docker POSIX 注入防御 + HumanNode timeout 1800 + parse_review_json(verdict规范/score clamp/正则兜底) - df-mcp: update 实体校验(防跨实体 B-260801-01) - df-storage: keyring 迁移失败达阈值清除明文 - df-ai: router estimated_context+tier tiebreak+DataReadOnly 兜底 + sanitize step4 显式不制造 orphan - df-ideas: adversarial tier:None 对齐
This commit is contained in:
@@ -259,12 +259,16 @@ impl Node for AiSelfReviewNode {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": { "type": "string", "description": "自审目标任务 ID(必填)" },
|
||||
"provider_id": { "type": "string", "description": "AI Provider ID(密钥经 secret 解析不进 config;留空走默认 provider)" },
|
||||
"provider_id": { "type": "string", "description": "AI Provider ID(可选,留空走默认 provider;密钥经 secret 解析不进 config)" },
|
||||
"model": { "type": "string", "description": "模型名(可选,留空用 record.default_model)" },
|
||||
"max_tokens": { "type": "integer" },
|
||||
"gate": { "type": "boolean", "description": "闸门开关:false(默认)=自审辅助,verdict 仅透传展示;true=自审结果作 DAG 闸门,verdict=fail 返回 Err 阻断下游(工作流 failed → ②-4 退回),verdict=unknown/pass 放行" }
|
||||
},
|
||||
"required": ["task_id", "provider_id"]
|
||||
// SW-260802-01: schema 与 handler 行为对齐 — required 仅列 handler 真正强制必填的字段。
|
||||
// task_id: execute 第 100-104 行缺 task_id 直接 Err("缺少必填参数: task_id"),真必填 → 保留。
|
||||
// provider_id: execute 调 resolve_and_parse → resolve_provider(ai_node_helpers.rs:100-108),
|
||||
// 空串走路径 2(老明文)/路径 3(默认 provider)兜底,运行时非必填 → 移出 required。
|
||||
"required": ["task_id"]
|
||||
}),
|
||||
output: serde_json::json!({
|
||||
"type": "object",
|
||||
@@ -408,6 +412,95 @@ mod tests {
|
||||
assert_eq!(v["summary"], json!("缺边界处理"));
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// P2:parse_review_json 三道加固(verdict 规范 / score clamp / 正则兜底)
|
||||
// ============================================================
|
||||
//
|
||||
// 真实 bug 场景:
|
||||
// - LLM 输出 verdict="Pass"/"PASS" → 原 == "fail" 精确匹配 → 漏阻断(放行 fail 项)
|
||||
// - LLM 输出 score=99/-1 → 越界值污染闸门阈值(维度 score<6 视 fail)与前端展示
|
||||
// - LLM 前置解释文字 "审查结果:\n{...}" → 整段 serde 失败 → 兜底 unknown(本可救回)
|
||||
// 加固后:verdict 规范化 / score clamp [0,10] / 首个 { 到末 } 正则兜底提取。
|
||||
|
||||
/// P2-加固1:verdict 大小写/空白变体规范化为 pass/fail/unknown 三态。
|
||||
/// 防 gate_should_block 精确匹配 "fail" 对 "Fail"/"FAIL" 漏阻断。
|
||||
#[test]
|
||||
fn parse_review_json_normalizes_verdict_case_variants() {
|
||||
// "Pass" → "pass"(防 LLM 首字母大写)
|
||||
let v = parse_review_json(r#"{"verdict":"Pass","summary":"ok"}"#);
|
||||
assert_eq!(v["verdict"], json!("pass"), "Pass 应规范化为 pass");
|
||||
|
||||
// "FAIL" → "fail"(防漏阻断:原 == "fail" 对 FAIL 放行)
|
||||
let v = parse_review_json(r#"{"verdict":"FAIL","summary":"缺单测"}"#);
|
||||
assert_eq!(v["verdict"], json!("fail"), "FAIL 应规范化为 fail");
|
||||
|
||||
// " fail "(含空白)→ "fail"
|
||||
let v = parse_review_json(r#"{"verdict":" fail ","summary":"x"}"#);
|
||||
assert_eq!(v["verdict"], json!("fail"), "含空白 verdict 应 trim 后规范化");
|
||||
|
||||
// "PASS" → "pass"
|
||||
let v = parse_review_json(r#"{"verdict":"PASS"}"#);
|
||||
assert_eq!(v["verdict"], json!("pass"), "PASS 应规范化为 pass");
|
||||
|
||||
// 拼写错/非标准值 → "unknown"(保守不阻断,保人定权)
|
||||
let v = parse_review_json(r#"{"verdict":"passed"}"#);
|
||||
assert_eq!(v["verdict"], json!("unknown"), "非 pass/fail 的值应归 unknown");
|
||||
}
|
||||
|
||||
/// P2-加固1 联动:规范化后 verdict 经 gate_should_block 正确阻断 fail。
|
||||
/// 验证 "FAIL"/"Fail" 经 parse_review_json 规范化 → gate_should_block 阻断(原会漏)。
|
||||
#[test]
|
||||
fn parse_review_json_fail_variants_trigger_gate_block() {
|
||||
for raw_verdict in ["fail", "Fail", "FAIL", " fail ", "FaIl"] {
|
||||
let v = parse_review_json(&format!(r#"{{"verdict":"{raw_verdict}"}}"#));
|
||||
let normalized = v["verdict"].as_str().unwrap();
|
||||
assert_eq!(
|
||||
normalized, "fail",
|
||||
"verdict={raw_verdict:?} 应规范化为 fail"
|
||||
);
|
||||
assert!(
|
||||
gate_should_block(true, normalized),
|
||||
"gate 开 + 规范化后 fail 应阻断(raw={raw_verdict:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// P2-加固2:dimensions.*.score 越界值 clamp 到 [0,10]。
|
||||
/// 防 LLM 输出 99/-1/NaN 类越界值污染闸门阈值(score<6 视 fail)与前端展示。
|
||||
#[test]
|
||||
fn parse_review_json_clamps_out_of_range_scores() {
|
||||
let raw = r#"{"verdict":"pass","dimensions":{
|
||||
"requirement_fit":{"score":99,"issues":[]},
|
||||
"completeness":{"score":-5,"issues":[]},
|
||||
"correctness":{"score":7.5,"issues":[]},
|
||||
"boundary":{"score":10,"issues":[]}
|
||||
},"summary":"ok"}"#;
|
||||
let v = parse_review_json(raw);
|
||||
// 99 → 10(上界)
|
||||
assert_eq!(v["dimensions"]["requirement_fit"]["score"], json!(10.0), "score 99 应 clamp 到 10");
|
||||
// -5 → 0(下界)
|
||||
assert_eq!(v["dimensions"]["completeness"]["score"], json!(0.0), "score -5 应 clamp 到 0");
|
||||
// 区间内值不变
|
||||
assert_eq!(v["dimensions"]["correctness"]["score"], json!(7.5), "score 7.5 区间内不变");
|
||||
assert_eq!(v["dimensions"]["boundary"]["score"], json!(10.0), "score 10 边界值不变");
|
||||
}
|
||||
|
||||
/// P2-加固3:LLM 前置解释文字 + JSON,正则兜底提取首个 { 到末 } 解析成功。
|
||||
/// 场景:LLM 无视「只输出 JSON」输出 "审查结果:\n{...}\n以上。" → 原整段失败兜底 unknown。
|
||||
#[test]
|
||||
fn parse_review_json_extracts_json_from_leading_text() {
|
||||
let raw = "好的,以下是审查结果:\n{\"verdict\":\"fail\",\"summary\":\"缺边界处理\"}\n以上为审查结论。";
|
||||
let v = parse_review_json(raw);
|
||||
assert_eq!(v["verdict"], json!("fail"), "前置文字应被正则兜底剥离,verdict 正确解析");
|
||||
assert_eq!(v["summary"], json!("缺边界处理"));
|
||||
|
||||
// 前置文字 + 代码块围栏混杂(更极端:LLM 既加解释又加 ```json)
|
||||
let raw = "审查如下:\n```json\n{\"verdict\":\"pass\",\"summary\":\"ok\"}\n```\n完毕。";
|
||||
let v = parse_review_json(raw);
|
||||
// 围栏不在开头 → strip_prefix 不命中 → 正则兜底提取 {...}
|
||||
assert_eq!(v["verdict"], json!("pass"), "前置文字+围栏混杂应正则兜底解析");
|
||||
}
|
||||
|
||||
/// 步骤③:truncate_for_summary 长文截断。
|
||||
#[test]
|
||||
fn truncate_for_summary_long_text() {
|
||||
@@ -605,4 +698,51 @@ mod tests {
|
||||
);
|
||||
assert!(!gate_should_block(true, ""), "空 verdict 不应阻断");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SW-260802-01: schema required 与 handler 行为对齐测试
|
||||
// ============================================================
|
||||
//
|
||||
// 真实 bug:schema `required=["task_id","provider_id"]` 与 handler 不一致 ——
|
||||
// task_id 缺失 → execute Err(真必填,对齐 schema)
|
||||
// provider_id 缺失 → execute 走 resolve_provider 路径 3 默认 provider(非必填,schema 误导)
|
||||
// 修复后 schema `required=["task_id"]`。此处直接断言 schema,防回归。
|
||||
//
|
||||
// 不真调 execute(需真 LLM + 默认 provider 完整链),改为断言 schema 这份"契约"本身 +
|
||||
// resolve_provider 路径 3 行为(已由 ai_node.rs resolve_provider_fallback_default_provider 覆盖),
|
||||
// 即足以守 schema↔handler 对齐不被无意改回。
|
||||
|
||||
/// schema.required 应仅含 task_id(handler 真必填),不含 provider_id(运行时可空走默认)。
|
||||
#[tokio::test]
|
||||
async fn schema_required_matches_handler() {
|
||||
let db = Database::open_in_memory().await.expect("open_in_memory");
|
||||
let node = AiSelfReviewNode::new(Arc::new(db));
|
||||
let schema = node.schema();
|
||||
let params = schema
|
||||
.params
|
||||
.as_object()
|
||||
.expect("schema.params 应是 object");
|
||||
let required = params
|
||||
.get("required")
|
||||
.and_then(|v| v.as_array())
|
||||
.expect("schema 应有 required 数组");
|
||||
|
||||
// task_id 真必填(execute 第 100-104 行缺 task_id → Err)
|
||||
assert!(
|
||||
required.iter().any(|v| v == "task_id"),
|
||||
"task_id 应在 required(handler 真必填)"
|
||||
);
|
||||
// provider_id 非必填(resolve_provider 路径 3 空串走默认 provider)
|
||||
assert!(
|
||||
!required.iter().any(|v| v == "provider_id"),
|
||||
"provider_id 不应在 required(运行时留空走默认 provider,schema 不得误导)"
|
||||
);
|
||||
// 必填字段集恰好为 {"task_id"}(防后续误加回 provider_id 或漏列 task_id)
|
||||
assert_eq!(
|
||||
required.len(),
|
||||
1,
|
||||
"required 应仅 1 项(task_id), 实际: {:?}",
|
||||
required
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user