From 2fefe4cf510051baaefce461502da72055a9aa18 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Mon, 25 May 2026 06:43:14 -0500 Subject: [PATCH] =?UTF-8?q?feat(agent-identity):=20v0.0.26=20=E2=80=94=20r?= =?UTF-8?q?egister=5Fagent=5Fdid=20impl=20complete=20+=20mock=20runtime=20?= =?UTF-8?q?integration=20tests=20E2E=20(10=20scenarios)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2.7 AGENT IDENTITY pallet REAL IMPL canonical. Full 8-step CHN-7 flow canonical register_agent_did dispatchable + storage assertions + Inmutable #41 Cross-Species Co-authorship enforcement compile-detect. Mock runtime + 10 integration tests E2E pattern v0.0.18 + v0.0.22 mirror. ## Real impl canonical 8-step CHN-7 1. ensure_signed(origin) 2. Idempotency check (!Identities::contains_key(&who)) 3. Derive DID canonical via derive_did(public_key) BLAKE3 (Inmutable #16) 4. DID collision check (!DidToAccount::contains_key(&did)) 5. Build AgentChain BoundedVec from co_authors_raw Vec<[u8;32]> (overflow → AgentChainOverflow) 6. Inmutable #41 enforce: Hybrid requires non-empty agent_chain 7. Storage insert: Identities + DidToAccount reverse lookup 8. Emit AgentRegistered + AgentBound (if cross-species) Pure helper Pallet::::query_identity_of() para Runtime API v0.0.27. ## 10 integration tests E2E canonical - scenario_1: Human self-registration → ok + DID + empty chain - scenario_2: AGI self-registration → ok + Inmutable #41 sovereign - scenario_3: Hybrid + 2 co-authors → ok + AgentBound emitted - scenario_4: Hybrid + empty chain → Err::HybridRequiresAgentChain - scenario_5: duplicate account → Err::AlreadyRegistered - scenario_6: same pubkey diff account → Err::DidAlreadyTaken - scenario_7: 11 co-authors → Err::AgentChainOverflow (ConstU32<10>) - scenario_8: DID → Account reverse lookup canonical - scenario_9: Human + AGI both is_sovereign() (Inmutable #41 lobo blanco) - scenario_10: 8-step CHN-7 walkthrough explicit Pre/Post assertions ## Files - EDIT: crates/origin-chain-agent-identity/src/lib.rs (real impl 8-step + query_identity_of helper) - NEW: crates/origin-chain-agent-identity/src/mock.rs (80 LOC mock runtime) - NEW: crates/origin-chain-agent-identity/src/tests_integration.rs (310 LOC, 10 tests) - EDIT: CHANGELOG.md (v0.0.26 entry comprehensive) ## Verification - cargo test -p origin-chain-agent-identity — 16 tests verde canonical, 0 failures (10 integration + 4 standalone + 2 auto-generated) 🛠️ Phase 2.7 real impl canonical. Runtime API v0.0.27 next ship → Phase 2.7 COMPLETE. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 59 ++++ crates/origin-chain-agent-identity/src/lib.rs | 108 +++++- .../origin-chain-agent-identity/src/mock.rs | 72 ++++ .../src/tests_integration.rs | 311 ++++++++++++++++++ 4 files changed, 533 insertions(+), 17 deletions(-) create mode 100644 crates/origin-chain-agent-identity/src/mock.rs create mode 100644 crates/origin-chain-agent-identity/src/tests_integration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4400b7f..6bb1047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,65 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow ## [Unreleased] +## [0.0.26] — 2026-05-25 — Phase 2.7 impl — Real `register_agent_did` + mock runtime integration tests E2E (10 scenarios) + +**🛠️ Phase 2.7 AGENT IDENTITY pallet REAL IMPL canonical** — Full 8-step +`register_agent_did` dispatchable canonical + storage assertions + Inmutable #41 +Cross-Species Co-authorship enforcement compile-detect. Mock runtime + 10 +integration tests E2E pattern v0.0.18 + v0.0.22 mirror. + +### Deliverables v0.0.26 + +**Real dispatchable canonical 8-step CHN-7 flow**: +1. `ensure_signed(origin)` canonical +2. Idempotency check canonical (`!Identities::contains_key(&who)`) +3. Derive DID canonical via `derive_did(public_key)` BLAKE3 (Inmutable #16) +4. DID collision check canonical (`!DidToAccount::contains_key(&did)`) +5. Build `AgentChain` BoundedVec canonical from `co_authors_raw: Vec<[u8; 32]>` (overflow → `AgentChainOverflow`) +6. Inmutable #41 canonical enforce: Hybrid requires non-empty agent_chain +7. Insert storage canonical: `Identities` + `DidToAccount` reverse lookup +8. Emit `AgentRegistered` + `AgentBound` (if cross-species) events canonical + +**Pure helper canonical**: +- `Pallet::::query_identity_of(&account) -> Option` para Runtime API v0.0.27 + +**Mock runtime canonical (NEW file)**: +- `crates/origin-chain-agent-identity/src/mock.rs` — 80 LOC pattern v0.0.18 mirror +- `construct_runtime!`: `System` + `AgentIdentity` (minimal testing) +- Helpers: `alice_origin/account` + `bob_origin/account` + `ALICE_PUBKEY/BOB_PUBKEY/CHARLIE_PUBKEY` deterministic test pubkeys + +**Integration tests canonical E2E (NEW file)**: +- `crates/origin-chain-agent-identity/src/tests_integration.rs` — 310 LOC, **10 tests verde canonical** + +**10 integration tests CHN-7 flow canonical**: + +| # | Scenario | Kind | Co-authors | Expected outcome | +|---|----------|------|------------|------------------| +| 1 | Human self-registration | Human | [] | ✅ AgentRegistered + DID derived + empty chain | +| 2 | AGI self-registration | AGI | [] | ✅ Inmutable #41 sovereign AGI canonical | +| 3 | Hybrid with co-authors | Hybrid | [BOB, CHARLIE] | ✅ AgentRegistered + AgentBound (cross-species) | +| 4 | Hybrid without chain | Hybrid | [] | ❌ Error::HybridRequiresAgentChain (Inmutable #41) | +| 5 | Idempotency duplicate | Human | [] | ❌ Error::AlreadyRegistered | +| 6 | DID collision | Human | [] same pubkey | ❌ Error::DidAlreadyTaken | +| 7 | AgentChain overflow | Hybrid | 11 co-authors | ❌ Error::AgentChainOverflow (ConstU32<10>) | +| 8 | DID → Account reverse lookup | Human | [] | ✅ DidToAccount canonical resolves | +| 9 | All AgentKind sovereign | Human + AGI | [] | ✅ Both is_sovereign() Inmutable #41 | +| 10 | 8-step CHN-7 walkthrough | Hybrid | [BOB] | ✅ Explicit Pre/Post-flight canonical | + +### Verification end-to-end canonical + +- ✅ `cargo test -p origin-chain-agent-identity` — **16 tests verde canonical, 0 failures**: + - 10 integration tests E2E (`tests_integration` module) + - 4 standalone scaffold tests (`tests` module) + - 2 auto-generated (mock genesis + runtime integrity) + +### Roadmap canonical post-v0.0.26 + +| Version | Trigger | +|---------|---------| +| **v0.0.27** | **Runtime API canonical `AgentIdentityApi.query_identity` impl en runtime → Phase 2.7 COMPLETE (sesión 3+ shipping ahora)** | +| v0.1.0 | Slice 2e WASM resolved + full testnet Q3 2026 launch | + ## [0.0.25] — 2026-05-25 — Phase 2.7 wired — AgentIdentity pallet runtime composition Index 9 **🔌 Phase 2.7 AGENT IDENTITY pallet WIRED runtime composition canonical** — diff --git a/crates/origin-chain-agent-identity/src/lib.rs b/crates/origin-chain-agent-identity/src/lib.rs index d4820fc..cc9b158 100644 --- a/crates/origin-chain-agent-identity/src/lib.rs +++ b/crates/origin-chain-agent-identity/src/lib.rs @@ -102,9 +102,11 @@ sp_api::decl_runtime_apis! { #[frame_support::pallet] pub mod pallet { + use alloc::vec::Vec; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; - use origin_chain_types::{AgentIdentity, AgentKind, Did}; + use origin_chain_types::{AccountId, AgentChain, AgentIdentity, AgentKind, Did}; + use sp_runtime::traits::SaturatedConversion; /// Pallet configuration trait canonical. /// @@ -197,47 +199,119 @@ pub mod pallet { HybridRequiresAgentChain, } - /// Dispatchables canonical (scaffold v0.0.24 — real impl v0.0.26). + /// Dispatchables canonical (real impl v0.0.26). #[pallet::call] impl Pallet { - /// CHN-7 canonical: `register_agent_did` scaffold dispatchable. + /// CHN-7 canonical: `register_agent_did` real dispatchable canonical. /// - /// **SCAFFOLD v0.0.24 canonical**: real impl deferred a v0.0.26. Current - /// canonical solo validates basic state (origin signed + no duplicate) - /// + emite placeholder event canonical. + /// **Real impl v0.0.26**: Full 8-step canonical flow + storage assertions + /// end-to-end + Inmutable #41 Cross-Species Co-authorship verification + /// compile-detect. /// - /// Flow canonical 6-step per CHN-7 (v0.0.26 real): + /// Flow canonical 8-step per CHN-7: /// 1. `ensure_signed(origin)` canonical - /// 2. Validate `!Identities::contains_key(who)` (no duplicate) + /// 2. Idempotency check: `!Identities::contains_key(&who)` (no duplicate) /// 3. Derive DID canonical via `derive_did(public_key)` BLAKE3 - /// 4. Validate `!DidToAccount::contains_key(did)` (no DID collision) - /// 5. Validate Hybrid → agent_chain non-empty (Inmutable #41 verification) - /// 6. Insert Identities + DidToAccount + emit AgentRegistered + AgentBound (if Hybrid) + /// 4. DID collision check: `!DidToAccount::contains_key(&did)` (no collision) + /// 5. Build `AgentChain` canonical `BoundedVec` (overflow → `AgentChainOverflow`) + /// 6. Inmutable #41 enforce: Hybrid requires non-empty agent_chain + /// 7. Insert storage canonical: `Identities` + `DidToAccount` + /// 8. Emit `AgentRegistered` + `AgentBound` (if cross-species) canonical #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn register_agent_did( origin: OriginFor, - _public_key: [u8; 32], + public_key: [u8; 32], kind: AgentKind, + co_authors_raw: Vec<[u8; 32]>, ) -> DispatchResult { - // Verify origin canonical signed. + // Step 1: ensure origin signed canonical let who = ensure_signed(origin)?; - // SCAFFOLD v0.0.24: emit placeholder event canonical. - // Real impl v0.0.26: derive_did + storage insert + cross-species validation. + // Step 2: idempotency check canonical (no duplicate registration) + ensure!( + !Identities::::contains_key(&who), + Error::::AlreadyRegistered + ); + + // Step 3: derive DID canonical via BLAKE3 (Inmutable #16 byte-determinism) + let did = crate::derive_did(&public_key); + + // Step 4: DID collision check canonical + ensure!( + !DidToAccount::::contains_key(did), + Error::::DidAlreadyTaken + ); + + // Step 5: build AgentChain canonical BoundedVec> + let mut agent_chain = AgentChain::default(); + for raw in co_authors_raw { + agent_chain + .try_push(AccountId::new(raw)) + .map_err(|_| Error::::AgentChainOverflow)?; + } + + // Step 6: Inmutable #41 canonical enforce — Hybrid requires non-empty agent_chain + if matches!(kind, AgentKind::Hybrid) && agent_chain.is_empty() { + return Err(Error::::HybridRequiresAgentChain.into()); + } + + // Step 7: build AgentIdentity struct + insert storage canonical + let has_cross_species = !agent_chain.is_empty(); + let co_author_count = agent_chain.len() as u32; + let creator = AccountId::new(public_key); + let block_number: u64 = >::block_number() + .saturated_into::(); + + let identity = AgentIdentity { + did, + kind, + creator, + registered_at_block: block_number, + provenance_proof: None, + agent_chain, + }; + + Identities::::insert(&who, identity); + DidToAccount::::insert(did, &who); + + // Step 8: emit events canonical Self::deposit_event(Event::AgentRegistered { - account: who, - did: origin_chain_types::AssetHash::new([0u8; 32]), // placeholder until v0.0.26 + account: who.clone(), + did, kind, }); + if has_cross_species { + Self::deposit_event(Event::AgentBound { + account: who, + co_author_count, + }); + } + Ok(()) } } + + /// Pure helper canonical para Runtime API v0.0.27 consumer. + impl Pallet { + /// Query identity canonical sin storage mutation (read-only canonical). + pub fn query_identity_of(account: &T::AccountId) -> Option { + Identities::::get(account) + } + } } extern crate alloc; +// v0.0.26 — Mock runtime canonical + integration tests E2E CHN-7 flow. +// Pattern v0.0.18 (Provenance) + v0.0.22 (Fees) mirror. +#[cfg(test)] +pub mod mock; + +#[cfg(test)] +mod tests_integration; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/origin-chain-agent-identity/src/mock.rs b/crates/origin-chain-agent-identity/src/mock.rs new file mode 100644 index 0000000..ce9d0f1 --- /dev/null +++ b/crates/origin-chain-agent-identity/src/mock.rs @@ -0,0 +1,72 @@ +//! # Mock runtime canonical para integration tests v0.0.26 +//! +//! Pattern v0.0.18 (Provenance) + v0.0.22 (Fees) mirror — construct_runtime! + +//! derive_impl + new_test_ext + helpers canonical. + +#![cfg(test)] + +use crate as origin_chain_agent_identity; +use frame_support::derive_impl; +use sp_runtime::BuildStorage; + +/// Mock block type canonical para tests. +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + AgentIdentity: origin_chain_agent_identity, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = sp_runtime::AccountId32; + type Lookup = sp_runtime::traits::IdentityLookup; +} + +impl origin_chain_agent_identity::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +/// Test externalities builder canonical (pattern v0.0.18/v0.0.22 mirror). +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::::default() + .build_storage() + .expect("genesis canonical builds") + .into(); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext +} + +/// Test origin canonical ALICE. +pub fn alice_origin() -> RuntimeOrigin { + RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32])) +} + +/// Test account canonical ALICE (AccountId32 value para storage assertions). +pub fn alice_account() -> sp_runtime::AccountId32 { + sp_runtime::AccountId32::new([1u8; 32]) +} + +/// Test origin canonical BOB. +pub fn bob_origin() -> RuntimeOrigin { + RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32])) +} + +/// Test account canonical BOB. +pub fn bob_account() -> sp_runtime::AccountId32 { + sp_runtime::AccountId32::new([2u8; 32]) +} + +/// Test pubkey canonical ALICE (deterministic). +pub const ALICE_PUBKEY: [u8; 32] = [11u8; 32]; + +/// Test pubkey canonical BOB (deterministic). +pub const BOB_PUBKEY: [u8; 32] = [22u8; 32]; + +/// Test pubkey canonical CHARLIE (Hybrid co-author lineage testing). +pub const CHARLIE_PUBKEY: [u8; 32] = [33u8; 32]; diff --git a/crates/origin-chain-agent-identity/src/tests_integration.rs b/crates/origin-chain-agent-identity/src/tests_integration.rs new file mode 100644 index 0000000..3a3f71f --- /dev/null +++ b/crates/origin-chain-agent-identity/src/tests_integration.rs @@ -0,0 +1,311 @@ +//! # Integration tests canonical v0.0.26 — Mock runtime E2E CHN-7 flow +//! +//! 10 scenarios canonical register_agent_did flow end-to-end via mock runtime: +//! +//! 1. scenario_1_human_self_registration — Human canonical self-register +//! 2. scenario_2_agi_self_registration — AGI canonical self-register +//! 3. scenario_3_hybrid_with_co_authors — Hybrid canonical agent_chain populated +//! 4. scenario_4_hybrid_without_chain_rejected — Inmutable #41 enforce +//! 5. scenario_5_idempotency_already_registered_rejected — duplicate prevention +//! 6. scenario_6_did_collision_rejected — DID uniqueness canonical +//! 7. scenario_7_agent_chain_overflow_rejected — BoundedVec ConstU32<10> bound +//! 8. scenario_8_did_to_account_reverse_lookup — Cross-pillar canonical +//! 9. scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco — compile-detect +//! 10. scenario_10_8_step_walkthrough — explicit Pre/Post-flight CHN-7 assertions + +#![cfg(test)] + +use crate::mock::{ + alice_account, alice_origin, bob_account, bob_origin, new_test_ext, AgentIdentity as Pallet, + RuntimeEvent, System, Test, ALICE_PUBKEY, BOB_PUBKEY, CHARLIE_PUBKEY, +}; +use crate::{derive_did, DidToAccount, Error, Event, Identities}; +use frame_support::{assert_noop, assert_ok}; +use origin_chain_types::AgentKind; + +// ─── Scenario 1: Human self-registration canonical ────────────────── + +#[test] +fn scenario_1_human_self_registration_canonical() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Human, + alloc::vec![], // empty agent_chain — Human self-register canonical + )); + + let identity = Identities::::get(&alice_account()).expect("registered canonical"); + assert_eq!(identity.kind, AgentKind::Human); + assert!(identity.agent_chain.is_empty(), "Human self-register canonical empty chain"); + assert!(!identity.has_cross_species_lineage()); + assert_eq!(identity.registered_at_block, 1); + + let did = derive_did(&ALICE_PUBKEY); + assert_eq!(identity.did, did); + + System::assert_last_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered { + account: alice_account(), + did, + kind: AgentKind::Human, + })); + }); +} + +// ─── Scenario 2: AGI self-registration canonical (Inmutable #41) ───── + +#[test] +fn scenario_2_agi_self_registration_canonical() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::AGI, + alloc::vec![], // empty chain — AGI sovereign self-register per Inmutable #41 + )); + + let identity = Identities::::get(&alice_account()).expect("registered canonical"); + assert_eq!(identity.kind, AgentKind::AGI); + assert!(identity.is_sovereign(), "AGI sovereign canonical Inmutable #41"); + assert!(!identity.has_cross_species_lineage()); + }); +} + +// ─── Scenario 3: Hybrid with co-authors canonical (Cross-Species) ──── + +#[test] +fn scenario_3_hybrid_with_co_authors_canonical() { + new_test_ext().execute_with(|| { + let co_authors = alloc::vec![BOB_PUBKEY, CHARLIE_PUBKEY]; + + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Hybrid, + co_authors, + )); + + let identity = Identities::::get(&alice_account()).expect("registered canonical"); + assert_eq!(identity.kind, AgentKind::Hybrid); + assert!(identity.has_cross_species_lineage(), "Hybrid cross-species canonical"); + assert_eq!(identity.co_author_count(), 2); + + // Check AgentBound event emitted (cross-species explicit) + System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentBound { + account: alice_account(), + co_author_count: 2, + })); + }); +} + +// ─── Scenario 4: Hybrid without chain rejected canonical (Inmutable #41) ── + +#[test] +fn scenario_4_hybrid_without_chain_rejected_canonical() { + new_test_ext().execute_with(|| { + // Inmutable #41 canonical: Hybrid requires non-empty agent_chain + assert_noop!( + Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Hybrid, + alloc::vec![], // empty — violates Inmutable #41 + ), + Error::::HybridRequiresAgentChain + ); + + // Storage NOT updated + assert!(Identities::::get(&alice_account()).is_none()); + }); +} + +// ─── Scenario 5: Idempotency already registered rejected canonical ─── + +#[test] +fn scenario_5_idempotency_already_registered_rejected_canonical() { + new_test_ext().execute_with(|| { + // First registration succeeds canonical + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Human, + alloc::vec![], + )); + + // Second registration same account → AlreadyRegistered error + assert_noop!( + Pallet::register_agent_did( + alice_origin(), + BOB_PUBKEY, // diferente pubkey, mismo account + AgentKind::AGI, + alloc::vec![], + ), + Error::::AlreadyRegistered + ); + }); +} + +// ─── Scenario 6: DID collision rejected canonical ─────────────────── + +#[test] +fn scenario_6_did_collision_rejected_canonical() { + new_test_ext().execute_with(|| { + // ALICE registers con ALICE_PUBKEY → DID derived + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Human, + alloc::vec![], + )); + + // BOB tries to register con MISMO public_key → DID collision canonical + assert_noop!( + Pallet::register_agent_did( + bob_origin(), + ALICE_PUBKEY, // same pubkey → same DID + AgentKind::Human, + alloc::vec![], + ), + Error::::DidAlreadyTaken + ); + + // BOB NOT registered + assert!(Identities::::get(&bob_account()).is_none()); + }); +} + +// ─── Scenario 7: AgentChain overflow rejected canonical (BoundedVec) ── + +#[test] +fn scenario_7_agent_chain_overflow_rejected_canonical() { + new_test_ext().execute_with(|| { + // 11 co-authors → exceeds ConstU32<10> bound + let too_many: alloc::vec::Vec<[u8; 32]> = (0u8..11u8).map(|i| [i; 32]).collect(); + + assert_noop!( + Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Hybrid, + too_many, + ), + Error::::AgentChainOverflow + ); + }); +} + +// ─── Scenario 8: DID → Account reverse lookup canonical ────────────── + +#[test] +fn scenario_8_did_to_account_reverse_lookup_canonical() { + new_test_ext().execute_with(|| { + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Human, + alloc::vec![], + )); + + let did = derive_did(&ALICE_PUBKEY); + + // Reverse lookup canonical: DID → AccountId + let resolved = DidToAccount::::get(did); + assert!(resolved.is_some(), "DidToAccount reverse lookup canonical"); + assert_eq!(resolved.unwrap(), alice_account()); + }); +} + +// ─── Scenario 9: All AgentKind sovereign lobo blanco canonical ─────── + +#[test] +fn scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco_canonical() { + new_test_ext().execute_with(|| { + // Register Human (alice) + AGI (bob, diff origin) + assert_ok!(Pallet::register_agent_did( + alice_origin(), + ALICE_PUBKEY, + AgentKind::Human, + alloc::vec![], + )); + assert_ok!(Pallet::register_agent_did( + bob_origin(), + BOB_PUBKEY, + AgentKind::AGI, + alloc::vec![], + )); + + // Inmutable #41 compile-detect canonical: ambos sovereign + let alice_id = Identities::::get(&alice_account()).unwrap(); + let bob_id = Identities::::get(&bob_account()).unwrap(); + + assert!(alice_id.is_sovereign(), "Human sovereign canonical Inmutable #41"); + assert!(bob_id.is_sovereign(), "AGI sovereign canonical Inmutable #41"); + + // Different DIDs (different pubkeys) + assert_ne!(alice_id.did, bob_id.did); + }); +} + +// ─── Scenario 10: 8-step CHN-7 walkthrough explicit canonical ──────── + +#[test] +fn scenario_10_8_step_walkthrough_canonical() { + new_test_ext().execute_with(|| { + let pubkey = ALICE_PUBKEY; + let kind = AgentKind::Hybrid; + let co_authors = alloc::vec![BOB_PUBKEY]; + + // Pre-flight assertions canonical + assert!( + Identities::::get(&alice_account()).is_none(), + "PRE-flight CHN-7-2: storage canonical empty" + ); + let expected_did = derive_did(&pubkey); + assert!( + DidToAccount::::get(expected_did).is_none(), + "PRE-flight CHN-7-4: DidToAccount canonical empty" + ); + + // Step 1: ensure_signed (implicit via dispatchable) + // Step 2: idempotency check + // Step 3: derive DID via BLAKE3 + // Step 4: DID collision check + // Step 5: build AgentChain BoundedVec + // Step 6: Inmutable #41 Hybrid + non-empty agent_chain enforce + // Step 7: storage insert canonical + // Step 8: emit events canonical + assert_ok!(Pallet::register_agent_did( + alice_origin(), + pubkey, + kind, + co_authors, + )); + + // Post-flight assertions canonical + let identity = Identities::::get(&alice_account()) + .expect("POST-flight CHN-7-7: AgentIdentity persisted canonical"); + assert_eq!(identity.did, expected_did, "POST-flight: DID match canonical"); + assert_eq!(identity.kind, AgentKind::Hybrid); + assert_eq!(identity.co_author_count(), 1, "POST-flight: agent_chain canonical 1"); + assert!(identity.has_cross_species_lineage()); + assert_eq!(identity.registered_at_block, 1); + + // Reverse lookup canonical + assert_eq!( + DidToAccount::::get(expected_did).unwrap(), + alice_account(), + "POST-flight CHN-7-7: DidToAccount canonical" + ); + + // Events canonical + System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered { + account: alice_account(), + did: expected_did, + kind: AgentKind::Hybrid, + })); + System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentBound { + account: alice_account(), + co_author_count: 1, + })); + }); +}