MSG Chain 测试与 CI/CD 指南 — 完整质量保障体系
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
目录
1. 测试体系总览
1.1 MSG Chain 测试金字塔
MSG Chain 采用经典测试金字塔架构,从底层到顶层覆盖完整的质量保障体系。类似 Cosmos SDK 的测试架构,但针对 MSG Chain 的后量子密码学(Dilithium-5)、CosmWasm 合约体系、以及 AI Agent 经济体进行了定制化扩展。
┌──────────┐
│ E2E │ ← 端到端测试 (Playwright/Cypress)
│ 测试 │
┌┴──────────┴┐
│ 集成测试 │ ← cw-multi-test / Go testnet
│ CosmWasm │ ← 多合约交互 / 跨合约调用
┌┴────────────┴┐
│ 合约单元测试 │ ← #[cfg(test)] + mock_dependencies
│ Rust 测试 │ ← cargo test 每个合约包
┌┴──────────────┴┐
│ Go 端测试 │ ← make test (pkg 测试)
│ 节点测试 │ ← make test-quantum (Dilithium-5 PQ)
┌┴────────────────┴┐
│ 模糊测试 │ ← proptest / fuzz testing
│ 属性测试 │ ← 状态不变量检查
┌┴──────────────────┴┐
│ 静态分析 │ ← golangci-lint / go vet / gofmt
│ 安全扫描 │ ← gosec / cargo audit
└────────────────────┘
1.2 测试命令速查表
所有命令均来自 MSG Chain 的 Makefile 和 command_registry.json:
| 命令 | Make 目标 | 阶段 | 产出 |
|---|---|---|---|
make deps |
deps |
prepare | Go 依赖安装完成 |
make lint |
lint |
quality_gate | gofmt 检查 + go vet + golangci-lint |
make test |
test |
quality_gate | pkg 测试结果 |
make test-quantum |
test-quantum |
quality_gate | pkg/quantum 测试结果 |
make build-linux |
build-linux |
build | bin/genesis_node_linux, bin/quantum_node_linux |
make ci-contracts |
ci-contracts |
contract_validation | 所有合约 wasm 构建 + cargo test |
make package |
package |
artifact_packaging | deploy-<version>, deploy-<version>.tar.gz |
make test-coverage |
test-coverage |
coverage | coverage.out, coverage.html |
make ci-tests |
ci-tests |
CI | 带 race detector 和覆盖率 |
make ci-security |
ci-security |
CI | gosec + cargo audit |
1.3 核心测试原则
┌─────────────────────────────────────────────────────────────┐
│ MSG Chain 测试原则: │
│ │
│ 1. 每个合约包必须包含单元测试 + 集成测试 │
│ 2. 所有 Dilithium-5 密码学操作必须通过 test-quantum 验证 │
│ 3. CI 中先跑 lint,再跑 test,最后 ci-contracts │
│ 4. 没有真实 receipt/query/log 时,不得把 CI 通过等同于 │
│ 链上上线成功 │
│ 5. 生产发布必须经过 3 道人工审批门禁 │
└─────────────────────────────────────────────────────────────┘
2. Rust 单元测试
2.1 CosmWasm 测试基础
MSG Chain 的 CosmWasm 合约测试遵循标准 CosmWasm 测试模式,使用 cosmwasm-std 提供的 mock 工具。类似 Hardhat 的合约测试框架,但使用 Rust 的 #[cfg(test)] 模式。
每个合约包位于 contracts/cosmwasm/all/<合约名>/,测试可以通过以下命令运行:
# 运行单个合约的全部测试
cd contracts/cosmwasm/all/aidid_did_registry_v1 && cargo test
# 运行特定测试
cd contracts/cosmwasm/all/aidid_did_registry_v1 && cargo test test_create_and_resolve_did
# 构建 WASM(测试前需要确保编译通过)
cd contracts/cosmwasm/all/aidid_did_registry_v1 && cargo build --target wasm32-unknown-unknown --release
2.2 单元测试标准模式
每个 CosmWasm 合约的标准测试结构如下。以 aidid_did_registry_v1 为例,完整的嵌入测试代码:
2.2.1 测试模块结构
#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, Addr, Binary, Uint128};
use crate::contract::{instantiate, execute, query, execute_create_did};
use crate::msg::{
InstantiateMsg, ExecuteMsg, QueryMsg, VerificationMethod, ServiceEndpoint,
ResolveDIDResponse, CheckDIDActiveResponse, ListDIDsByControllerResponse,
};
use crate::error::ContractError;
use crate::state::{Config, CONFIG, DID_DOCUMENTS};
}
2.2.2 辅助函数
fn create_test_vm(id: &str) -> VerificationMethod {
VerificationMethod {
id: id.to_string(),
controller: "controller".to_string(),
key_type: "Dilithium5VerificationKey2026".to_string(),
public_key_multibase: Some("z6Mk".to_string()),
dilithium5_public_key: Some("dilithium5_pk_example".to_string()),
}
}
fn create_test_service(id: &str) -> ServiceEndpoint {
ServiceEndpoint {
id: id.to_string(),
service_type: "LinkedDomains".to_string(),
service_endpoint: "https://agent.example.com".to_string(),
metadata: None,
}
}
fn setup_contract() -> (cosmwasm_std::OwnedDeps<cosmwasm_std::MemoryStorage, cosmwasm_std::testing::MockApi, cosmwasm_std::testing::MockQuerier>, cosmwasm_std::Env) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "MSG DID Registry".to_string(),
},
)
.unwrap();
(deps, env)
}
2.2.3 Instantiate 测试
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies();
let env = mock_env();
let info = mock_info("admin", &[]);
let msg = InstantiateMsg {
admin: "admin".to_string(),
registry_name: "MSG DID Registry".to_string(),
};
let res = instantiate(deps.as_mut(), env, info, msg).unwrap();
// 验证响应属性
assert_eq!(res.attributes.len(), 3);
assert_eq!(res.attributes[0].value, "instantiate");
assert_eq!(res.attributes[1].value, "admin");
assert_eq!(res.attributes[2].value, "MSG DID Registry");
// 验证状态
let config = CONFIG.load(&deps.storage).unwrap();
assert_eq!(config.admin, "admin");
assert_eq!(config.registry_name, "MSG DID Registry");
}
2.2.4 Execute — Create DID 测试
#[test]
fn create_and_resolve_did() {
let mut deps = mock_dependencies();
let env = mock_env();
// 实例化
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "test".to_string(),
},
)
.unwrap();
// 创建 DID
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: "did:msg:agent:test-1".to_string(),
controller: "controller".to_string(),
verification_methods: vec![create_test_vm("vm-1")],
services: vec![create_test_service("svc-1")],
metadata: None,
},
)
.unwrap();
// 查询验证
let res = query(
deps.as_ref(),
env.clone(),
QueryMsg::ResolveDID {
did_id: "did:msg:agent:test-1".to_string(),
},
)
.unwrap();
let response: ResolveDIDResponse = from_binary(&res).unwrap();
assert_eq!(response.document.did_id, "did:msg:agent:test-1");
assert!(response.document.active);
assert_eq!(response.document.verification_methods.len(), 1);
assert_eq!(response.document.services.len(), 1);
}
2.2.5 Execute — Deactivate DID 测试
#[test]
fn deactivate_did() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "test".to_string(),
},
)
.unwrap();
let did_id = "did:msg:agent:to-deactivate";
// 创建
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: did_id.to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
// 停用
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::DeactivateDID {
did_id: did_id.to_string(),
},
)
.unwrap();
// 验证
let res = query(
deps.as_ref(),
env,
QueryMsg::CheckDIDActive {
did_id: did_id.to_string(),
},
)
.unwrap();
let response: CheckDIDActiveResponse = from_binary(&res).unwrap();
assert!(!response.active);
}
2.2.6 错误路径测试
#[test]
fn create_duplicate_did_fails() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "test".to_string(),
},
)
.unwrap();
let did_id = "did:msg:agent:dup";
// 首次创建成功
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: did_id.to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
// 重复创建失败
let err = execute(
deps.as_mut(),
env,
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: did_id.to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap_err();
assert_eq!(
err,
ContractError::DIDAlreadyExists {
did_id: did_id.to_string()
}
);
}
2.2.7 权限控制测试
#[test]
fn only_controller_can_modify_did() {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "test".to_string(),
},
)
.unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: "did:msg:agent:protected".to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
let err = execute(
deps.as_mut(),
env,
mock_info("attacker", &[]),
ExecuteMsg::UpdateDID {
did_id: "did:msg:agent:protected".to_string(),
verification_methods: None,
services: None,
metadata: Some(Binary::from(b"evil".as_ref())),
},
)
.unwrap_err();
assert_eq!(
err,
ContractError::NotController {
did_id: "did:msg:agent:protected".to_string(),
controller: "attacker".to_string(),
}
);
}
2.2.8 验证方法操作测试
#[test]
fn add_and_remove_verification_method() {
let (mut deps, env) = setup_contract();
let did_id = "did:msg:agent:vm-test";
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: did_id.to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
// 添加验证方法
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::AddVerificationMethod {
did_id: did_id.to_string(),
method: create_test_vm("vm-new"),
},
)
.unwrap();
let res = query(
deps.as_ref(),
env.clone(),
QueryMsg::ResolveDID {
did_id: did_id.to_string(),
},
)
.unwrap();
let response: ResolveDIDResponse = from_binary(&res).unwrap();
assert_eq!(response.document.verification_methods.len(), 1);
// 移除验证方法
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::RemoveVerificationMethod {
did_id: did_id.to_string(),
method_id: "vm-new".to_string(),
},
)
.unwrap();
let res = query(
deps.as_ref(),
env,
QueryMsg::ResolveDID {
did_id: did_id.to_string(),
},
)
.unwrap();
let response: ResolveDIDResponse = from_binary(&res).unwrap();
assert_eq!(response.document.verification_methods.len(), 0);
}
2.2.9 Service 操作测试
#[test]
fn add_and_remove_service() {
let (mut deps, env) = setup_contract();
let did_id = "did:msg:agent:svc-test";
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: did_id.to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::AddService {
did_id: did_id.to_string(),
service: create_test_service("svc-new"),
},
)
.unwrap();
execute(
deps.as_mut(),
env,
mock_info("controller", &[]),
ExecuteMsg::RemoveService {
did_id: did_id.to_string(),
service_id: "svc-new".to_string(),
},
)
.unwrap();
}
#[test]
fn remove_nonexistent_service_fails() {
let (mut deps, env) = setup_contract();
let did_id = "did:msg:agent:no-svc";
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: did_id.to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
let err = execute(
deps.as_mut(),
env,
mock_info("controller", &[]),
ExecuteMsg::RemoveService {
did_id: did_id.to_string(),
service_id: "nonexistent-svc".to_string(),
},
)
.unwrap_err();
assert_eq!(
err,
ContractError::ServiceNotFound {
did_id: did_id.to_string(),
service_id: "nonexistent-svc".to_string(),
}
);
}
2.2.10 列表查询测试
#[test]
fn list_dids_by_controller() {
let (mut deps, env) = setup_contract();
// 创建多个 DID
for i in 0..5 {
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: format!("did:msg:agent:list-{}", i),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
}
// 创建另一个控制器的 DID
execute(
deps.as_mut(),
env.clone(),
mock_info("other", &[]),
ExecuteMsg::CreateDID {
did_id: "did:msg:agent:other-1".to_string(),
controller: "other".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
// 按 controller 查询
let res = query(
deps.as_ref(),
env,
QueryMsg::ListDIDsByController {
controller: "controller".to_string(),
start_after: None,
limit: Some(100),
},
)
.unwrap();
let response: ListDIDsByControllerResponse = from_binary(&res).unwrap();
assert_eq!(response.dids.len(), 5);
}
2.3 Agent Registry 合约测试
agent_registry_v1 是 AI Agent 注册合约,使用 IndexedMap 进行多索引查询。
#[cfg(test)]
mod tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, Binary};
use crate::contract::{instantiate, execute, query};
use crate::msg::{
Agent, AgentStatus, ExecuteMsg, GetAgentResponse, InstantiateMsg,
ListAgentsResponse, QueryMsg,
};
use crate::error::ContractError;
use crate::state::{Config, CONFIG};
fn setup() -> (cosmwasm_std::OwnedDeps<cosmwasm_std::MemoryStorage, cosmwasm_std::testing::MockApi, cosmwasm_std::testing::MockQuerier>, cosmwasm_std::Env) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
did_registry_address: "msg1didregistry".to_string(),
},
)
.unwrap();
(deps, env)
}
fn register_test_agent(
deps: &mut cosmwasm_std::OwnedDeps<cosmwasm_std::MemoryStorage, cosmwasm_std::testing::MockApi, cosmwasm_std::testing::MockQuerier>,
env: &cosmwasm_std::Env,
agent_id: &str,
owner: &str,
) {
execute(
deps.as_mut(),
env.clone(),
mock_info(owner, &[]),
ExecuteMsg::RegisterAgent {
agent_id: agent_id.to_string(),
did_id: format!("did:msg:agent:{}", agent_id),
owner: owner.to_string(),
name: format!("Agent {}", agent_id),
description: Some("Test agent".to_string()),
endpoint: Some(format!("https://{}.example.com", agent_id)),
metadata: None,
agent_type: "worker".to_string(),
capabilities: vec!["text-generation".to_string(), "code-review".to_string()],
},
)
.unwrap();
}
#[test]
fn proper_initialization() {
let mut deps = mock_dependencies();
let env = mock_env();
let res = instantiate(
deps.as_mut(),
env,
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
did_registry_address: "msg1didregistry".to_string(),
},
)
.unwrap();
assert_eq!(res.attributes.len(), 3);
assert_eq!(res.attributes[0].value, "instantiate");
let config = CONFIG.load(&deps.storage).unwrap();
assert_eq!(config.admin, "admin");
assert_eq!(config.did_registry_address, "msg1didregistry");
}
#[test]
fn register_and_get_agent() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner");
let res = query(
deps.as_ref(),
env,
QueryMsg::GetAgent {
agent_id: "agent-001".to_string(),
},
)
.unwrap();
let response: GetAgentResponse = from_binary(&res).unwrap();
assert_eq!(response.agent.agent_id, "agent-001");
assert_eq!(response.agent.owner, "owner");
assert_eq!(response.agent.name, "Agent agent-001");
assert_eq!(response.agent.capabilities.len(), 2);
}
#[test]
fn duplicate_agent_fails() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner");
let err = execute(
deps.as_mut(),
env,
mock_info("owner", &[]),
ExecuteMsg::RegisterAgent {
agent_id: "agent-001".to_string(),
did_id: "did:msg:agent:dup".to_string(),
owner: "owner".to_string(),
name: "Duplicate".to_string(),
description: None,
endpoint: None,
metadata: None,
agent_type: "worker".to_string(),
capabilities: vec![],
},
)
.unwrap_err();
assert_eq!(
err,
ContractError::AgentAlreadyExists {
agent_id: "agent-001".to_string()
}
);
}
#[test]
fn update_agent() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner");
execute(
deps.as_mut(),
env.clone(),
mock_info("owner", &[]),
ExecuteMsg::UpdateAgent {
agent_id: "agent-001".to_string(),
name: Some("Updated Agent".to_string()),
description: Some("Updated description".to_string()),
endpoint: Some("https://new-endpoint.com".to_string()),
metadata: None,
capabilities: Some(vec!["data-analysis".to_string()]),
},
)
.unwrap();
let res = query(
deps.as_ref(),
env,
QueryMsg::GetAgent {
agent_id: "agent-001".to_string(),
},
)
.unwrap();
let response: GetAgentResponse = from_binary(&res).unwrap();
assert_eq!(response.agent.name, "Updated Agent");
assert_eq!(response.agent.capabilities, vec!["data-analysis"]);
}
#[test]
fn deregister_agent() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner");
execute(
deps.as_mut(),
env.clone(),
mock_info("owner", &[]),
ExecuteMsg::DeregisterAgent {
agent_id: "agent-001".to_string(),
},
)
.unwrap();
let res = query(
deps.as_ref(),
env,
QueryMsg::GetAgent {
agent_id: "agent-001".to_string(),
},
)
.unwrap();
let response: GetAgentResponse = from_binary(&res).unwrap();
assert_eq!(response.agent.status, AgentStatus::Deregistered);
}
#[test]
fn unauthorized_update_fails() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner");
let err = execute(
deps.as_mut(),
env,
mock_info("attacker", &[]),
ExecuteMsg::UpdateAgent {
agent_id: "agent-001".to_string(),
name: None,
description: None,
endpoint: None,
metadata: None,
capabilities: None,
},
)
.unwrap_err();
assert_eq!(err, ContractError::Unauthorized {});
}
#[test]
fn list_agents() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner-1");
register_test_agent(&mut deps, &env, "agent-002", "owner-1");
register_test_agent(&mut deps, &env, "agent-003", "owner-2");
let res = query(
deps.as_ref(),
env.clone(),
QueryMsg::ListAgents {
start_after: None,
limit: None,
},
)
.unwrap();
let response: ListAgentsResponse = from_binary(&res).unwrap();
assert_eq!(response.agents.len(), 3);
// 按 owner 过滤
let res = query(
deps.as_ref(),
env,
QueryMsg::GetAgentsByOwner {
owner: "owner-1".to_string(),
start_after: None,
limit: None,
},
)
.unwrap();
let response: ListAgentsResponse = from_binary(&res).unwrap();
assert_eq!(response.agents.len(), 2);
}
#[test]
fn set_status_by_admin() {
let (mut deps, env) = setup();
register_test_agent(&mut deps, &env, "agent-001", "owner");
execute(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
ExecuteMsg::SetStatus {
agent_id: "agent-001".to_string(),
status: AgentStatus::Suspended,
},
)
.unwrap();
let res = query(
deps.as_ref(),
env,
QueryMsg::GetAgent {
agent_id: "agent-001".to_string(),
},
)
.unwrap();
let response: GetAgentResponse = from_binary(&res).unwrap();
assert_eq!(response.agent.status, AgentStatus::Suspended);
}
}
2.4 Schema 生成测试
每个合约必须包含 schema 生成示例,确保合约接口文档自动生成:
// examples/schema.rs
use cosmwasm_schema::write_api;
use aidid_did_registry::msg::{ExecuteMsg, InstantiateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
execute: ExecuteMsg,
query: QueryMsg,
}
}
运行 schema 生成:
cargo run --example schema
ls -la schema/
3. 集成测试
3.1 cw-multi-test 基础
MSG Chain 使用 cw-multi-test 进行合约集成测试,支持多合约交互场景。每个合约需要 cw-multi-test = "0.18" 作为 dev-dependency。
3.2 DID Registry + Agent Registry 集成测试
以下示例展示 aidid_did_registry_v1 和 agent_registry_v1 的跨合约交互测试。
// tests/integration.rs
use cosmwasm_std::testing::{mock_env, mock_info, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::{Addr, Coin, Empty, Uint128};
use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper, Executor};
use aidid_did_registry::msg::{
ExecuteMsg as DIDExecuteMsg, InstantiateMsg as DIDInstantiateMsg,
QueryMsg as DIDQueryMsg, ResolveDIDResponse, VerificationMethod, ServiceEndpoint,
};
use aidid_did_registry::contract::{
instantiate as did_instantiate, execute as did_execute, query as did_query,
};
use agent_registry::msg::{
ExecuteMsg as AgentExecuteMsg, InstantiateMsg as AgentInstantiateMsg,
QueryMsg as AgentQueryMsg, GetAgentResponse, AgentStatus,
};
use agent_registry::contract::{
instantiate as agent_instantiate, execute as agent_execute, query as agent_query,
};
fn did_contract() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(did_execute, did_instantiate, did_query);
Box::new(contract)
}
fn agent_contract() -> Box<dyn Contract<Empty>> {
let contract = ContractWrapper::new(agent_execute, agent_instantiate, agent_query);
Box::new(contract)
}
fn create_test_vm(id: &str) -> VerificationMethod {
VerificationMethod {
id: id.to_string(),
controller: "controller".to_string(),
key_type: "Dilithium5VerificationKey2026".to_string(),
public_key_multibase: Some("z6Mk".to_string()),
dilithium5_public_key: Some("pk_example".to_string()),
}
}
fn create_test_service(id: &str) -> ServiceEndpoint {
ServiceEndpoint {
id: id.to_string(),
service_type: "LinkedDomains".to_string(),
service_endpoint: "https://agent.example.com".to_string(),
metadata: None,
}
}
#[test]
fn full_did_and_agent_integration() {
let owner = Addr::unchecked("owner");
let admin = Addr::unchecked("admin");
let controller = Addr::unchecked("controller");
let alice = Addr::unchecked("alice");
let mut app = AppBuilder::new().build(|router, _, storage| {
router
.bank
.init_balance(
storage,
&owner,
vec![Coin {
denom: "umsg".to_string(),
amount: Uint128::new(1_000_000_000_000_000_000u128),
}],
)
.unwrap();
});
// 部署 DID Registry 合约
let did_code_id = app.store_code(did_contract());
let did_contract_addr = app
.instantiate_contract(
did_code_id,
admin.clone(),
&DIDInstantiateMsg {
admin: admin.to_string(),
registry_name: "MSG DID Registry".to_string(),
},
&[],
"aidid_did_registry_v1",
None,
)
.unwrap();
// 部署 Agent Registry 合约(引用 DID Registry 地址)
let agent_code_id = app.store_code(agent_contract());
let agent_contract_addr = app
.instantiate_contract(
agent_code_id,
admin.clone(),
&AgentInstantiateMsg {
admin: admin.to_string(),
did_registry_address: did_contract_addr.to_string(),
},
&[],
"agent_registry_v1",
None,
)
.unwrap();
// 1. 创建 DID
app.execute_contract(
controller.clone(),
did_contract_addr.clone(),
&DIDExecuteMsg::CreateDID {
did_id: "did:msg:agent:integration-1".to_string(),
controller: controller.to_string(),
verification_methods: vec![create_test_vm("vm-key-1")],
services: vec![create_test_service("svc-endpoint-1")],
metadata: None,
},
&[],
)
.unwrap();
// 2. 验证 DID 已创建
let did_res: ResolveDIDResponse = app
.wrap()
.query_wasm_smart(
did_contract_addr.clone(),
&DIDQueryMsg::ResolveDID {
did_id: "did:msg:agent:integration-1".to_string(),
},
)
.unwrap();
assert_eq!(did_res.document.did_id, "did:msg:agent:integration-1");
assert_eq!(did_res.document.controller, controller.to_string());
assert!(did_res.document.active);
// 3. 注册 Agent
app.execute_contract(
owner.clone(),
agent_contract_addr.clone(),
&AgentExecuteMsg::RegisterAgent {
agent_id: "agent-integration-1".to_string(),
did_id: "did:msg:agent:integration-1".to_string(),
owner: owner.to_string(),
name: "Integration Test Agent".to_string(),
description: Some("Agent created in integration test".to_string()),
endpoint: Some("https://integration-agent.example.com".to_string()),
metadata: None,
agent_type: "worker".to_string(),
capabilities: vec!["text-generation".to_string(), "code-review".to_string()],
},
&[],
)
.unwrap();
// 4. 验证 Agent
let agent_res: GetAgentResponse = app
.wrap()
.query_wasm_smart(
agent_contract_addr.clone(),
&AgentQueryMsg::GetAgent {
agent_id: "agent-integration-1".to_string(),
},
)
.unwrap();
assert_eq!(
agent_res.agent.did_id,
"did:msg:agent:integration-1"
);
assert_eq!(agent_res.agent.owner, owner.to_string());
assert_eq!(agent_res.agent.status, AgentStatus::Active);
// 5. 停用 DID — 验证 DID 变为非活跃
app.execute_contract(
controller.clone(),
did_contract_addr.clone(),
&DIDExecuteMsg::DeactivateDID {
did_id: "did:msg:agent:integration-1".to_string(),
},
&[],
)
.unwrap();
let check_res: cw_multi_test::QuerierResult = app
.wrap()
.query_wasm_smart(
did_contract_addr.clone(),
&DIDQueryMsg::CheckDIDActive {
did_id: "did:msg:agent:integration-1".to_string(),
},
);
// 注意: 这里需要根据实际返回类型调整
println!("DID deactivated, integration complete");
// 6. 验证非 controller 无法修改
let err = app
.execute_contract(
alice.clone(),
did_contract_addr,
&DIDExecuteMsg::UpdateDID {
did_id: "did:msg:agent:integration-1".to_string(),
verification_methods: None,
services: None,
metadata: None,
},
&[],
)
.unwrap_err();
assert!(err
.root()
.to_string()
.contains("does not own DID"));
}
#[test]
fn multiple_agents_same_did_rejected() {
let admin = Addr::unchecked("admin");
let owner = Addr::unchecked("owner");
let mut app = App::default();
let did_code_id = app.store_code(did_contract());
let did_contract_addr = app
.instantiate_contract(
did_code_id,
admin.clone(),
&DIDInstantiateMsg {
admin: admin.to_string(),
registry_name: "MSG DID Registry".to_string(),
},
&[],
"aidid_did_registry_v1",
None,
)
.unwrap();
let agent_code_id = app.store_code(agent_contract());
let agent_contract_addr = app
.instantiate_contract(
agent_code_id,
admin.clone(),
&AgentInstantiateMsg {
admin: admin.to_string(),
did_registry_address: did_contract_addr.to_string(),
},
&[],
"agent_registry_v1",
None,
)
.unwrap();
// 注册 agent-001
app.execute_contract(
owner.clone(),
agent_contract_addr.clone(),
&AgentExecuteMsg::RegisterAgent {
agent_id: "agent-001".to_string(),
did_id: "did:msg:agent:test".to_string(),
owner: owner.to_string(),
name: "Agent One".to_string(),
description: None,
endpoint: None,
metadata: None,
agent_type: "worker".to_string(),
capabilities: vec![],
},
&[],
)
.unwrap();
// 重复 agent_id 应被拒绝
let err = app
.execute_contract(
owner.clone(),
agent_contract_addr,
&AgentExecuteMsg::RegisterAgent {
agent_id: "agent-001".to_string(),
did_id: "did:msg:agent:other".to_string(),
owner: owner.to_string(),
name: "Agent One Duplicate".to_string(),
description: None,
endpoint: None,
metadata: None,
agent_type: "worker".to_string(),
capabilities: vec![],
},
&[],
)
.unwrap_err();
assert!(err.root().to_string().contains("already exists"));
}
3.3 运行集成测试
# 运行所有集成测试
cd contracts/cosmwasm/all/aidid_did_registry_v1 && cargo test --test integration
# 运行特定集成测试
cd contracts/cosmwasm/all/aidid_did_registry_v1 && cargo test full_did_and_agent_integration
# 使用 ci-contracts 运行所有合约的测试
make ci-contracts
4. Go 端测试
4.1 Go pkg 测试
MSG Chain 的 Go 端测试覆盖完整的链基础设施,包括 p2p、共识、存储、密码学等模块。
# 运行所有 Go 测试
make test
# 等价于: go test -v ./pkg/...
# 运行特定包测试
go test -v ./pkg/crypto/...
go test -v ./pkg/consensus/...
go test -v ./pkg/storage/...
go test -v ./pkg/p2p/...
4.2 Dilithium-5 后量子密码学测试
# 运行量子节点模块测试(包含 Dilithium-5 PQ 测试)
make test-quantum
# 等价于: cd pkg/quantum && go test -v
# 运行密码学模块测试
make test-crypto
# 等价于: cd pkg/crypto && go test -v
量子测试的关键验证点:
// pkg/quantum/quantum_test.go
package quantum
import (
"testing"
"crypto/rand"
"github.com/cloudflare/circl/sign/dilithium"
"github.com/cloudflare/circl/sign/dilithium/mode5"
)
func TestDilithium5KeyGeneration(t *testing.T) {
pk, sk, err := mode5.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Dilithium-5 key generation failed: %v", err)
}
if pk == nil || sk == nil {
t.Fatal("Generated key pair is nil")
}
t.Logf("Public key size: %d bytes", len(pk.Bytes()))
t.Logf("Secret key size: %d bytes", len(sk.Bytes()))
}
func TestDilithium5SignVerify(t *testing.T) {
pk, sk, err := mode5.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Key generation failed: %v", err)
}
message := []byte("MSG Chain test message for Dilithium-5")
signature, err := sk.Sign(rand.Reader, message, nil)
if err != nil {
t.Fatalf("Signing failed: %v", err)
}
if !pk.Verify(message, signature) {
t.Fatal("Signature verification failed")
}
t.Logf("Signature size: %d bytes", len(signature))
}
func TestDilithium5TamperedSignature(t *testing.T) {
pk, sk, err := mode5.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Key generation failed: %v", err)
}
message := []byte("Original message")
signature, _ := sk.Sign(rand.Reader, message, nil)
tamperedMessage := []byte("Tampered message")
if pk.Verify(tamperedMessage, signature) {
t.Fatal("Tampered message should not verify")
}
}
func TestDilithium5PublicKeyEncoding(t *testing.T) {
pk, _, err := mode5.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("Key generation failed: %v", err)
}
pkBytes := pk.Bytes()
pkRestored, err := mode5.PublicKeyFromBytes(pkBytes)
if err != nil {
t.Fatalf("Public key deserialization failed: %v", err)
}
if !pkRestored.Equals(pk) {
t.Fatal("Public key round-trip failed")
}
}
4.3 GitHub Actions 集成测试
# .github/workflows/go-tests.yml
name: Go Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: true
- name: Install dependencies
run: make deps
- name: Run Go tests
run: make test
- name: Run quantum tests
run: make test-quantum
- name: Generate coverage
run: make test-coverage
4.4 NAT 组网诊断测试
# 运行 NAT 组网诊断矩阵测试
make test-nat-readiness
# 包含: TestNetworkDetectorDetermineBestStrategy
# TestDefaultConfig_NATBootstrapDependencies
# TestShouldSendSmartKeepalive
# TestCheckP2PAndToggleHeartbeat
# TestGetPublicRelayNodes_EmptyRegistryURLUsesFallback
# TestGetPublicRelayNodes_RegistryFailureFallsBack
# TestGetAllBootstrapPeers_FallsBackToOfficialWhenMSGBootstrapEmpty
# TestCircuitRelayIntegration_BasicRelay
5. 模糊测试与属性测试
5.1 Rust proptest 集成
MSG Chain 使用 proptest 对 CosmWasm 合约进行属性基测试。
# Cargo.toml (dev-dependencies)
[dev-dependencies]
proptest = "1.4"
5.2 合约属性测试示例
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{from_binary, Binary, Addr};
use crate::contract::{instantiate, execute, query};
use crate::msg::*;
use crate::error::ContractError;
use crate::state::CONFIG;
proptest! {
#[test]
fn instantiate_always_sets_config(
admin in "[a-z]{5,20}",
registry_name in "[a-zA-Z0-9 _]{1,50}",
) {
let mut deps = mock_dependencies();
let env = mock_env();
let res = instantiate(
deps.as_mut(),
env,
mock_info(&admin, &[]),
InstantiateMsg {
admin: admin.clone(),
registry_name: registry_name.clone(),
},
);
prop_assert!(res.is_ok());
let config = CONFIG.load(&deps.storage).unwrap();
prop_assert_eq!(config.admin, admin);
prop_assert_eq!(config.registry_name, registry_name);
}
#[test]
fn create_did_with_valid_key_type_succeeds(
did_id in "[a-zA-Z0-9:-]{5,30}",
controller in "[a-z]{5,20}",
) {
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "test".to_string(),
},
).unwrap();
let vm = VerificationMethod {
id: "vm-1".to_string(),
controller: controller.clone(),
key_type: "Dilithium5VerificationKey2026".to_string(),
public_key_multibase: Some("z6Mk".to_string()),
dilithium5_public_key: Some("pk".to_string()),
};
let result = execute(
deps.as_mut(),
env,
mock_info(&controller, &[]),
ExecuteMsg::CreateDID {
did_id,
controller,
verification_methods: vec![vm],
services: vec![],
metadata: None,
},
);
prop_assert!(result.is_ok());
}
#[test]
fn deactivated_did_always_shows_inactive(
did_id in "[a-zA-Z0-9:-]{5,30}",
) {
let mut deps = mock_dependencies();
let env = mock_env();
let controller = "controller";
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "test".to_string(),
},
).unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info(controller, &[]),
ExecuteMsg::CreateDID {
did_id: did_id.clone(),
controller: controller.to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
).unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info(controller, &[]),
ExecuteMsg::DeactivateDID {
did_id: did_id.clone(),
},
).unwrap();
let res = query(
deps.as_ref(),
env,
QueryMsg::CheckDIDActive {
did_id,
},
).unwrap();
let response: CheckDIDActiveResponse = from_binary(&res).unwrap();
prop_assert!(!response.active);
}
}
}
5.3 Go Fuzz Testing
// pkg/crypto/fuzz_test.go
package crypto
import (
"testing"
"testing/quick"
"crypto/rand"
"github.com/cloudflare/circl/sign/dilithium/mode5"
)
func FuzzDilithium5SignVerify(f *testing.F) {
pk, sk, err := mode5.GenerateKey(rand.Reader)
if err != nil {
f.Fatalf("Key generation failed: %v", err)
}
f.Fuzz(func(t *testing.T, message []byte, signature []byte) {
// 模糊测试:随机消息和签名不应导致 panic
pk.Verify(message, signature)
})
// 也测试合法场景
f.Add([]byte("test message"), []byte{})
sk.Sign(rand.Reader, []byte("test message"), nil)
}
func FuzzDIDDocumentSerialization(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
// 模糊测试 DID 文档序列化/反序列化
var doc DIDDocument
err := json.Unmarshal(data, &doc)
if err == nil {
_, err := json.Marshal(doc)
if err != nil {
t.Errorf("round-trip failed: %v", err)
}
}
})
}
// 快速检查属性测试
func TestDIDProperties(t *testing.T) {
property := func(didID string, controller string) bool {
if len(didID) == 0 || len(controller) == 0 {
return true // 跳过空值
}
// 验证 DID 格式
if !strings.HasPrefix(didID, "did:msg:") {
return false
}
return true
}
if err := quick.Check(property, nil); err != nil {
t.Error(err)
}
}
5.4 状态不变量测试
#[cfg(test)]
mod invariance_tests {
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use crate::contract::{instantiate, execute, query};
use crate::msg::*;
#[test]
fn total_did_count_invariant() {
// 不变量:创建后创建数增加,停用后总数不变
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "invariant-test".to_string(),
},
)
.unwrap();
// 初始应无 DID
let res = query(
deps.as_ref(),
env.clone(),
QueryMsg::ListDIDsByController {
controller: "controller".to_string(),
start_after: None,
limit: Some(100),
},
)
.unwrap();
let response: ListDIDsByControllerResponse = from_binary(&res).unwrap();
assert_eq!(response.dids.len(), 0);
// 创建 3 个
for i in 0..3 {
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: format!("did:msg:agent:inv-{}", i),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
}
// 应仍有 3 个(不变量:停用不解散)
let res = query(
deps.as_ref(),
env.clone(),
QueryMsg::ListDIDsByController {
controller: "controller".to_string(),
start_after: None,
limit: Some(100),
},
)
.unwrap();
let response: ListDIDsByControllerResponse = from_binary(&res).unwrap();
assert_eq!(response.dids.len(), 3);
}
#[test]
fn create_time_invariant() {
// 不变量:created <= updated
let mut deps = mock_dependencies();
let env = mock_env();
instantiate(
deps.as_mut(),
env.clone(),
mock_info("admin", &[]),
InstantiateMsg {
admin: "admin".to_string(),
registry_name: "invariant-test".to_string(),
},
)
.unwrap();
execute(
deps.as_mut(),
env.clone(),
mock_info("controller", &[]),
ExecuteMsg::CreateDID {
did_id: "did:msg:agent:time-test".to_string(),
controller: "controller".to_string(),
verification_methods: vec![],
services: vec![],
metadata: None,
},
)
.unwrap();
let res = query(
deps.as_ref(),
env,
QueryMsg::ResolveDID {
did_id: "did:msg:agent:time-test".to_string(),
},
)
.unwrap();
let response: ResolveDIDResponse = from_binary(&res).unwrap();
assert!(response.document.created <= response.document.updated);
}
}
6. CI/CD 管线
6.1 管线架构
┌──────────────┐
│ 代码提交 │
│ git push │
└──────┬───────┘
▼
┌──────────────┐
│ Trigger │
│ (push/PR) │
└──────┬───────┘
▼
┌────────────────────────┐
│ Prepare (make deps) │
│ 安装依赖 + 缓存 │
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Quality Gate (lint) │
│ gofmt → go vet → linter│
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Test (make test) │
│ pkg tests │
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ PQ Test (test-quantum)│
│ Dilithium-5 crypto │
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Contract CI │
│ ci-contracts │
│ build + cargo test │
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Build (build-linux) │
│ genesis + quantum │
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Package (make package)│
│ deploy-<version>.tar.gz│
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Security Scan │
│ gosec + cargo audit │
└──────────┬─────────────┘
▼
┌────────────────────────┐
│ Approval Gates │
│ 3 道审批 → 生产部署 │
└────────────────────────┘
6.2 GitHub Actions 完整 CI 模板
# .github/workflows/ci.yml
name: MSG Chain CI Pipeline
on:
push:
branches: [main, develop, 'release/*', 'feat/*', 'fix/*']
pull_request:
branches: [main, develop]
env:
GO_VERSION: '1.24'
GO_CACHE_PATH: /tmp/msg-chain-go-build-cache
GOLANGCI_LINT_CACHE: /tmp/msg-chain-golangci-cache
RUST_TOOLCHAIN: stable
WASMVM_LIB_DIR: ./lib
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
prepare:
name: Prepare Dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
target: wasm32-unknown-unknown
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
${{ env.GO_CACHE_PATH }}
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Cache Cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install system dependencies
run: |
sudo apt-get update && sudo apt-get install -y \
build-essential clang wabt binaryen jq
- name: Install wasm-opt
run: cargo install wasm-opt || true
- name: Install Go tools
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install github.com/securego/gosec/v2/cmd/gosec@latest
- name: Install Rust tools
run: |
cargo install cargo-audit || true
- name: Install Go dependencies
run: make deps
- name: Verify dependencies
run: go mod verify
lint:
name: Code Quality
needs: [prepare]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Cache
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
${{ env.GO_CACHE_PATH }}
${{ env.GOLANGCI_LINT_CACHE }}
key: ${{ runner.os }}-lint-${{ hashFiles('**/go.sum') }}
- name: Run lint (make lint)
run: make lint
env:
GOCACHE: ${{ env.GO_CACHE_PATH }}
GOLANGCI_LINT_CACHE: ${{ env.GOLANGCI_LINT_CACHE }}
test:
name: Go Tests
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Cache
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
${{ env.GO_CACHE_PATH }}
key: ${{ runner.os }}-test-${{ hashFiles('**/go.sum') }}
- name: Run tests (make test)
run: make test
env:
GOCACHE: ${{ env.GO_CACHE_PATH }}
- name: Run quantum tests (make test-quantum)
run: make test-quantum
env:
GOCACHE: ${{ env.GO_CACHE_PATH }}
contracts:
name: Contract Build & Test
needs: [lint]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
- name: Cache Cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
contracts/cosmwasm/all/*/target
key: ${{ runner.os }}-contract-${{ hashFiles('contracts/cosmwasm/all/**/Cargo.lock') }}
- name: Build & test all contracts (make ci-contracts)
run: make ci-contracts
build:
name: Build Binaries
needs: [test, contracts]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Cache
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
${{ env.GO_CACHE_PATH }}
key: ${{ runner.os }}-build-${{ hashFiles('**/go.sum') }}
- name: Build Linux binaries (make build-linux)
run: make build-linux
env:
GOCACHE: ${{ env.GO_CACHE_PATH }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: msg-chain-binaries
path: |
bin/genesis_node_linux
bin/quantum_node_linux
bin/hashcheck_json
bin/hashcheck_dir
bin/hashcheck_http
package:
name: Package Deployment
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: msg-chain-binaries
path: bin/
- name: Make binaries executable
run: chmod +x bin/*
- name: Package (make package)
run: make package
- name: Upload deployment package
uses: actions/upload-artifact@v4
with:
name: msg-chain-deploy-package
path: deploy-*.tar.gz
security:
name: Security Audit
needs: [build]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache: true
- name: Set up Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: stable
- name: Run security audit
run: make ci-security
# # requires_human_approval: true
# production-deploy:
# name: Production Deploy (Manual)
# needs: [package, security]
# runs-on: ubuntu-latest
# environment: production
# if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# steps:
# - name: Deploy to production
# run: echo "Manual approval required before production deployment"
6.3 GitLab CI 模板
# .gitlab-ci.yml
stages:
- prepare
- quality_gate
- test
- contracts
- build
- package
- security
- deploy
variables:
GO_VERSION: "1.24"
GIT_SUBMODULE_STRATEGY: recursive
CARGO_HOME: "${CI_PROJECT_DIR}/.cargo"
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .cargo/
- contracts/cosmwasm/all/*/target/
- ${GO_CACHE_PATH}
before_script:
- apt-get update && apt-get install -y build-essential clang wabt binaryen jq
- curl -LO https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz
- tar -C /usr/local -xzf go${GO_VERSION}.linux-amd64.tar.gz
- export PATH=/usr/local/go/bin:$PATH
- rustup target add wasm32-unknown-unknown
prepare:
stage: prepare
script:
- make deps
artifacts:
paths:
- go.sum
lint:
stage: quality_gate
script:
- make lint
test:
stage: test
script:
- make test
- make test-quantum
contracts:
stage: contracts
script:
- make ci-contracts
build:
stage: build
script:
- make build-linux
artifacts:
paths:
- bin/
expire_in: 1 week
package:
stage: package
script:
- make package
artifacts:
paths:
- deploy-*.tar.gz
expire_in: 1 month
security:
stage: security
script:
- make ci-security
# requires_human_approval: true
production:
stage: deploy
script:
- echo "Production deployment requires manual approval"
when: manual
only:
- main
environment:
name: production
6.4 本地 CI 模拟
# 完整本地 CI(提交前运行)
make ci-local
# 等价于: ci-lint → ci-tests → ci-nodes
# 完整 CI(包含合约)
make ci-full
# 等价于: ci-lint → ci-contracts → ci-nodes → ci-tests → ci-security
# 逐步运行
make ci-lint # 代码检查
make ci-tests # 带 race detector 的测试
make ci-contracts # 合约编译 + 测试
make ci-nodes # 节点编译
make ci-security # 安全扫描
7. 质量门禁
7.1 门禁架构
MSG Chain 采用四层质量门禁体系:
Layer 1: Lint Gate
├── gofmt: 所有 .go 文件必须格式化
├── go vet: 静态分析通过
└── golangci-lint: 零错误
Layer 2: Test Gate
├── make test: 全部通过
├── make test-quantum: 全部通过
└── 代码覆盖率 ≥ 60% (建议目标 80%)
Layer 3: Contract Gate
├── 所有合约 cargo build --target wasm32-unknown-unknown --release
├── 所有合约 cargo test 通过
└── WASM 二进制 ≤ 800KB
Layer 4: Security Gate
├── gosec: 无 High/Critical 漏洞
├── cargo audit: 无已知安全漏洞
└── 依赖无已知 CVE
7.2 Lint Gate 配置
# .golangci.yml
run:
timeout: 10m
modules-download-mode: readonly
linters:
enable:
- gofmt
- govet
- staticcheck
- gosimple
- ineffassign
- misspell
- unconvert
- prealloc
disable:
- errcheck
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
linters-settings:
staticcheck:
checks:
- "all"
misspell:
locale: US
// .golangci.json (备用)
{
"run": {
"timeout": "10m",
"modules-download-mode": "readonly"
},
"linters": {
"enable": ["goformat", "govet", "staticcheck", "gosimple"],
"disable": ["errcheck"]
},
"issues": {
"max-issues-per-linter": 0,
"max-same-issues": 0
}
}
7.3 测试覆盖率阈值
# coverage-config.yml
coverage:
threshold:
total: 60 # 总体覆盖率 ≥ 60%
packages:
pkg/crypto: 80 # 密码学模块 ≥ 80%
pkg/consensus: 70
pkg/storage: 65
pkg/quantum: 75
pkg/p2p: 60
pkg/txpool: 60
exclude:
- "**/*.pb.go" # 排除生成的 protobuf 代码
- "**/mocks/**"
7.4 合约验证门禁
make ci-contracts 门禁包含以下检查:
# 单个合约验证门禁
#!/bin/bash
set -euo pipefail
CONTRACT_DIR=$1
CONTRACT_NAME=$(basename "$CONTRACT_DIR")
echo "[GATE] Validating contract: $CONTRACT_NAME"
# 1. Cargo.toml 存在性检查
if [ ! -f "$CONTRACT_DIR/Cargo.toml" ]; then
echo "[GATE] ❌ Cargo.toml not found"
exit 1
fi
# 2. Rust 编译
cd "$CONTRACT_DIR"
CC=/usr/bin/gcc CXX=/usr/bin/g++ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/gcc \
cargo build --target wasm32-unknown-unknown --release
echo "[GATE] ✅ Build succeeded"
# 3. 测试
cargo test
echo "[GATE] ✅ Tests passed"
# 4. WASM 文件大小检查
WASM_FILE="target/wasm32-unknown-unknown/release/${CONTRACT_NAME//-/_}.wasm"
if [ -f "$WASM_FILE" ]; then
SIZE=$(stat -c%s "$WASM_FILE" 2>/dev/null || stat -f%z "$WASM_FILE" 2>/dev/null)
echo "[GATE] WASM size: $SIZE bytes"
if [ "$SIZE" -gt 819200 ]; then
echo "[GATE] ❌ WASM exceeds 800KB limit"
exit 1
fi
fi
# 5. Schema 生成
if [ -f "examples/schema.rs" ]; then
cargo run --example schema
echo "[GATE] ✅ Schema generated"
fi
# 6. wasm-opt 优化
if command -v wasm-opt &> /dev/null && [ -f "$WASM_FILE" ]; then
wasm-opt -Os "$WASM_FILE" -o "${WASM_FILE}.opt" && mv "${WASM_FILE}.opt" "$WASM_FILE"
echo "[GATE] ✅ WASM optimized"
fi
echo "[GATE] ✅ Contract $CONTRACT_NAME validated successfully"
7.5 审批门禁
合约交付工作流定义了 3 道必备审批门禁:
| 门禁名称 | 阶段 | 触发条件 | 审批人 | # requires_human_approval |
|---|---|---|---|---|
scope_lock |
Phase 1 | 范围确定后 | PM + Tech Lead | true |
secret_injection |
Phase 4 | 部署计划完成后 | Security Engineer | true |
production_release |
Phase 5 | 生产发布前 | CTO/VP Eng | true |
8. 合约交付工作流
8.1 工作流总览
MSG Chain 采用 contract_delivery_guarded_v1 工作流,确保每个合约从设计到上线都经过严格的审核和测试。
workflow_id: contract_delivery_guarded_v1
Phase 1: Scope ──[scope_lock]──→ Phase 2: Design & Codegen
│
▼
Phase 5: Real Release ←[production_release]── Phase 4: Deploy Plan ←[secret_injection]── Phase 3: Build & Test
8.2 Phase 1: 范围与需求
输入:
- 产品需求文档 (PRD)
- 合约功能规格
- 安全需求
活动:
- 确定合约功能范围
- 定义 ExecuteMsg / QueryMsg 接口
- 确定状态存储结构
- 安全需求评估
产出:
- 合约接口文档
- 状态存储设计
- 安全评估报告
审批门禁: scope_lock — # requires_human_approval: true
gate:
id: scope_lock
phase: 1
required_approvers:
- project_manager
- tech_lead
artifacts_required:
- contract_interface_spec
- state_storage_design
- security_assessment
8.3 Phase 2: 设计与代码生成
输入:
- Phase 1 通过审批
活动:
- 编写合约 Rust 代码
- 编写消息类型 (msg.rs)
- 编写状态存储 (state.rs)
- 编写错误类型 (error.rs)
- 编写入口函数 (contract.rs)
- 编写 Schema 生成器 (examples/schema.rs)
产出:
- 完整合约源代码
- Schema JSON
关键命令:
# 生成 Schema
cd contracts/cosmwasm/all/<contract_name>/ && cargo run --example schema
8.4 Phase 3: 构建与测试
输入:
- Phase 2 产出源代码
活动:
make deps— 安装 Go 依赖make lint— 静态检查 (gofmt + go vet + golangci-lint)make test— Go 侧测试make test-quantum— Dilithium-5 PQ 测试make ci-contracts— 合约 build + cargo test
产出:
make lint输出make test输出make ci-contracts输出
使用真实命令:
# 完整 Phase 3 命令链
make deps && make lint && make test && make test-quantum && make ci-contracts
边界条件:
没有真实 receipt/query/log 时,不得把 CI 通过等同于链上上线成功。
8.5 Phase 4: 部署计划
输入:
- Phase 3 通过的构建产物
活动:
- 编写部署脚本
- 创建密钥注入计划
- 制定回滚方案
- 准备 Gas 估算
产出:
- 部署计划文档
- 密钥注入方案
- 回滚策略
审批门禁: secret_injection — # requires_human_approval: true
gate:
id: secret_injection
phase: 4
required_approvers:
- security_engineer
artifacts_required:
- deploy_plan
- secret_injection_plan
- rollback_strategy
8.6 Phase 5: 生产发布
输入:
- Phase 4 通过的部署计划
活动:
make build-linux— 构建生产二进制make package— 打包部署包- StoreCode 上传 WASM
- InstantiateContract 实例化
- 验证合约状态
产出:
bin/genesis_node_linux+bin/quantum_node_linuxdeploy-<version>.tar.gz- 链上合约实例
审批门禁: production_release — # requires_human_approval: true
gate:
id: production_release
phase: 5
required_approvers:
- cto_or_vp_engineering
artifacts_required:
- production_binaries
- deploy_package
- onchain_contract_address
发布后验证:
# 验证合约已部署
./build/msgd query wasm list-contract-by-code <CODE_ID>
# 验证合约可查询
./build/msgd query wasm contract-state smart <CONTRACT_ADDR> '{"get_config": {}}'
# 验证事件日志
./build/msgd query tx <TX_HASH> | jq '.logs'
9. 自动化脚本
9.1 test-all.sh — 运行全部测试
#!/bin/bash
# test-all.sh — MSG Chain 全部测试运行器
# 运行所有测试层级:lint → Go test → PQ test → contract test → coverage
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
TESTS_PASSED=0
TESTS_FAILED=0
START_TIME=$(date +%s)
print_banner() {
echo ""
echo "============================================"
echo " MSG Chain Test Runner"
echo " $(date)"
echo "============================================"
echo ""
}
print_result() {
local name=$1
local status=$2
if [ "$status" -eq 0 ]; then
echo -e "${GREEN}[PASS]${NC} $name"
TESTS_PASSED=$((TESTS_PASSED + 1))
else
echo -e "${RED}[FAIL]${NC} $name"
TESTS_FAILED=$((TESTS_FAILED + 1))
fi
}
run_test() {
local name=$1
shift
echo -e "${BLUE}[RUN]${NC} $name..."
if "$@" 2>&1 | tail -5; then
print_result "$name" 0
else
print_result "$name" 1
fi
}
# ============================================
print_banner
# Phase 1: Environment Check
echo -e "${YELLOW}[Phase 1]${NC} Environment Check"
run_test "Go version" go version
run_test "Rust toolchain" rustc --version
run_test "wasm32 target" rustup target list --installed --toolchain stable | grep wasm32-unknown-unknown
run_test "Cargo" cargo --version
# Phase 2: Dependencies
echo ""
echo -e "${YELLOW}[Phase 2]${NC} Dependencies"
run_test "make deps" make deps
# Phase 3: Lint
echo ""
echo -e "${YELLOW}[Phase 3]${NC} Code Quality"
run_test "make lint" make lint
# Phase 4: Go Tests
echo ""
echo -e "${YELLOW}[Phase 4]${NC} Go Tests"
run_test "make test" make test
run_test "make test-quantum" make test-quantum
# Phase 5: Contract Tests
echo ""
echo -e "${YELLOW}[Phase 5]${NC} Contract Tests"
run_test "make ci-contracts" make ci-contracts
# Phase 6: Build
echo ""
echo -e "${YELLOW}[Phase 6]${NC} Build"
run_test "make build-linux" make build-linux
# Phase 7: Coverage
echo ""
echo -e "${YELLOW}[Phase 7]${NC} Coverage"
run_test "make test-coverage" make test-coverage
# ============================================
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo ""
echo "============================================"
echo -e " Results: ${GREEN}$TESTS_PASSED passed${NC}, ${RED}$TESTS_FAILED failed${NC}"
echo " Duration: ${DURATION}s"
echo "============================================"
if [ "$TESTS_FAILED" -gt 0 ]; then
exit 1
fi
exit 0
9.2 ci-pipeline.sh — 完整 CI 管线
#!/bin/bash
# ci-pipeline.sh — MSG Chain 本地 CI 管线模拟
# 执行完整的 CI 流程,匹配 GitHub Actions 行为
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_DIR"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PIPELINE_LOG="ci-pipeline-$(date +%Y%m%d-%H%M%S).log"
FAILED_STEPS=()
log_step() {
echo -e "${YELLOW}[CI]${NC} $1" | tee -a "$PIPELINE_LOG"
}
pass_step() {
echo -e "${GREEN}[CI ✅]${NC} $1" | tee -a "$PIPELINE_LOG"
}
fail_step() {
echo -e "${RED}[CI ❌]${NC} $1" | tee -a "$PIPELINE_LOG"
FAILED_STEPS+=("$1")
}
run_step() {
local step_name=$1
shift
log_step "Running: $step_name"
if "$@" 2>&1 | tee -a "$PIPELINE_LOG"; then
pass_step "$step_name"
return 0
else
fail_step "$step_name"
return 1
fi
}
echo "============================================" | tee "$PIPELINE_LOG"
echo " MSG Chain CI Pipeline" | tee -a "$PIPELINE_LOG"
echo " Started: $(date)" | tee -a "$PIPELINE_LOG"
echo "============================================" | tee -a "$PIPELINE_LOG"
# Step 1: Prepare
run_step "make deps" make deps
# Step 2: Quality Gate
run_step "make lint" make lint
# Step 3: Test Gate
run_step "make test" make test
run_step "make test-quantum" make test-quantum
# Step 4: Contract Validation
run_step "make ci-contracts" make ci-contracts
# Step 5: Build
run_step "make build-linux" make build-linux
# Step 6: Package
run_step "make package" make package
# Step 7: Security
run_step "make ci-security" make ci-security
# Step 8: Coverage
run_step "make test-coverage" make test-coverage
# Summary
echo "" | tee -a "$PIPELINE_LOG"
echo "============================================" | tee -a "$PIPELINE_LOG"
if [ ${#FAILED_STEPS[@]} -eq 0 ]; then
echo -e "${GREEN}CI Pipeline: ALL PASSED ✅${NC}" | tee -a "$PIPELINE_LOG"
exit 0
else
echo -e "${RED}CI Pipeline: FAILED ❌${NC}" | tee -a "$PIPELINE_LOG"
echo "Failed steps:" | tee -a "$PIPELINE_LOG"
for step in "${FAILED_STEPS[@]}"; do
echo " - $step" | tee -a "$PIPELINE_LOG"
done
exit 1
fi
9.3 validate-contract.sh — 单合约验证循环
#!/bin/bash
# validate-contract.sh — 单个 MSG Chain CosmWasm 合约验证脚本
# 用法: ./validate-contract.sh contracts/cosmwasm/all/<contract_name>
set -euo pipefail
CONTRACT_DIR="${1:-}"
if [ -z "$CONTRACT_DIR" ]; then
echo "用法: $0 <contract-directory>"
echo "示例: $0 contracts/cosmwasm/all/aidid_did_registry_v1"
exit 1
fi
if [ ! -d "$CONTRACT_DIR" ]; then
echo "❌ 目录不存在: $CONTRACT_DIR"
exit 1
fi
CONTRACT_NAME=$(basename "$CONTRACT_DIR")
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo ""
echo "============================================"
echo " Validating Contract: $CONTRACT_NAME"
echo " Directory: $CONTRACT_DIR"
echo " $(date)"
echo "============================================"
VALIDATION_PASSED=0
VALIDATION_FAILED=0
check_step() {
local step_name=$1
shift
echo -e "${YELLOW}[CHECK]${NC} $step_name..."
if "$@" 2>&1; then
echo -e "${GREEN}[PASS]${NC} $step_name"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo -e "${RED}[FAIL]${NC} $step_name"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
}
# Step 1: 检查项目结构
check_step "Cargo.toml 存在" test -f "$CONTRACT_DIR/Cargo.toml"
check_step "src/lib.rs 存在" test -f "$CONTRACT_DIR/src/lib.rs"
check_step "src/contract.rs 存在" test -f "$CONTRACT_DIR/src/contract.rs"
check_step "src/msg.rs 存在" test -f "$CONTRACT_DIR/src/msg.rs"
check_step "src/state.rs 存在" test -f "$CONTRACT_DIR/src/state.rs"
check_step "src/error.rs 存在" test -f "$CONTRACT_DIR/src/error.rs"
# Step 2: 检查包名
check_step "包名包含 _v1 后缀" grep -q 'name.*_v1' "$CONTRACT_DIR/Cargo.toml"
# Step 3: Rust 编译
echo ""
echo -e "${YELLOW}[BUILD]${NC} 编译 WASM..."
cd "$CONTRACT_DIR"
check_step "cargo build (非 WASM)" \
CC=/usr/bin/gcc CXX=/usr/bin/g++ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/gcc \
cargo build
check_step "cargo build (WASM)" \
CC=/usr/bin/gcc CXX=/usr/bin/g++ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/gcc \
cargo build --target wasm32-unknown-unknown --release
# Step 4: 测试
echo ""
echo -e "${YELLOW}[TEST]${NC} 运行 cargo test..."
check_step "cargo test" \
CC=/usr/bin/gcc CXX=/usr/bin/g++ CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=/usr/bin/gcc \
cargo test
# Step 5: WASM 体积
echo ""
echo -e "${YELLOW}[SIZE]${NC} 检查 WASM 体积..."
WASM_FILE="target/wasm32-unknown-unknown/release/${CONTRACT_NAME//-/_}.wasm"
if [ -f "$WASM_FILE" ]; then
WASM_SIZE=$(stat -c%s "$WASM_FILE" 2>/dev/null || stat -f%z "$WASM_FILE" 2>/dev/null)
MAX_SIZE=819200
if [ "$WASM_SIZE" -le "$MAX_SIZE" ]; then
echo -e "${GREEN}[PASS]${NC} WASM 体积: ${WASM_SIZE} bytes (限制: ${MAX_SIZE})"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
else
echo -e "${RED}[FAIL]${NC} WASM 体积: ${WASM_SIZE} bytes (超过 ${MAX_SIZE})"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
fi
else
echo -e "${YELLOW}[SKIP]${NC} WASM 文件未找到,跳过体积检查"
fi
# Step 6: Schema 生成
echo ""
echo -e "${YELLOW}[SCHEMA]${NC} 生成 Schema..."
if [ -f "examples/schema.rs" ]; then
cargo run --example schema && {
echo -e "${GREEN}[PASS]${NC} Schema 生成成功"
VALIDATION_PASSED=$((VALIDATION_PASSED + 1))
} || {
echo -e "${RED}[FAIL]${NC} Schema 生成失败"
VALIDATION_FAILED=$((VALIDATION_FAILED + 1))
}
else
echo -e "${YELLOW}[SKIP]${NC} examples/schema.rs 不存在"
fi
# 返回项目根目录
cd "$PROJECT_DIR"
# 总结
echo ""
echo "============================================"
echo " Validation Results for $CONTRACT_NAME"
echo -e " ${GREEN}Passed: $VALIDATION_PASSED${NC}"
echo -e " ${RED}Failed: $VALIDATION_FAILED${NC}"
echo "============================================"
if [ "$VALIDATION_FAILED" -gt 0 ]; then
exit 1
fi
exit 0
9.4 run-all-contracts.sh — 批量合约验证
#!/bin/bash
# run-all-contracts.sh — 批量验证所有 MSG Chain CosmWasm 合约
# 遍历 contracts/cosmwasm/all/ 下所有合约并执行 validate-contract.sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
CONTRACTS_DIR="$PROJECT_DIR/contracts/cosmwasm/all"
VALIDATE_SCRIPT="$SCRIPT_DIR/validate-contract.sh"
if [ ! -d "$CONTRACTS_DIR" ]; then
echo "❌ 合约目录不存在: $CONTRACTS_DIR"
exit 1
fi
if [ ! -f "$VALIDATE_SCRIPT" ]; then
echo "❌ validate-contract.sh 未找到"
exit 1
fi
TOTAL_CONTRACTS=0
PASSED_CONTRACTS=0
FAILED_CONTRACTS=0
echo ""
echo "============================================"
echo " MSG Chain 批量合约验证"
echo " $(date)"
echo "============================================"
for contract_dir in "$CONTRACTS_DIR"/*/; do
if [ -f "${contract_dir}Cargo.toml" ]; then
TOTAL_CONTRACTS=$((TOTAL_CONTRACTS + 1))
contract_name=$(basename "$contract_dir")
echo ""
echo "────────────────────────────────────────"
echo " [${TOTAL_CONTRACTS}] 验证: $contract_name"
echo "────────────────────────────────────────"
if bash "$VALIDATE_SCRIPT" "$contract_dir"; then
PASSED_CONTRACTS=$((PASSED_CONTRACTS + 1))
else
FAILED_CONTRACTS=$((FAILED_CONTRACTS + 1))
fi
fi
done
echo ""
echo "============================================"
echo " 批量验证完成"
echo " 总计: $TOTAL_CONTRACTS"
echo " 通过: $PASSED_CONTRACTS"
echo " 失败: $FAILED_CONTRACTS"
echo "============================================"
if [ "$FAILED_CONTRACTS" -gt 0 ]; then
exit 1
fi
exit 0
10. 覆盖率与报告
10.1 Rust 测试覆盖率 (Tarpaulin)
# 安装 tarpaulin
cargo install cargo-tarpaulin
# 运行覆盖率和合约测试
cd contracts/cosmwasm/all/aidid_did_registry_v1
cargo tarpaulin --out Html --output-dir coverage
ls -la coverage/
# 查看覆盖率报告
open coverage/tarpaulin-report.html
# CI 中使用 tarpaulin
cargo tarpaulin --out Xml --output-dir coverage --fail-under 70
10.2 Go 测试覆盖率
# 生成覆盖率报告
make test-coverage
# 产出: coverage.out (原始数据), coverage.html (HTML报告)
# 命令行查看函数级覆盖率
go tool cover -func=coverage.out
# 仅查看未覆盖的代码
go tool cover -func=coverage.out | grep "0%"
# 指定包生成覆盖率
go test -v -coverprofile=pkg_quantum.out ./pkg/quantum/...
go tool cover -html=pkg_quantum.out -o pkg_quantum_coverage.html
10.3 覆盖率聚合
#!/bin/bash
# aggregate-coverage.sh — 聚合 Rust 和 Go 覆盖率
set -euo pipefail
echo "=== MSG Chain 覆盖率聚合 ==="
# Go 覆盖率
go test -coverprofile=coverage.out ./pkg/...
go tool cover -func=coverage.out
go tool cover -html=coverage.out -o coverage_go.html
# Rust 合约覆盖率
if command -v cargo-tarpaulin &> /dev/null; then
for contract in contracts/cosmwasm/all/*/; do
if [ -f "$contract/Cargo.toml" ]; then
name=$(basename "$contract")
echo "=== Rust Coverage: $name ==="
cd "$contract"
cargo tarpaulin --out Html --output-dir coverage --skip-clean
cd - > /dev/null
fi
done
fi
echo ""
echo "覆盖率报告已生成:"
echo " - coverage_go.html (Go)"
echo " - contracts/cosmwasm/all/*/coverage/ (Rust)"
10.4 CI 覆盖率徽章
# 在 README.md 中添加覆盖率徽章
# 使用 GitHub Actions 和 codecov 集成
# .github/workflows/coverage.yml
name: Coverage
on:
push:
branches: [main]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Generate coverage
run: |
go test -v -race -coverprofile=coverage.out -covermode=atomic ./pkg/...
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
flags: unittests
11. DApp 测试
11.1 CosmJS 客户端测试 (Jest)
// tests/cosmjs-client.test.ts
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { GasPrice } from "@cosmjs/stargate";
import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx";
// 使用实时测试网 RPC(或 mock)
const TESTNET_RPC = "http://localhost:26657";
const TESTNET_CHAIN_ID = "msg-chain-1";
// Mock 合约地址
const MOCK_DID_REGISTRY = "msg14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmal";
const MOCK_AGENT_REGISTRY = "msg1qypqxpq9kcrn2c9afea5lq35ef37c5x7jqylz4";
describe("CosmWasm Contracts", () => {
let client: SigningCosmWasmClient | null = null;
beforeAll(async () => {
// 在测试环境中创建真实或 mock 客户端
// 这里使用 mock 配置
client = {} as SigningCosmWasmClient;
});
afterAll(async () => {
if (client) {
// await client.disconnect();
}
});
describe("DID Registry", () => {
it("should create a DID document", async () => {
const createMsg = {
create_did: {
did_id: "did:msg:agent:test-agent-001",
controller: "msg1controller",
verification_methods: [
{
id: "vm-1",
controller: "msg1controller",
key_type: "Dilithium5VerificationKey2026",
public_key_multibase: "z6Mk",
dilithium5_public_key: "pk_example",
},
],
services: [],
metadata: null,
},
};
// 当使用真实客户端:
// const result = await client!.execute(
// senderAddress,
// MOCK_DID_REGISTRY,
// createMsg,
// "auto"
// );
// expect(result.code).toBe(0);
// Mock 测试
expect(createMsg.create_did.did_id).toContain("did:msg:");
expect(createMsg.create_did.verification_methods[0].key_type).toBe(
"Dilithium5VerificationKey2026"
);
});
it("should resolve a DID document", async () => {
const queryMsg = {
resolve_did: {
did_id: "did:msg:agent:test-agent-001",
},
};
// Mock 期望响应
const mockResponse = {
document: {
did_id: "did:msg:agent:test-agent-001",
controller: "msg1controller",
active: true,
verification_methods: [],
services: [],
},
};
expect(mockResponse.document.did_id).toBe(
"did:msg:agent:test-agent-001"
);
expect(mockResponse.document.active).toBe(true);
});
it("should reject deactivated DID modifications", async () => {
const deactivateMsg = {
deactivate_did: {
did_id: "did:msg:agent:test-agent-001",
},
};
expect(deactivateMsg.deactivate_did.did_id).toBeDefined();
const updateMsg = {
update_did: {
did_id: "did:msg:agent:test-agent-001",
verification_methods: null,
services: null,
metadata: null,
},
};
// 验证停用后的 DID 不应允许更新
// 真实场景下这个 execute 应返回错误
expect(updateMsg.update_did.did_id).toBe(
deactivateMsg.deactivate_did.did_id
);
});
});
describe("Agent Registry", () => {
it("should register an agent", async () => {
const registerMsg = {
register_agent: {
agent_id: "agent-integration-001",
did_id: "did:msg:agent:integration-001",
owner: "msg1owner",
name: "Integration Test Agent",
description: "Agent created in integration test",
endpoint: "https://test-agent.example.com",
metadata: null,
agent_type: "worker",
capabilities: ["text-generation", "code-review"],
},
};
expect(registerMsg.register_agent.agent_id).toBe(
"agent-integration-001"
);
expect(registerMsg.register_agent.capabilities).toContain(
"text-generation"
);
});
it("should query agents by owner", async () => {
const queryMsg = {
get_agents_by_owner: {
owner: "msg1owner",
start_after: null,
limit: 20,
},
};
expect(queryMsg.get_agents_by_owner.owner).toBe("msg1owner");
expect(queryMsg.get_agents_by_owner.limit).toBeLessThanOrEqual(100);
});
});
describe("Gas Estimation", () => {
it("should use correct gas price steps", () => {
const gasPrices = {
gas_price: "1000000000umsg",
};
const gasPrice = GasPrice.fromString(gasPrices.average);
expect(gasPrice.denom).toBe("umsg");
// 检查金额解析
const coin = { denom: "umsg", amount: "1000000000000000000" };
expect(Number(coin.amount)).toBe(1e18); // 18 decimals
});
});
});
11.2 React 组件测试 (React Testing Library)
// tests/components/ContractInteraction.test.tsx
import React from "react";
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ContractInteraction } from "../src/components/ContractInteraction";
// Mock CosmJS
jest.mock("@cosmjs/cosmwasm-stargate", () => ({
SigningCosmWasmClient: {
connectWithSigner: jest.fn(),
},
}));
jest.mock("@cosmjs/proto-signing", () => ({
DirectSecp256k1HdWallet: {
fromMnemonic: jest.fn(),
},
}));
describe("ContractInteraction Component", () => {
beforeEach(() => {
jest.clearAllMocks();
});
it("renders contract query form", () => {
render(<ContractInteraction contractAddress="msg1test" />);
expect(screen.getByText(/查询合约/i)).toBeInTheDocument();
expect(screen.getByPlaceholderText(/JSON 查询消息/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /执行查询/i })).toBeInTheDocument();
});
it("handles query input and submission", async () => {
const mockOnQuery = jest.fn();
render(
<ContractInteraction
contractAddress="msg1test"
onQuery={mockOnQuery}
/>
);
const input = screen.getByPlaceholderText(/JSON 查询消息/i);
await userEvent.type(input, '{"get_count": {}}');
const submitButton = screen.getByRole("button", { name: /执行查询/i });
await userEvent.click(submitButton);
expect(mockOnQuery).toHaveBeenCalledWith({ get_count: {} });
});
it("shows error for invalid JSON", async () => {
render(<ContractInteraction contractAddress="msg1test" />);
const input = screen.getByPlaceholderText(/JSON 查询消息/i);
await userEvent.type(input, "invalid json");
const submitButton = screen.getByRole("button", { name: /执行查询/i });
await userEvent.click(submitButton);
expect(screen.getByText(/JSON 格式错误/i)).toBeInTheDocument();
});
it("displays query result", async () => {
const mockResult = { count: 42 };
const mockExecute = jest.fn().mockResolvedValue(mockResult);
render(
<ContractInteraction
contractAddress="msg1test"
executeQuery={mockExecute}
/>
);
const input = screen.getByPlaceholderText(/JSON 查询消息/i);
await userEvent.type(input, '{"get_count": {}}');
const submitButton = screen.getByRole("button", { name: /执行查询/i });
await userEvent.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/count.*42/i)).toBeInTheDocument();
});
});
});
11.3 E2E 测试 (Playwright)
// e2e/contract-interaction.spec.ts
import { test, expect } from "@playwright/test";
test.describe("MSG Chain DApp E2E", () => {
test.beforeEach(async ({ page }) => {
await page.goto("http://localhost:3000");
});
test("connect wallet and query contract", async ({ page }) => {
// 连接钱包
await page.click('[data-testid="connect-wallet"]');
await page.waitForSelector('[data-testid="wallet-connected"]');
// 输入合约地址
await page.fill(
'[data-testid="contract-address-input"]',
"msg14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s4hmal"
);
// 输入查询消息
await page.fill(
'[data-testid="query-msg-input"]',
'{"resolve_did": {"did_id": "did:msg:agent:test-1"}}'
);
// 执行查询
await page.click('[data-testid="execute-query"]');
// 等待响应
await page.waitForSelector('[data-testid="query-result"]');
const result = await page.textContent('[data-testid="query-result"]');
expect(result).toContain("did:msg:agent:test-1");
});
test("execute contract and verify transaction", async ({ page }) => {
// 连接钱包
await page.click('[data-testid="connect-wallet"]');
// 发送执行交易
await page.fill('[data-testid="contract-address-input"]', MOCK_CONTRACT);
await page.fill(
'[data-testid="execute-msg-input"]',
'{"create_did": {"did_id": "did:msg:agent:e2e-test", "controller": "msg1controller", "verification_methods": [], "services": []}}'
);
// 设置 gas
await page.selectOption('[data-testid="gas-price-select"]', "1000000000umsg");
await page.click('[data-testid="execute-tx"]');
// 确认交易
await page.waitForSelector('[data-testid="tx-success"]');
const txHash = await page.textContent('[data-testid="tx-hash"]');
expect(txHash).toMatch(/^0x[a-fA-F0-9]{64}$/);
});
test("display wallet balance", async ({ page }) => {
await page.click('[data-testid="connect-wallet"]');
await page.waitForSelector('[data-testid="wallet-balance"]');
const balance = await page.textContent('[data-testid="wallet-balance"]');
expect(balance).toContain("umsg");
});
});
11.4 Playwright 配置
// playwright.config.ts
import { PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
testDir: "./e2e",
timeout: 60000,
retries: 2,
use: {
baseURL: "http://localhost:3000",
headless: true,
viewport: { width: 1280, height: 720 },
screenshot: "only-on-failure",
video: "retain-on-failure",
},
projects: [
{
name: "Chrome",
use: { browserName: "chromium" },
},
{
name: "Firefox",
use: { browserName: "firefox" },
},
],
webServer: {
command: "npm run dev",
port: 3000,
timeout: 120000,
reuseExistingServer: !process.env.CI,
},
};
export default config;
11.5 前端测试 CI 集成
# .github/workflows/frontend-tests.yml
name: Frontend Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: 'frontend/package-lock.json'
- name: Install dependencies
working-directory: frontend
run: npm ci
- name: Lint
working-directory: frontend
run: npm run lint
- name: Unit tests
working-directory: frontend
run: npm test -- --coverage
- name: TypeScript check
working-directory: frontend
run: npx tsc --noEmit
- name: Install Playwright
working-directory: frontend
run: npx playwright install
- name: E2E tests
working-directory: frontend
run: npx playwright test
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: frontend-coverage
path: frontend/coverage/
12. 附录
12.1 MSG Chain 46 个合约包列表
以下合约均位于 contracts/cosmwasm/all/ 目录,每个包可使用相同模式验证:
# 列举所有合约
ls -1 contracts/cosmwasm/all/
# 每个合约的标准操作:
# cd contracts/cosmwasm/all/<name> && cargo test
# cd contracts/cosmwasm/all/<name> && cargo build --target wasm32-unknown-unknown --release
核心合约包示例:
adapter_registry_v1agent_a2a_v1agent_intent_v1agent_model_v1agent_payment_v1agent_registry_v1agent_sandbox_v1agent_verify_v1agent_work_v1ai_agent_constitution_v1aidid_did_registry_v1block_reward_decay_v1
12.2 故障排查速查
| 问题 | 原因 | 解决方法 |
|---|---|---|
cargo test 编译失败 |
wasm32 target 未安装 | rustup target add wasm32-unknown-unknown |
make lint 失败 |
gofmt 格式错误 | make fmt |
make test 失败 |
Go 依赖过期 | make deps |
make test-quantum 失败 |
Dilithium-5 库缺失 | 检查 go.sum 中的 cloudflare/circl |
make ci-contracts 失败 |
合约编译错误 | 单独运行 cargo build --target wasm32-unknown-unknown --release 查看错误 |
| CI 通过但链上上线失败 | 模拟环境差异 | 检查真实 receipt/query/log,使用 --trace 调试 |
12.3 环境变量与配置
# MSG Chain CI/CD 环境变量
export GO_VERSION="1.24"
export GO_CACHE_PATH="/tmp/msg-chain-go-build-cache"
export GOLANGCI_LINT_CACHE="/tmp/msg-chain-golangci-cache"
export RUST_TOOLCHAIN="stable"
export WASMVM_LIB_DIR="$(pwd)/lib"
export LD_LIBRARY_PATH="$WASMVM_LIB_DIR:$LD_LIBRARY_PATH"
export RUST_CC="/usr/bin/gcc"
export RUST_CXX="/usr/bin/g++"
export RUST_LINKER="/usr/bin/gcc"
# CI 参数
export CI_COVERAGE_THRESHOLD="60"
export CONTRACT_WASM_MAX_SIZE="819200" # 800KB в bytes
12.4 参考文档
- CosmWasm 官方测试文档
- cw-multi-test 文档
- proptest 文档
- MSG Chain CosmWasm 合约模板库
- MSG Chain API 接口大全
- MSG Chain 开发排错大全
本文档是 MSG Chain 开发团队的测试与 CI/CD 标准参考。
所有命令和配置均基于msg-chain-1主网参数。
生产发布必须在所有门禁通过后进行,且必须经过人工审批。
