Skip to content

Commit eadf13b

Browse files
committed
fix(warp-core): recognize create-from-absence patch shape in WAL recovery
operation_patch_scope_v1 only recognized the single-SetAttachment-op update shape. A create-from-absence patch (WarpOp::UpsertNode then WarpOp::SetAttachment, two-element out_slots) never matched, so validate_operation_receipt_state_delta rejected every durable create-from-absence commit with EchoOperationExecutionMismatch -- not only on recovery by a fresh host, but synchronously at the original commit whenever the runtime WAL is enabled. Found by Codex's automated review on PR #686. Extended the function to recognize both canonical patch shapes explicitly, rather than adding a second unrelated function, since both are the same program's only two possible outputs. RED: filesystem_wal_recovers_a_create_from_absence_commit failed at commit_prepared_echo_operation_v1 with exactly this error before the fix. GREEN: same test now passes; full pipeline suite (20 tests) and existing update-path WAL recovery test both remain green.
1 parent 53fed09 commit eadf13b

2 files changed

Lines changed: 125 additions & 17 deletions

File tree

crates/warp-core/src/trusted_runtime_host.rs

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3163,25 +3163,57 @@ fn recover_echo_operation_material(
31633163
})
31643164
}
31653165

3166+
/// Recognizes the two canonical patch shapes the anchored-node-attachment
3167+
/// compare-and-set program produces: an update (`SetAttachment` alone) and,
3168+
/// since ADR 0024, a create-from-absence (`UpsertNode` then `SetAttachment`,
3169+
/// writing both slots). Anything else -- including either op alone, either
3170+
/// op out of order, or a mismatched key/owner -- does not scope to a node.
31663171
fn operation_patch_scope_v1(patch: &crate::WorldlineTickPatchV1) -> Option<crate::NodeKey> {
3167-
let [crate::WarpOp::SetAttachment { key, .. }] = patch.ops.as_slice() else {
3168-
return None;
3169-
};
3170-
let crate::AttachmentOwner::Node(node) = key.owner else {
3171-
return None;
3172+
let node = match patch.ops.as_slice() {
3173+
[crate::WarpOp::SetAttachment { key, .. }] => {
3174+
let crate::AttachmentOwner::Node(node) = key.owner else {
3175+
return None;
3176+
};
3177+
let attachment_slot = crate::AttachmentKey::node_alpha(node);
3178+
if *key != attachment_slot
3179+
|| patch.warp_id != node.warp_id
3180+
|| patch.in_slots.as_slice()
3181+
!= [
3182+
crate::SlotId::Node(node),
3183+
crate::SlotId::Attachment(attachment_slot),
3184+
]
3185+
|| patch.out_slots.as_slice() != [crate::SlotId::Attachment(attachment_slot)]
3186+
{
3187+
return None;
3188+
}
3189+
node
3190+
}
3191+
[crate::WarpOp::UpsertNode { node, .. }, crate::WarpOp::SetAttachment { key, .. }] => {
3192+
let node = *node;
3193+
let crate::AttachmentOwner::Node(attachment_node) = key.owner else {
3194+
return None;
3195+
};
3196+
let attachment_slot = crate::AttachmentKey::node_alpha(node);
3197+
if attachment_node != node
3198+
|| *key != attachment_slot
3199+
|| patch.warp_id != node.warp_id
3200+
|| patch.in_slots.as_slice()
3201+
!= [
3202+
crate::SlotId::Node(node),
3203+
crate::SlotId::Attachment(attachment_slot),
3204+
]
3205+
|| patch.out_slots.as_slice()
3206+
!= [
3207+
crate::SlotId::Node(node),
3208+
crate::SlotId::Attachment(attachment_slot),
3209+
]
3210+
{
3211+
return None;
3212+
}
3213+
node
3214+
}
3215+
_ => return None,
31723216
};
3173-
let attachment_slot = crate::AttachmentKey::node_alpha(node);
3174-
if *key != attachment_slot
3175-
|| patch.warp_id != node.warp_id
3176-
|| patch.in_slots.as_slice()
3177-
!= [
3178-
crate::SlotId::Node(node),
3179-
crate::SlotId::Attachment(attachment_slot),
3180-
]
3181-
|| patch.out_slots.as_slice() != [crate::SlotId::Attachment(attachment_slot)]
3182-
{
3183-
return None;
3184-
}
31853217
Some(node)
31863218
}
31873219

crates/warp-core/tests/executable_operation_pipeline_tests.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,3 +2080,79 @@ fn create_from_absence_cannot_commit_after_its_parent_basis_changes() {
20802080
)))
20812081
);
20822082
}
2083+
2084+
/// Regression for PR #686 review finding #1 (Codex, P0): a durably committed
2085+
/// create-from-absence patch (WarpOp::UpsertNode + WarpOp::SetAttachment,
2086+
/// two-element out_slots) must be recoverable by a fresh host, exactly like
2087+
/// an update-shaped patch already is.
2088+
#[test]
2089+
fn filesystem_wal_recovers_a_create_from_absence_commit() {
2090+
let wal_dir = TempWalDir::new();
2091+
let attachment_type = make_type_id("operation-fixture-atom");
2092+
let new_node = NodeKey {
2093+
warp_id: make_warp_id("operation-fixture"),
2094+
local_id: make_node_id("operation-fixture-wal-created"),
2095+
};
2096+
let application_basis =
2097+
warp_core::echo_operation_anchored_node_absent_application_basis_v1(new_node);
2098+
2099+
{
2100+
let (mut host, head_key, _existing_node) = fixture_host();
2101+
host.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path()))
2102+
.expect("the fresh filesystem WAL opens");
2103+
let installed = install_fixture_operation(&mut host);
2104+
let evaluation_basis = host
2105+
.echo_operation_evaluation_basis_v1(head_key, application_basis)
2106+
.expect("Echo resolves the exact current parent basis");
2107+
let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set(
2108+
installed.package_id(),
2109+
installed.operation_coordinate(),
2110+
evaluation_basis,
2111+
digest("fixture-authority-grant"),
2112+
EchoOperationBudgetV1::new(16, 4_096, 4_096),
2113+
new_node,
2114+
None,
2115+
b"wal-created".to_vec(),
2116+
);
2117+
let invocation_bytes = invocation.to_canonical_bytes().expect("invocation encodes");
2118+
let admitted = host
2119+
.admit_echo_operation_invocation_v1(
2120+
&EchoOperationInvocationAdmissionPolicyV1::new(
2121+
digest("fixture-authority-profile"),
2122+
digest("fixture-authority-grant"),
2123+
EchoOperationBudgetV1::new(16, 4_096, 4_096),
2124+
),
2125+
&invocation_bytes,
2126+
)
2127+
.expect("the create-from-absence invocation is independently admitted");
2128+
let EchoOperationPreparationV1::Prepared(prepared) =
2129+
host.prepare_echo_operation_v1(admitted)
2130+
else {
2131+
panic!("the lawful create-from-absence invocation must prepare");
2132+
};
2133+
host.commit_prepared_echo_operation_v1(prepared)
2134+
.expect("the durable create-from-absence operation commits");
2135+
}
2136+
2137+
let (mut recovered, head_key, _existing_node) = fixture_host();
2138+
recovered
2139+
.enable_runtime_wal(TrustedRuntimeWalConfig::filesystem(wal_dir.path()))
2140+
.expect(
2141+
"a fresh host recovers a create-from-absence commit without replaying application code",
2142+
);
2143+
let state = recovered
2144+
.runtime()
2145+
.worldlines()
2146+
.get(&head_key.worldline_id)
2147+
.expect("the recovered worldline exists")
2148+
.state();
2149+
assert_eq!(
2150+
state
2151+
.store(&new_node.warp_id)
2152+
.and_then(|store| store.node_attachment(&new_node.local_id)),
2153+
Some(&AttachmentValue::Atom(AtomPayload::new(
2154+
attachment_type,
2155+
Bytes::from_static(b"wal-created"),
2156+
)))
2157+
);
2158+
}

0 commit comments

Comments
 (0)