P2:reject 不是死路,锁住真正的约束
原判定「reject 后同一 session 无法再次提交,因为 CAS 门只放行 collecting / revision_requested」不成立。提交门在 phase 判据之前先要求 session 的 activeRunId 等于 当前策划子 run,而 schema 不变量禁止 rejected / revision_requested / approved / awaiting_* / recovery_required 保留 activeRunId——两者互斥,终态 phase 根本到不了那条 phase 判据。把 rejected 加进允许集只会多一个不可达分支,续跑仍然起不来;连既有的 revision_requested 也已经是不可达的。 真正决定 reject 能否重做的是 M1C-2b 的 continuation 起点 writer:技术方案 §8.6 要求它 「以新 activeRunId 写 revision+1 successor」,而唯一能同时带 activeRunId 又过提交门的 phase 只有 collecting。本提交不改行为,只把这条因果写进门旁注释,并加一条回归锁住它: 终态 session 挂 activeRunId 在指纹阶段就被 PLAN_INVALID_SCHEMA 拒绝,而落回 collecting 的 continuation 能过提交门。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1252,6 +1252,14 @@ fn validate_current_session_cas(
|
||||
"plan.submit_gdd 必须绑定当前活跃策划子 Run",
|
||||
));
|
||||
}
|
||||
// 这条 phase 判据实际只可能看到 `collecting`:上面的 activeRunId 判据要求
|
||||
// session 绑着当前策划子 run,而 schema 不变量禁止 `awaiting_user_input`、
|
||||
// `awaiting_gdd_approval`、`revision_requested`、`approved`、`rejected`、
|
||||
// `recovery_required` 保留 activeRunId(planning_storage.rs 的
|
||||
// 「session 进入审批/终态/recovery_required 后不得保留 activeRunId」)。
|
||||
// 因此 revise/reject 之后能不能重做,不由这条门决定,而由 M1C-2b 的 continuation
|
||||
// 起点 writer 决定——它必须以新 activeRunId 写 revision+1 successor,phase 只能落回
|
||||
// `collecting`。这里保留 `revision_requested` 作为既有冗余,不再新增更多不可达分支。
|
||||
if !matches!(session.phase.as_str(), "collecting" | "revision_requested") {
|
||||
return Err(submit_error(
|
||||
"PLAN_PENDING_GDD_EXISTS",
|
||||
@@ -3535,6 +3543,66 @@ mod tests {
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
/// reject 之后能不能在同一 lineage 重做,**不由提交门的 phase 判据决定**。
|
||||
///
|
||||
/// 提交门要求 session 的 activeRunId 等于当前策划子 run,而 schema 不变量禁止
|
||||
/// `rejected`(以及 `revision_requested`、`approved`、`awaiting_*`)保留
|
||||
/// activeRunId——两者互斥,所以终态 phase 永远到不了那条 phase 判据。真正决定重做
|
||||
/// 能力的是 M1C-2b 的 continuation 起点 writer:技术方案 §8.6 要求它「以新
|
||||
/// activeRunId 写 revision+1 successor」,而唯一能同时带 activeRunId 又过提交门的
|
||||
/// phase 只有 `collecting`。
|
||||
///
|
||||
/// 这条测试把该约束锁住,免得日后有人以为「把 rejected 加进提交门允许集」就能让
|
||||
/// reject 重做——那只会多一个不可达分支,真正的续跑仍然起不来。
|
||||
#[test]
|
||||
fn rejected_session_needs_a_collecting_continuation_not_a_wider_submit_gate() {
|
||||
let (root, context, input) = submit_fixture();
|
||||
execute_plan_submit_gdd(&root, &context, &input).expect("submit v1");
|
||||
let gdd = read_plan_gdd_chain(&root)
|
||||
.expect("read submitted GDD")
|
||||
.pop()
|
||||
.expect("GDD exists");
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
decide_plan_gdd_at(
|
||||
&root,
|
||||
&approval_input(
|
||||
&gdd,
|
||||
"reject",
|
||||
"gdd-response-00000000-0000-4000-8000-000000000040",
|
||||
Some("当前方向需要重新梳理".to_string()),
|
||||
),
|
||||
)
|
||||
.expect("commit reject receipt");
|
||||
|
||||
let session = read_plan_session_with_recovery(&root)
|
||||
.expect("read session after reject")
|
||||
.expect("session exists after reject");
|
||||
assert_eq!(session.phase, "rejected");
|
||||
assert!(session.active_run_id.is_none());
|
||||
|
||||
// 终态 session 直接挂 activeRunId 连指纹都算不出来——schema 层就禁止。
|
||||
let mut forged = session.clone();
|
||||
forged.active_run_id = Some(context.created_by_run_id.clone());
|
||||
let error =
|
||||
plan_session_fingerprint(&forged).expect_err("终态 session 不得保留 activeRunId");
|
||||
assert_eq!(error.code(), "PLAN_INVALID_SCHEMA");
|
||||
|
||||
// continuation 起点把 phase 落回 collecting 之后,同一 lineage 才能继续提交。
|
||||
let mut continuation = session.clone();
|
||||
continuation.session_revision += 1;
|
||||
continuation.previous_fingerprint = Some(session.session_fingerprint.clone());
|
||||
continuation.phase = "collecting".to_string();
|
||||
continuation.active_run_id = Some(context.created_by_run_id.clone());
|
||||
continuation.session_fingerprint =
|
||||
plan_session_fingerprint(&continuation).expect("continuation session fp");
|
||||
let mut next_context = context.clone();
|
||||
next_context.source_session_revision = continuation.session_revision;
|
||||
next_context.source_session_fingerprint = continuation.session_fingerprint.clone();
|
||||
validate_current_session_cas(&continuation, &next_context, &input)
|
||||
.expect("reject 之后的 continuation 必须能提交同一 lineage 的下一版本");
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn approval_actions_rebuild_receipt_aware_index_and_are_idempotent() {
|
||||
for (action, comment, expected_status) in [
|
||||
|
||||
Reference in New Issue
Block a user