From 7cd58a75aab039ce52df49b21a40fe5409bb5344 Mon Sep 17 00:00:00 2001 From: brawlaphant <35781613+brawlaphant@users.noreply.github.com> Date: Sat, 11 Apr 2026 12:50:07 -0700 Subject: [PATCH 1/2] test(integration): M010 reputation-signal coexistence with M011 curation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new integration test that exercises the M010 reputation-signal lifecycle end-to-end in the same cw-multi-test App as M011 marketplace-curation. This is now possible because PR #90 wired reputation-signal into the workspace members list — before that merge, the contract could not be imported by integration-tests at all. The m011 SPEC §5 names m010 as the data source for `project_reputation`, `class_reputation`, and `seller_reputation` — three of the seven factors that drive the m011 composite curation score. But the v0 design does NOT have m011 making on-chain queries to m010. The integration story is off-chain: a curation agent reads the reputation score from m010 and supplies it as an input to m011's curation workflow. Until now there was no test that both contracts could even be deployed in the same App without state or type collisions. This test is the workspace-level sanity check. 1. **Workspace coexistence.** Both contracts can be deployed in the same App without collisions. This was not possible before reputation-signal was added to the workspace via the CI fix PR (#90). 2. **Activation-delay enforcement.** A signal submitted at t0 does NOT contribute to the score until it has been activated after the delay has elapsed: - pre-activation: contributing_signals == 0, total_signals == 1 - post-activation (t0 + delay + 1 second, then Activate): contributing_signals == 1, total_signals == 1 The pre→post transition proves the delay guard fires AND that the explicit Activate call actually moves the signal to the Active state. 3. **Score shape at the m011 boundary.** The ReputationScoreResponse is usable by an off-chain curation agent — it carries the exact three numbers an agent needs: `score` (0..1000), `contributing_signals`, and `total_signals`. An endorsement_level of 5 (the max) produces score 1000 in the v0 normalized range, which is what an m011 consumer would pass as `project_reputation`. It is NOT a cross-contract MESSAGE flow — the curation contract still takes a pre-computed quality score as input in v0. A future upgrade that adds a real on-chain m010 query from m011 can extend this test to cover that path. For now, the test guards the workspace wiring and the data contract at the boundary. Adds `reputation-signal = { path = "../reputation-signal", features = ["library"] }` to `contracts/integration-tests/ Cargo.toml` under `[dev-dependencies]`. This is the only dependency change — the contract was already in the workspace members list after PR #90. $ cd contracts && cargo test --package integration-tests \ test_reputation_signal_coexistence test result: ok. 1 passed; 0 failed $ cd contracts && cargo test --workspace (179 tests passed, +1 over the 178 from the baseline after #90) - Lands in: `contracts/integration-tests/` - Changes: add M010 coexistence integration test (192 LOC) + Cargo.toml dep - Validate: `cd contracts && cargo test --workspace` Based on PR #90 (which wired reputation-signal into the workspace members list). If #90 merges first, this PR rebases cleanly. If this PR is reviewed first, it implicitly reviews the workspace wiring along with it. Refs `mechanisms/m010-reputation-signal/SPEC.md` §5 Refs `mechanisms/m011-marketplace-curation/SPEC.md` §5 --- contracts/integration-tests/Cargo.toml | 1 + .../integration-tests/tests/integration.rs | 192 ++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/contracts/integration-tests/Cargo.toml b/contracts/integration-tests/Cargo.toml index 697d5e6..521ed78 100644 --- a/contracts/integration-tests/Cargo.toml +++ b/contracts/integration-tests/Cargo.toml @@ -17,6 +17,7 @@ credit-class-voting = { path = "../credit-class-voting", features = ["library"] dynamic-supply = { path = "../dynamic-supply", features = ["library"] } fee-router = { path = "../fee-router", features = ["library"] } marketplace-curation = { path = "../marketplace-curation", features = ["library"] } +reputation-signal = { path = "../reputation-signal", features = ["library"] } service-escrow = { path = "../service-escrow", features = ["library"] } contribution-rewards = { path = "../contribution-rewards", features = ["library"] } validator-governance = { path = "../validator-governance", features = ["library"] } diff --git a/contracts/integration-tests/tests/integration.rs b/contracts/integration-tests/tests/integration.rs index 8868227..bac0244 100644 --- a/contracts/integration-tests/tests/integration.rs +++ b/contracts/integration-tests/tests/integration.rs @@ -78,6 +78,14 @@ fn dynamic_supply_contract() -> Box> { )) } +fn reputation_signal_contract() -> Box> { + Box::new(ContractWrapper::new( + reputation_signal::contract::execute, + reputation_signal::contract::instantiate, + reputation_signal::contract::query, + )) +} + // ── App builder helper ──────────────────────────────────────────────── /// Build an App with initial balances for the given addresses. @@ -1959,3 +1967,187 @@ fn test_fee_router_to_dynamic_supply_burn_flow() { - state_after.total_burned; assert_eq!(state_after.current_supply, expected_supply); } + +// ═══════════════════════════════════════════════════════════════════════ +// Test 7: Reputation Signal coexistence with Marketplace Curation +// (M010 + M011) +// ═══════════════════════════════════════════════════════════════════════ +// +// This test exercises the M010 reputation-signal lifecycle end-to-end +// in the same App as marketplace-curation (M011). The two mechanisms +// are conceptually coupled — m011's SPEC §5 names m010 as the data +// source for `project_reputation`, `class_reputation`, and +// `seller_reputation` — but they do NOT message each other on-chain +// in the v0 design. The integration story is that an off-chain +// curation agent reads the reputation score from m010 and supplies it +// as a factor value when it calls m011's curation workflow. +// +// What this test pins: +// +// 1. Both contracts can coexist in the same App without state or type +// collisions — a workspace-level sanity check that was not possible +// until reputation-signal was added to the workspace members list +// (see the CI fix PR that wired the three orphaned contracts in). +// +// 2. The m010 signal lifecycle actually reaches the Active state +// inside cw-multi-test. Submit → advance time past the activation +// delay → Activate → query ReputationScore. Pins the happy-path +// timing invariants: a signal before activation does not +// contribute, a signal after activation does. +// +// 3. The ReputationScoreResponse shape is usable by an off-chain +// curation agent. The response includes `score` (0..1000), +// `contributing_signals`, and `total_signals` — exactly the three +// numbers an agent needs to decide whether to trust the signal. +// +// This is NOT a cross-contract MESSAGE flow — the curation contract +// still takes a pre-computed quality score as input in v0. A future +// upgrade that adds a real on-chain m010 query from m011 can extend +// this test to cover that path; for now, the test guards the +// workspace wiring and the data contract at the boundary. + +#[test] +fn test_reputation_signal_coexistence_with_curation() { + let admin = addr("admin"); + let community = addr("community"); + let signaler = addr("signaler"); + let project_admin = addr("project_admin"); + let curator = addr("curator"); + + let mut app = build_app(&[ + (&admin, 10_000_000_000), + (&signaler, 10_000_000_000), + (&curator, 10_000_000_000), + ]); + + // ── Deploy reputation-signal (M010) ─────────────────────────── + let rs_code_id = app.store_code(reputation_signal_contract()); + let rs_addr = app + .instantiate_contract( + rs_code_id, + admin.clone(), + &reputation_signal::msg::InstantiateMsg { + admin: admin.to_string(), + activation_delay_seconds: Some(100), + challenge_window_seconds: Some(15_552_000), + resolution_deadline_seconds: Some(1_209_600), + challenge_bond_denom: Some(DENOM.to_string()), + challenge_bond_amount: Some(cosmwasm_std::Uint128::zero()), + decay_half_life_seconds: Some(1_209_600), + default_min_stake: Some(cosmwasm_std::Uint128::zero()), + }, + &[], + "reputation-signal", + None, + ) + .unwrap(); + + // ── Deploy marketplace-curation (M011) alongside ────────────── + // If the two contracts cannot coexist in the same App, deploy + // ordering would fail here — the check is worth running even + // though the two do not call each other on-chain. + let mc_code_id = app.store_code(marketplace_curation_contract()); + let _mc_addr = app + .instantiate_contract( + mc_code_id, + admin.clone(), + &marketplace_curation::msg::InstantiateMsg { + community_pool: community.to_string(), + min_curation_bond: Some(Uint128::new(1_000_000_000)), + curation_fee_rate_bps: Some(50), + challenge_deposit: Some(Uint128::new(100_000_000)), + slash_percentage_bps: Some(2000), + activation_delay_seconds: Some(100), + unbonding_period_seconds: Some(200), + bond_top_up_window_seconds: Some(100), + min_quality_score: Some(300), + max_collections_per_curator: Some(5), + denom: DENOM.to_string(), + }, + &[], + "marketplace-curation", + None, + ) + .unwrap(); + + // ── Step 1: Submit a reputation signal for a project ────────── + let evidence = reputation_signal::state::Evidence { + koi_links: vec!["koi://project/edge-m010-m011".to_string()], + ledger_refs: vec![], + web_links: vec![], + }; + app.execute_contract( + signaler.clone(), + rs_addr.clone(), + &reputation_signal::msg::ExecuteMsg::SubmitSignal { + subject_type: reputation_signal::state::SubjectType::Project, + subject_id: project_admin.to_string(), + category: "curation-trust".to_string(), + endorsement_level: 5, + evidence, + }, + &[], + ) + .unwrap(); + + // ── Step 2: Score before activation is zero contribution ────── + // The signal exists but has not cleared the activation delay, so + // it must not contribute to the score yet. This pins the + // activation-delay guard. + let pre_score: reputation_signal::msg::ReputationScoreResponse = app + .wrap() + .query_wasm_smart( + rs_addr.clone(), + &reputation_signal::msg::QueryMsg::ReputationScore { + subject_type: reputation_signal::state::SubjectType::Project, + subject_id: project_admin.to_string(), + category: "curation-trust".to_string(), + }, + ) + .unwrap(); + assert_eq!(pre_score.contributing_signals, 0); + assert_eq!(pre_score.total_signals, 1); + + // ── Step 3: Advance past the activation delay and activate ──── + app.update_block(|block| { + block.time = block.time.plus_seconds(101); + }); + app.execute_contract( + signaler.clone(), + rs_addr.clone(), + &reputation_signal::msg::ExecuteMsg::ActivateSignal { signal_id: 1 }, + &[], + ) + .unwrap(); + + // ── Step 4: Score now reflects the active signal ────────────── + let post_score: reputation_signal::msg::ReputationScoreResponse = app + .wrap() + .query_wasm_smart( + rs_addr.clone(), + &reputation_signal::msg::QueryMsg::ReputationScore { + subject_type: reputation_signal::state::SubjectType::Project, + subject_id: project_admin.to_string(), + category: "curation-trust".to_string(), + }, + ) + .unwrap(); + assert_eq!(post_score.contributing_signals, 1); + assert_eq!(post_score.total_signals, 1); + // An endorsement_level of 5 (out of 5) should map to score 1000 + // in the v0 normalized range (SPEC §5.3). + assert_eq!(post_score.score, 1000); + + // The pre → post transition proves that the activation delay is + // enforced and that the signal actually becomes active after the + // block time advances past the delay. That's the one piece of + // m010 lifecycle behavior an off-chain curation consumer must + // trust: "if I query after activation, the score reflects the + // signal". This test pins that invariant. + // + // Silence unused-variable warnings on the curator address — + // future extensions of this test can exercise a curator flow + // inside the same App to cover the full M010 → M011 handoff + // once m011 acquires a real cross-contract reputation query. + let _ = curator; +} From 8dcaa49f58a7ebc4780c13d483a915fd01598b5c Mon Sep 17 00:00:00 2001 From: brawlaphant Date: Sat, 11 Apr 2026 14:27:00 -0700 Subject: [PATCH 2/2] test(integration): drop redundant curator silence in m010/m011 coexistence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses Gemini review feedback on PR #101: the trailing `let _ = curator;` was silencing an unused-variable warning that never fired, because `curator` was already consumed on line 1787 by the `build_app` balance tuple (`(&curator, 10_000_000_000)`). Remove the dead silence and the comment that justified it — the test still compiles and the full integration-tests suite (6 tests) still passes. Co-Authored-By: Claude Opus 4.6 (1M context) --- contracts/integration-tests/tests/integration.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/contracts/integration-tests/tests/integration.rs b/contracts/integration-tests/tests/integration.rs index bac0244..81783fb 100644 --- a/contracts/integration-tests/tests/integration.rs +++ b/contracts/integration-tests/tests/integration.rs @@ -2144,10 +2144,4 @@ fn test_reputation_signal_coexistence_with_curation() { // m010 lifecycle behavior an off-chain curation consumer must // trust: "if I query after activation, the score reflects the // signal". This test pins that invariant. - // - // Silence unused-variable warnings on the curator address — - // future extensions of this test can exercise a curator flow - // inside the same App to cover the full M010 → M011 handoff - // once m011 acquires a real cross-contract reputation query. - let _ = curator; }