合并原分支最新修复
合入 P4 的 Fast GDD 识别顺序修复。 合入 P5 的委派栅栏 detail 等价性锁定。
This commit is contained in:
@@ -1729,3 +1729,97 @@ pub(crate) fn acquire_game_creator_agent_provider_plan_project_write_lock_with_w
|
||||
) -> Result<ProjectWriteLock, String> {
|
||||
acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, command_id)
|
||||
}
|
||||
|
||||
/// M1C-1 把 `userRevisionPending` 同时加进了 `StaticDelegateCompletionBarrier::detail()`
|
||||
/// 的输出和本模块的三个解析门,两边靠一个字段名字符串隔空对齐,中间没有共享 schema。
|
||||
///
|
||||
/// 当时生产侧只有 `delegation.rs` 一条 `detail().contains("userRevisionPending=1")`,
|
||||
/// 解析侧一条测试都没有,两者之间也没有任何东西相连。字段名一改,生产侧那条照过,而这
|
||||
/// 三个门会静默返回 false——父 run 于是越过用户修订边界收束。同样,往 `has_waiting()`
|
||||
/// 里加一个计数而忘了加进解析器(或反之),也没有任何用例会报警。
|
||||
///
|
||||
/// 所以这里锁的不是拼写,是**等价关系**:对七个计数的全部组合,解析门的判定必须与
|
||||
/// barrier 自己的语义谓词逐一相等。
|
||||
#[cfg(test)]
|
||||
mod static_delegate_barrier_detail_gate_tests {
|
||||
use super::*;
|
||||
use crate::delegation::StaticDelegateCompletionBarrier;
|
||||
|
||||
fn assert_gates_agree_with_barrier(barrier: StaticDelegateCompletionBarrier) {
|
||||
let detail = barrier.detail();
|
||||
assert_eq!(
|
||||
static_delegate_barrier_has_waiting_deliveries(&detail),
|
||||
barrier.has_waiting(),
|
||||
"has_waiting() 与 detail 解析必须等价:{barrier:?}\ndetail={detail}"
|
||||
);
|
||||
assert_eq!(
|
||||
static_delegate_barrier_requires_repair(&detail),
|
||||
barrier.repair_required_count > 0,
|
||||
"repairRequired 往返失真:{barrier:?}\ndetail={detail}"
|
||||
);
|
||||
assert_eq!(
|
||||
static_delegate_barrier_requires_user_revision(&detail),
|
||||
barrier.user_revision_pending_count > 0,
|
||||
"userRevisionPending 往返失真:{barrier:?}\ndetail={detail}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn barrier_detail_round_trips_through_every_gate_for_all_count_combinations() {
|
||||
let mut checked = 0usize;
|
||||
for bits in 0u32..(1 << 7) {
|
||||
let present = |index: u32| usize::from(bits & (1 << index) != 0);
|
||||
assert_gates_agree_with_barrier(StaticDelegateCompletionBarrier {
|
||||
waiting_count: present(0),
|
||||
ready_unclaimed_count: present(1),
|
||||
unobserved_claim_count: present(2),
|
||||
repair_required_count: present(3),
|
||||
user_input_required_count: present(4),
|
||||
user_revision_pending_count: present(5),
|
||||
unknown_contract_status_count: present(6),
|
||||
});
|
||||
checked += 1;
|
||||
}
|
||||
assert_eq!(checked, 128, "必须覆盖七个计数的全部 0/1 组合");
|
||||
}
|
||||
|
||||
/// 门读的是计数而不是「等于 1」:`detail()` 里出现多位数时不能失配。
|
||||
#[test]
|
||||
fn barrier_detail_gates_read_multi_digit_counts() {
|
||||
assert_gates_agree_with_barrier(StaticDelegateCompletionBarrier {
|
||||
waiting_count: 12,
|
||||
ready_unclaimed_count: 34,
|
||||
unobserved_claim_count: 56,
|
||||
repair_required_count: 78,
|
||||
user_input_required_count: 90,
|
||||
user_revision_pending_count: 123,
|
||||
unknown_contract_status_count: 456,
|
||||
});
|
||||
}
|
||||
|
||||
/// 没有任何键是另一个键的前缀——否则 `strip_prefix` 会读到隔壁字段的值。
|
||||
/// 这条是给未来改名加的护栏:等价关系测试能抓到读错值,但抓不到「读对了值却
|
||||
/// 是因为两个键碰巧不冲突」这层前提何时被打破。
|
||||
#[test]
|
||||
fn barrier_detail_keys_are_prefix_free() {
|
||||
let detail = StaticDelegateCompletionBarrier::default().detail();
|
||||
let keys = detail
|
||||
.split_whitespace()
|
||||
.filter_map(|part| part.split_once('=').map(|(key, _)| key))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(
|
||||
keys.len() >= 7,
|
||||
"detail 必须仍以 key=value 形式给出全部计数:{detail}"
|
||||
);
|
||||
for (index, key) in keys.iter().enumerate() {
|
||||
for (other_index, other) in keys.iter().enumerate() {
|
||||
if index != other_index {
|
||||
assert!(
|
||||
!other.starts_with(key),
|
||||
"detail 键 `{key}` 是 `{other}` 的前缀,strip_prefix 会读错字段"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+91
-1
@@ -1579,6 +1579,22 @@ fn plan_gdd_session_matches_receipt(
|
||||
})
|
||||
}
|
||||
|
||||
// 这道门用 `PLAN_GDD_APPROVAL_SOURCE` 识别策划根,而随后的
|
||||
// `validate_project_supervisor_plan_root_binding_at` 用 `AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE`
|
||||
// 复核同一条绑定。两个常量分别定义在 planning_storage 与 runtime_driver,值必须相同:
|
||||
// 一旦分叉,每个策划根都会先通过识别、再被复核拒掉,全部塌成 needs-reconciliation,
|
||||
// 而且没有任何测试会直接指向这个原因。钉成编译期条件。
|
||||
const _: () = {
|
||||
let identified = PLAN_GDD_APPROVAL_SOURCE.as_bytes();
|
||||
let validated = AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE.as_bytes();
|
||||
assert!(identified.len() == validated.len());
|
||||
let mut index = 0;
|
||||
while index < identified.len() {
|
||||
assert!(identified[index] == validated[index]);
|
||||
index += 1;
|
||||
}
|
||||
};
|
||||
|
||||
fn plan_root_completion_identity_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -1587,8 +1603,15 @@ fn plan_root_completion_identity_at(
|
||||
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
// 识别只看 run 自己那条绑定记录。链走版本会遍历并校验整条祖先链;把它排在识别
|
||||
// 前面,等于让任何祖先绑定的毛病都先变成 Err,而调用方会把 Err 一律翻成
|
||||
// needs-reconciliation——扣在一个下一行本来就会被判「不是策划根」的 run 头上,让它
|
||||
// 再也收束不了。被误伤的只可能是非策划 run:真正的策划根无父(下面
|
||||
// `validate_project_supervisor_plan_root_binding_at` 强制 parent 必须为空),链走
|
||||
// 对它本就是空转。两个入口对同一个 (agent, run) 返回的绑定值完全相同,差别仅在于
|
||||
// 是否顺带校验祖先链,所以识别判据一字未变。
|
||||
let Some(binding) =
|
||||
read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)?
|
||||
read_game_creator_agent_runtime_run_profile_binding_once(root, agent_id, run_id.trim())?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -1597,6 +1620,8 @@ fn plan_root_completion_identity_at(
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
// 确认是策划根之后才走完整父链:严格度一点没降——
|
||||
// `validate_project_supervisor_plan_root_binding_at` 内部读的就是链走版本。
|
||||
let binding = validate_project_supervisor_plan_root_binding_at(root, agent_id, run_id)?;
|
||||
let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state;
|
||||
if runtime.run_id != run_id
|
||||
@@ -2053,4 +2078,69 @@ mod tests {
|
||||
);
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
|
||||
/// 祖先绑定坏掉不能让一个跟立项策划无关的 supervisor run 被判成「策划根身份不明」。
|
||||
///
|
||||
/// `plan_root_completion_identity_at` 读绑定走的是会遍历完整父链的入口,而识别
|
||||
/// Fast GDD 只看 run 自己那条记录的 source/profile。链走排在识别前面,于是任何
|
||||
/// 祖先绑定的问题都先变成 Err,再被完成门统一翻成 needs-reconciliation——扣在一个
|
||||
/// 下一行就会被判为「不是策划根」的 run 头上,让它再也收束不了。
|
||||
///
|
||||
/// 真正的策划根没有父(`validate_project_supervisor_plan_root_binding_at` 强制
|
||||
/// parent 必须为空),所以链走对它本来就是空转;会被链走误伤的只可能是非策划 run。
|
||||
#[test]
|
||||
fn broken_ancestor_binding_must_not_make_a_non_plan_supervisor_run_a_plan_root_suspect() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-plan-root-identity-{}",
|
||||
uuid::Uuid::new_v4().simple()
|
||||
));
|
||||
init_local_game_project_at(&root, "project-test-plan-root", "plan root identity")
|
||||
.expect("init project");
|
||||
|
||||
let parent_run_id = "p4-isolated-join-parent";
|
||||
let child_run_id = "p4-isolated-join-child";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
None,
|
||||
)
|
||||
.expect("bind ancestor run");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
child_run_id,
|
||||
AGENT_RUNTIME_ISOLATED_JOIN_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
|
||||
parent_run_id: Some(parent_run_id.to_string()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
.expect("bind isolated-join child run");
|
||||
|
||||
// 破坏祖先:子记录仍带着 parentBindingFingerprint,父绑定却没了,正好命中
|
||||
// 父链遍历里的「父绑定缺失」。子 run 自己那条记录始终是完好可读的。
|
||||
fs::remove_file(game_creator_agent_runtime_run_profile_binding_path(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
))
|
||||
.expect("remove ancestor binding");
|
||||
|
||||
let blocker = plan_gdd_typed_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
child_run_id,
|
||||
);
|
||||
assert!(
|
||||
blocker.is_none(),
|
||||
"isolated-join run 的 source 不是立项策划,完成门不该拦它,更不该说它策划根身份不明:{:?}",
|
||||
blocker.map(|blocker| blocker.kind)
|
||||
);
|
||||
let _ = fs::remove_dir_all(root);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user