修复: TD-260621-05审批状态双轨统一(executed→completed+V27迁移+测试)

This commit is contained in:
2026-06-24 17:48:42 +08:00
parent ebba9735ed
commit ba811ead96
2 changed files with 114 additions and 11 deletions

View File

@@ -29,7 +29,10 @@ pub fn run(conn: &Connection) -> Result<()> {
// V24 = ideas.related_ids 列(灵感间关联关系持久化打底);
// V25 = idea_evaluations (idea_id, version) 唯一约束(评估版本并发重复兜底);
// V26 = F-260621-02 任务索引缺口补全(priority/assignee,对齐 idx_tasks_status 同类索引)。
let steps: [(i32, fn(&Connection) -> Result<()>); 26] = [
// V27 = TD-260621-05 审批状态统一(ai_tool_executions.status executed→completed,
// 对齐 DTO 契约 audit/mod.rs:53 只列 completed + 前端 i18n auditLog.status 无 executed 键 +
// 治 AuditLog executed 记录显示错位蓝pending标签+raw"executed")。
let steps: [(i32, fn(&Connection) -> Result<()>); 27] = [
(1, migrate_v1),
(2, migrate_v2),
(3, migrate_v3),
@@ -56,6 +59,7 @@ pub fn run(conn: &Connection) -> Result<()> {
(24, migrate_v24),
(25, migrate_v25),
(26, migrate_v26),
(27, migrate_v27),
];
for (version, migrate_fn) in steps {
@@ -657,6 +661,24 @@ fn migrate_v26(conn: &Connection) -> Result<()> {
Ok(())
}
/// V27: 审批状态统一 executed→completed(TD-260621-05)
///
/// chat.rs ai_approve/ai_authorize_dir 审批通过后工具执行成功历史写 "executed",audit 内联执行
/// (低风险无审批)写 "completed",双轨并存致:① DTO 文档(audit/mod.rs:53)只列 completed 契约失配;
/// ② 前端 AuditLog statusClass/i18n auditLog.status 无 executed 键 → executed 记录显示蓝pending
/// 标签 + raw"executed"文案矛盾(用户见"待审批 executed");③ 未来 WHERE status='completed'
/// 统计会漏 executed 路径。统一为 completed(find_cached SW-16 透传已兼容双值,无破坏)。
/// 数据迁移:存量 executed→completed,新库/老库均跑(UPDATE 0 行也安全)。
fn migrate_v27(conn: &Connection) -> Result<()> {
conn.execute(
"UPDATE ai_tool_executions SET status = 'completed' WHERE status = 'executed'",
[],
)?;
conn.execute("INSERT INTO schema_version (version) VALUES (?)", [27])?;
tracing::info!("迁移 v27 完成(审批状态 executed→completed 统一)");
Ok(())
}
/// V21 建表 SQL — 消息拆分存储 ai_messages 表
///
/// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。
@@ -1261,4 +1283,85 @@ mod tests {
.unwrap();
assert_eq!(v, 20);
}
/// V27:审批状态统一 executed→completed(TD-260621-05)
/// 存量 executed 记录转 completed,rejected/completed/failed 不变(只动 executed)
#[test]
fn v27_unifies_executed_to_completed() {
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch(
"CREATE TABLE schema_version (version INTEGER PRIMARY KEY);
CREATE TABLE ai_tool_executions (
id TEXT PRIMARY KEY,
tool_call_id TEXT,
tool_name TEXT,
status TEXT NOT NULL,
requested_at TEXT,
executed_at TEXT
);",
)
.expect("create ai_tool_executions");
// 混合状态:2 executed(待转)+ completed/rejected/failed(应不变)
conn.execute_batch(
"INSERT INTO ai_tool_executions (id, tool_call_id, tool_name, status) VALUES
('e1', 'tc1', 'write_file', 'executed'),
('e2', 'tc2', 'read_file', 'executed'),
('c1', 'tc3', 'list_directory', 'completed'),
('r1', 'tc4', 'write_file', 'rejected'),
('f1', 'tc5', 'run_command', 'failed');",
)
.unwrap();
migrate_v27(&conn).expect("v27 应成功统一状态");
// executed 全部转 completed
let executed_left: i64 = conn
.query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'executed'", [], |r| r.get(0))
.unwrap();
assert_eq!(executed_left, 0, "executed 应全部转为 completed");
let completed: i64 = conn
.query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'completed'", [], |r| r.get(0))
.unwrap();
assert_eq!(completed, 3, "原 2 executed + 1 completed = 3 completed");
// 其他状态不受影响
let rejected: i64 = conn
.query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'rejected'", [], |r| r.get(0))
.unwrap();
assert_eq!(rejected, 1, "rejected 不变");
let failed: i64 = conn
.query_row("SELECT COUNT(*) FROM ai_tool_executions WHERE status = 'failed'", [], |r| r.get(0))
.unwrap();
assert_eq!(failed, 1, "failed 不变");
let v: i64 = conn
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
.unwrap();
assert_eq!(v, 27, "应写入版本号 27");
}
/// V27 幂等:无 executed 时再跑不崩(UPDATE 0 行),版本号照写
#[test]
fn v27_idempotent_no_executed() {
let conn = Connection::open_in_memory().expect("open in-memory db");
conn.execute_batch(
"CREATE TABLE schema_version (version INTEGER PRIMARY KEY);
CREATE TABLE ai_tool_executions (id TEXT PRIMARY KEY, status TEXT NOT NULL);",
)
.expect("create tables");
conn.execute(
"INSERT INTO ai_tool_executions (id, status) VALUES ('c1', 'completed')",
[],
)
.unwrap();
migrate_v27(&conn).expect("v27 无 executed 时应幂等成功(UPDATE 0 行)");
let v: i64 = conn
.query_row("SELECT MAX(version) FROM schema_version", [], |r| r.get(0))
.unwrap();
assert_eq!(v, 27);
}
}

View File

@@ -457,7 +457,7 @@ pub async fn ai_approve(
// Err 压成 None,DB 故障被「未找到」误导(实为可重试故障),对齐 audit.rs audit_finalize 三路分流。
match state.ai_tool_executions.find_by_tool_call_id(&tool_call_id).await {
Ok(Some(rec)) => {
if rec.status == "executed" || rec.status == "rejected" || rec.status == "failed" {
if rec.status == "completed" || rec.status == "rejected" || rec.status == "failed" {
return Ok(format!("已处理({})", rec.status));
}
}
@@ -534,7 +534,7 @@ pub async fn ai_approve(
// session_trust —— 下次同会话同类操作(同工具+同目录 TrustKey)自动放行,跳过 pending + 二次确认。
// 仅首批工具(write_file/run_command)命中 trust_key_for 的 Some,其余工具 noop(None 不写)。
// F-260616-09 B 批4:per_conv.session_trust 唯一真相源(conv_id 来源 approval.conversation_id)。
// 无 conv_id(无主审批 R-9 异常数据)时无 per_conv 可写,信任不记(审计仍记 executed)。
// 无 conv_id(无主审批 R-9 异常数据)时无 per_conv 可写,信任不记(审计仍记 completed)。
if let Some(key) = super::super::trust_key_for(&approval.tool_name, &approval.arguments) {
let inserted = if let Some(ref cid) = conv_id {
session.conv(cid).session_trust.insert(key.clone())
@@ -581,7 +581,7 @@ pub async fn ai_approve(
// 工具失败不 return Err把错误包成 tool_result落库 + emit completed + 续循环全走通。
// 否则前端 approveToolCall 的 catch 会回滚 pending_approval审批按钮卡死无法消除。
let (audit_status, result_val) = match &exec_result {
Ok(val) => ("executed", val.clone()),
Ok(val) => ("completed", val.clone()),
Err(e) => ("failed", serde_json::Value::String(e.to_string())),
};
// 审计:人工审批后无论成败回填(决策者=human
@@ -601,7 +601,7 @@ pub async fn ai_approve(
// 重新获取锁,替换占位 tool_result 为真实结果失败时为错误信息LLM 据此决定下一步)
// F-260616-09 B 批4:per_conv.messages 唯一真相源(conv_id 来源 approval.conversation_id)。
// 无 conv_id 的无主审批不入 messages(审计仍记 executed/failed)。
// 无 conv_id 的无主审批不入 messages(审计仍记 completed/failed)。
let mut session = state.ai_session.lock().await;
if let Some(ref cid) = conv_id {
session.conv(cid).messages.replace_tool_result_content(&id, &result_val.to_string());
@@ -627,11 +627,11 @@ pub async fn ai_approve(
// F-260619-04 P2(方案 B): create_idea source 消息级溯源补全。
// create_idea 是 Medium risk → 经审批执行(本路径),非 process_tool_calls 内联执行,
// 故补全点放此(工具已 executed,可拿到 result.id + args.source)。
// 故补全点放此(工具已 completed,可拿到 result.id + args.source)。
// message_id 从 per_conv.messages 取末条 assistant id(同 process_tool_calls 的 current_message_id 语义);
// assistant_with_tools 消息持久驻留 messages,审批时仍可读。
// 仅 executed(成功)且 create_idea 才补;失败/其他工具 noop(helper 内判定,不污染本路径主流程)。
if audit_status == "executed" {
// 仅 completed(成功)且 create_idea 才补;失败/其他工具 noop(helper 内判定,不污染本路径主流程)。
if audit_status == "completed" {
let message_id = {
let session = state.ai_session.lock().await;
conv_id.as_deref().and_then(|cid| {
@@ -665,7 +665,7 @@ pub async fn ai_approve(
// 所有待审批处理完毕后恢复 agentic 循环(recovered 无 live loop,try_continue 因 generating=false 自然不续)
try_continue_agent_loop(&app, &state, &cont_conv_id, start_iter).await;
Ok("executed".to_string())
Ok("completed".to_string())
}
/// F-260620 临时诊断:授权/审批 IPC 调用链文件日志(不依赖终端 stderr,读 authz-debug.log 定位)。
@@ -882,11 +882,11 @@ pub async fn ai_authorize_dir(
};
tracing::debug!(
ok = exec_result.is_ok(),
status = if exec_result.is_ok() { "executed" } else { "failed" },
status = if exec_result.is_ok() { "completed" } else { "failed" },
"[AI-AUTHZ] execute 后"
);
let (audit_status, mut result_val) = match &exec_result {
Ok(val) => ("executed", val.clone()),
Ok(val) => ("completed", val.clone()),
Err(e) => ("failed", serde_json::Value::String(e.to_string())),
};
// 阶段4(容错/恢复):跨盘预检 → tool_result 提示。