M1C-0b:补齐静态委派状态前向兼容
- 未知 contractStatus 保留为 Unknown(raw) 并接入完成屏障、等待、返工与谱系门禁 - 补齐 planning Provider、自治 liveness 与终态扫描的 fail-closed 消费路径及回归 - 保持已知状态和损坏 sidecar 行为不变,更新技术方案与共享决策记录
This commit is contained in:
@@ -573,17 +573,21 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
&& agent_runtime_autonomous_supervisor_plan_prepares_repair(plan)
|
||||
{
|
||||
let barrier = static_delegate_completion_barrier_at(root, agent_id, run_id)?;
|
||||
if barrier.ready_unclaimed_count > 0 || barrier.unobserved_claim_count > 0 {
|
||||
if barrier.ready_unclaimed_count > 0
|
||||
|| barrier.unobserved_claim_count > 0
|
||||
|| barrier.unknown_contract_status_count > 0
|
||||
{
|
||||
let active_delegations =
|
||||
active_static_delegate_delivery_count_at(root, agent_id, run_id)?;
|
||||
if is_agent_runtime_autonomous_supervisor_delivery_convergence_plan(plan) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!(
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 在准备专业 repair 前仍有 activeDelegations={active_delegations}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、repairRequired={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)原子认领并观察已有回执,再基于权威合同准备 repair",
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 在准备专业 repair 前仍有 activeDelegations={active_delegations}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、repairRequired={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)原子认领并观察已有回执,再基于权威合同准备 repair",
|
||||
barrier.ready_unclaimed_count,
|
||||
barrier.unobserved_claim_count,
|
||||
barrier.repair_required_count,
|
||||
barrier.unknown_contract_status_count,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -597,16 +601,18 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at(
|
||||
let active_delegations = active_static_delegate_delivery_count_at(root, agent_id, run_id)?;
|
||||
let must_claim_or_wait = barrier.ready_unclaimed_count > 0
|
||||
|| barrier.unobserved_claim_count > 0
|
||||
|| barrier.unknown_contract_status_count > 0
|
||||
|| active_delegations >= 3;
|
||||
if must_claim_or_wait {
|
||||
if is_agent_runtime_autonomous_supervisor_delivery_convergence_plan(plan) {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(format!(
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 的 activeDelegations={active_delegations}、waitingDelegations={}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)认领并观察 ready delivery,或等待已有委派推进;不得创建第四次 agent.delegate。收束后若项目 revision 已推进,再验证当前 revision",
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX};当前父 run 的 activeDelegations={active_delegations}、waitingDelegations={}、readyUnclaimedReceipts={}、unobservedReceiptClaims={}、unknownContractStatus={}。必须先只调用 agent.run_status(agentId=null、scope=all、delegationId=null)认领并观察 ready delivery,或等待已有委派推进;不得创建第四次 agent.delegate。收束后若项目 revision 已推进,再验证当前 revision",
|
||||
barrier.waiting_count,
|
||||
barrier.ready_unclaimed_count,
|
||||
barrier.unobserved_claim_count,
|
||||
barrier.unknown_contract_status_count,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,11 +883,17 @@ pub(in crate::agent) fn isolated_join_barrier_has_waiting_groups(detail: &str) -
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn static_delegate_barrier_has_waiting_deliveries(detail: &str) -> bool {
|
||||
detail
|
||||
let waiting = detail
|
||||
.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix("waitingDelegations="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0)
|
||||
.is_some_and(|count| count > 0);
|
||||
let unknown_contract_status = detail
|
||||
.split_whitespace()
|
||||
.find_map(|part| part.strip_prefix("unknownContractStatus="))
|
||||
.and_then(|value| value.parse::<usize>().ok())
|
||||
.is_some_and(|count| count > 0);
|
||||
waiting || unknown_contract_status
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) -> bool {
|
||||
|
||||
@@ -86,6 +86,16 @@ pub(crate) fn capture_plan_provider_structured_injections_at(
|
||||
if clarification_round == u32::MAX {
|
||||
return Err("planning Provider 无法从委派链推导 clarificationRound".to_string());
|
||||
}
|
||||
if static_delegate_lineage_contains_unknown_contract_status(
|
||||
&deliveries,
|
||||
&session.latest_delegation_id,
|
||||
)
|
||||
.map_err(|error| format!("planning Provider 无法确认委派 contractStatus:{error}"))?
|
||||
{
|
||||
return Err(
|
||||
"planning Provider 委派谱系含更新版本 contractStatus,当前版本拒绝继续".to_string(),
|
||||
);
|
||||
}
|
||||
validate_plan_session_for_clarification_round(&session, clarification_round)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let approval_observation = observations
|
||||
@@ -2146,6 +2156,38 @@ mod tests {
|
||||
assert!(validate_plan_gdd(&gdd).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_structured_injections_reject_unknown_delegate_lineage() {
|
||||
let (root, context, _) = submit_fixture();
|
||||
let mut delivery = new_static_delegate_delivery(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"m1c0b-provider-root-session",
|
||||
&context.root_run_id,
|
||||
"m1c0b-provider-parent-action",
|
||||
&context.delegation_id,
|
||||
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||
&context.session_id,
|
||||
&context.created_by_run_id,
|
||||
);
|
||||
delivery.status = StaticDelegateDeliveryStatus::Ready;
|
||||
delivery.terminal_status = Some("completed".to_string());
|
||||
delivery.result_summary = Some("future status fixture".to_string());
|
||||
let mut structured_result = StaticDelegateStructuredResult::default();
|
||||
structured_result.contract_status =
|
||||
StaticDelegateContractStatus::Unknown("future-contract-status".to_string());
|
||||
delivery.structured_result = Some(structured_result);
|
||||
write_static_delegate_delivery_at(&root, &delivery)
|
||||
.expect("write unknown planning delivery");
|
||||
|
||||
let error = capture_plan_provider_structured_injections_at(&root, &context.session_id, &[])
|
||||
.expect_err("unknown planning lineage must block Provider injection");
|
||||
assert!(
|
||||
error.contains("更新版本 contractStatus"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_binding_is_strict_and_recomputes_canonical_request_identity() {
|
||||
let binding = provider_binding();
|
||||
|
||||
@@ -960,7 +960,7 @@ pub(crate) fn publish_game_creator_agent_delegate_result(
|
||||
"contractStatus": delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.map(|result| result.contract_status),
|
||||
.map(|result| result.contract_status.clone()),
|
||||
"artifactCount": delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
|
||||
@@ -30,7 +30,7 @@ pub(in crate::agent) fn observe_claimed_static_delegate_contract_at(
|
||||
"contractStatus": delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.map(|result| result.contract_status),
|
||||
.map(|result| result.contract_status.clone()),
|
||||
}
|
||||
}))
|
||||
.map_err(|error| format!("序列化已认领委派合同失败:{error}"))?;
|
||||
@@ -236,7 +236,7 @@ pub(crate) fn observe_agent_runtime_run_status(
|
||||
"contractStatus": delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.map(|result| result.contract_status),
|
||||
.map(|result| result.contract_status.clone()),
|
||||
"needsUserInput": delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
|
||||
@@ -31,13 +31,58 @@ pub(crate) enum StaticDelegateDeliveryStatus {
|
||||
Suppressed,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum StaticDelegateContractStatus {
|
||||
EvidenceReady,
|
||||
NeedsRepair,
|
||||
NeedsUserInput,
|
||||
UserRevisionRequested,
|
||||
/// A structurally valid durable status introduced by a newer client.
|
||||
///
|
||||
/// The raw wire value is retained so an old client can safely read and
|
||||
/// rewrite the record without silently changing the newer status.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
impl StaticDelegateContractStatus {
|
||||
pub(crate) fn is_unknown(&self) -> bool {
|
||||
matches!(self, Self::Unknown(_))
|
||||
}
|
||||
|
||||
fn durable_value(&self) -> &str {
|
||||
match self {
|
||||
Self::EvidenceReady => "evidence-ready",
|
||||
Self::NeedsRepair => "needs-repair",
|
||||
Self::NeedsUserInput => "needs-user-input",
|
||||
Self::UserRevisionRequested => "user-revision-requested",
|
||||
Self::Unknown(value) => value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for StaticDelegateContractStatus {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.durable_value())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for StaticDelegateContractStatus {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = <String as serde::Deserialize>::deserialize(deserializer)?;
|
||||
Ok(match value.as_str() {
|
||||
"evidence-ready" => Self::EvidenceReady,
|
||||
"needs-repair" => Self::NeedsRepair,
|
||||
"needs-user-input" => Self::NeedsUserInput,
|
||||
"user-revision-requested" => Self::UserRevisionRequested,
|
||||
_ => Self::Unknown(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
@@ -171,6 +216,7 @@ pub(crate) struct StaticDelegateCompletionBarrier {
|
||||
pub(crate) unobserved_claim_count: usize,
|
||||
pub(crate) repair_required_count: usize,
|
||||
pub(crate) user_input_required_count: usize,
|
||||
pub(crate) unknown_contract_status_count: usize,
|
||||
}
|
||||
|
||||
impl StaticDelegateCompletionBarrier {
|
||||
@@ -180,20 +226,22 @@ impl StaticDelegateCompletionBarrier {
|
||||
&& self.unobserved_claim_count == 0
|
||||
&& self.repair_required_count == 0
|
||||
&& self.user_input_required_count == 0
|
||||
&& self.unknown_contract_status_count == 0
|
||||
}
|
||||
|
||||
pub(crate) fn has_waiting(self) -> bool {
|
||||
self.waiting_count > 0
|
||||
self.waiting_count > 0 || self.unknown_contract_status_count > 0
|
||||
}
|
||||
|
||||
pub(crate) fn detail(self) -> String {
|
||||
format!(
|
||||
"waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · 必须认领专业 Agent 回执,并处理 needs-user-input 或对 needs-repair 原委派发起唯一返工后再继续",
|
||||
"waitingDelegations={} · readyUnclaimedReceipts={} · unobservedReceiptClaims={} · repairRequired={} · userInputRequired={} · unknownContractStatus={} · 必须认领专业 Agent 回执,处理 needs-user-input/needs-repair,或升级客户端后再继续",
|
||||
self.waiting_count,
|
||||
self.ready_unclaimed_count,
|
||||
self.unobserved_claim_count,
|
||||
self.repair_required_count,
|
||||
self.user_input_required_count
|
||||
self.user_input_required_count,
|
||||
self.unknown_contract_status_count
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -510,6 +558,16 @@ pub(crate) fn static_delegate_completion_barrier_at(
|
||||
})
|
||||
})
|
||||
.count();
|
||||
barrier.unknown_contract_status_count = deliveries
|
||||
.iter()
|
||||
.filter(|delivery| {
|
||||
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
|
||||
&& delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.is_some_and(|result| result.contract_status.is_unknown())
|
||||
})
|
||||
.count();
|
||||
Ok(barrier)
|
||||
}
|
||||
|
||||
@@ -1112,6 +1170,18 @@ fn static_delegate_original_is_user_revision_requested(
|
||||
})
|
||||
}
|
||||
|
||||
/// A newer durable contract status is intentionally not a repairable contract.
|
||||
/// The old client may preserve and report it, but must not manufacture a
|
||||
/// mutation under semantics it does not understand.
|
||||
fn static_delegate_original_has_unknown_contract_status(
|
||||
delivery: &StaticDelegateDeliveryRecord,
|
||||
) -> bool {
|
||||
delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.is_some_and(|result| result.contract_status.is_unknown())
|
||||
}
|
||||
|
||||
/// 沿 repair_of_delegation_id 反向重放整条链,现算目标 delivery 的
|
||||
/// (repair_depth, clarification_round)。两个维度都是运行时派生值,故意不落盘:
|
||||
/// - 链根(repair_of_delegation_id 为 None):depth = 0,round = 0。
|
||||
@@ -1129,25 +1199,9 @@ pub(crate) fn static_delegate_lineage_counters(
|
||||
deliveries: &[StaticDelegateDeliveryRecord],
|
||||
delegation_id: &str,
|
||||
) -> (u32, u32) {
|
||||
let mut chain: Vec<&StaticDelegateDeliveryRecord> = Vec::new();
|
||||
let mut seen_ids: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
|
||||
let mut current_id = delegation_id;
|
||||
loop {
|
||||
if !seen_ids.insert(current_id) || chain.len() >= STATIC_DELEGATE_LINEAGE_MAX_HOPS {
|
||||
return (u32::MAX, u32::MAX);
|
||||
}
|
||||
let Some(node) = deliveries
|
||||
.iter()
|
||||
.find(|delivery| delivery.delegation_id == current_id)
|
||||
else {
|
||||
return (u32::MAX, u32::MAX);
|
||||
};
|
||||
chain.push(node);
|
||||
match node.repair_of_delegation_id.as_deref() {
|
||||
Some(parent_id) => current_id = parent_id,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
let Some(mut chain) = static_delegate_lineage_nodes(deliveries, delegation_id) else {
|
||||
return (u32::MAX, u32::MAX);
|
||||
};
|
||||
// chain 目前是 [目标 .. 根],反转成 [根 .. 目标] 便于按 R1/R2/R3 正向传播。
|
||||
chain.reverse();
|
||||
let mut depth = 0u32;
|
||||
@@ -1165,6 +1219,49 @@ pub(crate) fn static_delegate_lineage_counters(
|
||||
(depth, round)
|
||||
}
|
||||
|
||||
/// Return whether the target's durable lineage contains a status introduced by
|
||||
/// a newer client. A malformed lineage is an error rather than a negative
|
||||
/// answer: callers that use this as a mutation/Provider gate must fail closed.
|
||||
pub(crate) fn static_delegate_lineage_contains_unknown_contract_status(
|
||||
deliveries: &[StaticDelegateDeliveryRecord],
|
||||
delegation_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
let chain = static_delegate_lineage_nodes(deliveries, delegation_id)
|
||||
.ok_or_else(|| "静态委派谱系无效,无法检查未知 contractStatus".to_string())?;
|
||||
Ok(chain.iter().any(|delivery| {
|
||||
delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.is_some_and(|result| result.contract_status.is_unknown())
|
||||
}))
|
||||
}
|
||||
|
||||
fn static_delegate_lineage_nodes<'a>(
|
||||
deliveries: &'a [StaticDelegateDeliveryRecord],
|
||||
delegation_id: &str,
|
||||
) -> Option<Vec<&'a StaticDelegateDeliveryRecord>> {
|
||||
let mut chain: Vec<&StaticDelegateDeliveryRecord> = Vec::new();
|
||||
let mut seen_ids: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
|
||||
let mut current_id = delegation_id;
|
||||
loop {
|
||||
if !seen_ids.insert(current_id) || chain.len() >= STATIC_DELEGATE_LINEAGE_MAX_HOPS {
|
||||
return None;
|
||||
}
|
||||
let Some(node) = deliveries
|
||||
.iter()
|
||||
.find(|delivery| delivery.delegation_id == current_id)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
chain.push(node);
|
||||
match node.repair_of_delegation_id.as_deref() {
|
||||
Some(parent_id) => current_id = parent_id,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Some(chain)
|
||||
}
|
||||
|
||||
/// 澄清轮次上限只按发起请求所在 Run 的 source 区分(详见常量注释),
|
||||
/// 与仓库已有的 game-chat 分支先例(见 agent/runtime_tools/delegation.rs 的
|
||||
/// may_be_game_chat 判定)同源:source 缺失 binding 时按非 game-chat 处理。
|
||||
@@ -1221,6 +1318,11 @@ pub(crate) fn validate_static_delegate_repair_request_at(
|
||||
if original.target_agent_id != target_agent_id {
|
||||
return Err("静态委派返工必须交回原专业 Agent".to_string());
|
||||
}
|
||||
if static_delegate_original_has_unknown_contract_status(original) {
|
||||
return Err(
|
||||
"静态委派原 delivery 的 contractStatus 由更新版本写入,当前版本拒绝返工".to_string(),
|
||||
);
|
||||
}
|
||||
let (original_repair_depth, original_clarification_round) =
|
||||
static_delegate_lineage_counters(&deliveries, &original.delegation_id);
|
||||
if static_delegate_original_is_awaiting_clarification(original) {
|
||||
@@ -1238,6 +1340,16 @@ pub(crate) fn validate_static_delegate_repair_request_at(
|
||||
} else if original_repair_depth >= 1 {
|
||||
return Err("静态委派返工深度最多为 1".to_string());
|
||||
}
|
||||
if static_delegate_lineage_contains_unknown_contract_status(
|
||||
&deliveries,
|
||||
&original.delegation_id,
|
||||
)
|
||||
.map_err(|_| "静态委派谱系计数无效,拒绝继续".to_string())?
|
||||
{
|
||||
return Err(
|
||||
"静态委派原 delivery 所在谱系含更新版本 contractStatus,当前版本拒绝返工".to_string(),
|
||||
);
|
||||
}
|
||||
if original.acceptance_criteria != acceptance_criteria
|
||||
|| original.expected_artifacts != expected_artifacts
|
||||
{
|
||||
@@ -2856,22 +2968,48 @@ mod tests {
|
||||
"超过 32-hop 安全阀必须 fail closed"
|
||||
);
|
||||
|
||||
let encoded = serde_json::to_value(StaticDelegateContractStatus::UserRevisionRequested)
|
||||
.expect("serialize user revision status");
|
||||
assert_eq!(encoded, serde_json::json!("user-revision-requested"));
|
||||
for (status, wire) in [
|
||||
(
|
||||
StaticDelegateContractStatus::EvidenceReady,
|
||||
"evidence-ready",
|
||||
),
|
||||
(StaticDelegateContractStatus::NeedsRepair, "needs-repair"),
|
||||
(
|
||||
StaticDelegateContractStatus::NeedsUserInput,
|
||||
"needs-user-input",
|
||||
),
|
||||
(
|
||||
StaticDelegateContractStatus::UserRevisionRequested,
|
||||
"user-revision-requested",
|
||||
),
|
||||
] {
|
||||
let encoded = serde_json::to_value(&status).expect("serialize known status");
|
||||
assert_eq!(encoded, serde_json::json!(wire));
|
||||
assert_eq!(
|
||||
serde_json::from_value::<StaticDelegateContractStatus>(encoded)
|
||||
.expect("round-trip known status"),
|
||||
status
|
||||
);
|
||||
}
|
||||
let unknown_wire = "future-contract-status";
|
||||
let unknown =
|
||||
serde_json::from_value::<StaticDelegateContractStatus>(serde_json::json!(unknown_wire))
|
||||
.expect("unknown durable contract status is preserved explicitly");
|
||||
assert_eq!(
|
||||
serde_json::from_value::<StaticDelegateContractStatus>(encoded)
|
||||
.expect("round-trip user revision status"),
|
||||
StaticDelegateContractStatus::UserRevisionRequested
|
||||
unknown,
|
||||
StaticDelegateContractStatus::Unknown(unknown_wire.to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&unknown).expect("serialize unknown status"),
|
||||
serde_json::json!(unknown_wire)
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_value::<StaticDelegateContractStatus>(serde_json::json!(42)).is_err(),
|
||||
"non-string durable contract status must still fail closed"
|
||||
);
|
||||
let unknown = serde_json::from_value::<StaticDelegateContractStatus>(serde_json::json!(
|
||||
"future-contract-status"
|
||||
))
|
||||
.expect_err("unknown durable contract status must fail closed");
|
||||
assert!(unknown.to_string().contains("unknown variant"));
|
||||
|
||||
// 额外钉住 durable sidecar 读取入口:未知状态不能在 JSON sidecar 层被吞掉或
|
||||
// 降级成 Default(NeedsRepair)。
|
||||
// Durable sidecar 的未知变体可读,但必须进入 barrier、拒绝返工,并在读-改-写后
|
||||
// 保留原始 wire 字符串;不能吞掉或降级成 Default(NeedsRepair)。
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-static-unknown-status-{}-{}",
|
||||
std::process::id(),
|
||||
@@ -2882,7 +3020,8 @@ mod tests {
|
||||
));
|
||||
init_local_game_project_at(&root, "m1c0-unknown-status", "未知静态委派状态解析测试")
|
||||
.expect("project init");
|
||||
let mut record = new_static_delegate_delivery(
|
||||
let acceptance_criteria = vec!["保留未知合同状态".to_string()];
|
||||
let mut record = new_static_delegate_delivery_with_contract(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"m1c0-unknown-status-session",
|
||||
"m1c0-unknown-status-run",
|
||||
@@ -2891,30 +3030,113 @@ mod tests {
|
||||
"design-director",
|
||||
"m1c0-unknown-status-target-session",
|
||||
"m1c0-unknown-status-target-run",
|
||||
&acceptance_criteria,
|
||||
&[],
|
||||
None,
|
||||
);
|
||||
record.status = StaticDelegateDeliveryStatus::Ready;
|
||||
record.status = StaticDelegateDeliveryStatus::ClaimedByParent;
|
||||
record.terminal_status = Some("completed".to_string());
|
||||
record.result_summary = Some("unknown status fixture".to_string());
|
||||
record.claimed_by_action_id = Some("m1c0-unknown-status-claim-action".to_string());
|
||||
let mut result = StaticDelegateStructuredResult::default();
|
||||
result.contract_status = StaticDelegateContractStatus::UserRevisionRequested;
|
||||
result.contract_status = StaticDelegateContractStatus::Unknown(unknown_wire.to_string());
|
||||
record.structured_result = Some(result);
|
||||
write_static_delegate_delivery_at(&root, &record).expect("write known status fixture");
|
||||
write_static_delegate_delivery_at(&root, &record).expect("write unknown status fixture");
|
||||
let loaded = read_static_delegate_delivery_at(&root, &record.delegation_id)
|
||||
.expect("read unknown status fixture")
|
||||
.expect("unknown status delivery exists");
|
||||
assert_eq!(loaded, record);
|
||||
|
||||
let barrier = static_delegate_completion_barrier_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"m1c0-unknown-status-run",
|
||||
)
|
||||
.expect("read unknown status completion barrier");
|
||||
assert_eq!(barrier.unknown_contract_status_count, 1);
|
||||
assert!(!barrier.is_clear());
|
||||
assert!(barrier.has_waiting());
|
||||
assert!(barrier.detail().contains("unknownContractStatus=1"));
|
||||
|
||||
let repair_error = validate_static_delegate_repair_request_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"m1c0-unknown-status-run",
|
||||
"m1c0-unknown-status-repair-candidate",
|
||||
"design-director",
|
||||
&acceptance_criteria,
|
||||
&[],
|
||||
Some(&record.delegation_id),
|
||||
)
|
||||
.expect_err("unknown root status must reject the first repair unconditionally");
|
||||
assert!(repair_error.contains("更新版本") && repair_error.contains("拒绝返工"));
|
||||
|
||||
// 读-改-写只改变外层字段时,未知 status 的 raw wire 值必须原样保留。
|
||||
let mut rewritten = loaded;
|
||||
rewritten.result_summary = Some("unknown status rewritten summary".to_string());
|
||||
rewritten.updated_at = unix_timestamp();
|
||||
write_static_delegate_delivery_at(&root, &rewritten).expect("rewrite unknown status");
|
||||
let delivery_path = root
|
||||
.join(STATIC_DELEGATE_DELIVERY_DIR)
|
||||
.join(format!("{}.json", record.delegation_id));
|
||||
let mut raw = serde_json::to_value(&record).expect("serialize status fixture");
|
||||
raw["structuredResult"]["contractStatus"] = serde_json::json!("future-contract-status");
|
||||
fs::write(
|
||||
&delivery_path,
|
||||
serde_json::to_vec(&raw).expect("serialize unknown status fixture"),
|
||||
let rewritten_raw: serde_json::Value = serde_json::from_slice(
|
||||
&fs::read(&delivery_path).expect("read rewritten unknown status sidecar"),
|
||||
)
|
||||
.expect("overwrite unknown status fixture");
|
||||
let sidecar_error = read_static_delegate_delivery_at(&root, &record.delegation_id)
|
||||
.expect_err("unknown durable status must fail at sidecar read");
|
||||
assert!(
|
||||
sidecar_error.contains("解析") && sidecar_error.contains("静态委派 delivery"),
|
||||
"unexpected sidecar parse error: {sidecar_error}"
|
||||
.expect("parse rewritten unknown status sidecar");
|
||||
assert_eq!(
|
||||
rewritten_raw["structuredResult"]["contractStatus"],
|
||||
serde_json::json!(unknown_wire)
|
||||
);
|
||||
|
||||
// lineage 中的 Unknown 按“其它”分支计数,不能被误识别成用户修订或澄清。
|
||||
let mut unknown_lineage = deliveries[..2].to_vec();
|
||||
unknown_lineage[0]
|
||||
.structured_result
|
||||
.as_mut()
|
||||
.expect("lineage root structured result")
|
||||
.contract_status = StaticDelegateContractStatus::Unknown(unknown_wire.to_string());
|
||||
assert_eq!(
|
||||
static_delegate_lineage_counters(&unknown_lineage, &ids[1]),
|
||||
(1, 0)
|
||||
);
|
||||
assert!(static_delegate_lineage_contains_unknown_contract_status(
|
||||
&unknown_lineage,
|
||||
&ids[1]
|
||||
)
|
||||
.expect("inspect unknown lineage"));
|
||||
|
||||
// 损坏仍按原有整体 fail-closed 语义处理;即使目录里另有合法 delivery,也不能
|
||||
// 跳过坏记录后继续计算 barrier/lineage。
|
||||
let unrelated = new_static_delegate_delivery(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"m1c0-unrelated-session",
|
||||
"m1c0-unrelated-run",
|
||||
"m1c0-unrelated-action",
|
||||
"m1c0-unrelated-delivery",
|
||||
"art-director",
|
||||
"m1c0-unrelated-target-session",
|
||||
"m1c0-unrelated-target-run",
|
||||
);
|
||||
write_static_delegate_delivery_at(&root, &unrelated)
|
||||
.expect("write unrelated valid delivery");
|
||||
for (label, bytes) in [
|
||||
("截断 JSON", b"{not-json".to_vec()),
|
||||
("非 UTF-8", vec![b'{', 0xff, b'}']),
|
||||
(
|
||||
"超限 sidecar",
|
||||
vec![b' '; STATIC_DELEGATE_DELIVERY_MAX_BYTES + 1],
|
||||
),
|
||||
] {
|
||||
fs::write(&delivery_path, &bytes).expect("write damaged delivery sidecar");
|
||||
let error = list_static_delegate_deliveries_at(&root)
|
||||
.expect_err("damaged sidecar must lock the whole delivery directory");
|
||||
assert!(
|
||||
error.contains("静态委派 delivery"),
|
||||
"{label} returned an unrelated error: {error}"
|
||||
);
|
||||
write_static_delegate_delivery_at(&root, &rewritten)
|
||||
.expect("restore valid unknown status sidecar");
|
||||
}
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,6 +322,10 @@ pub(super) fn original_delivery_has_successful_repair(
|
||||
claimed_deliveries: &[StaticDelegateDeliveryRecord],
|
||||
) -> bool {
|
||||
original.repair_of_delegation_id.is_none()
|
||||
&& !original
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.is_some_and(|result| result.contract_status.is_unknown())
|
||||
&& claimed_deliveries.iter().any(|candidate| {
|
||||
candidate.repair_of_delegation_id.as_deref() == Some(original.delegation_id.as_str())
|
||||
&& candidate.terminal_status.as_deref() == Some("completed")
|
||||
|
||||
@@ -1293,6 +1293,26 @@ fn original_specialist_failure_is_recoverable_but_repair_failure_closes() {
|
||||
classify_failed_specialist(&parent, &child, Some(&repair), false),
|
||||
SwarmSpecialistFailureDisposition::Failed
|
||||
);
|
||||
|
||||
let mut successful_repair = repair.clone();
|
||||
successful_repair.terminal_status = Some("completed".to_string());
|
||||
let mut evidence_ready = StaticDelegateStructuredResult::default();
|
||||
evidence_ready.contract_status = StaticDelegateContractStatus::EvidenceReady;
|
||||
successful_repair.structured_result = Some(evidence_ready);
|
||||
assert!(original_delivery_has_successful_repair(
|
||||
&original,
|
||||
&[successful_repair.clone()]
|
||||
));
|
||||
|
||||
let mut unknown_original = original.clone();
|
||||
let mut unknown_result = StaticDelegateStructuredResult::default();
|
||||
unknown_result.contract_status =
|
||||
StaticDelegateContractStatus::Unknown("future-contract-status".to_string());
|
||||
unknown_original.structured_result = Some(unknown_result);
|
||||
assert!(
|
||||
!original_delivery_has_successful_repair(&unknown_original, &[successful_repair]),
|
||||
"a newer contract status must not be classified as already repaired"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user