From 8af9e8bb962a4e2a777117bfbea2dedad6b50d0d Mon Sep 17 00:00:00 2001 From: Ayfataan Date: Sat, 26 Sep 2026 10:08:44 +0100 Subject: [PATCH 1/2] test(e2e): add comprehensive integration test suite for agent spending flow (#309) --- tests/src/e2e_agent_spending.rs | 82 ++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/tests/src/e2e_agent_spending.rs b/tests/src/e2e_agent_spending.rs index cd804aef..ac6d8a87 100644 --- a/tests/src/e2e_agent_spending.rs +++ b/tests/src/e2e_agent_spending.rs @@ -3,9 +3,9 @@ //! Deploys all eight workspace contracts into one mock Soroban [`Env`] and //! exercises the full lifecycle of an agent spend: //! -//! 1. `initialize` the registry and register the organization. -//! 2. Deploy treasury, budget, policy, wallet and escrow; register their -//! addresses in the registry under the organization. +//! 1. Deploy and initialize all eight contracts in architectural order: +//! registry, wallet, treasury, multisig, proposal, budget, policy, escrow. +//! 2. Register their addresses under the organization. //! 3. Mint a real SAC token and fund the treasury. //! 4. Create a wallet for the org's owner and give the agent the Agent role. //! 5. Allocate a budget and register a policy that caps agent spending. @@ -20,7 +20,9 @@ use astroid_budget::{BudgetContract, BudgetContractClient, Period}; use astroid_escrow::{EscrowContract, EscrowContractClient, EscrowState}; +use astroid_multisig::{MultiSigContract, MultiSigContractClient, SignerWeight}; use astroid_policy::{PolicyContract, PolicyContractClient}; +use astroid_proposal::{ProposalContract, ProposalContractClient}; use astroid_registry::{RegistryContract, RegistryContractClient}; use astroid_shared::errors::Error; use astroid_shared::types::{AssetAmount, ModuleKind, ResourceState}; @@ -67,71 +69,89 @@ fn setup() -> Harness<'static> { let org_owner = Address::generate(&env); let agent = Address::generate(&env); let recipient = Address::generate(&env); - let multisig = Address::generate(&env); - // 1. Registry — source of truth for module addresses. let registry_id = env.register_contract(None, RegistryContract); let registry = RegistryContractClient::new(&env, ®istry_id); registry.initialize(&admin); registry.register_org(&admin, &String::from_str(&env, ORG), &org_owner); - // 2. Treasury — custodies org funds. The multisig is recorded so the - // emergency freeze path has a counterparty. + // 2. Wallet — per-org custody with role-based access. + let wallet_id = env.register_contract(None, WalletContract); + let wallet = WalletContractClient::new(&env, &wallet_id); + wallet.initialize(&admin); + + // 3. Treasury — custodies org funds. let treasury_id = env.register_contract(None, TreasuryContract); let treasury = TreasuryContractClient::new(&env, &treasury_id); treasury.initialize(&String::from_str(&env, ORG), &admin); - treasury.set_multisig(&admin, &multisig); - // 3. Budget — spending limits consumed by the treasury. + // 4. Multisig — threshold governance for treasury actions. + let multisig_id = env.register_contract(None, MultiSigContract); + let multisig = MultiSigContractClient::new(&env, &multisig_id); + multisig.initialize( + &vec![ + &env, + SignerWeight { + address: admin.clone(), + weight: 1, + }, + SignerWeight { + address: org_owner.clone(), + weight: 1, + }, + SignerWeight { + address: agent.clone(), + weight: 1, + }, + ], + &2, + ); + treasury.set_multisig(&admin, &multisig_id); + + // 5. Proposal — organization action approval flow. + let proposal_id = env.register_contract(None, ProposalContract); + let proposal = ProposalContractClient::new(&env, &proposal_id); + proposal.initialize(&0); + + // 6. Budget — spending limits consumed by the treasury. let budget_id = env.register_contract(None, BudgetContract); let budget = BudgetContractClient::new(&env, &budget_id); budget.initialize(&admin); - // 4. Policy — rule engine consulted before every spend. + // 7. Policy — rule engine consulted before every spend. let policy_id = env.register_contract(None, PolicyContract); let policy = PolicyContractClient::new(&env, &policy_id); policy.initialize(); - // 5. Wallet — per-org custody with role-based access. - let wallet_id = env.register_contract(None, WalletContract); - let wallet = WalletContractClient::new(&env, &wallet_id); - wallet.initialize(&admin); - - // 6. Escrow — time-locked conditional custody. + // 8. Escrow — time-locked conditional custody. let escrow_id = env.register_contract(None, EscrowContract); let escrow = EscrowContractClient::new(&env, &escrow_id); escrow.initialize(); - // 7. Multisig + proposal — deployed and recorded in the registry like the - // other modules; governance flows in these tests act through the - // admin/owner roles directly. - let multisig_contract = env.register_contract(None, astroid_multisig::MultiSigContract); - let proposal_contract = env.register_contract(None, astroid_proposal::ProposalContract); + // Register modules in the same order as the architecture. registry.register_module( &admin, &String::from_str(&env, ORG), - &ModuleKind::Multisig, - &multisig_contract, + &ModuleKind::Wallet, + &wallet_id, ); registry.register_module( &admin, &String::from_str(&env, ORG), - &ModuleKind::Proposal, - &proposal_contract, + &ModuleKind::Treasury, + &treasury_id, ); - - // 8. Register the org's core modules so `lookup` resolves them. registry.register_module( &admin, &String::from_str(&env, ORG), - &ModuleKind::Treasury, - &treasury_id, + &ModuleKind::Multisig, + &multisig_id, ); registry.register_module( &admin, &String::from_str(&env, ORG), - &ModuleKind::Wallet, - &wallet_id, + &ModuleKind::Proposal, + &proposal_id, ); registry.register_module( &admin, From 537a14d360ec8c106b5c38e199c388ab91357121 Mon Sep 17 00:00:00 2001 From: Ayfataan Date: Mon, 5 Oct 2026 10:57:11 +0000 Subject: [PATCH 2/2] test(e2e): fix contract registration sequence in agent spending integration tests --- contracts/registry/src/test.rs | 11 ----------- tests/src/e2e_agent_spending.rs | 23 +++++++++++------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/contracts/registry/src/test.rs b/contracts/registry/src/test.rs index 84e7a207..64e2ffe1 100644 --- a/contracts/registry/src/test.rs +++ b/contracts/registry/src/test.rs @@ -24,17 +24,6 @@ fn assert_event(env: &Env, variant: &str) { assert!(found, "expected ContractEvent::{} to be emitted", variant); } -/// Count canonical `ContractEvent` emissions of the given variant symbol so -/// tests can also assert that an event did *not* fire. -fn count_events(env: &Env, variant: &str) -> usize { - let want: Val = Symbol::new(env, variant).into_val(env); - env.events() - .all() - .iter() - .filter(|(_contract_id, topics, _data)| topics.contains(want)) - .count() -} - fn setup() -> (Env, RegistryContractClient<'static>, Address) { let env = Env::default(); env.mock_all_auths(); diff --git a/tests/src/e2e_agent_spending.rs b/tests/src/e2e_agent_spending.rs index 40f853d6..3b067f7e 100644 --- a/tests/src/e2e_agent_spending.rs +++ b/tests/src/e2e_agent_spending.rs @@ -20,8 +20,9 @@ use astroid_budget::{BudgetContract, BudgetContractClient, Period}; use astroid_escrow::{EscrowContract, EscrowContractClient, EscrowState, ReleaseConditionConfig}; +use astroid_multisig::{MultiSigContract, MultiSigContractClient, SignerWeight}; use astroid_policy::{PolicyContract, PolicyContractClient}; -use astroid_proposal::{ProposalContractClient, ProposalState}; +use astroid_proposal::{ProposalContract, ProposalContractClient, ProposalState}; use astroid_registry::{RegistryContract, RegistryContractClient}; use astroid_shared::errors::Error; use astroid_shared::types::{AssetAmount, ModuleKind, ResourceState}; @@ -60,6 +61,10 @@ struct Harness<'a> { /// Deploy every workspace contract in dependency order, register the org's /// module addresses with the registry, and fund the treasury. fn setup() -> Harness<'static> { + setup_with_timelock(0) +} + +fn setup_with_timelock(timelock: u64) -> Harness<'static> { let env = Env::default(); env.mock_all_auths(); env.ledger().with_mut(|l| l.timestamp = START); @@ -110,7 +115,7 @@ fn setup() -> Harness<'static> { // 5. Proposal — organization action approval flow. let proposal_id = env.register_contract(None, ProposalContract); let proposal = ProposalContractClient::new(&env, &proposal_id); - proposal.initialize(&0); + proposal.initialize(&timelock); // 6. Budget — spending limits consumed by the treasury. let budget_id = env.register_contract(None, BudgetContract); @@ -122,12 +127,7 @@ fn setup() -> Harness<'static> { let policy = PolicyContractClient::new(&env, &policy_id); policy.initialize(); - // 5. Wallet — per-org custody with role-based access. - let wallet_id = env.register_contract(None, WalletContract); - let wallet = WalletContractClient::new(&env, &wallet_id); - wallet.initialize(&admin); - - // 6. Escrow — time-locked conditional custody. The token whitelist starts + // 8. Escrow — time-locked conditional custody. The token whitelist starts // empty, so the admin must approve the asset before anything can be // escrowed. let escrow_id = env.register_contract(None, EscrowContract); @@ -206,7 +206,7 @@ fn setup() -> Harness<'static> { policy, wallet, escrow, - multisig, + multisig: multisig_id, admin, org_owner, agent, @@ -960,15 +960,14 @@ fn registry_links_the_deployed_modules() { /// through the contract's own views and the registry. #[test] fn proposal_timelock_gates_execution_end_to_end() { - let h = setup(); + let h = setup_with_timelock(100); // The harness deployed the proposal contract and registered it in the // registry; resolve it through the registry like a real integrator. let proposal_id = h.registry.lookup(&string(&h, ORG), &ModuleKind::Proposal); let proposal = ProposalContractClient::new(&h.env, &proposal_id); - // Configure a 100-second timelock (the contract is fresh in this env). - proposal.initialize(&100); + // The harness configured a 100-second timelock at deployment. assert_eq!(h.env.ledger().timestamp(), START); let proposer = h.org_owner.clone();