Skip to content

Commit c2e438b

Browse files
committed
fix(warp-core): classify every occupied create target as PreconditionMismatch
record.ty was checked before expects_creation in the Some(record) arm, so create-from-absence against an existing node with the wrong type returned NodeTypeMismatch instead of PreconditionMismatch -- contradicting ADR 0024's own stated contract that every occupied create target refuses uniformly as a precondition failure, regardless of what specifically differs. Found by Codex's automated review on PR #686. Moved the expects_creation check to the top of the Some(record) arm, before the type/attachment checks that exist to serve the update path. This also simplified the code: the now-unreachable expects_creation branches inside the attachment-lookup ternary and the redundant standalone check after it are gone. Updated create_from_absence_refuses_when_the_node_exists_with_the_wrong_type to expect PreconditionMismatch. This left NodeTypeMismatch completely untested, so added update_precondition_refuses_when_the_node_has_the_wrong_type proving it's still correctly reachable on the update path, where a caller who correctly expected the node to exist named the wrong type. 24 tests pass (was 23); full pipeline suite green.
1 parent 658daec commit c2e438b

2 files changed

Lines changed: 78 additions & 15 deletions

File tree

crates/warp-core/src/echo_operation.rs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2443,6 +2443,19 @@ pub(crate) fn prepare_operation_v1(
24432443
]
24442444
}
24452445
Some(record) => {
2446+
// PR #686 review finding #4 (Codex): every occupied
2447+
// create target refuses with PreconditionMismatch per
2448+
// ADR 0024's own stated contract ("something already
2449+
// exists where absence was claimed"), regardless of
2450+
// *what* about it differs from the package's
2451+
// requirements. Checking this before the type/attachment
2452+
// checks below (which exist to serve the update path)
2453+
// keeps that uniform for callers, rather than leaking
2454+
// update-path-specific obstruction kinds through a
2455+
// creation-shaped precondition.
2456+
if expects_creation {
2457+
return obstruction(EchoOperationObstructionKindV1::PreconditionMismatch);
2458+
}
24462459
if record.ty != required_node_type {
24472460
return obstruction(EchoOperationObstructionKindV1::NodeTypeMismatch);
24482461
}
@@ -2451,15 +2464,8 @@ pub(crate) fn prepare_operation_v1(
24512464
}
24522465
actual_footprint.a_read.insert(slot);
24532466
let Some(attachment) = store.node_attachment(&node.local_id) else {
2454-
return obstruction(if expects_creation {
2455-
EchoOperationObstructionKindV1::PreconditionMismatch
2456-
} else {
2457-
EchoOperationObstructionKindV1::AttachmentMissing
2458-
});
2467+
return obstruction(EchoOperationObstructionKindV1::AttachmentMissing);
24592468
};
2460-
if expects_creation {
2461-
return obstruction(EchoOperationObstructionKindV1::PreconditionMismatch);
2462-
}
24632469
let AttachmentValue::Atom(atom) = attachment else {
24642470
return obstruction(EchoOperationObstructionKindV1::AttachmentNotAtom);
24652471
};

crates/warp-core/tests/executable_operation_pipeline_tests.rs

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1978,12 +1978,16 @@ fn update_precondition_still_refuses_when_the_node_is_absent() {
19781978
);
19791979
}
19801980

1981-
/// ADR 0024: create-from-absence refuses with `NodeTypeMismatch`, not a
1982-
/// generic precondition failure, when a node exists at the claimed-absent
1983-
/// coordinate but with a different `NodeRecord.ty` than the installed
1984-
/// package declares. The node has no attachment set, so admission's coarser
1985-
/// check (which never inspects node type) still sees "absent" and admits;
1986-
/// `prepare_operation_v1`'s finer check is what refuses.
1981+
/// Regression for PR #686 review finding #4 (Codex, P2): create-from-absence
1982+
/// refuses with `PreconditionMismatch`, not `NodeTypeMismatch`, when a node
1983+
/// exists at the claimed-absent coordinate with a different `NodeRecord.ty`
1984+
/// than the installed package declares. ADR 0024 states every occupied
1985+
/// create target refuses as a precondition failure, regardless of what about
1986+
/// it differs -- `NodeTypeMismatch` is reserved for the update path, where a
1987+
/// caller who correctly expected the node to exist got its type wrong. The
1988+
/// node has no attachment set, so admission's coarser check (which never
1989+
/// inspects node type) still sees "absent" and admits; `prepare_operation_v1`'s
1990+
/// finer check is what refuses.
19871991
#[test]
19881992
fn create_from_absence_refuses_when_the_node_exists_with_the_wrong_type() {
19891993
let wrong_type_node_id = make_node_id("operation-fixture-wrong-type");
@@ -2024,7 +2028,7 @@ fn create_from_absence_refuses_when_the_node_exists_with_the_wrong_type() {
20242028
};
20252029
assert_eq!(
20262030
obstruction.kind(),
2027-
EchoOperationObstructionKindV1::NodeTypeMismatch
2031+
EchoOperationObstructionKindV1::PreconditionMismatch
20282032
);
20292033
}
20302034

@@ -2412,3 +2416,56 @@ fn create_from_absence_charges_the_node_record_against_the_write_budget() {
24122416
EchoOperationObstructionKindV1::BudgetExceeded
24132417
);
24142418
}
2419+
2420+
/// Companion to the PR #686 review finding #4 fix: `NodeTypeMismatch` is not
2421+
/// dead code after `expects_creation` is checked first in the `Some(record)`
2422+
/// arm -- it remains the correct obstruction for the update path, where a
2423+
/// caller who correctly expected the node to exist (`Some(digest)`) named
2424+
/// the wrong type.
2425+
#[test]
2426+
fn update_precondition_refuses_when_the_node_has_the_wrong_type() {
2427+
let wrong_type_node_id = make_node_id("operation-fixture-update-wrong-type");
2428+
let wrong_node_type = make_type_id("operation-fixture-wrong-node-type");
2429+
let attachment_type = make_type_id("operation-fixture-atom");
2430+
let (mut host, head_key, _existing_node, wrong_type_node) =
2431+
fixture_host_with_bare_node(wrong_type_node_id, wrong_node_type);
2432+
let installed = install_fixture_operation(&mut host);
2433+
2434+
let application_basis =
2435+
warp_core::echo_operation_anchored_node_absent_application_basis_v1(wrong_type_node);
2436+
let evaluation_basis = host
2437+
.echo_operation_evaluation_basis_v1(head_key, application_basis)
2438+
.expect("Echo resolves the exact current parent basis");
2439+
let invocation = EchoOperationInvocationV1::anchored_node_attachment_compare_and_set(
2440+
installed.package_id(),
2441+
installed.operation_coordinate(),
2442+
evaluation_basis,
2443+
digest("fixture-authority-grant"),
2444+
EchoOperationBudgetV1::new(16, 4_096, 4_096),
2445+
wrong_type_node,
2446+
Some(warp_core::echo_operation_atom_value_digest_v1(
2447+
attachment_type,
2448+
b"before",
2449+
)),
2450+
b"after".to_vec(),
2451+
);
2452+
let invocation_bytes = invocation
2453+
.to_canonical_bytes()
2454+
.expect("the update invocation is canonical");
2455+
let invocation_policy = EchoOperationInvocationAdmissionPolicyV1::new(
2456+
digest("fixture-authority-profile"),
2457+
digest("fixture-authority-grant"),
2458+
EchoOperationBudgetV1::new(16, 4_096, 4_096),
2459+
);
2460+
let admitted_invocation = host
2461+
.admit_echo_operation_invocation_v1(&invocation_policy, &invocation_bytes)
2462+
.expect("Echo admits the invocation -- node type is not an admission-time check");
2463+
let preparation = host.prepare_echo_operation_v1(admitted_invocation);
2464+
let EchoOperationPreparationV1::Obstructed(obstruction) = preparation else {
2465+
panic!("an update against a wrong-typed existing node must not prepare a patch");
2466+
};
2467+
assert_eq!(
2468+
obstruction.kind(),
2469+
EchoOperationObstructionKindV1::NodeTypeMismatch
2470+
);
2471+
}

0 commit comments

Comments
 (0)