清理策划运行时无锁包装入口
删除无锁 tool-plan request builder wrapper 删除 approval pending 与 submit input parser wrapper 测试改为显式持锁调用 locked 实现及底层严格解析器
This commit is contained in:
+82
-72
@@ -715,36 +715,6 @@ fn build_game_creator_agent_background_tool_plan_request_at(
|
||||
))
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
run_id: &str,
|
||||
task: &str,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
loop_index: usize,
|
||||
) -> Result<
|
||||
(
|
||||
GameCreatorLlmConfig,
|
||||
String,
|
||||
LlmRunRequest,
|
||||
String,
|
||||
AgentRuntimeToolPlanRequestSnapshot,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
build_game_creator_agent_background_tool_plan_request_at(
|
||||
root,
|
||||
None,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
task,
|
||||
observations,
|
||||
loop_index,
|
||||
)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request_locked(
|
||||
root: &Path,
|
||||
project_lock: &ProjectWriteLock,
|
||||
@@ -972,17 +942,20 @@ mod tests {
|
||||
use crate::{update_manifest_task_status_at, GameCreationAppTaskStatus};
|
||||
|
||||
use super::{
|
||||
acquire_game_creator_agent_provider_plan_project_write_lock_with_wait,
|
||||
agent_runtime_root_source_at, bind_game_creator_agent_runtime_run_profile_at,
|
||||
build_game_creator_agent_background_final_reply_request,
|
||||
build_game_creator_agent_background_tool_plan_request,
|
||||
build_game_creator_agent_background_tool_plan_request_locked,
|
||||
game_creator_agent_context_preload_notice,
|
||||
game_creator_agent_runtime_run_profile_binding_path,
|
||||
game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at,
|
||||
new_game_creation_app_seed_tasks, provider_command_exec_contract,
|
||||
provider_command_start_contract, render_relaxed_autonomous_manifest_ready_task_background_prompt,
|
||||
provider_command_start_contract,
|
||||
render_relaxed_autonomous_manifest_ready_task_background_prompt,
|
||||
required_runtime_prompt_section, start_game_creator_agent_runtime_task_at,
|
||||
AgentRuntimeGoalContractAcceptanceNodeDraft, AgentRuntimeGoalContractDraft,
|
||||
AgentRuntimeTaskLink, AgentRuntimeToolObservation, AgentRuntimeToolPlan,
|
||||
AgentRuntimeToolPlanRequestSnapshot, GameCreatorLlmConfig,
|
||||
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT,
|
||||
AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
|
||||
AGENT_RUNTIME_PLAN_AUTONOMOUS_PROFILE_UNSUPPORTED_KIND,
|
||||
@@ -996,6 +969,40 @@ mod tests {
|
||||
RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
|
||||
};
|
||||
|
||||
fn build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
root: &std::path::Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
run_id: &str,
|
||||
task: &str,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
loop_index: usize,
|
||||
) -> Result<
|
||||
(
|
||||
GameCreatorLlmConfig,
|
||||
String,
|
||||
platform_llm::LlmRunRequest,
|
||||
String,
|
||||
AgentRuntimeToolPlanRequestSnapshot,
|
||||
),
|
||||
String,
|
||||
> {
|
||||
let lock = acquire_game_creator_agent_provider_plan_project_write_lock_with_wait(
|
||||
root,
|
||||
"test.provider_request.build.tool_plan",
|
||||
)?;
|
||||
build_game_creator_agent_background_tool_plan_request_locked(
|
||||
root,
|
||||
&lock,
|
||||
agent_id,
|
||||
session_id,
|
||||
run_id,
|
||||
task,
|
||||
observations,
|
||||
loop_index,
|
||||
)
|
||||
}
|
||||
|
||||
fn native_input_required_fields(
|
||||
request: &platform_llm::LlmRunRequest,
|
||||
tool: &str,
|
||||
@@ -1066,7 +1073,7 @@ mod tests {
|
||||
summary: "结构化计划更新被 Runtime 拒绝".to_string(),
|
||||
detail: Some("计划状态回退".to_string()),
|
||||
};
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -1155,16 +1162,17 @@ mod tests {
|
||||
// Relaxed orchestration does not convert an idle planning counter into
|
||||
// a tool-removal gate; the Provider remains free to choose its next
|
||||
// action.
|
||||
let (_, _, baseline, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&[],
|
||||
1,
|
||||
)
|
||||
.expect("build baseline request");
|
||||
let (_, _, baseline, _, _) =
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
&state.run_id,
|
||||
&state.current_task,
|
||||
&[],
|
||||
1,
|
||||
)
|
||||
.expect("build baseline request");
|
||||
assert!(baseline
|
||||
.function_tools
|
||||
.iter()
|
||||
@@ -1175,7 +1183,7 @@ mod tests {
|
||||
crate::agent::write_game_creator_agent_runtime_state(&root, &idle_state)
|
||||
.expect("persist idle rounds");
|
||||
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -1256,7 +1264,7 @@ mod tests {
|
||||
vec!["交付当前 manifest task".to_string()],
|
||||
)
|
||||
.expect("start autonomous ready child task");
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
agent_id,
|
||||
&state.session_id,
|
||||
@@ -1506,7 +1514,7 @@ mod tests {
|
||||
vec!["冻结 Goal Contract".to_string()],
|
||||
)
|
||||
.expect("start trusted root");
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, request, _, _) = build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -1566,7 +1574,7 @@ mod tests {
|
||||
)
|
||||
.expect("start plan root");
|
||||
|
||||
let (_, _, first, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, first, _, _) = build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -1633,7 +1641,7 @@ mod tests {
|
||||
},
|
||||
)
|
||||
.expect("freeze plan contract");
|
||||
let (_, _, later, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
let (_, _, later, _, _) = build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
&state.agent_id,
|
||||
&state.session_id,
|
||||
@@ -1719,7 +1727,7 @@ mod tests {
|
||||
)
|
||||
.expect("start supervisor runtime state");
|
||||
let (_, _, supervisor_request, _, _) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&supervisor_state.session_id,
|
||||
@@ -1865,16 +1873,17 @@ mod tests {
|
||||
vec!["核对普通说明".to_string()],
|
||||
)
|
||||
.expect("start ordinary runtime state");
|
||||
let (_, _, ordinary_request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
"code-prototype",
|
||||
&ordinary_state.session_id,
|
||||
&ordinary_state.run_id,
|
||||
&ordinary_state.current_task,
|
||||
&[],
|
||||
0,
|
||||
)
|
||||
.expect("build ordinary planning request");
|
||||
let (_, _, ordinary_request, _, _) =
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
"code-prototype",
|
||||
&ordinary_state.session_id,
|
||||
&ordinary_state.run_id,
|
||||
&ordinary_state.current_task,
|
||||
&[],
|
||||
0,
|
||||
)
|
||||
.expect("build ordinary planning request");
|
||||
assert!(ordinary_request.messages[0]
|
||||
.content
|
||||
.contains("你正在使用 Genarrative AI 游戏创作多智能体 Runtime"));
|
||||
@@ -1989,16 +1998,17 @@ mod tests {
|
||||
vec!["读取需求并准备澄清".to_string()],
|
||||
)
|
||||
.expect("start planning child");
|
||||
let (_, _, planning_request, _, _) = build_game_creator_agent_background_tool_plan_request(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
&planning_state.session_id,
|
||||
&planning_state.run_id,
|
||||
&planning_state.current_task,
|
||||
&[],
|
||||
0,
|
||||
)
|
||||
.expect("build planning request");
|
||||
let (_, _, planning_request, _, _) =
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
&planning_state.session_id,
|
||||
&planning_state.run_id,
|
||||
&planning_state.current_task,
|
||||
&[],
|
||||
0,
|
||||
)
|
||||
.expect("build planning request");
|
||||
let planning_system_prompt = &planning_request.messages[0].content;
|
||||
let planning_brief_marker = "你是“立项策划 Agent”(`agentId=project-planning`)";
|
||||
assert!(planning_system_prompt.contains(planning_brief_marker));
|
||||
@@ -2074,7 +2084,7 @@ mod tests {
|
||||
)
|
||||
.expect("start supervisor");
|
||||
let (_, _, supervisor_request, _, _) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&supervisor_state.session_id,
|
||||
@@ -2183,7 +2193,7 @@ mod tests {
|
||||
},
|
||||
];
|
||||
let (_, _, request, _, request_snapshot) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&state.session_id,
|
||||
@@ -2209,7 +2219,7 @@ mod tests {
|
||||
),
|
||||
});
|
||||
let (_, _, _, _, settled_request_snapshot) =
|
||||
build_game_creator_agent_background_tool_plan_request(
|
||||
build_game_creator_agent_background_tool_plan_request_for_test(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&state.session_id,
|
||||
|
||||
@@ -216,23 +216,6 @@ pub(crate) fn pending_matches_receipt(
|
||||
/// Construct the independent planning pending projection after an external
|
||||
/// acceptance gate has succeeded. M1C-1 does not decide whether the gate
|
||||
/// passed; the caller must supply that fact and the exact GDD identity.
|
||||
pub(crate) fn create_plan_gdd_approval_pending_at(
|
||||
root: &Path,
|
||||
gdd: &PlanGddV1,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
if !crate::config::game_creator_planning_capability_enabled()
|
||||
.map_err(|error| approval_error("PLAN_CAPABILITY_DISABLED", error))?
|
||||
{
|
||||
return Err(approval_error(
|
||||
"PLAN_CAPABILITY_DISABLED",
|
||||
"立项策划能力当前已停用",
|
||||
));
|
||||
}
|
||||
let _lock = acquire_project_write_lock(root, "planning.approval-pending.create")
|
||||
.map_err(|error| approval_error("PLAN_DURABILITY_FAILED", error))?;
|
||||
create_plan_gdd_approval_pending_locked(root, gdd)
|
||||
}
|
||||
|
||||
pub(crate) fn create_plan_gdd_approval_pending_locked(
|
||||
root: &Path,
|
||||
gdd: &PlanGddV1,
|
||||
|
||||
+12
-23
@@ -2712,18 +2712,6 @@ pub(crate) fn canonical_plan_submit_gdd_input_bytes(
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_plan_submit_gdd_input_bytes(
|
||||
bytes: &[u8],
|
||||
) -> Result<PlanSubmitGddInputV1, PlanningStorageError> {
|
||||
let value = parse_strict_canonical::<PlanSubmitGddInputV1>(
|
||||
bytes,
|
||||
"plan.submit_gdd input",
|
||||
PLAN_GDD_MAX_BYTES,
|
||||
)?;
|
||||
validate_plan_submit_gdd_input(&value)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_plan_gdd_chain(values: &[PlanGddV1]) -> Result<(), PlanningStorageError> {
|
||||
if values.len() > PLAN_MAX_VERSIONS as usize {
|
||||
return Err(PlanningStorageError::new(
|
||||
@@ -5067,22 +5055,23 @@ mod tests {
|
||||
fn submit_input_has_strict_canonical_parser_and_runtime_field_boundary() {
|
||||
let value = golden_submit_input();
|
||||
let bytes = canonical_plan_submit_gdd_input_bytes(&value).expect("submit input bytes");
|
||||
assert_eq!(
|
||||
parse_plan_submit_gdd_input_bytes(&bytes).expect("parse input"),
|
||||
value
|
||||
);
|
||||
let parse = |bytes: &[u8]| -> Result<PlanSubmitGddInputV1, PlanningStorageError> {
|
||||
let value = parse_strict_canonical::<PlanSubmitGddInputV1>(
|
||||
bytes,
|
||||
"plan.submit_gdd input",
|
||||
PLAN_GDD_MAX_BYTES,
|
||||
)?;
|
||||
validate_plan_submit_gdd_input(&value)?;
|
||||
Ok(value)
|
||||
};
|
||||
assert_eq!(parse(&bytes).expect("parse input"), value);
|
||||
let mut newline = bytes.clone();
|
||||
newline.push(b'\n');
|
||||
assert_eq!(
|
||||
parse_plan_submit_gdd_input_bytes(&newline)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_NON_CANONICAL_BYTES"
|
||||
);
|
||||
assert_eq!(parse(&newline).unwrap_err().code(), "PLAN_NON_CANONICAL_BYTES");
|
||||
let mut object = serde_json::from_slice::<serde_json::Value>(&bytes).expect("input json");
|
||||
object["projectId"] = serde_json::Value::String("forged-project".to_string());
|
||||
let forged = serde_json::to_vec(&object).expect("forged input");
|
||||
assert!(parse_plan_submit_gdd_input_bytes(&forged).is_err());
|
||||
assert!(parse(&forged).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+23
-13
@@ -1896,6 +1896,15 @@ mod tests {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn create_plan_gdd_approval_pending_for_test(
|
||||
root: &std::path::Path,
|
||||
gdd: &PlanGddV1,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "test.planning.approval-pending.create")
|
||||
.map_err(|error| PlanningStorageError::new("PLAN_DURABILITY_FAILED", error))?;
|
||||
create_plan_gdd_approval_pending_locked(root, gdd)
|
||||
}
|
||||
|
||||
fn valid_input() -> PlanSubmitGddInputV1 {
|
||||
PlanSubmitGddInputV1 {
|
||||
schema_version: PLAN_SUBMIT_INPUT_SCHEMA.to_string(),
|
||||
@@ -3628,7 +3637,7 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
let decision_input = approval_input(
|
||||
&gdd,
|
||||
"approve",
|
||||
@@ -3654,13 +3663,13 @@ mod tests {
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
let pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read approval pending")
|
||||
.expect("pending exists");
|
||||
assert_eq!(pending.status, "awaiting_decision");
|
||||
// Recreating the exact card is an idempotent replay.
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("replay approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("replay approval pending");
|
||||
|
||||
let mut forged_next = gdd.clone();
|
||||
forged_next.version = 2;
|
||||
@@ -3669,7 +3678,7 @@ mod tests {
|
||||
"gdd-approval-00000000-0000-4000-8000-000000000003".to_string();
|
||||
forged_next.action_fingerprint = "4".repeat(64);
|
||||
forged_next.fingerprint = plan_gdd_fingerprint(&forged_next).expect("next fingerprint");
|
||||
let stale = create_plan_gdd_approval_pending_at(&root, &forged_next)
|
||||
let stale = create_plan_gdd_approval_pending_for_test(&root, &forged_next)
|
||||
.expect_err("a non-latest GDD cannot receive an approval card");
|
||||
assert_eq!(stale.code(), "PLAN_STALE_APPROVAL");
|
||||
|
||||
@@ -3684,7 +3693,7 @@ mod tests {
|
||||
)
|
||||
.expect("commit approval receipt");
|
||||
assert_eq!(decision.outcome, "committed");
|
||||
let after_receipt = create_plan_gdd_approval_pending_at(&root, &gdd)
|
||||
let after_receipt = create_plan_gdd_approval_pending_for_test(&root, &gdd)
|
||||
.expect_err("a receipt must close awaiting_decision recreation");
|
||||
assert_eq!(after_receipt.code(), "PLAN_STALE_APPROVAL");
|
||||
cleanup_fixture(root);
|
||||
@@ -3698,7 +3707,7 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
|
||||
let task_path =
|
||||
game_creator_agent_runtime_task_path(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID);
|
||||
@@ -3738,7 +3747,7 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
let _config_guard = crate::tests::write_test_local_config(
|
||||
r#"{"planning":{"capabilityEnabled":false}}"#.to_string(),
|
||||
);
|
||||
@@ -3802,7 +3811,7 @@ mod tests {
|
||||
assert_eq!(blocker.status, "needs-reconciliation");
|
||||
assert!(blocker.summary.contains("审批 pending"));
|
||||
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
let blocker = plan_gdd_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
@@ -3955,7 +3964,7 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
@@ -4089,7 +4098,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_revision_comment_reaches_the_supervisor_conversation_once() {
|
||||
let (root, gdd, _root_runtime) = acceptance_gate_fixture(true);
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
@@ -4143,7 +4152,7 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
@@ -4230,7 +4239,8 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd)
|
||||
.expect("create approval pending");
|
||||
let first_input = approval_input(
|
||||
&gdd,
|
||||
action,
|
||||
@@ -4461,7 +4471,7 @@ mod tests {
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
create_plan_gdd_approval_pending_for_test(&root, &gdd).expect("create approval pending");
|
||||
let decision_input = approval_input(
|
||||
&gdd,
|
||||
"approve",
|
||||
|
||||
Reference in New Issue
Block a user