清理立项策划运行时死代码
删除未使用的 planning storage 加锁 wrapper 和未接入授权层 删除旧 provider binding 与 approval fingerprint 入口 修正测试 fixture 直接调用 _locked 核心实现 修复非 Unix 分支不可达返回
This commit is contained in:
@@ -2102,7 +2102,7 @@ mod pending_recovery_tests {
|
||||
updated_at_utc: "2026-08-14T00:00:00.000Z".to_string(),
|
||||
};
|
||||
session.session_fingerprint = plan_session_fingerprint(&session).expect("session fp");
|
||||
write_plan_session_atomic(root, &session).expect("write planning session");
|
||||
write_plan_session_atomic_locked(root, &session).expect("write planning session");
|
||||
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: PLAN_SUBMIT_GDD_TOOL.to_string(),
|
||||
|
||||
@@ -2341,8 +2341,13 @@ mod orphaned_external_generation_recovery_tests {
|
||||
)
|
||||
.expect("build committed GDD");
|
||||
let gdd_bytes = canonical_plan_gdd_bytes(&gdd).expect("canonical committed GDD");
|
||||
durable_create_json_no_replace(root, ".agent/planning/gdd.v1.json", &gdd_bytes, "GDD")
|
||||
.expect("persist committed GDD");
|
||||
durable_create_json_no_replace_locked(
|
||||
root,
|
||||
".agent/planning/gdd.v1.json",
|
||||
&gdd_bytes,
|
||||
"GDD",
|
||||
)
|
||||
.expect("persist committed GDD");
|
||||
assert_eq!(
|
||||
read_plan_gdd_chain(root)
|
||||
.expect("read committed GDD chain")
|
||||
@@ -2384,7 +2389,7 @@ mod orphaned_external_generation_recovery_tests {
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("恢复锚点同时缺失")));
|
||||
assert!(read_plan_gdd_approval_pending(root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(root)
|
||||
.expect("read planning approval pending")
|
||||
.is_none());
|
||||
let audit_count = read_agent_db_records_bounded(root, 1024 * 1024)
|
||||
@@ -2403,7 +2408,7 @@ mod orphaned_external_generation_recovery_tests {
|
||||
);
|
||||
let _ = resume_game_creator_agent_background_tasks_at(root)
|
||||
.expect("second recovery scan may surface the existing reconciliation state");
|
||||
assert!(read_plan_gdd_approval_pending(root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(root)
|
||||
.expect("reread planning approval pending")
|
||||
.is_none());
|
||||
let audit_count_after_second_scan = read_agent_db_records_bounded(root, 1024 * 1024)
|
||||
|
||||
+1
-1
@@ -619,7 +619,7 @@ mod tests {
|
||||
updated_at_utc: current_plan_timestamp_utc(),
|
||||
};
|
||||
session.session_fingerprint = plan_session_fingerprint(&session).expect("session fp");
|
||||
write_plan_session_atomic(&root, &session).expect("write usage session");
|
||||
write_plan_session_atomic_locked(&root, &session).expect("write usage session");
|
||||
UsageFixture {
|
||||
_temporary: temporary,
|
||||
root,
|
||||
|
||||
+60
-226
@@ -1498,32 +1498,6 @@ fn validate_approval_response_id(value: &str) -> Result<(), PlanningStorageError
|
||||
validate_uuid_prefixed(value, "gdd-response-", "responseId")
|
||||
}
|
||||
|
||||
pub(crate) fn plan_gdd_approval_decision_fingerprint(
|
||||
input: &PlanGddApprovalDecisionInputV1,
|
||||
normalized_comment: Option<&str>,
|
||||
) -> Result<String, PlanningStorageError> {
|
||||
typed_serde_fingerprint(
|
||||
PLAN_GDD_APPROVAL_DECISION_FINGERPRINT_DOMAIN,
|
||||
&PlanGddApprovalDecisionFingerprintValue {
|
||||
project_id: &input.project_id,
|
||||
gdd_id: &input.gdd_id,
|
||||
version: input.version,
|
||||
fingerprint: &input.fingerprint,
|
||||
pending_action_id: &input.pending_action_id,
|
||||
action_fingerprint: &input.action_fingerprint,
|
||||
approval_request_id: &input.approval_request_id,
|
||||
response_id: &input.response_id,
|
||||
source: PLAN_GDD_APPROVAL_SOURCE,
|
||||
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
run_profile_binding_fingerprint: "",
|
||||
session_id: "",
|
||||
run_id: "",
|
||||
action: &input.action,
|
||||
normalized_comment,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn plan_gdd_approval_decision_fingerprint_for_identity(
|
||||
input: &PlanGddApprovalDecisionInputV1,
|
||||
source: &str,
|
||||
@@ -3112,20 +3086,6 @@ fn is_recoverable_index_projection_error(error: &PlanningStorageError) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Read the derived index and repair a missing, malformed, or stale primary
|
||||
/// from the validated GDD lineage while holding the project lock. The index
|
||||
/// is never used to invent authority: if no GDD fact exists, an absent index
|
||||
/// is returned as `None`, while an index left behind without a GDD is a hard
|
||||
/// reconciliation failure.
|
||||
pub(crate) fn read_plan_gdd_index_with_recovery(
|
||||
root: &Path,
|
||||
rebuilt_at_utc: &str,
|
||||
) -> Result<Option<PlanGddIndexV1>, PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.index.read")
|
||||
.map_err(|error| io_error("读取 planning index 时取得项目锁失败", error))?;
|
||||
read_plan_gdd_index_with_recovery_locked(root, rebuilt_at_utc)
|
||||
}
|
||||
|
||||
pub(crate) fn read_plan_gdd_index_with_recovery_locked(
|
||||
root: &Path,
|
||||
rebuilt_at_utc: &str,
|
||||
@@ -3309,18 +3269,6 @@ pub(crate) fn read_plan_gdd_chain_locked(
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
/// Read the immutable approval receipts. Only exact `vN.json` entries are
|
||||
/// facts; a malformed entry that claims the reserved prefix is rejected rather
|
||||
/// than silently ignored. The caller is responsible for reconciling each
|
||||
/// receipt against the corresponding GDD lineage.
|
||||
pub(crate) fn read_plan_gdd_approvals(
|
||||
root: &Path,
|
||||
) -> Result<Vec<PlanGddApprovalV1>, PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.read-approvals")
|
||||
.map_err(|error| io_error("读取 planning approval receipts 时取得项目锁失败", error))?;
|
||||
read_plan_gdd_approvals_locked(root)
|
||||
}
|
||||
|
||||
pub(crate) fn read_plan_gdd_approvals_locked(
|
||||
root: &Path,
|
||||
) -> Result<Vec<PlanGddApprovalV1>, PlanningStorageError> {
|
||||
@@ -4042,29 +3990,15 @@ fn verify_planning_hardlink_publish_identity(
|
||||
format!("{label} fallback 发布前 temp/target 文件身份不一致"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
return Err(PlanningStorageError::new(
|
||||
Err(PlanningStorageError::new(
|
||||
"PLAN_UNSUPPORTED_PLATFORM",
|
||||
format!("{label} fallback 无法证明 temp/target 文件身份"),
|
||||
));
|
||||
))
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Publish an immutable planning JSON file without replacing an existing
|
||||
/// target. A byte-identical target is an idempotent replay; every other
|
||||
/// existing target is an identity conflict.
|
||||
pub(crate) fn durable_create_json_no_replace(
|
||||
root: &Path,
|
||||
relative_path: &str,
|
||||
bytes: &[u8],
|
||||
label: &str,
|
||||
) -> Result<PlanningCreateOutcome, PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.create")
|
||||
.map_err(|error| io_error("取得 planning create 项目锁失败", error))?;
|
||||
durable_create_json_no_replace_locked(root, relative_path, bytes, label)
|
||||
}
|
||||
|
||||
pub(crate) fn durable_create_json_no_replace_locked(
|
||||
@@ -4177,14 +4111,6 @@ pub(crate) fn durable_create_json_no_replace_locked(
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
pub(crate) fn read_plan_gdd_approval_pending(
|
||||
root: &Path,
|
||||
) -> Result<Option<PlanGddApprovalPendingV1>, PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.approval-pending.read")
|
||||
.map_err(|error| io_error("读取 GDD approval pending 时取得项目锁失败", error))?;
|
||||
read_plan_gdd_approval_pending_locked(root)
|
||||
}
|
||||
|
||||
pub(crate) fn read_plan_gdd_approval_pending_locked(
|
||||
root: &Path,
|
||||
) -> Result<Option<PlanGddApprovalPendingV1>, PlanningStorageError> {
|
||||
@@ -4199,18 +4125,6 @@ pub(crate) fn read_plan_gdd_approval_pending_locked(
|
||||
}
|
||||
}
|
||||
|
||||
/// The pending file is a replaceable projection. It is written atomically,
|
||||
/// but unlike a receipt it may be advanced from awaiting to observed or
|
||||
/// rebuilt after a crash. Equal bytes are treated as an idempotent replay.
|
||||
pub(crate) fn write_plan_gdd_approval_pending_atomic(
|
||||
root: &Path,
|
||||
value: &PlanGddApprovalPendingV1,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.approval-pending.write")
|
||||
.map_err(|error| io_error("写入 GDD approval pending 时取得项目锁失败", error))?;
|
||||
write_plan_gdd_approval_pending_atomic_locked(root, value)
|
||||
}
|
||||
|
||||
pub(crate) fn write_plan_gdd_approval_pending_atomic_locked(
|
||||
root: &Path,
|
||||
value: &PlanGddApprovalPendingV1,
|
||||
@@ -4266,24 +4180,9 @@ pub(crate) fn remove_plan_gdd_approval_pending_locked(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn remove_plan_gdd_approval_pending(root: &Path) -> Result<(), PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.approval-pending.remove")
|
||||
.map_err(|error| io_error("清理 GDD approval pending 时取得项目锁失败", error))?;
|
||||
remove_plan_gdd_approval_pending_locked(root)
|
||||
}
|
||||
|
||||
/// Rebuild and atomically replace the derived GDD index. The index is not an
|
||||
/// immutable fact: a new GDD version must replace it, while a corrupt or
|
||||
/// missing index can always be rebuilt from the authoritative GDD chain.
|
||||
pub(crate) fn write_plan_gdd_index_atomic(
|
||||
root: &Path,
|
||||
value: &PlanGddIndexV1,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.index")
|
||||
.map_err(|error| io_error("取得 planning index 项目锁失败", error))?;
|
||||
write_plan_gdd_index_atomic_locked(root, value)
|
||||
}
|
||||
|
||||
pub(crate) fn write_plan_gdd_index_atomic_locked(
|
||||
root: &Path,
|
||||
value: &PlanGddIndexV1,
|
||||
@@ -4342,19 +4241,6 @@ pub(crate) fn write_plan_gdd_index_atomic_locked(
|
||||
result
|
||||
}
|
||||
|
||||
/// Atomically publish the human-readable Fast GDD projection. This is a
|
||||
/// Runtime-owned projection writer, deliberately separate from the generic
|
||||
/// `file.write` gate (which rejects this path for planning Agents). The
|
||||
/// caller must hold the project write lock when using the `_locked` variant.
|
||||
pub(crate) fn write_plan_fast_gdd_markdown_atomic(
|
||||
root: &Path,
|
||||
markdown: &str,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.fast-gdd")
|
||||
.map_err(|error| io_error("取得 Fast GDD 投影项目锁失败", error))?;
|
||||
write_plan_fast_gdd_markdown_atomic_locked(root, markdown)
|
||||
}
|
||||
|
||||
pub(crate) fn write_plan_fast_gdd_markdown_atomic_locked(
|
||||
root: &Path,
|
||||
markdown: &str,
|
||||
@@ -4516,19 +4402,6 @@ fn verify_replace_target_is_safe(target: &Path, label: &str) -> Result<(), Plann
|
||||
}
|
||||
}
|
||||
|
||||
/// Atomically advance `session.json`, retaining exactly one `.session.previous`
|
||||
/// recovery copy. Callers should hold the project write lock; the public
|
||||
/// wrapper acquires it for standalone/tests and the `_locked` variant is used
|
||||
/// by Runtime code that already owns the lock.
|
||||
pub(crate) fn write_plan_session_atomic(
|
||||
root: &Path,
|
||||
value: &PlanSessionV1,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
let _lock = acquire_project_write_lock(root, "planning.session")
|
||||
.map_err(|error| io_error("取得 planning session 项目锁失败", error))?;
|
||||
write_plan_session_atomic_locked(root, value)
|
||||
}
|
||||
|
||||
pub(crate) fn write_plan_session_atomic_locked(
|
||||
root: &Path,
|
||||
value: &PlanSessionV1,
|
||||
@@ -4834,50 +4707,6 @@ pub(crate) fn read_plan_session_read_only_locked(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct PlanningRuntimeWriteIdentity<'a> {
|
||||
pub(crate) agent_id: &'a str,
|
||||
pub(crate) source: &'a str,
|
||||
pub(crate) run_profile: &'a str,
|
||||
pub(crate) parent_agent_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
pub(crate) fn validate_planning_runtime_write_identity(
|
||||
identity: &PlanningRuntimeWriteIdentity<'_>,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
if identity.agent_id != "project-planning"
|
||||
|| identity.source != "agent-delegate"
|
||||
|| identity.run_profile != "standard"
|
||||
|| identity.parent_agent_id != Some("project-supervisor")
|
||||
{
|
||||
return Err(PlanningStorageError::new(
|
||||
"PLAN_WRITE_AUTHORIZATION_DENIED",
|
||||
"只有 project-planning/agent-delegate/standard/Supervisor 子 Run 可以申请规划 Runtime 写入",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn durable_create_json_no_replace_authorized(
|
||||
root: &Path,
|
||||
identity: &PlanningRuntimeWriteIdentity<'_>,
|
||||
relative_path: &str,
|
||||
bytes: &[u8],
|
||||
label: &str,
|
||||
) -> Result<PlanningCreateOutcome, PlanningStorageError> {
|
||||
validate_planning_runtime_write_identity(identity)?;
|
||||
durable_create_json_no_replace(root, relative_path, bytes, label)
|
||||
}
|
||||
|
||||
pub(crate) fn write_plan_session_atomic_authorized(
|
||||
root: &Path,
|
||||
identity: &PlanningRuntimeWriteIdentity<'_>,
|
||||
value: &PlanSessionV1,
|
||||
) -> Result<(), PlanningStorageError> {
|
||||
validate_planning_runtime_write_identity(identity)?;
|
||||
write_plan_session_atomic(root, value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -5196,24 +5025,24 @@ mod tests {
|
||||
let path = ".agent/planning/gdd.v1.json";
|
||||
let malformed_root = tempfile::tempdir().expect("malformed root");
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(malformed_root.path(), path, br"{}", "GDD",)
|
||||
durable_create_json_no_replace_locked(malformed_root.path(), path, br"{}", "GDD",)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_INVALID_JSON"
|
||||
);
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(root, path, &bytes, "GDD").expect("first create"),
|
||||
durable_create_json_no_replace_locked(root, path, &bytes, "GDD").expect("first create"),
|
||||
PlanningCreateOutcome::Created
|
||||
);
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(root, path, &bytes, "GDD").expect("replay"),
|
||||
durable_create_json_no_replace_locked(root, path, &bytes, "GDD").expect("replay"),
|
||||
PlanningCreateOutcome::Replayed
|
||||
);
|
||||
let mut changed = bytes.clone();
|
||||
let changed_index = changed.len() - 2;
|
||||
changed[changed_index] ^= 1;
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(root, path, &changed, "GDD")
|
||||
durable_create_json_no_replace_locked(root, path, &changed, "GDD")
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_INVALID_JSON"
|
||||
@@ -5224,27 +5053,6 @@ mod tests {
|
||||
assert!(delete_local_project_file_at(root, "game/fast_gdd.md").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planning_write_identity_is_fail_closed() {
|
||||
let valid = PlanningRuntimeWriteIdentity {
|
||||
agent_id: "project-planning",
|
||||
source: "agent-delegate",
|
||||
run_profile: "standard",
|
||||
parent_agent_id: Some("project-supervisor"),
|
||||
};
|
||||
assert!(validate_planning_runtime_write_identity(&valid).is_ok());
|
||||
let invalid_identity = PlanningRuntimeWriteIdentity {
|
||||
agent_id: "project-supervisor",
|
||||
..valid
|
||||
};
|
||||
assert_eq!(
|
||||
validate_planning_runtime_write_identity(&invalid_identity)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_WRITE_AUTHORIZATION_DENIED"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn submit_input_has_strict_canonical_parser_and_runtime_field_boundary() {
|
||||
let value = golden_submit_input();
|
||||
@@ -5273,8 +5081,13 @@ mod tests {
|
||||
let root = directory.path();
|
||||
let first = golden_gdd();
|
||||
let first_bytes = canonical_plan_gdd_bytes(&first).expect("first bytes");
|
||||
durable_create_json_no_replace(root, ".agent/planning/gdd.v1.json", &first_bytes, "GDD")
|
||||
.expect("write first GDD");
|
||||
durable_create_json_no_replace_locked(
|
||||
root,
|
||||
".agent/planning/gdd.v1.json",
|
||||
&first_bytes,
|
||||
"GDD",
|
||||
)
|
||||
.expect("write first GDD");
|
||||
let mut second = first.clone();
|
||||
second.version = 2;
|
||||
second.submission_id = "action-abcdefabcdefabcdefabcdef".to_string();
|
||||
@@ -5283,8 +5096,13 @@ mod tests {
|
||||
second.action_fingerprint = "4".repeat(64);
|
||||
second.fingerprint = plan_gdd_fingerprint(&second).expect("second fingerprint");
|
||||
let second_bytes = canonical_plan_gdd_bytes(&second).expect("second bytes");
|
||||
durable_create_json_no_replace(root, ".agent/planning/gdd.v2.json", &second_bytes, "GDD")
|
||||
.expect("write second GDD");
|
||||
durable_create_json_no_replace_locked(
|
||||
root,
|
||||
".agent/planning/gdd.v2.json",
|
||||
&second_bytes,
|
||||
"GDD",
|
||||
)
|
||||
.expect("write second GDD");
|
||||
let chain = read_plan_gdd_chain(root).expect("read chain");
|
||||
assert_eq!(chain, vec![first.clone(), second.clone()]);
|
||||
let index = build_plan_gdd_index(&chain, "2026-08-10T00:00:00.000Z").expect("index");
|
||||
@@ -5316,15 +5134,20 @@ mod tests {
|
||||
);
|
||||
let index_bytes = canonical_plan_index_bytes(&index).expect("index bytes");
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(root, PLAN_GDD_INDEX_PATH, &index_bytes, "GDD index")
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
durable_create_json_no_replace_locked(
|
||||
root,
|
||||
PLAN_GDD_INDEX_PATH,
|
||||
&index_bytes,
|
||||
"GDD index"
|
||||
)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_DEDICATED_WRITER_REQUIRED"
|
||||
);
|
||||
write_plan_gdd_index_atomic(root, &index).expect("write index");
|
||||
write_plan_gdd_index_atomic_locked(root, &index).expect("write index");
|
||||
let mut rebuilt = index.clone();
|
||||
rebuilt.rebuilt_at_utc = "2026-08-11T00:00:00.000Z".to_string();
|
||||
write_plan_gdd_index_atomic(root, &rebuilt).expect("replace index");
|
||||
write_plan_gdd_index_atomic_locked(root, &rebuilt).expect("replace index");
|
||||
assert_eq!(
|
||||
parse_plan_index_bytes(
|
||||
&fs::read(
|
||||
@@ -5337,7 +5160,7 @@ mod tests {
|
||||
);
|
||||
fs::remove_file(root.join(PLAN_GDD_INDEX_PATH.replace('/', std::path::MAIN_SEPARATOR_STR)))
|
||||
.expect("remove index for recovery");
|
||||
let recovered = read_plan_gdd_index_with_recovery(root, "2026-08-12T00:00:00.000Z")
|
||||
let recovered = read_plan_gdd_index_with_recovery_locked(root, "2026-08-12T00:00:00.000Z")
|
||||
.expect("recover missing index")
|
||||
.expect("recovered index");
|
||||
assert_eq!(recovered.entries, index.entries);
|
||||
@@ -5348,9 +5171,10 @@ mod tests {
|
||||
br"{}",
|
||||
)
|
||||
.expect("corrupt index");
|
||||
let recovered_corrupt = read_plan_gdd_index_with_recovery(root, "2026-08-13T00:00:00.000Z")
|
||||
.expect("recover corrupt index")
|
||||
.expect("recovered corrupt index");
|
||||
let recovered_corrupt =
|
||||
read_plan_gdd_index_with_recovery_locked(root, "2026-08-13T00:00:00.000Z")
|
||||
.expect("recover corrupt index")
|
||||
.expect("recovered corrupt index");
|
||||
assert_eq!(recovered_corrupt.entries, index.entries);
|
||||
assert_eq!(recovered_corrupt.rebuilt_at_utc, "2026-08-13T00:00:00.000Z");
|
||||
assert!(build_plan_gdd_index(&[], "2026-08-10T00:00:00.000Z").is_err());
|
||||
@@ -5363,12 +5187,17 @@ mod tests {
|
||||
let first = golden_session();
|
||||
let first_bytes = canonical_plan_session_bytes(&first).expect("session bytes");
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(root, PLAN_SESSION_PATH, &first_bytes, "plan session")
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
durable_create_json_no_replace_locked(
|
||||
root,
|
||||
PLAN_SESSION_PATH,
|
||||
&first_bytes,
|
||||
"plan session"
|
||||
)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_DEDICATED_WRITER_REQUIRED"
|
||||
);
|
||||
write_plan_session_atomic(root, &first).expect("write session v1");
|
||||
write_plan_session_atomic_locked(root, &first).expect("write session v1");
|
||||
let mut second = first.clone();
|
||||
second.session_revision = 2;
|
||||
second.previous_fingerprint = Some(first.session_fingerprint.clone());
|
||||
@@ -5376,7 +5205,7 @@ mod tests {
|
||||
second.active_run_id = None;
|
||||
second.phase = "awaiting_user_input".to_string();
|
||||
second.session_fingerprint = plan_session_fingerprint(&second).expect("v2 fingerprint");
|
||||
write_plan_session_atomic(root, &second).expect("write session v2");
|
||||
write_plan_session_atomic_locked(root, &second).expect("write session v2");
|
||||
assert!(root.join(PLAN_SESSION_PREVIOUS_PATH).exists());
|
||||
assert_eq!(
|
||||
read_plan_session_with_recovery(root).expect("recover session"),
|
||||
@@ -5388,7 +5217,7 @@ mod tests {
|
||||
invalid_next.previous_fingerprint = Some(second.session_fingerprint.clone());
|
||||
invalid_next.session_fingerprint = plan_session_fingerprint(&invalid_next).expect("bad fp");
|
||||
assert_eq!(
|
||||
write_plan_session_atomic(root, &invalid_next)
|
||||
write_plan_session_atomic_locked(root, &invalid_next)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_IDENTITY_CONFLICT"
|
||||
@@ -5452,7 +5281,7 @@ mod tests {
|
||||
forged_collecting.session_fingerprint =
|
||||
plan_session_fingerprint(&forged_collecting).expect("recompute forged fingerprint");
|
||||
assert_eq!(
|
||||
write_plan_session_atomic(root, &forged_collecting)
|
||||
write_plan_session_atomic_locked(root, &forged_collecting)
|
||||
.expect_err("runtime boundary must reject an unrelated latest delegation")
|
||||
.code(),
|
||||
"PLAN_NEEDS_RECONCILIATION"
|
||||
@@ -5554,7 +5383,7 @@ mod tests {
|
||||
plan_session_fingerprint(&session).expect("recompute forged fingerprint");
|
||||
validate_plan_session(&session).expect("standalone session shape remains valid");
|
||||
assert_eq!(
|
||||
write_plan_session_atomic(root, &session)
|
||||
write_plan_session_atomic_locked(root, &session)
|
||||
.expect_err("quality repair edge must clear old applied answers")
|
||||
.code(),
|
||||
"PLAN_IDENTITY_CONFLICT"
|
||||
@@ -5566,7 +5395,7 @@ mod tests {
|
||||
let directory = tempfile::tempdir().expect("temp root");
|
||||
let root = directory.path();
|
||||
let first = golden_session();
|
||||
write_plan_session_atomic(root, &first).expect("write session");
|
||||
write_plan_session_atomic_locked(root, &first).expect("write session");
|
||||
let primary_path = root.join(PLAN_SESSION_PATH.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
let previous_path =
|
||||
root.join(PLAN_SESSION_PREVIOUS_PATH.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
@@ -5597,7 +5426,7 @@ mod tests {
|
||||
second.active_run_id = None;
|
||||
second.phase = "awaiting_user_input".to_string();
|
||||
second.session_fingerprint = plan_session_fingerprint(&second).expect("second fp");
|
||||
write_plan_session_atomic(root, &second).expect("write successor");
|
||||
write_plan_session_atomic_locked(root, &second).expect("write successor");
|
||||
let mut forked_previous = first.clone();
|
||||
forked_previous.updated_at_utc = "2026-08-11T00:00:00.000Z".to_string();
|
||||
forked_previous.session_fingerprint =
|
||||
@@ -5626,9 +5455,14 @@ mod tests {
|
||||
ensure_planning_parent(&symlink_path).expect("planning dir");
|
||||
symlink(&outside, &symlink_path).expect("symlink");
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(root, ".agent/planning/gdd.v1.json", &bytes, "GDD")
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
durable_create_json_no_replace_locked(
|
||||
root,
|
||||
".agent/planning/gdd.v1.json",
|
||||
&bytes,
|
||||
"GDD"
|
||||
)
|
||||
.unwrap_err()
|
||||
.code(),
|
||||
"PLAN_UNTRUSTED_PATH"
|
||||
);
|
||||
|
||||
@@ -5640,7 +5474,7 @@ mod tests {
|
||||
ensure_planning_parent(&hardlink_path).expect("hardlink planning dir");
|
||||
fs::hard_link(&hardlink_outside, &hardlink_path).expect("hardlink");
|
||||
assert_eq!(
|
||||
durable_create_json_no_replace(
|
||||
durable_create_json_no_replace_locked(
|
||||
hardlink_root,
|
||||
".agent/planning/gdd.v1.json",
|
||||
&bytes,
|
||||
|
||||
+36
-135
@@ -466,111 +466,6 @@ pub(crate) fn validate_plan_provider_session_binding(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Capture the source session before a Provider action becomes durable. The
|
||||
/// action sidecar stores this value, so execution can reject a stale response
|
||||
/// instead of reading and trusting whatever session happens to be current.
|
||||
pub(crate) fn capture_plan_provider_session_binding(
|
||||
root: &std::path::Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
) -> Result<PlanProviderSessionBindingV1, String> {
|
||||
let session = read_plan_session_with_recovery(root)
|
||||
.map_err(|error| error.to_string())?
|
||||
.ok_or_else(|| "planning submit 建立前缺少 durable session".to_string())?;
|
||||
if session.project_id != game_creator_agent_runtime_context_project_id(root)?
|
||||
|| session.agent_id != runtime.agent_id
|
||||
|| session.source != runtime.source
|
||||
|| session.run_profile != runtime.run_profile
|
||||
|| session.run_profile_binding_fingerprint != runtime.run_profile_binding_fingerprint
|
||||
|| session.session_id != runtime.session_id
|
||||
|| session.active_run_id.as_deref() != Some(runtime.run_id.as_str())
|
||||
|| session.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| session.applied_steer_cursor != runtime.applied_steer_cursor
|
||||
{
|
||||
return Err("planning submit action 建立时 session identity 不匹配".to_string());
|
||||
}
|
||||
let delegation_id = runtime
|
||||
.delegation_id
|
||||
.clone()
|
||||
.ok_or_else(|| "planning submit action 建立时缺少 delegationId".to_string())?;
|
||||
if !session.latest_delegation_id.is_empty() && session.latest_delegation_id != delegation_id {
|
||||
return Err("planning submit action 建立时 delegation identity 已漂移".to_string());
|
||||
}
|
||||
let goal_snapshot_fingerprint = agent_goal_snapshot_fingerprint_for_state_at(root, runtime)?;
|
||||
let request_context_fingerprint = format!(
|
||||
"sha256-serde-json-v2:{:x}",
|
||||
Sha256::digest(
|
||||
format!(
|
||||
"plan-provider-context\n{}\n{}\n{}\n{}\n{}",
|
||||
runtime.agent_id,
|
||||
runtime.run_id,
|
||||
runtime.loop_iteration,
|
||||
session.session_revision,
|
||||
session.session_fingerprint
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
);
|
||||
let request_kind = "tool-plan";
|
||||
let request_slot = format!("loop-{}-plan-submit", runtime.loop_iteration);
|
||||
let provider_request_id = plan_provider_request_id(&PlanProviderRequestIdentityValue {
|
||||
project_id: &session.project_id,
|
||||
gdd_id: &session.gdd_id,
|
||||
agent_id: &runtime.agent_id,
|
||||
task_id: &runtime.task_id,
|
||||
session_id: &session.session_id,
|
||||
run_id: &runtime.run_id,
|
||||
root_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
root_run_id: &session.root_run_id,
|
||||
delegation_id: &delegation_id,
|
||||
source: &runtime.source,
|
||||
run_profile: &runtime.run_profile,
|
||||
run_profile_binding_fingerprint: &runtime.run_profile_binding_fingerprint,
|
||||
goal_id: runtime.goal_id.as_deref(),
|
||||
goal_revision: runtime.goal_revision,
|
||||
goal_snapshot_fingerprint: &goal_snapshot_fingerprint,
|
||||
session_revision: session.session_revision,
|
||||
session_fingerprint: &session.session_fingerprint,
|
||||
applied_steer_cursor: runtime.applied_steer_cursor,
|
||||
request_kind,
|
||||
request_slot: &request_slot,
|
||||
web_search_enabled: false,
|
||||
request_context_fingerprint: &request_context_fingerprint,
|
||||
})
|
||||
.map_err(|error| error.to_string())?;
|
||||
let binding = PlanProviderSessionBindingV1 {
|
||||
schema_version: PLAN_PROVIDER_SESSION_BINDING_SCHEMA_VERSION.to_string(),
|
||||
project_id: session.project_id,
|
||||
gdd_id: session.gdd_id,
|
||||
agent_id: runtime.agent_id.clone(),
|
||||
task_id: runtime.task_id.clone(),
|
||||
provider_request_id,
|
||||
session_id: session.session_id,
|
||||
run_id: runtime.run_id.clone(),
|
||||
root_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
|
||||
root_run_id: session.root_run_id,
|
||||
delegation_id,
|
||||
goal_id: runtime.goal_id.clone(),
|
||||
goal_revision: runtime.goal_revision,
|
||||
goal_snapshot_fingerprint,
|
||||
source: runtime.source.clone(),
|
||||
run_profile: runtime.run_profile.clone(),
|
||||
run_profile_binding_fingerprint: runtime.run_profile_binding_fingerprint.clone(),
|
||||
session_revision: session.session_revision,
|
||||
session_fingerprint: session.session_fingerprint,
|
||||
applied_steer_cursor: runtime.applied_steer_cursor,
|
||||
request_kind: request_kind.to_string(),
|
||||
request_slot,
|
||||
web_search_enabled: false,
|
||||
request_context_fingerprint,
|
||||
fingerprint: String::new(),
|
||||
};
|
||||
let mut binding = binding;
|
||||
binding.fingerprint =
|
||||
plan_provider_session_binding_fingerprint(&binding).map_err(|error| error.to_string())?;
|
||||
validate_plan_provider_session_binding(&binding).map_err(|error| error.to_string())?;
|
||||
Ok(binding)
|
||||
}
|
||||
|
||||
/// Capture the same planning source/session identity that was used to build a
|
||||
/// concrete Provider request. Unlike the batch-only helper above, this
|
||||
/// variant takes the immutable request snapshot, so retry attempts and repair
|
||||
@@ -2352,7 +2247,7 @@ mod tests {
|
||||
session.session_fingerprint = plan_session_fingerprint(&session).expect("session fp");
|
||||
context.source_session_revision = session.session_revision;
|
||||
context.source_session_fingerprint = session.session_fingerprint.clone();
|
||||
write_plan_session_atomic(&root, &session).expect("write session fixture");
|
||||
write_plan_session_atomic_locked(&root, &session).expect("write session fixture");
|
||||
(root, context, input)
|
||||
}
|
||||
|
||||
@@ -2453,20 +2348,26 @@ mod tests {
|
||||
)
|
||||
.expect("build committed GDD");
|
||||
let gdd_bytes = canonical_plan_gdd_bytes(&gdd).expect("canonical GDD");
|
||||
durable_create_json_no_replace(&root, ".agent/planning/gdd.v1.json", &gdd_bytes, "GDD")
|
||||
.expect("write immutable GDD");
|
||||
durable_create_json_no_replace_locked(
|
||||
&root,
|
||||
".agent/planning/gdd.v1.json",
|
||||
&gdd_bytes,
|
||||
"GDD",
|
||||
)
|
||||
.expect("write immutable GDD");
|
||||
let index =
|
||||
build_plan_gdd_index(&[gdd.clone()], &context.created_at_utc).expect("build GDD index");
|
||||
write_plan_gdd_index_atomic(&root, &index).expect("write GDD index");
|
||||
write_plan_gdd_index_atomic_locked(&root, &index).expect("write GDD index");
|
||||
let markdown = render_plan_fast_gdd_markdown(&gdd, "ready_for_approval")
|
||||
.expect("render Fast GDD Markdown");
|
||||
write_plan_fast_gdd_markdown_atomic(&root, &markdown).expect("write Fast GDD Markdown");
|
||||
write_plan_fast_gdd_markdown_atomic_locked(&root, &markdown)
|
||||
.expect("write Fast GDD Markdown");
|
||||
let source_session = read_plan_session_with_recovery(&root)
|
||||
.expect("read source session")
|
||||
.expect("source session exists");
|
||||
let successor = build_submit_session_successor(&source_session, &context, &gdd)
|
||||
.expect("build submitted session successor");
|
||||
write_plan_session_atomic(&root, &successor).expect("write submitted session");
|
||||
write_plan_session_atomic_locked(&root, &successor).expect("write submitted session");
|
||||
|
||||
let root_runtime =
|
||||
read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
@@ -3858,7 +3759,7 @@ mod tests {
|
||||
.expect("GDD exists");
|
||||
|
||||
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||
let pending = read_plan_gdd_approval_pending(&root)
|
||||
let pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read approval pending")
|
||||
.expect("pending exists");
|
||||
assert_eq!(pending.status, "awaiting_decision");
|
||||
@@ -3975,7 +3876,7 @@ mod tests {
|
||||
)
|
||||
.expect("disabled acceptance recovery is a no-op");
|
||||
assert_eq!(outcome, PlanGddAcceptanceGateOutcome::NotApplicable);
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending")
|
||||
.is_none());
|
||||
|
||||
@@ -4170,13 +4071,13 @@ mod tests {
|
||||
)
|
||||
.expect("commit receipt");
|
||||
|
||||
let mut pending = read_plan_gdd_approval_pending(&root)
|
||||
let mut pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read observed pending")
|
||||
.expect("observed pending remains during recovery");
|
||||
pending.run_identity.run_id = "run-other-001".to_string();
|
||||
pending.pending_fingerprint =
|
||||
plan_gdd_approval_pending_fingerprint(&pending).expect("recompute pending fingerprint");
|
||||
write_plan_gdd_approval_pending_atomic(&root, &pending)
|
||||
write_plan_gdd_approval_pending_atomic_locked(&root, &pending)
|
||||
.expect("write mismatched projection");
|
||||
|
||||
let blocker = plan_gdd_completion_blocker_at_locked(
|
||||
@@ -4367,7 +4268,7 @@ mod tests {
|
||||
assert_eq!(first.decision_ref.action, action);
|
||||
assert_eq!(first.approved_gdd_ref.is_some(), action == "approve");
|
||||
|
||||
let approvals = read_plan_gdd_approvals(&root).expect("read receipts");
|
||||
let approvals = read_plan_gdd_approvals_locked(&root).expect("read receipts");
|
||||
let index = build_plan_gdd_index_with_approvals(
|
||||
&read_plan_gdd_chain(&root).expect("read GDD chain"),
|
||||
&approvals,
|
||||
@@ -4679,7 +4580,7 @@ mod tests {
|
||||
assert_eq!(waiting_detail["approvalPending"], "not-created");
|
||||
assert_eq!(waiting_detail["nextRequiredAction"], "agent.delegate");
|
||||
assert_eq!(waiting_detail["repairOfDelegationId"], gdd.delegation_id);
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending before gate")
|
||||
.is_none());
|
||||
|
||||
@@ -4703,7 +4604,7 @@ mod tests {
|
||||
)
|
||||
.expect("parse passed detail");
|
||||
assert_eq!(passed_detail["approvalPending"], "created");
|
||||
let pending = read_plan_gdd_approval_pending(&root)
|
||||
let pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read created pending")
|
||||
.expect("pending exists");
|
||||
assert_eq!(pending.gdd_ref.gdd_id, gdd.gdd_id);
|
||||
@@ -4803,7 +4704,7 @@ mod tests {
|
||||
assert!(after_claim_detail.contains("planGddAcceptanceGate"));
|
||||
assert!(after_claim_detail.contains("\"nextRequiredAction\":\"agent.delegate\""));
|
||||
assert!(after_claim_detail.contains(&gdd.delegation_id));
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending")
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
@@ -4829,7 +4730,7 @@ mod tests {
|
||||
let detail = claim.detail.as_deref().expect("claim gate detail");
|
||||
assert!(detail.contains("\"nextRequiredAction\":\"file.read\""));
|
||||
assert!(!detail.contains("\"repairOfDelegationId\":\""));
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending before evidence")
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
@@ -4870,7 +4771,7 @@ mod tests {
|
||||
.expect("parse passed detail");
|
||||
assert_eq!(passed_detail["nextRequiredAction"], "agent.run_status");
|
||||
assert_eq!(passed_detail["delegationId"], gdd.delegation_id);
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending before claim")
|
||||
.is_none());
|
||||
|
||||
@@ -4893,7 +4794,7 @@ mod tests {
|
||||
.as_deref()
|
||||
.expect("claim detail")
|
||||
.contains("\"approvalPending\":\"created\""));
|
||||
let pending = read_plan_gdd_approval_pending(&root)
|
||||
let pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending after claim")
|
||||
.expect("pending exists after claim");
|
||||
assert_eq!(pending.gdd_ref.gdd_id, gdd.gdd_id);
|
||||
@@ -4928,7 +4829,7 @@ mod tests {
|
||||
}],
|
||||
)
|
||||
.expect("record passed graph without running gate");
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read missing pending")
|
||||
.is_none());
|
||||
|
||||
@@ -4949,7 +4850,7 @@ mod tests {
|
||||
let detail = replay.detail.as_deref().expect("run status detail");
|
||||
assert!(!detail.contains("readyDelegateReceipts"));
|
||||
assert!(detail.contains("\"approvalPending\":\"created\""));
|
||||
let pending = read_plan_gdd_approval_pending(&root)
|
||||
let pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read replayed pending")
|
||||
.expect("pending created after zero-ready replay");
|
||||
assert_eq!(pending.gdd_ref.gdd_id, gdd.gdd_id);
|
||||
@@ -4985,7 +4886,7 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(observation.status, "ok");
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending after dispatched poll")
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
@@ -5047,7 +4948,7 @@ mod tests {
|
||||
}),
|
||||
);
|
||||
assert_eq!(observation.status, "rejected", "case={case}");
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending after rejected evidence")
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
@@ -5110,7 +5011,7 @@ mod tests {
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("从 startLine=1 开始分页")));
|
||||
assert!(read_plan_gdd_approval_pending(&root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read pending after old hash")
|
||||
.is_none());
|
||||
cleanup_fixture(root);
|
||||
@@ -5160,7 +5061,7 @@ mod tests {
|
||||
}),
|
||||
);
|
||||
assert_eq!(partial.status, "rejected");
|
||||
assert!(read_plan_gdd_approval_pending(&partial_root)
|
||||
assert!(read_plan_gdd_approval_pending_locked(&partial_root)
|
||||
.expect("read partial pending")
|
||||
.is_none());
|
||||
cleanup_fixture(partial_root);
|
||||
@@ -5218,7 +5119,7 @@ mod tests {
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("\"approvalPending\":\"created\"")));
|
||||
let pending = read_plan_gdd_approval_pending(&complete_root)
|
||||
let pending = read_plan_gdd_approval_pending_locked(&complete_root)
|
||||
.expect("read complete pending")
|
||||
.expect("complete coverage creates pending");
|
||||
assert_eq!(pending.gdd_ref.version, gdd.version);
|
||||
@@ -5372,16 +5273,16 @@ mod tests {
|
||||
&root_runtime.run_id,
|
||||
)
|
||||
.expect("create initial pending");
|
||||
let original = read_plan_gdd_approval_pending(&root)
|
||||
let original = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read initial pending")
|
||||
.expect("initial pending exists");
|
||||
remove_plan_gdd_approval_pending(&root).expect("remove pending for recovery window");
|
||||
remove_plan_gdd_approval_pending_locked(&root).expect("remove pending for recovery window");
|
||||
|
||||
assert!(
|
||||
reconcile_plan_gdd_approval_projections_at(&root).expect("reconcile acceptance gate"),
|
||||
"recovery should repair the missing pending projection"
|
||||
);
|
||||
let recovered = read_plan_gdd_approval_pending(&root)
|
||||
let recovered = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read recovered pending")
|
||||
.expect("recovered pending exists");
|
||||
assert_eq!(recovered, original);
|
||||
@@ -5393,7 +5294,7 @@ mod tests {
|
||||
"second recovery must not rewrite an exact pending projection"
|
||||
);
|
||||
assert_eq!(
|
||||
read_plan_gdd_approval_pending(&root)
|
||||
read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read replayed pending")
|
||||
.expect("pending survives replay"),
|
||||
original
|
||||
@@ -5439,7 +5340,7 @@ mod tests {
|
||||
&root_runtime.run_id,
|
||||
)
|
||||
.expect("create approval pending");
|
||||
let mut forged_pending = read_plan_gdd_approval_pending(&root)
|
||||
let mut forged_pending = read_plan_gdd_approval_pending_locked(&root)
|
||||
.expect("read awaiting pending")
|
||||
.expect("awaiting pending exists");
|
||||
|
||||
@@ -5462,7 +5363,7 @@ mod tests {
|
||||
});
|
||||
forged_pending.pending_fingerprint = plan_gdd_approval_pending_fingerprint(&forged_pending)
|
||||
.expect("fingerprint forged pending");
|
||||
write_plan_gdd_approval_pending_atomic(&root, &forged_pending)
|
||||
write_plan_gdd_approval_pending_atomic_locked(&root, &forged_pending)
|
||||
.expect("write forged pending");
|
||||
|
||||
let error = ensure_plan_gdd_approval_pending_after_acceptance_at(
|
||||
|
||||
@@ -4776,7 +4776,7 @@ fn planning_clarification_user_revision_after_answer_preserves_round_for_revise_
|
||||
plan_session_fingerprint(&awaiting_approval).expect("approval-wait fingerprint");
|
||||
validate_plan_session_successor(&collecting, &awaiting_approval)
|
||||
.expect("project approval-wait successor");
|
||||
write_plan_session_atomic(&fixture.root, &awaiting_approval)
|
||||
write_plan_session_atomic_locked(&fixture.root, &awaiting_approval)
|
||||
.expect("persist approval-wait session");
|
||||
|
||||
let mut decided = awaiting_approval.clone();
|
||||
@@ -4805,7 +4805,8 @@ fn planning_clarification_user_revision_after_answer_preserves_round_for_revise_
|
||||
plan_session_fingerprint(&decided).expect("decision fingerprint");
|
||||
validate_plan_session_successor(&awaiting_approval, &decided)
|
||||
.expect("project revise/reject successor");
|
||||
write_plan_session_atomic(&fixture.root, &decided).expect("persist revise/reject session");
|
||||
write_plan_session_atomic_locked(&fixture.root, &decided)
|
||||
.expect("persist revise/reject session");
|
||||
|
||||
mark_static_delegate_delivery_user_revision_requested_at(
|
||||
&fixture.root,
|
||||
|
||||
Reference in New Issue
Block a user