feat(gitea): 增加 Agent PR 安全合并门禁 #14
@ -109,6 +109,21 @@ PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否
|
||||
- `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。
|
||||
- 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。
|
||||
|
||||
## PR 合并(炼境托管项目)
|
||||
|
||||
合并属于高影响操作,Agent 必须先读取交付状态并向用户展示 PR 编号、完整 head SHA、CI 与 review;仅在用户明确确认本次合并后调用:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "<id>",
|
||||
"pr_number": 123,
|
||||
"expected_head_sha": "<完整 40 位 SHA>",
|
||||
"confirmed": true
|
||||
}
|
||||
```
|
||||
|
||||
工具名为 `merge_pull_request`。能力本体会重新读取远端状态;PR 非 open、head SHA 漂移、CI 未通过、review 请求修改或任一门禁来源不可用时均拒绝合并。合并成功后再次调用 `get_delivery_status` 读取真实结果。`confirmed=true` 只能来自当前用户对该 PR 与 SHA 的明确确认,不能由 Agent 自行推断。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
|
||||
@ -11,7 +11,8 @@ description: Inspect an already onboarded Lian Jing project, use its existing MC
|
||||
2. 仅将 `.lian-jing/context.json` 中的 `project_id` 用作炼境 MCP 调用参数。`.claude/liangjing.json` 只用于识别旧项目并提示迁移,不能作为长期身份来源。
|
||||
3. 当项目已有 `lian-jing` MCP 时,可调用 `get_diagnostics`、`verify_onboarding`、`get_toolchain_status` 和 `get_delivery_status` 获取可观察信号;所有调用显式传入 `project_id`。
|
||||
4. Node/pnpm 项目运行包管理命令前先读取工具链状态;`ready_managed` 时使用 `mise exec -- pnpm ...`。只有用户明确确认下载安装后,才可调用 `prepare_toolchain(project_id, confirmed=true)`;不得为适配 Codex bundled runtime 修改项目版本或 lockfile。
|
||||
5. 若缺少 canonical context、MCP 或项目登记,不得猜测 project_id、MCP URL、凭证或状态。说明缺失项,并建议通过炼境接入或 `inject_mcp` 修复。
|
||||
6. 炼境提供状态、凭证托管和受控入口;项目 Agent 仍负责代码判断、测试选择、提交分组与业务交付决策。不要把 CI 成功或部署信号表述为业务验收。
|
||||
5. 合并 PR 前调用 `get_delivery_status(project_id)`,向用户展示 PR 编号、完整 head SHA、CI 与 review。只有当前用户明确确认该 PR 与 SHA 后,才调用 `merge_pull_request(project_id, pr_number, expected_head_sha, confirmed=true)`;合并后再次读取交付状态。不得自行推断确认。
|
||||
6. 若缺少 canonical context、MCP 或项目登记,不得猜测 project_id、MCP URL、凭证或状态。说明缺失项,并建议通过炼境接入或 `inject_mcp` 修复。
|
||||
7. 炼境提供状态、凭证托管和受控入口;项目 Agent 仍负责代码判断、测试选择、提交分组与业务交付决策。不要把 CI 成功或部署信号表述为业务验收。
|
||||
|
||||
本插件不包含 MCP server、MCP URL、项目 ID、凭证、部署命令或客户端项目 UI 映射;这些都属于项目接入与炼境控制面。
|
||||
|
||||
@ -14,5 +14,6 @@ description: 读取炼境项目健康状态,并使用炼境 MCP 的项目管
|
||||
3. 在 Node/pnpm 项目运行包管理命令前调用 `get_toolchain_status(project_id)`;`ready_managed` 时通过 `mise exec -- pnpm ...` 执行,其他状态先报告实际版本与 executable 证据。
|
||||
4. 只有用户明确确认本次下载安装后,才可调用 `prepare_toolchain(project_id, confirmed=true)`;不得为适配客户端 bundled runtime 而修改项目版本或 lockfile。
|
||||
5. 交付、Gitea、CI、蓝图等平台状态优先通过 MCP 查询,不凭静态文档猜测。
|
||||
6. 合并 PR 前调用 `get_delivery_status(project_id)`,向用户展示 PR 编号、完整 head SHA、CI 与 review。只有当前用户明确确认该 PR 与 SHA 后,才调用 `merge_pull_request(project_id, pr_number, expected_head_sha, confirmed=true)`;合并后再次读取交付状态。不得自行推断确认。
|
||||
|
||||
如果 MCP 未出现,说明 Codex 项目配置尚未被加载、项目未被信任,或炼境未运行。说明该限制后继续完成不依赖 MCP 的本地工作;不得伪造诊断结果。
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"packs": [
|
||||
{
|
||||
"pack_id": "base-onboarding",
|
||||
"version": "1.2.0",
|
||||
"version": "1.3.0",
|
||||
"supported_clients": ["codex", "claude"],
|
||||
"project_artifacts": [
|
||||
"AGENTS.md",
|
||||
|
||||
@ -109,6 +109,21 @@ PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否
|
||||
- `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。
|
||||
- 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。
|
||||
|
||||
## PR 合并(炼境托管项目)
|
||||
|
||||
合并属于高影响操作,Agent 必须先读取交付状态并向用户展示 PR 编号、完整 head SHA、CI 与 review;仅在用户明确确认本次合并后调用:
|
||||
|
||||
```json
|
||||
{
|
||||
"project_id": "<id>",
|
||||
"pr_number": 123,
|
||||
"expected_head_sha": "<完整 40 位 SHA>",
|
||||
"confirmed": true
|
||||
}
|
||||
```
|
||||
|
||||
工具名为 `merge_pull_request`。能力本体会重新读取远端状态;PR 非 open、head SHA 漂移、CI 未通过、review 请求修改或任一门禁来源不可用时均拒绝合并。合并成功后再次调用 `get_delivery_status` 读取真实结果。`confirmed=true` 只能来自当前用户对该 PR 与 SHA 的明确确认,不能由 Agent 自行推断。
|
||||
|
||||
## 常用命令
|
||||
|
||||
```bash
|
||||
|
||||
@ -1371,30 +1371,140 @@ pub fn gitea_api_get_ci_status(project_id: &str, commit_sha: &str) -> Result<Str
|
||||
Ok(lines.join("\n"))
|
||||
}
|
||||
|
||||
/// 合并指定 PR。调用 POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge
|
||||
pub fn gitea_api_merge_pull_request(project_id: &str, pr_number: i64) -> Result<String, String> {
|
||||
let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
|
||||
fn is_full_git_sha(value: &str) -> bool {
|
||||
value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
/// 合并门禁的纯函数部分。网络层先抓取同一时刻的 PR / CI / review,再一次性校验。
|
||||
/// 返回已确认的实际 head SHA,供执行结果与审计信息复用。
|
||||
fn validate_merge_preconditions(
|
||||
confirmed: bool,
|
||||
expected_head_sha: &str,
|
||||
pull_request: &serde_json::Value,
|
||||
ci: &DeliverySignal,
|
||||
review: &DeliverySignal,
|
||||
) -> Result<String, String> {
|
||||
if !confirmed {
|
||||
return Err("合并需要当前用户明确确认(confirmed=true)".to_string());
|
||||
}
|
||||
if !is_full_git_sha(expected_head_sha) {
|
||||
return Err("expected_head_sha 必须是完整 40 位 Git SHA".to_string());
|
||||
}
|
||||
|
||||
let state = pull_request["state"].as_str().unwrap_or("unknown");
|
||||
if state != "open" || pull_request["merged"].as_bool() == Some(true) {
|
||||
return Err(format!("PR 当前不可合并:state={state}"));
|
||||
}
|
||||
if pull_request["mergeable"].as_bool() == Some(false) {
|
||||
return Err("PR 存在冲突或被 Gitea 判定为不可合并".to_string());
|
||||
}
|
||||
|
||||
let actual_head_sha = pull_request["head"]["sha"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "Gitea PR 响应缺少 head.sha,拒绝合并".to_string())?;
|
||||
if actual_head_sha != expected_head_sha {
|
||||
return Err(format!(
|
||||
"PR head SHA 已变化:期望 {expected_head_sha},实际 {actual_head_sha};请重新检查后确认"
|
||||
));
|
||||
}
|
||||
|
||||
if ci.state != "passed" {
|
||||
return Err(format!("CI 门禁未通过:{}({})", ci.state, ci.detail));
|
||||
}
|
||||
if ci.data["target_sha"].as_str() != Some(actual_head_sha) {
|
||||
return Err("CI 证据未绑定当前 PR head SHA,拒绝合并".to_string());
|
||||
}
|
||||
|
||||
match review.state.as_str() {
|
||||
"approved" | "pending" => {}
|
||||
"changes_requested" => return Err("review 门禁阻塞:存在 reviewer 请求修改".to_string()),
|
||||
other => {
|
||||
return Err(format!(
|
||||
"review 状态不可用或未知:{other};为避免误合并已拒绝"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(actual_head_sha.to_string())
|
||||
}
|
||||
|
||||
fn merge_request_body(pr_number: i64, head_sha: &str) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"Do": "merge",
|
||||
"merge_title_field": format!("Merge PR #{pr_number}"),
|
||||
"merge_message_field": "",
|
||||
"head_commit_id": head_sha,
|
||||
})
|
||||
}
|
||||
|
||||
/// 合并指定 PR。所有高影响边界在能力本体内强制校验,MCP/UI 不得绕过。
|
||||
/// 调用 POST /api/v1/repos/{owner}/{repo}/pulls/{index}/merge。
|
||||
pub fn gitea_api_merge_pull_request(
|
||||
conn: &rusqlite::Connection,
|
||||
project_id: &str,
|
||||
pr_number: i64,
|
||||
expected_head_sha: &str,
|
||||
confirmed: bool,
|
||||
) -> Result<String, String> {
|
||||
if !confirmed {
|
||||
return Err("合并需要当前用户明确确认(confirmed=true)".to_string());
|
||||
}
|
||||
if !is_full_git_sha(expected_head_sha) {
|
||||
return Err("expected_head_sha 必须是完整 40 位 Git SHA".to_string());
|
||||
}
|
||||
let (instance_id, owner, repo_name): (String, String, String) = conn.query_row(
|
||||
"SELECT instance_id, gitea_owner, gitea_repo_name FROM gitea_repos WHERE project_id = ?1",
|
||||
params![project_id],
|
||||
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
|
||||
).map_err(|_| format!("项目 {} 未绑定 Gitea 仓库", project_id))?;
|
||||
|
||||
let (base_url, token) = instance_auth(&conn, &instance_id)?;
|
||||
let (base_url, token) = instance_auth(conn, &instance_id)?;
|
||||
|
||||
let pull_path = format!("/api/v1/repos/{owner}/{repo_name}/pulls/{pr_number}");
|
||||
let pull_request = gitea_get_json(&base_url, &token, &pull_path, "读取待合并 PR 失败")?;
|
||||
let actual_head_sha = pull_request["head"]["sha"]
|
||||
.as_str()
|
||||
.ok_or_else(|| "Gitea PR 响应缺少 head.sha,拒绝合并".to_string())?;
|
||||
|
||||
let status_path = format!(
|
||||
"/api/v1/repos/{owner}/{repo_name}/statuses/{actual_head_sha}?sort=recentupdate&limit=50"
|
||||
);
|
||||
let statuses = gitea_get_json(
|
||||
&base_url,
|
||||
&token,
|
||||
&status_path,
|
||||
"读取待合并 PR 的 CI 状态失败",
|
||||
)?;
|
||||
let ci = summarize_ci_statuses(&statuses, Some(actual_head_sha));
|
||||
|
||||
let reviews_path = format!("/api/v1/repos/{owner}/{repo_name}/pulls/{pr_number}/reviews");
|
||||
let reviews = gitea_get_json(
|
||||
&base_url,
|
||||
&token,
|
||||
&reviews_path,
|
||||
"读取待合并 PR 的 review 状态失败",
|
||||
)?;
|
||||
let review = summarize_reviews(&reviews);
|
||||
let validated_sha =
|
||||
validate_merge_preconditions(confirmed, expected_head_sha, &pull_request, &ci, &review)?;
|
||||
|
||||
let url = format!("{base_url}/api/v1/repos/{owner}/{repo_name}/pulls/{pr_number}/merge");
|
||||
ureq::post(&url)
|
||||
.set("Authorization", &format!("token {token}"))
|
||||
.set("Content-Type", "application/json")
|
||||
.send_json(serde_json::json!({
|
||||
"Do": "merge",
|
||||
"merge_title_field": format!("Merge PR #{}", pr_number),
|
||||
"merge_message_field": "",
|
||||
}))
|
||||
.send_json(merge_request_body(pr_number, &validated_sha))
|
||||
.map_err(|e| api_err("合并 PR 失败", e))?;
|
||||
|
||||
Ok(format!("PR #{pr_number} 已成功合并({owner}/{repo_name})"))
|
||||
serde_json::to_string_pretty(&serde_json::json!({
|
||||
"merged": true,
|
||||
"project_id": project_id,
|
||||
"repository": format!("{owner}/{repo_name}"),
|
||||
"pr_number": pr_number,
|
||||
"head_sha": validated_sha,
|
||||
"ci_state": ci.state,
|
||||
"review_state": review.state,
|
||||
}))
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
// ── MCP 工具:一键建仓库 + 配置 remote ───────────────────────────────────────
|
||||
@ -1891,4 +2001,116 @@ mod tests {
|
||||
);
|
||||
assert!(select_open_pull_request(&pulls, "main").is_none());
|
||||
}
|
||||
|
||||
fn merge_gate_fixture() -> (serde_json::Value, DeliverySignal, DeliverySignal) {
|
||||
let sha = "0123456789abcdef0123456789abcdef01234567";
|
||||
let pull_request = serde_json::json!({
|
||||
"state": "open",
|
||||
"merged": false,
|
||||
"mergeable": true,
|
||||
"head": { "sha": sha }
|
||||
});
|
||||
let ci = DeliverySignal::new(
|
||||
"passed",
|
||||
"gitea",
|
||||
"所有最新 CI context 均成功",
|
||||
serde_json::json!({ "target_sha": sha, "contexts": [] }),
|
||||
);
|
||||
let review = DeliverySignal::new(
|
||||
"pending",
|
||||
"gitea",
|
||||
"尚无有效 approve/reject review",
|
||||
serde_json::json!({ "reviewers": [] }),
|
||||
);
|
||||
(pull_request, ci, review)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_gate_requires_explicit_confirmation_and_full_sha() {
|
||||
let (pull_request, ci, review) = merge_gate_fixture();
|
||||
let sha = pull_request["head"]["sha"].as_str().unwrap();
|
||||
assert!(
|
||||
validate_merge_preconditions(false, sha, &pull_request, &ci, &review)
|
||||
.unwrap_err()
|
||||
.contains("明确确认")
|
||||
);
|
||||
assert!(
|
||||
validate_merge_preconditions(true, "01234567", &pull_request, &ci, &review)
|
||||
.unwrap_err()
|
||||
.contains("完整 40 位")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_gate_rejects_sha_drift_and_non_open_pr() {
|
||||
let (mut pull_request, ci, review) = merge_gate_fixture();
|
||||
let different_sha = "fedcba9876543210fedcba9876543210fedcba98";
|
||||
assert!(
|
||||
validate_merge_preconditions(true, different_sha, &pull_request, &ci, &review,)
|
||||
.unwrap_err()
|
||||
.contains("SHA 已变化")
|
||||
);
|
||||
|
||||
pull_request["state"] = serde_json::json!("closed");
|
||||
let original_sha = "0123456789abcdef0123456789abcdef01234567";
|
||||
assert!(
|
||||
validate_merge_preconditions(true, original_sha, &pull_request, &ci, &review,)
|
||||
.unwrap_err()
|
||||
.contains("不可合并")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_gate_rejects_unready_ci_and_blocking_review() {
|
||||
let (pull_request, mut ci, mut review) = merge_gate_fixture();
|
||||
let sha = pull_request["head"]["sha"].as_str().unwrap();
|
||||
|
||||
for state in ["pending", "failed", "unavailable"] {
|
||||
ci.state = state.to_string();
|
||||
assert!(
|
||||
validate_merge_preconditions(true, sha, &pull_request, &ci, &review)
|
||||
.unwrap_err()
|
||||
.contains("CI 门禁未通过")
|
||||
);
|
||||
}
|
||||
|
||||
ci.state = "passed".to_string();
|
||||
review.state = "changes_requested".to_string();
|
||||
assert!(
|
||||
validate_merge_preconditions(true, sha, &pull_request, &ci, &review)
|
||||
.unwrap_err()
|
||||
.contains("请求修改")
|
||||
);
|
||||
|
||||
review.state = "unavailable".to_string();
|
||||
assert!(
|
||||
validate_merge_preconditions(true, sha, &pull_request, &ci, &review)
|
||||
.unwrap_err()
|
||||
.contains("不可用或未知")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_gate_allows_current_sha_with_passed_ci_and_no_blocking_review() {
|
||||
let (pull_request, ci, mut review) = merge_gate_fixture();
|
||||
let sha = pull_request["head"]["sha"].as_str().unwrap();
|
||||
assert_eq!(
|
||||
validate_merge_preconditions(true, sha, &pull_request, &ci, &review).unwrap(),
|
||||
sha
|
||||
);
|
||||
|
||||
review.state = "approved".to_string();
|
||||
assert_eq!(
|
||||
validate_merge_preconditions(true, sha, &pull_request, &ci, &review).unwrap(),
|
||||
sha
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_request_body_binds_the_confirmed_head_sha() {
|
||||
let sha = "0123456789abcdef0123456789abcdef01234567";
|
||||
let body = merge_request_body(13, sha);
|
||||
assert_eq!(body["Do"], "merge");
|
||||
assert_eq!(body["head_commit_id"], sha);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ const TMPL_CAPABILITY_PACKS: &str =
|
||||
const TMPL_MISE: &str = include_str!("../../resources/onboarding/mise.toml.tmpl");
|
||||
const PROJECT_CONTEXT_SCHEMA_VERSION: u64 = 1;
|
||||
const CAPABILITY_PACK_SCHEMA_VERSION: u64 = 1;
|
||||
const BASE_ONBOARDING_PACK_VERSION: &str = "1.2.0";
|
||||
const BASE_ONBOARDING_PACK_VERSION: &str = "1.3.0";
|
||||
const TOOLCHAIN_PACK_VERSION: &str = "1.0.0";
|
||||
|
||||
/// 单个模板的分发结果。
|
||||
|
||||
@ -221,14 +221,16 @@ pub fn tools_list_result() -> Value {
|
||||
},
|
||||
{
|
||||
"name": "merge_pull_request",
|
||||
"description": "合并项目的指定 PR。建议在 get_ci_status 确认通过后再调用。等价于 Gitea UI「合并 PR」按钮。",
|
||||
"description": "在用户明确确认后合并指定 PR。能力本体会重新读取 PR、head SHA、CI 与 review;SHA 漂移、CI 未通过、changes requested、PR 非 open 或 confirmed=false 时拒绝执行。",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project_id": { "type": "string", "description": "项目 workspace ID(来自 list_group_projects 的返回结果)" },
|
||||
"pr_number": { "type": "integer", "description": "PR 编号(来自 create_pull_request 的返回结果或 Gitea UI)" }
|
||||
"pr_number": { "type": "integer", "description": "PR 编号(来自 create_pull_request 的返回结果或 Gitea UI)" },
|
||||
"expected_head_sha": { "type": "string", "description": "用户确认时看到的完整 40 位 PR head SHA;远端变化后旧确认自动失效" },
|
||||
"confirmed": { "type": "boolean", "description": "当前用户是否已明确确认合并该 PR 与 expected_head_sha" }
|
||||
},
|
||||
"required": ["project_id", "pr_number"]
|
||||
"required": ["project_id", "pr_number", "expected_head_sha", "confirmed"]
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -506,10 +508,34 @@ pub async fn tools_call(params: Option<&Value>) -> Value {
|
||||
"merge_pull_request" => {
|
||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let pr_num = args.get("pr_number").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||
let expected_head_sha = args
|
||||
.get("expected_head_sha")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let confirmed = args
|
||||
.get("confirmed")
|
||||
.and_then(|value| value.as_bool())
|
||||
.unwrap_or(false);
|
||||
if pid.is_empty() {
|
||||
return Err("project_id 不能为空".to_string());
|
||||
}
|
||||
if pr_num <= 0 {
|
||||
return Err("pr_number 必须为正整数".to_string());
|
||||
}
|
||||
crate::commands::gitea::gitea_api_merge_pull_request(pid, pr_num)
|
||||
if !confirmed {
|
||||
return Err("合并需要当前用户明确确认(confirmed=true)".to_string());
|
||||
}
|
||||
if expected_head_sha.is_empty() {
|
||||
return Err("expected_head_sha 不能为空".to_string());
|
||||
}
|
||||
let conn = db::pool().get().map_err(|e| e.to_string())?;
|
||||
crate::commands::gitea::gitea_api_merge_pull_request(
|
||||
&conn,
|
||||
pid,
|
||||
pr_num,
|
||||
expected_head_sha,
|
||||
confirmed,
|
||||
)
|
||||
}
|
||||
"get_pr_events" => {
|
||||
let pid = args.get("project_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
@ -1646,6 +1672,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_pull_request_schema_requires_confirmation_and_expected_sha() {
|
||||
let result = tools_list_result();
|
||||
let tools = result["tools"].as_array().expect("tools 应为数组");
|
||||
let merge = tools
|
||||
.iter()
|
||||
.find(|tool| tool["name"] == "merge_pull_request")
|
||||
.expect("缺少 merge_pull_request");
|
||||
let required: Vec<&str> = merge["inputSchema"]["required"]
|
||||
.as_array()
|
||||
.expect("required 应为数组")
|
||||
.iter()
|
||||
.filter_map(|value| value.as_str())
|
||||
.collect();
|
||||
assert!(required.contains(&"project_id"));
|
||||
assert!(required.contains(&"pr_number"));
|
||||
assert!(required.contains(&"expected_head_sha"));
|
||||
assert!(required.contains(&"confirmed"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn merge_pull_request_rejects_missing_confirmation_before_external_io() {
|
||||
let result = tools_call(Some(&json!({
|
||||
"name": "merge_pull_request",
|
||||
"arguments": {
|
||||
"project_id": "project-1",
|
||||
"pr_number": 13,
|
||||
"expected_head_sha": "0123456789abcdef0123456789abcdef01234567"
|
||||
}
|
||||
})))
|
||||
.await;
|
||||
assert_eq!(result["isError"], true);
|
||||
assert!(result["content"][0]["text"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("明确确认"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delivery_status_tool_requires_project_id() {
|
||||
let result = tools_call(Some(&json!({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user