gate: retarget the kernel test suites at identity binding
commit eb79d4a
gate: retarget the kernel test suites at identity binding
Rewrite the gate, receive, sync, and query integration tests for
oid-keyed hash-identified refnames and trailer-free commits, and add the
phase-10 identity cases: the doppelgänger replay (a signed mutation
re-proposed as a genesis) and the result replay (a signed pass for
another effect or commit) are refused through receive; the all-roots
walk holds across a sync-created merge and is never applied to a pin;
natural-key (member, effect) and composite (review, result) refnames
recompute; strict decode refuses an unknown genesis tree entry;
owner-mutation refuses the wrong reviewer and a non-owner advance;
creation via the inbox still works.
test: doppelgänger + result replay refused via receive (gate.identity-binding, model.result-identity)
test: all-roots across a merge; pins skip the walk (meta-ref.identity-binding)
test: natural-key/composite recompute and strict decode (gate.identity-binding)
test: owner-mutation review + comment advance cases (gate.owner-mutation)
Assisted-by: Claude:claude-opus-4-8
No reviews of this commit yet — record a verdict below.
Start a review
crates/kernel/ents-gate/tests/gate.rs
@@ -1,7 +1,7 @@
//! Integration tests for the gate: the verdict table (rstest — the spec
-//! enumerates the cases), the epoch and bootstrap windows, and the
-//! one-parameterized-test proof that all three call sites see identical
-//! verdicts.
+//! enumerates the cases), identity binding and owner mutation, the epoch
+//! and bootstrap windows, and the one-parameterized-test proof that all
+//! three call sites see identical verdicts.
#![expect(
clippy::expect_used,
@@ -11,8 +11,7 @@
)]
use ents_gate::{AdmissionKind, Config, Requirement, Update, Verdict, verify};
-use ents_model::trailer::Trailers;
-use ents_model::{Member, MemberId, Provenance, namespace};
+use ents_model::{Effect, Member, MemberId, Provenance, ResultRecord, Status, namespace};
use ents_testutil::{
CommitSpec, Keypair, MemRefStore, ObjectStore, empty_tree, enroll_member, write_commit,
write_member, write_meta_entity,
@@ -78,43 +77,57 @@
s.try_into().expect("valid refname in test")
}
-/// A proposal commit: empty tree, explicit parents/trailer/key/time.
-fn proposal(
- forge: &Forge,
- parents: Vec<ObjectId>,
- ents_ref: Option<&str>,
- key: Option<&Keypair>,
- seconds: i64,
-) -> ObjectId {
+/// The oid-keyed refname of a hash-identified issue whose genesis is
+/// `genesis` (`meta-ref.identity-binding`).
+fn issue_ref(genesis: ObjectId) -> FullName {
+ name(&format!("refs/meta/issues/{genesis}"))
+}
+
+/// A signed empty-tree commit — a generic meta-mutation body. A
+/// hash-identified entity binds by its genesis oid and the all-roots
+/// walk, not by tree content, so an empty tree exercises the tip
+/// invariant on `refs/meta/issues/*` and `refs/meta/comments/*` fully.
+fn commit(forge: &Forge, parents: Vec<ObjectId>, key: Option<&Keypair>, seconds: i64) -> ObjectId {
let tree = empty_tree(&forge.objects);
- let message = ents_ref.map_or_else(
- || "mutate\n\nno trailer here\n".to_owned(),
- |r| {
- let trailers = Trailers {
- ents_ref: Some(name(r)),
- schema_version: None,
- };
- format!("mutate\n\n{}", trailers.render())
- },
- );
write_commit(
&forge.objects,
&CommitSpec {
tree,
parents,
- message,
+ message: "mutate".into(),
seconds,
},
key,
)
}
-fn run(forge: &Forge, refname: &str, new: Option<ObjectId>) -> Verdict {
+/// A signed commit whose tree is `tree` — for the namespaces whose binding
+/// reads a tree field (natural-key, composite).
+fn tree_commit(
+ forge: &Forge,
+ tree: ObjectId,
+ parents: Vec<ObjectId>,
+ key: Option<&Keypair>,
+ seconds: i64,
+) -> ObjectId {
+ write_commit(
+ &forge.objects,
+ &CommitSpec {
+ tree,
+ parents,
+ message: "mutate".into(),
+ seconds,
+ },
+ key,
+ )
+}
+
+fn run(forge: &Forge, refname: &FullName, new: Option<ObjectId>) -> Verdict {
verify(
&forge.refs,
&forge.objects,
&Update {
- name: name(refname),
+ name: refname.clone(),
new,
},
)
@@ -136,16 +149,16 @@
}
// ---------------------------------------------------------------------
-// The verdict table: member × trailer × FF × namespace cases.
+// The verdict table: member × signature × FF × namespace cases.
// ---------------------------------------------------------------------
#[rstest]
// @relation(gate.tip-signed, gate.verdict-reason, scope=function, role=Verifies)
fn authorized_signed_mutation_passes_the_tip_invariant() {
let f = forge();
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), Some(&f.admin), 300);
+ let new = commit(&f, vec![], Some(&f.admin), 300);
expect_pass(
- &run(&f, "refs/meta/issues/1", Some(new)),
+ &run(&f, &issue_ref(new), Some(new)),
AdmissionKind::TipInvariant,
);
}
@@ -154,11 +167,8 @@
// @relation(gate.tip-signed, scope=function, role=Verifies)
fn unsigned_tip_is_refused() {
let f = forge();
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), None, 300);
- expect_fail(
- &run(&f, "refs/meta/issues/1", Some(new)),
- Requirement::TipSigned,
- );
+ let new = commit(&f, vec![], None, 300);
+ expect_fail(&run(&f, &issue_ref(new), Some(new)), Requirement::TipSigned);
}
#[rstest]
@@ -166,17 +176,14 @@
fn non_member_signature_is_refused() {
let f = forge();
let outsider = Keypair::from_seed(OUTSIDER_SEED);
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), Some(&outsider), 300);
- expect_fail(
- &run(&f, "refs/meta/issues/1", Some(new)),
- Requirement::TipSigned,
- );
+ let new = commit(&f, vec![], Some(&outsider), 300);
+ expect_fail(&run(&f, &issue_ref(new), Some(new)), Requirement::TipSigned);
}
/// Revoke `id`'s key in the fixture, as an admin-signed mutation of the
/// member's ref.
fn revoke(f: &Forge, id: &str, key: &Keypair, provenance: Provenance, seconds: i64) {
- let mut revoked = Member::new(key.public_openssh(), provenance);
+ let mut revoked = Member::new(id, key.public_openssh(), provenance);
revoked.revoke();
write_member(&f.refs, &f.objects, id, &revoked, Some(&f.admin), seconds);
}
@@ -187,8 +194,8 @@
let f = forge();
revoke(&f, "admin", &f.admin, Provenance::AdminRegistered, 400);
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), Some(&f.admin), 500);
- let verdict = run(&f, "refs/meta/issues/1", Some(new));
+ let new = commit(&f, vec![], Some(&f.admin), 500);
+ let verdict = run(&f, &issue_ref(new), Some(new));
expect_fail(&verdict, Requirement::TipSigned);
let Verdict::Fail(refusal) = &verdict else {
unreachable!()
@@ -200,35 +207,26 @@
// @relation(model.member-revocation, gate.tip-signed, scope=function, role=Verifies)
fn a_backdated_commit_cannot_reach_past_a_revocation() {
// The security-review regression: admission consults the member
- // entity currently in force, so a revoked key authoring a NEW
- // commit with a committer timestamp claimed from before the
- // revocation — descending cleanly from the live tip — is still
- // refused. No commit-supplied time participates in the judgment.
+ // entity currently in force, so a revoked key authoring a NEW commit
+ // with a committer timestamp claimed from before the revocation —
+ // descending cleanly from the live tip — is still refused.
let f = forge();
- let refname = "refs/meta/issues/1";
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 300,
- );
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = issue_ref(genesis);
+ f.refs.set(refname.as_ref(), genesis);
revoke(&f, "admin", &f.admin, Provenance::AdminRegistered, 400);
// Authored "at 300", pushed after the revocation at 400.
- let backdated = proposal(&f, vec![tip], Some(refname), Some(&f.admin), 300);
- expect_fail(&run(&f, refname, Some(backdated)), Requirement::TipSigned);
+ let backdated = commit(&f, vec![genesis], Some(&f.admin), 300);
+ expect_fail(&run(&f, &refname, Some(backdated)), Requirement::TipSigned);
}
#[rstest]
// @relation(model.member-revocation, gate.fast-forward, scope=function, role=Verifies)
fn refs_accepted_before_a_revocation_are_never_rejudged() {
- // A second admin keeps working on a ref whose current tip was
- // placed by a member revoked afterwards: the accepted tip stays
- // valid history — the gate judges only the proposed update, and
- // fast-forwarding over ancestry signed by a now-revoked key is
- // ordinary descent, not a re-judgment of past acceptance.
+ // A second admin keeps working on a ref whose current tip was placed
+ // by a member revoked afterwards: the accepted tip stays valid
+ // history, and fast-forwarding over it is ordinary descent.
let f = forge();
let second = Keypair::from_seed(OUTSIDER_SEED);
enroll_member(
@@ -240,34 +238,33 @@
310,
);
- let refname = "refs/meta/issues/1";
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 320,
- );
+ let genesis = commit(&f, vec![], Some(&f.admin), 320);
+ let refname = issue_ref(genesis);
+ f.refs.set(refname.as_ref(), genesis);
revoke(&f, "admin", &f.admin, Provenance::AdminRegistered, 400);
- let continued = proposal(&f, vec![tip], Some(refname), Some(&second), 500);
+ let continued = commit(&f, vec![genesis], Some(&second), 500);
expect_pass(
- &run(&f, refname, Some(continued)),
+ &run(&f, &refname, Some(continued)),
AdmissionKind::TipInvariant,
);
}
#[rstest]
-#[case::wrong_ref(Some("refs/meta/issues/2"))]
-#[case::missing_trailer(None)]
-// @relation(gate.refname-binding, scope=function, role=Verifies)
-fn refname_binding_mismatch_is_refused(#[case] trailer: Option<&str>) {
+// @relation(gate.identity-binding, meta-ref.identity-binding, scope=function, role=Verifies)
+fn a_refname_not_naming_the_genesis_oid_is_refused() {
+ // The identity binding replaces the retired Advance-ref trailer: a
+ // signed commit proposed under a refname whose final segment is not
+ // its genesis oid is refused, so it cannot be replayed as the tip of a
+ // different meta-ref than the one its content names.
let f = forge();
- let new = proposal(&f, vec![], trailer, Some(&f.admin), 300);
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ // A different, valid oid that is not this commit's genesis.
+ let wrong =
+ issue_ref(ObjectId::from_hex(b"00000000000000000000000000000000deadbeef").expect("hex"));
expect_fail(
- &run(&f, "refs/meta/issues/1", Some(new)),
- Requirement::RefnameBinding,
+ &run(&f, &wrong, Some(genesis)),
+ Requirement::IdentityBinding,
);
}
@@ -275,51 +272,352 @@
// @relation(gate.fast-forward, scope=function, role=Verifies)
fn non_fast_forward_is_refused() {
let f = forge();
- let refname = "refs/meta/issues/1";
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 300,
- );
- // A sibling that does not descend from `tip`.
- let sibling = proposal(&f, vec![], Some(refname), Some(&f.admin), 310);
- assert_ne!(sibling, tip);
- expect_fail(&run(&f, refname, Some(sibling)), Requirement::FastForward);
+ // A genesis and two children of it, all correctly bound to the same
+ // oid-keyed ref; the current tip is one child, the proposal the
+ // sibling — a genuine fast-forward refusal, not an identity mismatch.
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = issue_ref(genesis);
+ let current = commit(&f, vec![genesis], Some(&f.admin), 310);
+ let sibling = commit(&f, vec![genesis], Some(&f.admin), 311);
+ f.refs.set(refname.as_ref(), current);
+ assert_ne!(sibling, current);
+ expect_fail(&run(&f, &refname, Some(sibling)), Requirement::FastForward);
}
#[rstest]
// @relation(gate.fast-forward, scope=function, role=Verifies)
fn meta_ref_deletion_is_refused() {
let f = forge();
- write_meta_entity(
- &f.refs,
- &f.objects,
- name("refs/meta/issues/1"),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 300,
- );
- expect_fail(
- &run(&f, "refs/meta/issues/1", None),
- Requirement::FastForward,
- );
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = issue_ref(genesis);
+ f.refs.set(refname.as_ref(), genesis);
+ expect_fail(&run(&f, &refname, None), Requirement::FastForward);
}
#[rstest]
// @relation(gate.principled-split, scope=function, role=Verifies)
fn code_refs_are_not_subject_to_the_tip_invariant() {
let f = forge();
- // Unsigned, trailerless, non-FF — none of it matters outside refs/meta/*.
- let new = proposal(&f, vec![], None, None, 300);
+ // Unsigned, non-FF — none of it matters outside refs/meta/*.
+ let new = commit(&f, vec![], None, 300);
expect_pass(
- &run(&f, "refs/heads/main", Some(new)),
+ &run(&f, &name("refs/heads/main"), Some(new)),
AdmissionKind::CodeRef,
);
}
+// ---------------------------------------------------------------------
+// Identity binding: the refname is a function of signed content.
+// ---------------------------------------------------------------------
+
+#[rstest]
+// @relation(gate.identity-binding, meta-ref.identity-binding, model.member-identity, scope=function, role=Verifies)
+fn a_natural_key_member_binds_by_its_id_field() {
+ let f = forge();
+ // A member entity whose id field disagrees with the refname's final
+ // segment is refused; agreeing, it passes.
+ let mismatched = Member::new(
+ "someone-else",
+ f.admin.public_openssh(),
+ Provenance::AdminRegistered,
+ );
+ let tree = facet_git_tree::serialize_into(&mismatched, &f.objects).expect("ser");
+ let tip = tree_commit(&f, tree, vec![], Some(&f.admin), 300);
+ expect_fail(
+ &run(&f, &name("refs/meta/member/newcomer"), Some(tip)),
+ Requirement::IdentityBinding,
+ );
+
+ let matched = Member::new(
+ "newcomer",
+ f.admin.public_openssh(),
+ Provenance::AdminRegistered,
+ );
+ let tree = facet_git_tree::serialize_into(&matched, &f.objects).expect("ser");
+ let tip = tree_commit(&f, tree, vec![], Some(&f.admin), 300);
+ expect_pass(
+ &run(&f, &name("refs/meta/member/newcomer"), Some(tip)),
+ AdmissionKind::TipInvariant,
+ );
+}
+
+#[rstest]
+// @relation(gate.identity-binding, meta-ref.identity-binding, model.effect-definition, scope=function, role=Verifies)
+fn a_natural_key_effect_binds_by_its_name_field() {
+ let f = forge();
+ let effect = Effect {
+ name: "unit".into(),
+ trigger: "rev(refs/heads/main)".into(),
+ toolchains: vec![],
+ run: "true".into(),
+ };
+ let tree = facet_git_tree::serialize_into(&effect, &f.objects).expect("ser");
+ // Named `unit` in the tree — refused under a ref that names `lint`.
+ let tip = tree_commit(&f, tree, vec![], Some(&f.admin), 300);
+ expect_fail(
+ &run(&f, &name("refs/meta/effects/lint"), Some(tip)),
+ Requirement::IdentityBinding,
+ );
+ // Under the ref its name field recomputes, it passes.
+ expect_pass(
+ &run(&f, &name("refs/meta/effects/unit"), Some(tip)),
+ AdmissionKind::TipInvariant,
+ );
+}
+
+/// A commit recording `result` at `results/<effect>/<short>`.
+fn result_commit(
+ f: &Forge,
+ effect: &str,
+ target: ObjectId,
+ parents: Vec<ObjectId>,
+ key: Option<&Keypair>,
+ seconds: i64,
+) -> ObjectId {
+ let record = ResultRecord::new(effect, target, Status::Pass);
+ let tree = facet_git_tree::serialize_into(&record, &f.objects).expect("ser");
+ tree_commit(f, tree, parents, key, seconds)
+}
+
+#[rstest]
+// @relation(gate.identity-binding, model.result-identity, scope=function, role=Verifies)
+fn a_composite_result_binds_by_its_effect_and_target_fields() {
+ let f = forge();
+ let target = ObjectId::from_hex(b"abc1230000000000000000000000000000000000").expect("hex");
+ let tip = result_commit(&f, "unit", target, vec![], Some(&f.admin), 300);
+
+ // Correct effect and short-oid prefix: passes.
+ expect_pass(
+ &run(
+ &f,
+ &namespace::result_ref("unit", "abc123").expect("valid"),
+ Some(tip),
+ ),
+ AdmissionKind::TipInvariant,
+ );
+ // Wrong effect segment: refused.
+ expect_fail(
+ &run(
+ &f,
+ &namespace::result_ref("lint", "abc123").expect("valid"),
+ Some(tip),
+ ),
+ Requirement::IdentityBinding,
+ );
+ // Short oid that is not a prefix of the target field: refused.
+ expect_fail(
+ &run(
+ &f,
+ &namespace::result_ref("unit", "ffffff").expect("valid"),
+ Some(tip),
+ ),
+ Requirement::IdentityBinding,
+ );
+}
+
+/// A stand-in Review tree: the gate reads only the `target` field
+/// generically, so any struct carrying it exercises the composite key.
+#[derive(facet::Facet)]
+struct Review {
+ target: [u8; 20],
+ verdict: String,
+}
+
+fn review_commit(f: &Forge, target: ObjectId, key: Option<&Keypair>, seconds: i64) -> ObjectId {
+ let mut bytes = [0u8; 20];
+ bytes.copy_from_slice(target.as_slice());
+ let review = Review {
+ target: bytes,
+ verdict: "approve".into(),
+ };
+ let tree = facet_git_tree::serialize_into(&review, &f.objects).expect("ser");
+ tree_commit(f, tree, vec![], key, seconds)
+}
+
+#[rstest]
+// @relation(gate.identity-binding, model.review, scope=function, role=Verifies)
+fn a_composite_review_binds_by_its_target_field_and_signer() {
+ let f = forge();
+ let target = ObjectId::from_hex(b"deadbeef00000000000000000000000000000000").expect("hex");
+ let tip = review_commit(&f, target, Some(&f.admin), 300);
+ let good = namespace::review_ref(&target.to_string(), &MemberId::new("admin")).expect("valid");
+ expect_pass(&run(&f, &good, Some(tip)), AdmissionKind::TipInvariant);
+
+ // Right target, wrong reviewer segment: the signer is admin, not guest.
+ let wrong_member =
+ namespace::review_ref(&target.to_string(), &MemberId::new("guest")).expect("valid");
+ expect_fail(
+ &run(&f, &wrong_member, Some(tip)),
+ Requirement::IdentityBinding,
+ );
+
+ // Right reviewer, wrong target segment.
+ let other = ObjectId::from_hex(b"0000000000000000000000000000000000000001").expect("hex");
+ let wrong_target =
+ namespace::review_ref(&other.to_string(), &MemberId::new("admin")).expect("valid");
+ expect_fail(
+ &run(&f, &wrong_target, Some(tip)),
+ Requirement::IdentityBinding,
+ );
+}
+
+/// A result tree carrying an entry that is not a `ResultRecord` field —
+/// for the strict-decode disjointness check.
+#[derive(facet::Facet)]
+struct ResultPlus {
+ effect: String,
+ target: [u8; 20],
+ status: Status,
+ surprise: String,
+}
+
+#[rstest]
+// @relation(gate.identity-binding, meta-ref.typed-tree, scope=function, role=Verifies)
+fn strict_genesis_decode_refuses_an_unknown_tree_entry() {
+ let f = forge();
+ let target = ObjectId::from_hex(b"abc1230000000000000000000000000000000000").expect("hex");
+ let mut bytes = [0u8; 20];
+ bytes.copy_from_slice(target.as_slice());
+ let bogus = ResultPlus {
+ effect: "unit".into(),
+ target: bytes,
+ status: Status::Pass,
+ surprise: "not a result field".into(),
+ };
+ let tree = facet_git_tree::serialize_into(&bogus, &f.objects).expect("ser");
+ let tip = tree_commit(&f, tree, vec![], Some(&f.admin), 300);
+ // Even though effect and short-oid recompute, the extra tree entry
+ // makes strict decode refuse the genesis.
+ expect_fail(
+ &run(
+ &f,
+ &namespace::result_ref("unit", "abc123").expect("valid"),
+ Some(tip),
+ ),
+ Requirement::IdentityBinding,
+ );
+}
+
+#[rstest]
+// @relation(gate.identity-binding, gate.same-actor-divergence, scope=function, role=Verifies)
+fn the_all_roots_walk_holds_across_a_sync_created_merge() {
+ // Two children of the same genesis, merged: the merge has two parents
+ // but a single parentless root (the genesis), so the hash-identified
+ // binding still recomputes the genesis oid across the merge commit.
+ let f = forge();
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = issue_ref(genesis);
+ let a = commit(&f, vec![genesis], Some(&f.admin), 310);
+ let b = commit(&f, vec![genesis], Some(&f.admin), 311);
+ f.refs.set(refname.as_ref(), a);
+ let merge = commit(&f, vec![a, b], Some(&f.admin), 320);
+ expect_pass(&run(&f, &refname, Some(merge)), AdmissionKind::TipInvariant);
+
+ // The same merge proposed under a doppelgänger genesis id (the merge's
+ // own oid) is refused: its parentless root is still the genesis.
+ expect_fail(
+ &run(&f, &issue_ref(merge), Some(merge)),
+ Requirement::IdentityBinding,
+ );
+}
+
+#[rstest]
+// @relation(gate.identity-binding, model.review-pin, scope=function, role=Verifies)
+fn a_pin_is_never_subjected_to_the_all_roots_walk() {
+ // A pin's ancestry reaches into code history (its parents include the
+ // reviewed commit), so the parentless-roots walk must not apply. A pin
+ // commit whose parent is an arbitrary reviewed commit — not a genesis
+ // whose oid is the pin's id — still binds.
+ let f = forge();
+ let reviewed = commit(&f, vec![], Some(&f.admin), 250);
+ let target = reviewed.to_string();
+ let pin_ref = namespace::review_pin_ref(&target, &MemberId::new("admin")).expect("valid");
+ // The pin's tip is a signed commit whose parents include the reviewed
+ // commit; its tree is empty. The all-roots walk would reject it (the
+ // root is `reviewed`, not the pin's segments), so this passing verdict
+ // proves the walk is skipped for pins.
+ let pin_tip = commit(&f, vec![reviewed], Some(&f.admin), 300);
+ expect_pass(
+ &run(&f, &pin_ref, Some(pin_tip)),
+ AdmissionKind::TipInvariant,
+ );
+}
+
+// ---------------------------------------------------------------------
+// Owner mutation: an advance is keyed to ownership.
+// ---------------------------------------------------------------------
+
+#[rstest]
+// @relation(gate.owner-mutation, scope=function, role=Verifies)
+fn an_admin_may_advance_another_members_hash_identified_entity() {
+ // Ownership of a hash-identified entity is intrinsic to its id, but an
+ // admin-registered member may advance it too (∪ admins). A second
+ // admin advances the first admin's comment.
+ let f = forge();
+ let second = Keypair::from_seed(OUTSIDER_SEED);
+ enroll_member(
+ &f.refs,
+ &f.objects,
+ "second",
+ &second,
+ Provenance::AdminRegistered,
+ 210,
+ );
+
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = name(&format!("refs/meta/comments/{genesis}"));
+ f.refs.set(refname.as_ref(), genesis);
+ let advance = commit(&f, vec![genesis], Some(&second), 310);
+ expect_pass(
+ &run(&f, &refname, Some(advance)),
+ AdmissionKind::TipInvariant,
+ );
+}
+
+#[rstest]
+// @relation(gate.owner-mutation, model.member-provenance, scope=function, role=Verifies)
+fn a_self_attested_non_owner_cannot_advance_a_comment() {
+ // A self-attested member is not authorized for canonical refs at all
+ // (creation stays provenance-keyed, routed to the inbox); it therefore
+ // cannot advance someone else's comment either.
+ let f = forge();
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = name(&format!("refs/meta/comments/{genesis}"));
+ f.refs.set(refname.as_ref(), genesis);
+ let advance = commit(&f, vec![genesis], Some(&f.guest), 310);
+ let verdict = run(&f, &refname, Some(advance));
+ expect_fail(&verdict, Requirement::TipSigned);
+}
+
+#[rstest]
+// @relation(gate.owner-mutation, model.review, scope=function, role=Verifies)
+fn the_wrong_member_cannot_advance_a_review() {
+ // A review advances only under the member its refname names. `guest`
+ // (were it registered) could not advance admin's review; here we prove
+ // the composite key + owner rule refuse a mismatched signer.
+ let f = forge();
+ let second = Keypair::from_seed(OUTSIDER_SEED);
+ enroll_member(
+ &f.refs,
+ &f.objects,
+ "second",
+ &second,
+ Provenance::AdminRegistered,
+ 210,
+ );
+ let target = ObjectId::from_hex(b"deadbeef00000000000000000000000000000000").expect("hex");
+ // A review ref named for admin, but signed by `second`.
+ let tip = review_commit(&f, target, Some(&second), 300);
+ let refname =
+ namespace::review_ref(&target.to_string(), &MemberId::new("admin")).expect("valid");
+ let verdict = run(&f, &refname, Some(tip));
+ let Verdict::Fail(_) = verdict else {
+ panic!(
+ "a review signed by a member other than the one it names must be refused: {verdict:?}"
+ );
+ };
+}
+
// ---------------------------------------------------------------------
// Provenance-keyed authorization.
// ---------------------------------------------------------------------
@@ -334,9 +632,11 @@
#[case] refname: &str,
#[case] inbox_alternative: bool,
) {
+ // Authorization is judged before identity binding, so an empty-tree
+ // commit under any of these canonical refs is refused on provenance.
let f = forge();
- let new = proposal(&f, vec![], Some(refname), Some(&f.guest), 300);
- let verdict = run(&f, refname, Some(new));
+ let new = commit(&f, vec![], Some(&f.guest), 300);
+ let verdict = run(&f, &name(refname), Some(new));
expect_fail(&verdict, Requirement::TipSigned);
let Verdict::Fail(refusal) = &verdict else {
unreachable!()
@@ -351,9 +651,11 @@
// @relation(meta-ref.inbox, effect.self-run, gate.tip-signed, scope=function, role=Verifies)
fn a_member_may_write_its_own_self_run_namespace() {
let f = forge();
- let refname = "refs/meta/self/guest/unit/abc123";
- let new = proposal(&f, vec![], Some(refname), Some(&f.guest), 300);
- expect_pass(&run(&f, refname, Some(new)), AdmissionKind::TipInvariant);
+ let target = ObjectId::from_hex(b"abc1230000000000000000000000000000000000").expect("hex");
+ let tip = result_commit(&f, "unit", target, vec![], Some(&f.guest), 300);
+ let refname =
+ namespace::self_result_ref(&MemberId::new("guest"), "unit", "abc123").expect("valid");
+ expect_pass(&run(&f, &refname, Some(tip)), AdmissionKind::TipInvariant);
}
#[rstest]
@@ -370,8 +672,8 @@
) {
let f = forge();
let key = if as_admin { &f.admin } else { &f.guest };
- let new = proposal(&f, vec![], Some(refname), Some(key), 300);
- let verdict = run(&f, refname, Some(new));
+ let new = commit(&f, vec![], Some(key), 300);
+ let verdict = run(&f, &name(refname), Some(new));
if admitted {
expect_pass(&verdict, AdmissionKind::TipInvariant);
} else {
@@ -380,13 +682,21 @@
}
#[rstest]
-#[case::effects("refs/meta/effects/unit")]
-#[case::results("refs/meta/results/unit/abc")]
// @relation(effect.admin-only, gate.tip-signed, scope=function, role=Verifies)
-fn admin_registered_members_may_write_canonical_namespaces(#[case] refname: &str) {
+fn admin_registered_members_may_write_the_effects_namespace() {
let f = forge();
- let new = proposal(&f, vec![], Some(refname), Some(&f.admin), 300);
- expect_pass(&run(&f, refname, Some(new)), AdmissionKind::TipInvariant);
+ let effect = Effect {
+ name: "unit".into(),
+ trigger: "rev(refs/heads/main)".into(),
+ toolchains: vec![],
+ run: "true".into(),
+ };
+ let tree = facet_git_tree::serialize_into(&effect, &f.objects).expect("ser");
+ let tip = tree_commit(&f, tree, vec![], Some(&f.admin), 300);
+ expect_pass(
+ &run(&f, &name("refs/meta/effects/unit"), Some(tip)),
+ AdmissionKind::TipInvariant,
+ );
}
// ---------------------------------------------------------------------
@@ -397,68 +707,45 @@
// @relation(gate.adoption-merge, scope=function, role=Verifies)
fn adoption_is_a_merge_that_keeps_the_contributor_commit_in_ancestry() {
let f = forge();
- let refname = "refs/meta/comments/c1";
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 300,
- );
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = name(&format!("refs/meta/comments/{genesis}"));
+ f.refs.set(refname.as_ref(), genesis);
// The contributor's own signed commit, not authorized for this ref.
- let contributed = proposal(&f, vec![tip], Some(refname), Some(&f.guest), 310);
+ let contributed = commit(&f, vec![genesis], Some(&f.guest), 310);
// The authorized member merges it: the merge tip satisfies the
// invariant; the contributor's signature survives in ancestry.
- let merge = proposal(
- &f,
- vec![tip, contributed],
- Some(refname),
- Some(&f.admin),
- 320,
- );
- expect_pass(&run(&f, refname, Some(merge)), AdmissionKind::TipInvariant);
+ let merge = commit(&f, vec![genesis, contributed], Some(&f.admin), 320);
+ expect_pass(&run(&f, &refname, Some(merge)), AdmissionKind::TipInvariant);
}
#[rstest]
// @relation(gate.adoption-no-fast-forward, scope=function, role=Verifies)
fn fast_forwarding_to_a_contributor_commit_is_not_adoption() {
let f = forge();
- let refname = "refs/meta/comments/c1";
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 300,
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = name(&format!("refs/meta/comments/{genesis}"));
+ f.refs.set(refname.as_ref(), genesis);
+ let contributed = commit(&f, vec![genesis], Some(&f.guest), 310);
+ // Descends fine — but the tip signature is the contributor's, and the
+ // contributor is not authorized for this refname.
+ expect_fail(
+ &run(&f, &refname, Some(contributed)),
+ Requirement::TipSigned,
);
- let contributed = proposal(&f, vec![tip], Some(refname), Some(&f.guest), 310);
- // Descends fine — but the tip signature is the contributor's, and
- // the contributor is not authorized for this refname.
- expect_fail(&run(&f, refname, Some(contributed)), Requirement::TipSigned);
}
#[rstest]
// @relation(gate.same-actor-divergence, scope=function, role=Verifies)
fn a_members_own_divergent_heads_merge_cleanly() {
let f = forge();
- let refname = "refs/meta/issues/1";
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 300,
- );
+ let genesis = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = issue_ref(genesis);
+ f.refs.set(refname.as_ref(), genesis);
// Two of the member's own machines raced the single-writer ref.
- let a = proposal(&f, vec![tip], Some(refname), Some(&f.admin), 310);
- let b = proposal(&f, vec![tip], Some(refname), Some(&f.admin), 311);
- // Either head alone is a non-fast-forward once the other landed;
- // the resolution is the member merging their own heads.
- let merge = proposal(&f, vec![a, b], Some(refname), Some(&f.admin), 320);
- expect_pass(&run(&f, refname, Some(merge)), AdmissionKind::TipInvariant);
+ let a = commit(&f, vec![genesis], Some(&f.admin), 310);
+ let b = commit(&f, vec![genesis], Some(&f.admin), 311);
+ let merge = commit(&f, vec![a, b], Some(&f.admin), 320);
+ expect_pass(&run(&f, &refname, Some(merge)), AdmissionKind::TipInvariant);
}
// ---------------------------------------------------------------------
@@ -477,9 +764,9 @@
guest: Keypair::from_seed(SELF_ATTESTED_SEED),
};
// No config, no members: an unsigned meta write passes as pre-epoch.
- let new = proposal(&f, vec![], None, None, 100);
+ let new = commit(&f, vec![], None, 100);
expect_pass(
- &run(&f, "refs/meta/issues/1", Some(new)),
+ &run(&f, &issue_ref(new), Some(new)),
AdmissionKind::PreEpoch,
);
}
@@ -508,16 +795,12 @@
let tree = facet_git_tree::serialize_into(&Config { epoch: Some(200) }, &f.objects)
.expect("config serializes");
let make = |key: Option<&Keypair>| {
- let trailers = Trailers {
- ents_ref: Some(name(namespace::CONFIG_REF)),
- schema_version: None,
- };
write_commit(
&f.objects,
&CommitSpec {
tree,
parents: vec![],
- message: format!("enable verification\n\n{}", trailers.render()),
+ message: "enable verification".into(),
seconds: 200,
},
key,
@@ -527,12 +810,12 @@
// Unsigned epoch-setting is refused: the circularity resolves by
// gating the very commit that turns gating on.
expect_fail(
- &run(&f, namespace::CONFIG_REF, Some(make(None))),
+ &run(&f, &name(namespace::CONFIG_REF), Some(make(None))),
Requirement::TipSigned,
);
// Signed by an enrolled member, it passes under the tip invariant.
expect_pass(
- &run(&f, namespace::CONFIG_REF, Some(make(Some(&f.admin)))),
+ &run(&f, &name(namespace::CONFIG_REF), Some(make(Some(&f.admin)))),
AdmissionKind::TipInvariant,
);
}
@@ -541,8 +824,7 @@
// Bootstrap: fail-closed empty-member-list handling.
// ---------------------------------------------------------------------
-/// A store with verification in force but no members at all — the shape
-/// a hosted deployment initializes (`roots.bootstrap` owns hardening).
+/// A store with verification in force but no members at all.
fn bare_forge_with_epoch() -> Forge {
let refs = MemRefStore::default();
let objects = ObjectStore::default();
@@ -564,19 +846,14 @@
}
fn enrollment_proposal(f: &Forge, id: &str, enrolled: &Keypair, signer: &Keypair) -> ObjectId {
- let member = Member::new(enrolled.public_openssh(), Provenance::AdminRegistered);
+ let member = Member::new(id, enrolled.public_openssh(), Provenance::AdminRegistered);
let tree = facet_git_tree::serialize_into(&member, &f.objects).expect("member serializes");
- let refname = namespace::member_ref(&MemberId::new(id)).expect("valid id");
- let trailers = Trailers {
- ents_ref: Some(refname),
- schema_version: None,
- };
write_commit(
&f.objects,
&CommitSpec {
tree,
parents: vec![],
- message: format!("enroll {id}\n\n{}", trailers.render()),
+ message: format!("enroll {id}"),
seconds: 100,
},
Some(signer),
@@ -589,11 +866,25 @@
let f = bare_forge_with_epoch();
let new = enrollment_proposal(&f, "first", &f.admin, &f.admin);
expect_pass(
- &run(&f, "refs/meta/member/first", Some(new)),
+ &run(&f, &name("refs/meta/member/first"), Some(new)),
AdmissionKind::Bootstrap,
);
}
+#[rstest]
+// @relation(gate.bootstrap, gate.identity-binding, scope=function, role=Verifies)
+fn a_bootstrap_enrollment_naming_the_wrong_ref_is_refused() {
+ // Even the self-admitting bootstrap write is bound by the member's own
+ // id field, not a trailer: an enrollment whose id is `first` cannot
+ // land on `refs/meta/member/other`.
+ let f = bare_forge_with_epoch();
+ let new = enrollment_proposal(&f, "first", &f.admin, &f.admin);
+ expect_fail(
+ &run(&f, &name("refs/meta/member/other"), Some(new)),
+ Requirement::IdentityBinding,
+ );
+}
+
#[rstest]
// @relation(gate.bootstrap, scope=function, role=Verifies)
fn bootstrap_enrollment_must_be_signed_by_the_key_it_enrolls() {
@@ -601,7 +892,7 @@
let other = Keypair::from_seed(OUTSIDER_SEED);
let new = enrollment_proposal(&f, "first", &f.admin, &other);
expect_fail(
- &run(&f, "refs/meta/member/first", Some(new)),
+ &run(&f, &name("refs/meta/member/first"), Some(new)),
Requirement::TipSigned,
);
}
@@ -610,11 +901,8 @@
// @relation(gate.bootstrap, scope=function, role=Verifies)
fn bootstrap_admits_only_enrollments() {
let f = bare_forge_with_epoch();
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), Some(&f.admin), 100);
- expect_fail(
- &run(&f, "refs/meta/issues/1", Some(new)),
- Requirement::TipSigned,
- );
+ let new = commit(&f, vec![], Some(&f.admin), 100);
+ expect_fail(&run(&f, &issue_ref(new), Some(new)), Requirement::TipSigned);
}
#[rstest]
@@ -628,7 +916,7 @@
} else {
Provenance::SelfAttested
};
- let mut member = Member::new(key.public_openssh(), provenance);
+ let mut member = Member::new(id, key.public_openssh(), provenance);
member.revoke();
write_member(&f.refs, &f.objects, id, &member, Some(&f.admin), 400);
}
@@ -637,13 +925,13 @@
let newcomer = Keypair::from_seed(OUTSIDER_SEED);
let new = enrollment_proposal(&f, "newcomer", &newcomer, &newcomer);
expect_fail(
- &run(&f, "refs/meta/member/newcomer", Some(new)),
+ &run(&f, &name("refs/meta/member/newcomer"), Some(new)),
Requirement::TipSigned,
);
// And the fully-revoked members cannot write anything either.
- let attempt = proposal(&f, vec![], Some("refs/meta/issues/9"), Some(&f.admin), 500);
+ let attempt = commit(&f, vec![], Some(&f.admin), 500);
expect_fail(
- &run(&f, "refs/meta/issues/9", Some(attempt)),
+ &run(&f, &issue_ref(attempt), Some(attempt)),
Requirement::TipSigned,
);
}
@@ -656,53 +944,37 @@
// @relation(gate.atomic-cas, scope=function, role=Verifies)
fn the_admission_carries_the_cas_precondition_from_the_same_read() {
let f = forge();
- let refname = "refs/meta/issues/1";
// Creation: the ref must still be absent at write time.
- let created = proposal(&f, vec![], Some(refname), Some(&f.admin), 300);
- let Verdict::Pass(admission) = run(&f, refname, Some(created)) else {
+ let created = commit(&f, vec![], Some(&f.admin), 300);
+ let refname = issue_ref(created);
+ let Verdict::Pass(admission) = run(&f, &refname, Some(created)) else {
panic!("expected a pass");
};
assert_eq!(admission.cas, Expected::MustNotExist);
// Update: the precondition is exactly the old tip the FF check used.
- let tip = write_meta_entity(
- &f.refs,
- &f.objects,
- name(refname),
- &ents_model::Status::Pass,
- Some(&f.admin),
- 310,
- );
- let advanced = proposal(&f, vec![tip], Some(refname), Some(&f.admin), 320);
- let Verdict::Pass(admission) = run(&f, refname, Some(advanced)) else {
+ f.refs.set(refname.as_ref(), created);
+ let advanced = commit(&f, vec![created], Some(&f.admin), 320);
+ let Verdict::Pass(admission) = run(&f, &refname, Some(advanced)) else {
panic!("expected a pass");
};
- assert_eq!(admission.cas, Expected::MustExistAndMatch(tip));
+ assert_eq!(admission.cas, Expected::MustExistAndMatch(created));
}
-/// Every scenario the verdict table distinguishes, evaluated the way
-/// each of the three call sites would evaluate it — hosted CAS on the
-/// live store, local UI verdict on the same store, and push pre-flight
-/// on a fetched copy of the refs — in one parameterized test: the gate
-/// is one function, and its verdict is identical at every site.
+/// Every scenario the verdict table distinguishes, evaluated the way each
+/// of the three call sites would evaluate it, in one parameterized test.
#[rstest]
-#[case::authorized_pass("refs/meta/issues/1", true, true, 300)]
-#[case::unsigned_fail("refs/meta/issues/1", false, true, 300)]
-#[case::unauthorized_namespace("refs/meta/effects/unit", true, false, 300)]
-#[case::self_run_pass("refs/meta/self/guest/unit/abc", true, false, 300)]
+#[case::authorized_pass(true, true)]
+#[case::unsigned_fail(false, true)]
+#[case::unauthorized_namespace_guest(true, false)]
// @relation(gate.call-sites, gate.mandatory-hosted, gate.advisory-local, scope=function, role=Verifies)
-fn all_three_call_sites_return_identical_verdicts(
- #[case] refname: &str,
- #[case] signed: bool,
- #[case] as_admin: bool,
- #[case] seconds: i64,
-) {
+fn all_three_call_sites_return_identical_verdicts(#[case] signed: bool, #[case] as_admin: bool) {
let f = forge();
let key = if as_admin { &f.admin } else { &f.guest };
- let new = proposal(&f, vec![], Some(refname), signed.then_some(key), seconds);
+ let new = commit(&f, vec![], signed.then_some(key), 300);
let update = Update {
- name: name(refname),
+ name: issue_ref(new),
new: Some(new),
};
@@ -721,13 +993,9 @@
#[rstest]
// @relation(gate.signature-artifact, gate.policy-as-state, scope=function, role=Verifies)
fn verdicts_reproduce_offline_from_repository_state_alone() {
- // Build the identical repository twice from deterministic seeds —
- // the fixture analogue of verifying in an independent clone. The
- // verdict depends only on refs/meta/* state and object bytes, so
- // both "clones" agree, with no transport artifact consulted.
let build = || {
let f = forge();
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), Some(&f.admin), 300);
+ let new = commit(&f, vec![], Some(&f.admin), 300);
(f, new)
};
let (origin, new_at_origin) = build();
@@ -735,7 +1003,7 @@
assert_eq!(new_at_origin, new_at_clone, "deterministic fixtures");
let update = Update {
- name: name("refs/meta/issues/1"),
+ name: issue_ref(new_at_origin),
new: Some(new_at_origin),
};
let at_origin = verify(&origin.refs, &origin.objects, &update).expect("verdict");
@@ -747,8 +1015,9 @@
// @relation(gate.verdict-reason, gate.advisory-local, scope=function, role=Verifies)
fn refusals_render_an_actionable_reason_with_the_inbox_alternative() {
let f = forge();
- let new = proposal(&f, vec![], Some("refs/meta/issues/1"), Some(&f.guest), 300);
- let Verdict::Fail(refusal) = run(&f, "refs/meta/issues/1", Some(new)) else {
+ let new = commit(&f, vec![], Some(&f.guest), 300);
+ let refname = issue_ref(new);
+ let Verdict::Fail(refusal) = run(&f, &refname, Some(new)) else {
panic!("self-attested member on a canonical ref must be refused");
};
let rendered = refusal.to_string();
@@ -757,7 +1026,7 @@
"names the rule: {rendered}"
);
assert!(
- rendered.contains("refs/meta/issues/1"),
+ rendered.contains("refs/meta/issues/"),
"names the subject ref: {rendered}"
);
assert!(
@@ -778,8 +1047,6 @@
#[rstest]
fn fixture_stores_read_back_what_they_seed() {
- // Guards the fixture itself: the enrolled member refs exist and the
- // ref store returns them through the production read trait.
let f = forge();
let member_ref = name("refs/meta/member/admin");
assert!(f.refs.get(member_ref.as_ref()).expect("readable").is_some());
crates/kernel/ents-query/tests/monotone.rs
@@ -135,8 +135,9 @@
let tree = CommitRef::from_bytes(data.data, tip.kind())
.expect("commit")
.tree();
- let status: Status = facet_git_tree::deserialize(&tree, objects).expect("status tree");
- (want.is_none() || want == Some(status)).then(|| short.to_owned())
+ let record: ents_model::ResultRecord =
+ facet_git_tree::deserialize(&tree, objects).expect("result tree");
+ (want.is_none() || want == Some(record.status)).then(|| short.to_owned())
})
.collect()
}
@@ -293,7 +294,7 @@
Op::Member { who } => {
let id = MEMBERS[who];
seconds += 100;
- let member = Member::new(format!("key-{id}"), Provenance::AdminRegistered);
+ let member = Member::new(id, format!("key-{id}"), Provenance::AdminRegistered);
write_member(&refs, &objects, id, &member, None, seconds);
Some(format!("refs/meta/member/{id}"))
}
crates/kernel/ents-receive/tests/receive.rs
@@ -7,13 +7,15 @@
#![expect(
clippy::expect_used,
clippy::indexing_slicing,
+ clippy::panic,
reason = "integration test: fixtures panic on setup failure"
)]
use ents_gate::Config;
-use ents_model::{Effect, Provenance, Redaction, namespace, trailer::Trailers};
+use ents_model::{Effect, MemberId, Provenance, Redaction, ResultRecord, Status, namespace};
use ents_receive::{
- MemoryEventSink, Mode, NullEventSink, Proposal, RefTransition, TxResult, receive, reconcile,
+ Identity, MemoryEventSink, Mode, NullEventSink, Proposal, RefTransition, TxResult,
+ propose_genesis, receive, reconcile,
};
use ents_testutil::{
CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity,
@@ -90,35 +92,45 @@
s.try_into().expect("valid refname in test")
}
-/// Build a signed (or unsigned) mutation commit that binds itself to
-/// `refname` via the `Advance-ref:` trailer, *without* moving the ref —
-/// unlike `ents_testutil::write_meta_entity`, so the test can hand the
-/// result to `receive` and observe whether *it* moves the ref.
+/// Build a signed (or unsigned) parentless genesis commit for `entity`,
+/// *without* moving any ref — unlike `ents_testutil::write_meta_entity`, so
+/// the test can hand the result to `receive` and observe whether *it* moves
+/// the ref. The commit names no ref (the gate recomputes the binding from
+/// signed content); callers derive the refname from the returned oid where
+/// the namespace is hash-identified.
fn build_mutation<T: for<'facet> facet::Facet<'facet>>(
objects: &ObjectStore,
- refname: &FullName,
entity: &T,
signer: Option<&Keypair>,
seconds: i64,
) -> ObjectId {
let tree = facet_git_tree::serialize_into(entity, objects).expect("serializes");
- let trailers = Trailers {
- ents_ref: Some(refname.clone()),
- schema_version: None,
- };
- let message = format!("Mutate {}\n\n{}", refname.as_bstr(), trailers.render());
write_commit(
objects,
&CommitSpec {
tree,
parents: vec![],
- message,
+ message: "Mutate entity".into(),
seconds,
},
signer,
)
}
+/// The oid-keyed refname of a hash-identified issue genesis
+/// (`meta-ref.identity-binding`).
+fn issue_ref(genesis: ObjectId) -> FullName {
+ name(&format!("refs/meta/issues/{genesis}"))
+}
+
+fn sample_issue() -> Issue {
+ Issue {
+ title: "t".into(),
+ body: "b".into(),
+ state: "open".into(),
+ }
+}
+
fn single(transition: RefTransition, objects: Vec<ObjectId>) -> Proposal {
Proposal {
transitions: vec![transition],
@@ -146,18 +158,13 @@
#[case] expect_verdict_pass: bool,
) {
let forge = forge();
- let refname = namespace::issue_ref("1").expect("valid");
let signer = if authorized {
&forge.admin
} else {
&forge.guest
};
- let issue = Issue {
- title: "t".into(),
- body: "b".into(),
- state: "open".into(),
- };
- let tip = build_mutation(&forge.objects, &refname, &issue, Some(signer), 300);
+ let tip = build_mutation(&forge.objects, &sample_issue(), Some(signer), 300);
+ let refname = issue_ref(tip);
let outcome = receive(
&forge.refs,
@@ -204,7 +211,7 @@
let refname = namespace::redaction_ref("r1").expect("valid");
let signer = if as_admin { &forge.admin } else { &forge.guest };
let redaction = Redaction::new(ObjectId::null(gix_hash::Kind::Sha1), "leaked credential");
- let tip = build_mutation(&forge.objects, &refname, &redaction, Some(signer), 300);
+ let tip = build_mutation(&forge.objects, &redaction, Some(signer), 300);
let outcome = receive(
&forge.refs,
@@ -252,13 +259,8 @@
// Someone tries to push it back in, as part of an ordinary issue
// mutation's object graph.
- let issue_ref = namespace::issue_ref("1").expect("valid");
- let issue = Issue {
- title: "t".into(),
- body: "b".into(),
- state: "open".into(),
- };
- let tip = build_mutation(&forge.objects, &issue_ref, &issue, Some(&forge.admin), 300);
+ let tip = build_mutation(&forge.objects, &sample_issue(), Some(&forge.admin), 300);
+ let issue_ref = issue_ref(tip);
let outcome = receive(
&forge.refs,
@@ -348,6 +350,7 @@
// @relation(receive.reconstructible, receive.event-sink, receive.never-blocks, query.workset, scope=function, role=Verifies)
fn reconcile_matches_incremental_delivery() {
let effect = Effect {
+ name: "unit".to_owned(),
trigger: "rev(refs/heads/main)".to_owned(),
toolchains: vec![],
run: "true".to_owned(),
@@ -455,7 +458,7 @@
};
let rounds = chain_commits(&forge.objects, 2, 250);
let (first_round, second_round) = (rounds[0], rounds[1]);
- let pin = namespace::review_pin_ref("7").expect("valid");
+ let pin = namespace::review_pin_ref("deadbeef", &MemberId::new("admin")).expect("valid");
let empty = ents_testutil::empty_tree(&forge.objects);
// First review: the pin's tip has the reviewed commit as its only
@@ -530,7 +533,7 @@
sign: &|payload| forge.guest.sign(payload),
};
let reviewed = chain_commits(&forge.objects, 1, 250)[0];
- let pin = namespace::review_pin_ref("7").expect("valid");
+ let pin = namespace::review_pin_ref("deadbeef", &MemberId::new("admin")).expect("valid");
let outcome = ents_receive::propose_pin(
&forge.refs,
@@ -545,3 +548,154 @@
.expect("reaches an outcome");
assert_eq!(outcome.result, TxResult::Refused);
}
+
+// ---------------------------------------------------------------------
+// Identity binding driven through receive: the replays the binding closes.
+// ---------------------------------------------------------------------
+
+fn comment_ref(genesis: ObjectId) -> FullName {
+ name(&format!("refs/meta/comments/{genesis}"))
+}
+
+/// The doppelgänger replay: a signed *mutation* commit (one with a parent)
+/// re-proposed as the genesis of a fresh entity is refused, because the
+/// all-roots walk reaches the original genesis, not the replayed commit
+/// (`gate.identity-binding`, `meta-ref.identity-binding`). This, not a
+/// creation-time-only check, is what makes the replay impossible.
+// @relation(gate.identity-binding, meta-ref.identity-binding, scope=function, role=Verifies)
+#[test]
+fn a_signed_mutation_replayed_as_a_fresh_genesis_is_refused() {
+ let forge = forge();
+ // A legitimate comment genesis and one signed mutation of it.
+ let genesis = build_mutation(&forge.objects, &sample_issue(), Some(&forge.admin), 300);
+ forge.refs.set(comment_ref(genesis).as_ref(), genesis);
+ let mut edited = sample_issue();
+ edited.state = "resolved".into();
+ let tree = facet_git_tree::serialize_into(&edited, &forge.objects).expect("ser");
+ let mutation = write_commit(
+ &forge.objects,
+ &CommitSpec {
+ tree,
+ parents: vec![genesis],
+ message: "Resolve".into(),
+ seconds: 310,
+ },
+ Some(&forge.admin),
+ );
+
+ // Re-propose that mutation commit as the genesis of a brand-new
+ // comment named for its own oid.
+ let outcome = receive(
+ &forge.refs,
+ &forge.objects,
+ &NullEventSink,
+ &single(
+ RefTransition {
+ name: comment_ref(mutation),
+ old: None,
+ new: Some(mutation),
+ },
+ vec![mutation],
+ ),
+ Mode::Mandatory,
+ )
+ .expect("evaluates");
+
+ assert_eq!(outcome.result, TxResult::Refused);
+ let (_, verdict) = &outcome.verdicts[0];
+ let ents_gate::Verdict::Fail(refusal) = verdict else {
+ panic!("the replay must be refused: {verdict:?}");
+ };
+ assert_eq!(refusal.requirement, ents_gate::Requirement::IdentityBinding);
+ assert!(
+ forge
+ .refs
+ .get(comment_ref(mutation).as_ref())
+ .expect("readable")
+ .is_none(),
+ "no doppelgänger entity was created"
+ );
+}
+
+/// The result replay: a signed `pass` re-proposed for a different effect,
+/// or a different commit, is refused — the result tree carries its own
+/// effect and target, so the refname is a function of signed content
+/// (`model.result-identity`, `gate.identity-binding`).
+// @relation(model.result-identity, gate.identity-binding, scope=function, role=Verifies)
+#[rstest]
+#[case::wrong_effect("lint", "abc123")]
+#[case::wrong_commit("unit", "ffffff")]
+fn a_signed_pass_replayed_for_another_effect_or_commit_is_refused(
+ #[case] effect_seg: &str,
+ #[case] short: &str,
+) {
+ let forge = forge();
+ let target =
+ ObjectId::from_hex(b"abc1230000000000000000000000000000000000").expect("valid hex");
+ let record = ResultRecord::new("unit", target, Status::Pass);
+ let tip = build_mutation(&forge.objects, &record, Some(&forge.admin), 300);
+
+ // The honest ref is results/unit/abc123; the replay targets a
+ // different effect or commit segment.
+ let replay = namespace::result_ref(effect_seg, short).expect("valid");
+ let outcome = receive(
+ &forge.refs,
+ &forge.objects,
+ &NullEventSink,
+ &single(
+ RefTransition {
+ name: replay.clone(),
+ old: None,
+ new: Some(tip),
+ },
+ vec![tip],
+ ),
+ Mode::Mandatory,
+ )
+ .expect("evaluates");
+
+ assert_eq!(outcome.result, TxResult::Refused);
+ assert!(forge.refs.get(replay.as_ref()).expect("readable").is_none());
+}
+
+/// Creation via the inbox still works: a self-attested member creates a
+/// hash-identified entity under its own inbox segment, awaiting adoption
+/// (`gate.owner-mutation`: creation stays provenance-keyed). The
+/// sign-then-name genesis flow names the ref from the commit's own oid.
+// @relation(gate.owner-mutation, meta-ref.inbox, meta-ref.identity-binding, scope=function, role=Verifies)
+#[test]
+fn a_self_attested_member_creates_a_comment_in_its_inbox() {
+ let forge = forge();
+ let identity = Identity {
+ actor: gix::actor::Signature {
+ name: "guest".into(),
+ email: "guest@ents.test".into(),
+ time: gix::date::Time {
+ seconds: 300,
+ offset: 0,
+ },
+ },
+ sign: &|payload| forge.guest.sign(payload),
+ };
+
+ let (landed, outcome) = propose_genesis(
+ &forge.refs,
+ &forge.objects,
+ &NullEventSink,
+ &sample_issue(),
+ |oid| namespace::inbox_ref(&MemberId::new("guest"), &format!("comments/{oid}")),
+ &identity,
+ "Comment awaiting adoption",
+ Mode::Mandatory,
+ )
+ .expect("reaches an outcome");
+
+ assert_eq!(outcome.result, TxResult::Applied, "{:?}", outcome.verdicts);
+ assert!(
+ landed
+ .as_bstr()
+ .starts_with(b"refs/meta/inbox/guest/comments/"),
+ "created under the contributor's own inbox segment: {landed}"
+ );
+ assert!(forge.refs.get(landed.as_ref()).expect("readable").is_some());
+}
crates/kernel/ents-sync/tests/preflight.rs
@@ -11,13 +11,13 @@
#![expect(clippy::unwrap_used, reason = "tests")]
use ents_gate::{Config, Update, verify};
-use ents_model::trailer::Trailers;
use ents_model::{MemberId, Provenance, namespace};
use ents_sync::{inbox_route, preflight};
use ents_testutil::{
CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity,
};
use gix::refs::FullName;
+use gix_hash::ObjectId;
use gix_ref_store::{Expected, RefEdit, RefStore, RefStoreRead};
use rstest::rstest;
@@ -39,6 +39,47 @@
}
}
+/// Build a parentless, signed genesis issue commit and the oid-keyed
+/// refname it binds (`meta-ref.identity-binding`). Does not touch the ref
+/// store — pre-flight judges a proposal against a snapshot.
+fn genesis_issue(objects: &ObjectStore, signer: &Keypair, seconds: i64) -> (FullName, ObjectId) {
+ let tree = facet_git_tree::serialize_into(&issue(), objects).unwrap();
+ let tip = write_commit(
+ objects,
+ &CommitSpec {
+ tree,
+ parents: vec![],
+ message: "Open issue".into(),
+ seconds,
+ },
+ Some(signer),
+ );
+ (format!("refs/meta/issues/{tip}").try_into().unwrap(), tip)
+}
+
+/// A child issue commit of `parent`, signed, for divergence scenarios.
+fn child_issue(
+ objects: &ObjectStore,
+ parent: ObjectId,
+ state: &str,
+ signer: &Keypair,
+ seconds: i64,
+) -> ObjectId {
+ let mut edited = issue();
+ edited.state = state.into();
+ let tree = facet_git_tree::serialize_into(&edited, objects).unwrap();
+ write_commit(
+ objects,
+ &CommitSpec {
+ tree,
+ parents: vec![parent],
+ message: "Edit issue".into(),
+ seconds,
+ },
+ Some(signer),
+ )
+}
+
/// A booted forge (admin enrolled, epoch set) plus a self-attested bob.
fn forge() -> (MemRefStore, ObjectStore, Keypair, Keypair) {
let refs = MemRefStore::default();
@@ -82,26 +123,25 @@
let signer = if seed == 1 { &admin } else { &bob };
let author = if seed == 1 { "admin" } else { "bob" };
- let name: FullName = "refs/meta/issues/9".try_into().unwrap();
- let tip = write_meta_entity(&refs, &objects, name.clone(), &issue(), Some(signer), 300);
-
- let before = refs.fetched_copy();
- before.remove(name.as_ref());
+ // The issue's id is its genesis commit's own oid; the ref does not yet
+ // exist, so pre-flight judges a fresh creation.
+ let (name, tip) = genesis_issue(&objects, signer, 300);
let pf = preflight(
- &before,
+ &refs,
&objects,
&Update {
- name,
+ name: name.clone(),
new: Some(tip),
},
&MemberId::new(author),
)
.unwrap();
- assert_eq!(pf.is_pass(), expect_pass);
+ assert_eq!(pf.is_pass(), expect_pass, "{:?}", pf.verdict);
assert_eq!(pf.inbox.is_some(), expect_inbox);
if let Some(inbox) = pf.inbox {
- assert_eq!(inbox.as_bstr(), "refs/meta/inbox/bob/issues/9");
+ let expected = format!("refs/meta/inbox/bob/issues/{tip}");
+ assert_eq!(inbox.as_bstr(), expected.as_str());
}
}
@@ -112,32 +152,15 @@
#[test]
fn a_divergence_refusal_offers_no_inbox() {
let (refs, objects, admin, _bob) = forge();
- let name: FullName = "refs/meta/issues/3".try_into().unwrap();
- write_meta_entity(&refs, &objects, name.clone(), &issue(), Some(&admin), 300);
-
- // An independent root correctly bound to the same ref: signed by an
- // authorized member with a matching trailer, but with no parents, so it
- // cannot descend from the current tip — a genuine fast-forward refusal.
- let mut other = issue();
- other.state = "closed".into();
- let sibling = {
- let tree = facet_git_tree::serialize_into(&other, &objects).unwrap();
- let trailers = Trailers {
- ents_ref: Some(name.clone()),
- schema_version: None,
- };
- let message = format!("Mutate {}\n\n{}", name.as_bstr(), trailers.render());
- write_commit(
- &objects,
- &CommitSpec {
- tree,
- parents: vec![],
- message,
- seconds: 350,
- },
- Some(&admin),
- )
- };
+ // A genesis and two divergent children of it, all correctly bound to
+ // the same oid-keyed ref (both descend from the same genesis, so
+ // identity binding holds); the current tip is one child, and the
+ // proposal is the sibling, which cannot descend from it — a genuine
+ // fast-forward refusal, not an identity mismatch.
+ let (name, genesis) = genesis_issue(&objects, &admin, 300);
+ let current = child_issue(&objects, genesis, "open", &admin, 350);
+ let sibling = child_issue(&objects, genesis, "closed", &admin, 350);
+ refs.set(name.as_ref(), current);
let pf = preflight(
&refs,
@@ -149,7 +172,7 @@
&MemberId::new("admin"),
)
.unwrap();
- assert!(!pf.is_pass());
+ assert!(!pf.is_pass(), "{:?}", pf.verdict);
assert!(
pf.inbox.is_none(),
"a divergence routes to a merge, not the inbox"
@@ -166,17 +189,14 @@
let (refs, objects, admin, bob) = forge();
for (author, signer) in [("admin", &admin), ("bob", &bob)] {
- let name: FullName = format!("refs/meta/issues/{author}").try_into().unwrap();
- let tip = write_meta_entity(&refs, &objects, name.clone(), &issue(), Some(signer), 300);
- let before = refs.fetched_copy();
- before.remove(name.as_ref());
+ let (name, tip) = genesis_issue(&objects, signer, 300);
let update = Update {
name,
new: Some(tip),
};
- let pf = preflight(&before, &objects, &update, &MemberId::new(author)).unwrap();
- let gate = verify(&before, &objects, &update).unwrap();
+ let pf = preflight(&refs, &objects, &update, &MemberId::new(author)).unwrap();
+ let gate = verify(&refs, &objects, &update).unwrap();
assert_eq!(
pf.verdict, gate,
"pre-flight must be the gate, not a copy of it"
@@ -192,13 +212,10 @@
#[test]
fn a_failing_preflight_never_blocks_the_local_write() {
let (refs, objects, _admin, bob) = forge();
- let name: FullName = "refs/meta/issues/9".try_into().unwrap();
- let tip = write_meta_entity(&refs, &objects, name.clone(), &issue(), Some(&bob), 300);
+ let (name, tip) = genesis_issue(&objects, &bob, 300);
- let before = refs.fetched_copy();
- before.remove(name.as_ref());
let pf = preflight(
- &before,
+ &refs,
&objects,
&Update {
name: name.clone(),
@@ -214,14 +231,14 @@
);
// Nothing sync did prevents the local write from applying.
- let outcome = before
+ let outcome = refs
.transaction(&[RefEdit {
name: name.clone(),
expected: Expected::MustNotExist,
new: Some(tip),
}])
.unwrap();
- assert_eq!(before.get(name.as_ref()).unwrap(), Some(tip));
+ assert_eq!(refs.get(name.as_ref()).unwrap(), Some(tip));
let _ = outcome;
}
crates/kernel/ents-sync/tests/resolve.rs
@@ -18,7 +18,6 @@
)]
use ents_gate::{Config, Update, Verdict, verify};
-use ents_model::trailer::Trailers;
use ents_model::{Provenance, namespace};
use ents_sync::{Heads, Merged, merge_heads};
use ents_testutil::{
@@ -45,34 +44,35 @@
}
}
-/// Build a signed commit recording `entity`, bound to `refname`, with the
-/// given parents — the general shape [`write_meta_entity`] specializes.
+/// Build a signed commit recording `entity` with the given parents. The
+/// commit names no ref of its own — the gate recomputes the binding from
+/// signed content (`gate.identity-binding`).
fn signed_commit(
objects: &ObjectStore,
- refname: &FullName,
entity: &Issue,
parents: Vec<ObjectId>,
key: &Keypair,
seconds: i64,
) -> ObjectId {
let tree = facet_git_tree::serialize_into(entity, objects).unwrap();
- let trailers = Trailers {
- ents_ref: Some(refname.clone()),
- schema_version: None,
- };
- let message = format!("Mutate {}\n\n{}", refname.as_bstr(), trailers.render());
write_commit(
objects,
&CommitSpec {
tree,
parents,
- message,
+ message: "Mutate issue".into(),
seconds,
},
Some(key),
)
}
+/// The canonical issue refname for a genesis commit, keyed by its oid
+/// (`meta-ref.identity-binding`).
+fn issue_ref(genesis: ObjectId) -> FullName {
+ format!("refs/meta/issues/{genesis}").try_into().unwrap()
+}
+
fn author(seconds: i64) -> gix::actor::Signature {
gix::actor::Signature {
name: "placer".into(),
@@ -121,14 +121,15 @@
let jdc = Keypair::from_seed(1);
boot(&refs, &objects, &jdc);
- let name: FullName = "refs/meta/issues/1".try_into().unwrap();
- let base = signed_commit(&objects, &name, &issue("open"), vec![], &jdc, 300);
+ let base = signed_commit(&objects, &issue("open"), vec![], &jdc, 300);
+ // The issue's id is its genesis commit's own oid.
+ let name = issue_ref(base);
// Two divergent children of the same base, editing different fields.
let mut ours_issue = issue("open");
ours_issue.title = "renamed".into();
- let ours = signed_commit(&objects, &name, &ours_issue, vec![base], &jdc, 400);
- let theirs = signed_commit(&objects, &name, &issue("closed"), vec![base], &jdc, 400);
+ let ours = signed_commit(&objects, &ours_issue, vec![base], &jdc, 400);
+ let theirs = signed_commit(&objects, &issue("closed"), vec![base], &jdc, 400);
let heads = Heads {
refname: name.clone(),
@@ -187,12 +188,12 @@
enroll_member(&refs, &objects, "bob", &bob, Provenance::SelfAttested, 250);
// Bob submits an issue under his own inbox segment (all he may write).
- let inbox: FullName = "refs/meta/inbox/bob/issues/5".try_into().unwrap();
- let contribution = signed_commit(&objects, &inbox, &issue("open"), vec![], &bob, 300);
+ let contribution = signed_commit(&objects, &issue("open"), vec![], &bob, 300);
// The maintainer adopts it onto the canonical ref via the *same*
- // machinery divergence uses — only the heads differ.
- let canonical: FullName = "refs/meta/issues/5".try_into().unwrap();
+ // machinery divergence uses — only the heads differ. The canonical id
+ // is bob's genesis commit oid, which stays the history's sole root.
+ let canonical = issue_ref(contribution);
let heads = Heads {
refname: canonical.clone(),
ours: None,
@@ -238,14 +239,13 @@
boot(&refs, &objects, &admin);
enroll_member(&refs, &objects, "bob", &bob, Provenance::SelfAttested, 250);
- let canonical: FullName = "refs/meta/issues/7".try_into().unwrap();
- let base = signed_commit(&objects, &canonical, &issue("open"), vec![], &admin, 300);
+ let base = signed_commit(&objects, &issue("open"), vec![], &admin, 300);
+ let canonical = issue_ref(base);
// Bob branches from the canonical base and edits a field in his inbox.
- let inbox: FullName = "refs/meta/inbox/bob/issues/7".try_into().unwrap();
let mut contributed = issue("open");
contributed.title = "bob's title".into();
- let contribution = signed_commit(&objects, &inbox, &contributed, vec![base], &bob, 350);
+ let contribution = signed_commit(&objects, &contributed, vec![base], &bob, 350);
let heads = Heads {
refname: canonical.clone(),
crates/kernel/ents-sync/tests/transfer.rs
@@ -22,6 +22,7 @@
CommitSpec, Keypair, MemRefStore, ObjectStore, enroll_member, write_commit, write_meta_entity,
};
use gix::refs::FullName;
+use gix_hash::ObjectId;
use gix_ref_store::RefStoreRead;
/// A stand-in for `ents-forge`'s `Issue` (this crate cannot depend on
@@ -42,6 +43,31 @@
}
}
+/// Write a parentless, signed genesis issue commit, set its oid-keyed ref
+/// (`meta-ref.identity-binding`), and return `(name, oid)`.
+fn genesis(
+ refs: &MemRefStore,
+ objects: &ObjectStore,
+ state: &str,
+ signer: &Keypair,
+ seconds: i64,
+) -> (FullName, ObjectId) {
+ let tree = facet_git_tree::serialize_into(&issue(state), objects).unwrap();
+ let tip = write_commit(
+ objects,
+ &CommitSpec {
+ tree,
+ parents: vec![],
+ message: "Open issue".into(),
+ seconds,
+ },
+ Some(signer),
+ );
+ let name: FullName = format!("refs/meta/issues/{tip}").try_into().unwrap();
+ refs.set(name.as_ref(), tip);
+ (name, tip)
+}
+
/// Enroll `admin` (and optionally `bob`) and record the epoch.
fn boot(refs: &MemRefStore, objects: &ObjectStore, admin: &Keypair, bob: Option<&Keypair>) {
enroll_member(
@@ -77,16 +103,8 @@
let admin = Keypair::from_seed(1);
boot(&remote_refs, &remote_objects, &admin, None);
- // An issue with two commits of history.
- let name: FullName = "refs/meta/issues/1".try_into().unwrap();
- let parent = write_meta_entity(
- &remote_refs,
- &remote_objects,
- name.clone(),
- &issue("open"),
- Some(&admin),
- 300,
- );
+ // An issue with two commits of history; its id is the genesis oid.
+ let (name, parent) = genesis(&remote_refs, &remote_objects, "open", &admin, 300);
let tip = write_meta_entity(
&remote_refs,
&remote_objects,
@@ -204,15 +222,7 @@
let local_objects = ObjectStore::default();
boot(&local_refs, &local_objects, &admin, None);
- let name: FullName = "refs/meta/issues/1".try_into().unwrap();
- let tip = write_meta_entity(
- &local_refs,
- &local_objects,
- name.clone(),
- &issue("open"),
- Some(&admin),
- 300,
- );
+ let (name, tip) = genesis(&local_refs, &local_objects, "open", &admin, 300);
let pushed = push(
&remote_refs,
@@ -336,15 +346,7 @@
let local_objects = ObjectStore::default();
boot(&local_refs, &local_objects, &admin, None);
- let name: FullName = "refs/meta/issues/1".try_into().unwrap();
- let ours = write_meta_entity(
- &local_refs,
- &local_objects,
- name.clone(),
- &issue("open"),
- Some(&admin),
- 300,
- );
+ let (name, ours) = genesis(&local_refs, &local_objects, "open", &admin, 300);
// The racing writer's competing tip, landed on the remote the instant
// push's transaction begins — after pre-flight has already passed.