feat(cover,guild,stand): P5 Anti-Capture Bill ceremony + secession + Pier
P5 (final execution phase) of v0.7 delivers REQ-056, REQ-059, REQ-064, REQ-066: x/cover — Bill of Rights ceremony (REQ-056) + Pier Selection (REQ-066): MsgCounselReviewBillOfRights (bonded Counsel, Staked=true gate); MsgSelectPier (Guild Council + GuildKeeper shim + PierSelectionIndex); MsgRevokePierSelection (supermajority + Counsel witness); PierSelectionIndex + PierSelectionRecord structs; bill_review/pier_selection/pier_index stores. x/guild — Secession cooling (REQ-064) + Stand->Pier (REQ-059, D-074): MsgInitiateSecession (SecessionStartedAt + lien audit); MsgCompleteSecession (21d Cover-active / 14d non-Cover cooling + lien audit + covenant clearance + pro-rata settlement event); MsgEscalateStandToPier (10M Grain-cents D-074 threshold, soft upgrade); MsgAcceptPierInvitation; Guild.SecessionStartedAt + SecededAt + Lien.Cleared additive; LOCAL StandPierEscalationAnnualPass VolumeCents const (G-003 local-const mirror of x/stand canonical). x/stand — StandPierEscalationAnnualPassVolumeCents=10000000 canonical const. Coverage: cover/keeper 94.6%, guild/keeper 94.1%, stand/types 100%. G-006/G-028 intact. go.mod/go.sum diff EMPTY. go vet clean. Lexicon green. ---ci--- project: oy phase: 5 milestone: v0.7 status: execute ---/ci---
This commit is contained in:
@@ -324,6 +324,214 @@ func (m *MsgAddLien) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- P5: Secession + Stand→Pier Escalation Msg types (REQ-064, REQ-059) --------
|
||||
//
|
||||
// (REQ-064 secession cooling enforcement, REQ-059/D-074 Stand→Pier boundary.)
|
||||
// The four P5 Msg types drive the secession lifecycle + the Stand→Pier
|
||||
// escalation:
|
||||
// - MsgInitiateSecession: a Chapter initiates secession (the handler loads
|
||||
// the Chapter, sets SecessionStartedAt=now, invokes the lien audit).
|
||||
// - MsgCompleteSecession: a Chapter completes secession after the cooling
|
||||
// period (Cover-active 21d, non-Cover 14d), the lien audit, and the
|
||||
// covenant clearance. The handler sets SecededAt=now + emits a pro-rata
|
||||
// settlement event.
|
||||
// - MsgEscalateStandToPier: a Stand's annual Pass volume exceeds the
|
||||
// StandPierEscalationAnnualPassVolumeCents const (D-074 — 10M Grain-cents
|
||||
// simtest placeholder for $100k USD) — the handler sets a Stand-Pier-
|
||||
// eligible flag (soft upgrade — the Stand may decline).
|
||||
// - MsgAcceptPierInvitation: a Stand accepts the Pier invitation (the
|
||||
// handler checks the eligibility flag + records the acceptance).
|
||||
//
|
||||
// All cross-module refs are by-ID-string (G-003). The
|
||||
// StandPierEscalationAnnualPassVolumeCents const lives in x/stand/types
|
||||
// (type ownership) and is imported by x/guild/keeper (the handler) — consts
|
||||
// are G-003-clean (only struct imports are forbidden).
|
||||
//
|
||||
// Lexicon note (REQ-012): "Secession", "Cooling", "Lien Audit", "Covenant
|
||||
// Clearance", "Pro-rata Settlement", "Stand→Pier Escalation", "Pier
|
||||
// Invitation" are lexicon-clean.
|
||||
|
||||
// MsgInitiateSecession initiates a Chapter's secession (REQ-064). The
|
||||
// handler loads the Chapter (must be IsChapter=true), sets
|
||||
// SecessionStartedAt=now, + invokes the lien audit (a simtest-grade
|
||||
// helper that returns true if all GoodStandingLiens are Cleared or
|
||||
// Amount=0). The handler REJECTS a non-Chapter Guild + a Chapter that has
|
||||
// already initiated (SecessionStartedAt > 0).
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty GuildID + Signer.
|
||||
type MsgInitiateSecession struct {
|
||||
GuildID string `json:"guild_id" yaml:"guild_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgInitiateSecession) Reset() { *m = MsgInitiateSecession{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgInitiateSecession) String() string {
|
||||
return fmt.Sprintf("MsgInitiateSecession{GuildID:%s Signer:%s}", m.GuildID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgInitiateSecession) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty GuildID + Signer.
|
||||
func (m *MsgInitiateSecession) ValidateBasic() error {
|
||||
if m.GuildID == "" {
|
||||
return fmt.Errorf("guild: empty guild-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("guild: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgInitiateSecession) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// MsgCompleteSecession completes a Chapter's secession (REQ-064). The
|
||||
// handler enforces:
|
||||
// 1. The Chapter must exist + be IsChapter=true.
|
||||
// 2. SecessionStartedAt > 0 (secession was initiated).
|
||||
// 3. Cooling check: now >= SecessionStartedAt + coolingSeconds where
|
||||
// coolingSeconds = CoolingSecessionCoverActiveDays*86400 (21d) if the
|
||||
// Chapter is Cover-active (any lien references a Cover Pool covenant),
|
||||
// else CoolingSecessionNonCoverDays*86400 (14d).
|
||||
// 4. Lien audit: all GoodStandingLiens must be Cleared (or Amount=0).
|
||||
// 5. Covenant clearance: the Cover Pool covenants must be cleared (a
|
||||
// simtest-grade CovenantClearancePassed bool on the Msg; if false,
|
||||
// REJECT).
|
||||
// 6. Pro-rata Cover-Fee settlement: emit guild.pro_rata_settlement with
|
||||
// the settlement amount (the actual settlement is a v0.8+ Grain-ledger
|
||||
// concern — the simtest-grade ProRataSettlementGrain field on the Msg
|
||||
// carries the amount for the event).
|
||||
// 7. Set SecededAt=now. Emit guild.secession_completed.
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty GuildID + Signer.
|
||||
type MsgCompleteSecession struct {
|
||||
GuildID string `json:"guild_id" yaml:"guild_id"`
|
||||
CovenantClearancePassed bool `json:"covenant_clearance_passed" yaml:"covenant_clearance_passed"`
|
||||
ProRataSettlementGrain int64 `json:"pro_rata_settlement_grain,omitempty" yaml:"pro_rata_settlement_grain,omitempty"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgCompleteSecession) Reset() { *m = MsgCompleteSecession{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgCompleteSecession) String() string {
|
||||
return fmt.Sprintf("MsgCompleteSecession{GuildID:%s CovenantClearancePassed:%v ProRataSettlementGrain:%d Signer:%s}",
|
||||
m.GuildID, m.CovenantClearancePassed, m.ProRataSettlementGrain, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgCompleteSecession) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty GuildID + Signer.
|
||||
func (m *MsgCompleteSecession) ValidateBasic() error {
|
||||
if m.GuildID == "" {
|
||||
return fmt.Errorf("guild: empty guild-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("guild: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgCompleteSecession) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// MsgEscalateStandToPier escalates a Stand to Pier-eligibility (REQ-059,
|
||||
// D-074). The handler checks AnnualPassVolumeCents >
|
||||
// StandPierEscalationAnnualPassVolumeCents (10M Grain-cents — the D-074
|
||||
// simtest placeholder for $100k USD); if so, sets a Stand-Pier-eligible
|
||||
// flag (a pier_eligible/ store keyed by StandID). Soft upgrade, not a ban
|
||||
// — the Stand may decline (the eligibility flag is set + the event is
|
||||
// emitted, but no enforcement follows; the Stand must separately accept
|
||||
// via MsgAcceptPierInvitation).
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty StandID + AnnualPassVolumeCents > 0.
|
||||
type MsgEscalateStandToPier struct {
|
||||
StandID string `json:"stand_id" yaml:"stand_id"`
|
||||
AnnualPassVolumeCents int64 `json:"annual_pass_volume_cents" yaml:"annual_pass_volume_cents"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgEscalateStandToPier) Reset() { *m = MsgEscalateStandToPier{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgEscalateStandToPier) String() string {
|
||||
return fmt.Sprintf("MsgEscalateStandToPier{StandID:%s AnnualPassVolumeCents:%d Signer:%s}",
|
||||
m.StandID, m.AnnualPassVolumeCents, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgEscalateStandToPier) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty StandID +
|
||||
// AnnualPassVolumeCents > 0 + non-empty Signer.
|
||||
func (m *MsgEscalateStandToPier) ValidateBasic() error {
|
||||
if m.StandID == "" {
|
||||
return fmt.Errorf("guild: empty stand-id")
|
||||
}
|
||||
if m.AnnualPassVolumeCents <= 0 {
|
||||
return fmt.Errorf("guild: AnnualPassVolumeCents %d <= 0", m.AnnualPassVolumeCents)
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("guild: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgEscalateStandToPier) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// MsgAcceptPierInvitation records a Stand's acceptance of a Pier invitation
|
||||
// (REQ-059, D-074). The handler checks the Stand is Pier-eligible (the
|
||||
// flag set by MsgEscalateStandToPier); if not, REJECT. Records the
|
||||
// acceptance (a pier_accepted/ store keyed by StandID). Emit
|
||||
// guild.stand_pier_accepted.
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty StandID + Signer.
|
||||
type MsgAcceptPierInvitation struct {
|
||||
StandID string `json:"stand_id" yaml:"stand_id"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgAcceptPierInvitation) Reset() { *m = MsgAcceptPierInvitation{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgAcceptPierInvitation) String() string {
|
||||
return fmt.Sprintf("MsgAcceptPierInvitation{StandID:%s Signer:%s}", m.StandID, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgAcceptPierInvitation) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty StandID + Signer.
|
||||
func (m *MsgAcceptPierInvitation) ValidateBasic() error {
|
||||
if m.StandID == "" {
|
||||
return fmt.Errorf("guild: empty stand-id")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("guild: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgAcceptPierInvitation) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgServer interface + Response types -------------------------------------
|
||||
|
||||
// MsgServer is the guild module's message server interface (one method per
|
||||
@@ -336,6 +544,12 @@ type MsgServer interface {
|
||||
OneTapExitStand(ctx interface{}, msg *MsgOneTapExitStand) (*MsgOneTapExitStandResponse, error)
|
||||
DelegateConfederationVoice(ctx interface{}, msg *MsgDelegateConfederationVoice) (*MsgDelegateConfederationVoiceResponse, error)
|
||||
AddLien(ctx interface{}, msg *MsgAddLien) (*MsgAddLienResponse, error)
|
||||
// v0.7 P5 Secession + Stand→Pier escalation handlers (REQ-064, REQ-059,
|
||||
// D-074) — defined above.
|
||||
InitiateSecession(ctx interface{}, msg *MsgInitiateSecession) (*MsgInitiateSecessionResponse, error)
|
||||
CompleteSecession(ctx interface{}, msg *MsgCompleteSecession) (*MsgCompleteSecessionResponse, error)
|
||||
EscalateStandToPier(ctx interface{}, msg *MsgEscalateStandToPier) (*MsgEscalateStandToPierResponse, error)
|
||||
AcceptPierInvitation(ctx interface{}, msg *MsgAcceptPierInvitation) (*MsgAcceptPierInvitationResponse, error)
|
||||
}
|
||||
|
||||
// --- Response types -----------------------------------------------------------
|
||||
@@ -419,3 +633,80 @@ func (m *MsgAddLienResponse) String() string { return "MsgAddLienResponse{}" }
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgAddLienResponse) ProtoMessage() {}
|
||||
|
||||
// --- P5 Response types --------------------------------------------------------
|
||||
|
||||
// MsgInitiateSecessionResponse is the response to MsgInitiateSecession.
|
||||
// LienAuditPassed reports the lien-audit result at initiation (for simtest
|
||||
// assertion: true = all liens cleared, false = outstanding liens remain —
|
||||
// the handler still records SecessionStartedAt so the cooling clock starts;
|
||||
// the lien audit is re-checked at completion).
|
||||
type MsgInitiateSecessionResponse struct {
|
||||
LienAuditPassed bool `json:"lien_audit_passed" yaml:"lien_audit_passed"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgInitiateSecessionResponse) Reset() { *m = MsgInitiateSecessionResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgInitiateSecessionResponse) String() string {
|
||||
return fmt.Sprintf("MsgInitiateSecessionResponse{LienAuditPassed:%v}", m.LienAuditPassed)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgInitiateSecessionResponse) ProtoMessage() {}
|
||||
|
||||
// MsgCompleteSecessionResponse is the response to MsgCompleteSecession.
|
||||
// CoolingSeconds reports the cooling period applied (for simtest assertion:
|
||||
// 21d Cover-active, 14d non-Cover). ProRataSettlementGrain reports the
|
||||
// pro-rata Cover-Fee settlement amount emitted in the event (a simtest-grade
|
||||
// placeholder; the actual settlement is a v0.8+ Grain-ledger concern).
|
||||
type MsgCompleteSecessionResponse struct {
|
||||
CoolingSeconds int64 `json:"cooling_seconds" yaml:"cooling_seconds"`
|
||||
ProRataSettlementGrain int64 `json:"pro_rata_settlement_grain" yaml:"pro_rata_settlement_grain"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgCompleteSecessionResponse) Reset() { *m = MsgCompleteSecessionResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgCompleteSecessionResponse) String() string {
|
||||
return fmt.Sprintf("MsgCompleteSecessionResponse{CoolingSeconds:%d ProRataSettlementGrain:%d}",
|
||||
m.CoolingSeconds, m.ProRataSettlementGrain)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgCompleteSecessionResponse) ProtoMessage() {}
|
||||
|
||||
// MsgEscalateStandToPierResponse is the response to MsgEscalateStandToPier.
|
||||
// PierEligible reports whether the Stand was marked Pier-eligible (true when
|
||||
// AnnualPassVolumeCents > StandPierEscalationAnnualPassVolumeCents; false
|
||||
// otherwise — the handler still emits the event but does not set the flag).
|
||||
type MsgEscalateStandToPierResponse struct {
|
||||
PierEligible bool `json:"pier_eligible" yaml:"pier_eligible"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgEscalateStandToPierResponse) Reset() { *m = MsgEscalateStandToPierResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgEscalateStandToPierResponse) String() string {
|
||||
return fmt.Sprintf("MsgEscalateStandToPierResponse{PierEligible:%v}", m.PierEligible)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgEscalateStandToPierResponse) ProtoMessage() {}
|
||||
|
||||
// MsgAcceptPierInvitationResponse is the response to MsgAcceptPierInvitation.
|
||||
type MsgAcceptPierInvitationResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgAcceptPierInvitationResponse) Reset() { *m = MsgAcceptPierInvitationResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgAcceptPierInvitationResponse) String() string {
|
||||
return "MsgAcceptPierInvitationResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgAcceptPierInvitationResponse) ProtoMessage() {}
|
||||
|
||||
@@ -303,3 +303,193 @@ func TestResponseMethods(t *testing.T) {
|
||||
r.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// --- P5: Secession + Stand→Pier escalation Msg methods (REQ-064, REQ-059) -----
|
||||
|
||||
func TestMsgInitiateSecessionMethods(t *testing.T) {
|
||||
m := &MsgInitiateSecession{GuildID: "g1", Signer: "s1"}
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgInitiateSecession ValidateBasic: %v", err)
|
||||
}
|
||||
if !strings.Contains(m.String(), "g1") {
|
||||
t.Errorf("MsgInitiateSecession String = %q, want g1", m.String())
|
||||
}
|
||||
m.Reset()
|
||||
if m.GuildID != "" {
|
||||
t.Errorf("MsgInitiateSecession Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage()
|
||||
m2 := &MsgInitiateSecession{Signer: "host-1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgInitiateSecession GetSigners = %v, want [host-1]", got)
|
||||
}
|
||||
var _ []sdk.AccAddress = m2.GetSigners()
|
||||
}
|
||||
|
||||
func TestMsgInitiateSecessionValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgInitiateSecession
|
||||
}{
|
||||
{"empty guild-id", MsgInitiateSecession{Signer: "s"}},
|
||||
{"empty signer", MsgInitiateSecession{GuildID: "g"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgCompleteSecessionMethods(t *testing.T) {
|
||||
m := &MsgCompleteSecession{
|
||||
GuildID: "g1",
|
||||
CovenantClearancePassed: true,
|
||||
ProRataSettlementGrain: 5000,
|
||||
Signer: "s1",
|
||||
}
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgCompleteSecession ValidateBasic: %v", err)
|
||||
}
|
||||
if !strings.Contains(m.String(), "g1") {
|
||||
t.Errorf("MsgCompleteSecession String = %q, want g1", m.String())
|
||||
}
|
||||
m.Reset()
|
||||
if m.GuildID != "" {
|
||||
t.Errorf("MsgCompleteSecession Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage()
|
||||
m2 := &MsgCompleteSecession{Signer: "host-1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgCompleteSecession GetSigners = %v, want [host-1]", got)
|
||||
}
|
||||
var _ []sdk.AccAddress = m2.GetSigners()
|
||||
}
|
||||
|
||||
func TestMsgCompleteSecessionValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgCompleteSecession
|
||||
}{
|
||||
{"empty guild-id", MsgCompleteSecession{Signer: "s"}},
|
||||
{"empty signer", MsgCompleteSecession{GuildID: "g"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgEscalateStandToPierMethods(t *testing.T) {
|
||||
m := &MsgEscalateStandToPier{StandID: "s1", AnnualPassVolumeCents: 100, Signer: "signer"}
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgEscalateStandToPier ValidateBasic: %v", err)
|
||||
}
|
||||
if !strings.Contains(m.String(), "s1") {
|
||||
t.Errorf("MsgEscalateStandToPier String = %q, want s1", m.String())
|
||||
}
|
||||
m.Reset()
|
||||
if m.StandID != "" {
|
||||
t.Errorf("MsgEscalateStandToPier Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage()
|
||||
m2 := &MsgEscalateStandToPier{Signer: "host-1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgEscalateStandToPier GetSigners = %v, want [host-1]", got)
|
||||
}
|
||||
var _ []sdk.AccAddress = m2.GetSigners()
|
||||
}
|
||||
|
||||
func TestMsgEscalateStandToPierValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgEscalateStandToPier
|
||||
}{
|
||||
{"empty stand-id", MsgEscalateStandToPier{AnnualPassVolumeCents: 100, Signer: "s"}},
|
||||
{"zero volume", MsgEscalateStandToPier{StandID: "s", Signer: "signer"}},
|
||||
{"negative volume", MsgEscalateStandToPier{StandID: "s", AnnualPassVolumeCents: -1, Signer: "signer"}},
|
||||
{"empty signer", MsgEscalateStandToPier{StandID: "s", AnnualPassVolumeCents: 100}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgAcceptPierInvitationMethods(t *testing.T) {
|
||||
m := &MsgAcceptPierInvitation{StandID: "s1", Signer: "signer"}
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgAcceptPierInvitation ValidateBasic: %v", err)
|
||||
}
|
||||
if !strings.Contains(m.String(), "s1") {
|
||||
t.Errorf("MsgAcceptPierInvitation String = %q, want s1", m.String())
|
||||
}
|
||||
m.Reset()
|
||||
if m.StandID != "" {
|
||||
t.Errorf("MsgAcceptPierInvitation Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage()
|
||||
m2 := &MsgAcceptPierInvitation{Signer: "host-1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgAcceptPierInvitation GetSigners = %v, want [host-1]", got)
|
||||
}
|
||||
var _ []sdk.AccAddress = m2.GetSigners()
|
||||
}
|
||||
|
||||
func TestMsgAcceptPierInvitationValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgAcceptPierInvitation
|
||||
}{
|
||||
{"empty stand-id", MsgAcceptPierInvitation{Signer: "s"}},
|
||||
{"empty signer", MsgAcceptPierInvitation{StandID: "s"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestP5ResponseMethods exercises the P5 Msg* Response Reset/String/ProtoMessage
|
||||
// methods for coverage.
|
||||
func TestP5ResponseMethods(t *testing.T) {
|
||||
r1 := &MsgInitiateSecessionResponse{LienAuditPassed: true}
|
||||
if !strings.Contains(r1.String(), "MsgInitiateSecessionResponse") {
|
||||
t.Errorf("MsgInitiateSecessionResponse String = %q", r1.String())
|
||||
}
|
||||
r1.Reset()
|
||||
if r1.LienAuditPassed {
|
||||
t.Errorf("MsgInitiateSecessionResponse Reset did not zero: %+v", r1)
|
||||
}
|
||||
r1.ProtoMessage()
|
||||
|
||||
r2 := &MsgCompleteSecessionResponse{CoolingSeconds: 100, ProRataSettlementGrain: 200}
|
||||
if !strings.Contains(r2.String(), "MsgCompleteSecessionResponse") {
|
||||
t.Errorf("MsgCompleteSecessionResponse String = %q", r2.String())
|
||||
}
|
||||
r2.Reset()
|
||||
if r2.CoolingSeconds != 0 || r2.ProRataSettlementGrain != 0 {
|
||||
t.Errorf("MsgCompleteSecessionResponse Reset did not zero: %+v", r2)
|
||||
}
|
||||
r2.ProtoMessage()
|
||||
|
||||
r3 := &MsgEscalateStandToPierResponse{PierEligible: true}
|
||||
if !strings.Contains(r3.String(), "MsgEscalateStandToPierResponse") {
|
||||
t.Errorf("MsgEscalateStandToPierResponse String = %q", r3.String())
|
||||
}
|
||||
r3.Reset()
|
||||
if r3.PierEligible {
|
||||
t.Errorf("MsgEscalateStandToPierResponse Reset did not zero: %+v", r3)
|
||||
}
|
||||
r3.ProtoMessage()
|
||||
|
||||
r4 := &MsgAcceptPierInvitationResponse{}
|
||||
r4.Reset()
|
||||
if !strings.Contains(r4.String(), "MsgAcceptPierInvitationResponse") {
|
||||
t.Errorf("MsgAcceptPierInvitationResponse String = %q", r4.String())
|
||||
}
|
||||
r4.ProtoMessage()
|
||||
}
|
||||
|
||||
+35
-1
@@ -39,6 +39,25 @@ const (
|
||||
// (the CreateChapter handler rejects shorter). Locked-const regression in
|
||||
// types_test.go.
|
||||
CoolingSecessionNonCoverDays = uint32(14)
|
||||
|
||||
// StandPierEscalationAnnualPassVolumeCents is the LOCAL cross-documented
|
||||
// const for the D-074 Stand→Pier escalation threshold (REQ-059). The
|
||||
// canonical const lives in x/stand/types (type ownership); this LOCAL
|
||||
// const mirrors it so x/guild/keeper can reference the threshold WITHOUT
|
||||
// importing x/stand/types (G-003 — no cross-module struct import; consts
|
||||
// are G-003-clean in principle, but the project's G-003 regression test
|
||||
// blocks ALL x/<other>/types imports, so the local-const mirror pattern
|
||||
// is used — mirroring the LendingCouponCapBps local-const pattern in
|
||||
// x/hub). The two consts MUST stay in sync (a change to
|
||||
// x/stand/types.StandPierEscalationAnnualPassVolumeCents requires a
|
||||
// matching change here). The value 10_000_000 is the D-074 simtest
|
||||
// placeholder for $100k USD in Grain-cents (no oracle exists in simtest
|
||||
// — the live v0.8+ mesh converts at the oracle rate). NOT LOCKED — it
|
||||
// is a simtest default; the live governance may tune it. A Stand whose
|
||||
// annual Pass volume exceeds this threshold is marked Pier-eligible (a
|
||||
// soft upgrade — the Stand may decline the Pier invitation via not
|
||||
// calling MsgAcceptPierInvitation).
|
||||
StandPierEscalationAnnualPassVolumeCents int64 = 10_000_000
|
||||
)
|
||||
|
||||
// Guild is a task-oriented collective (vision §16, REQ-017). A Guild may
|
||||
@@ -56,6 +75,14 @@ const (
|
||||
// with SecuredAtFounding=true; post-founding liens are SecuredAtFounding=false
|
||||
// (the AddLien handler rejects any new SecuredAtFounding=true lien — founding
|
||||
// is a one-time event).
|
||||
//
|
||||
// P5 extension (REQ-064): the Guild carries two additive secession-lifecycle
|
||||
// fields. SecessionStartedAt is the unix-seconds timestamp the secession was
|
||||
// initiated (0 = not seceding — the MsgInitiateSecession handler sets it).
|
||||
// SecededAt is the unix-seconds timestamp the secession completed (0 = not
|
||||
// yet seceded — the MsgCompleteSecession handler sets it). Both default to 0
|
||||
// (additive — existing Guild records keep zero values until a secession is
|
||||
// initiated/completed).
|
||||
type Guild struct {
|
||||
GuildID string `json:"guild_id" yaml:"guild_id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
@@ -68,6 +95,8 @@ type Guild struct {
|
||||
IsChapter bool `json:"is_chapter,omitempty" yaml:"is_chapter,omitempty"`
|
||||
SecessionTermsHash []byte `json:"secession_terms_hash,omitempty" yaml:"secession_terms_hash,omitempty"`
|
||||
GoodStandingLiens []Lien `json:"good_standing_liens,omitempty" yaml:"good_standing_liens,omitempty"`
|
||||
SecessionStartedAt int64 `json:"secession_started_at,omitempty" yaml:"secession_started_at,omitempty"`
|
||||
SecededAt int64 `json:"seceded_at,omitempty" yaml:"seceded_at,omitempty"`
|
||||
}
|
||||
|
||||
// GuildPublicProfile is a Guild's published profile (REQ-051). BondSummary is
|
||||
@@ -92,12 +121,17 @@ type GuildPublicProfile struct {
|
||||
// Guild/Chapter creation; NOT freely increasable post-founding — the AddLien
|
||||
// handler rejects any new SecuredAtFounding=true lien). CoverPoolCovenantRef
|
||||
// references a Cover Pool covenant by-ID-string (G-003); empty for a lien
|
||||
// with no Cover Pool covenant backing.
|
||||
// with no Cover Pool covenant backing. Cleared is the P5 secession lien-audit
|
||||
// flag (REQ-064): the MsgInitiateSecession + MsgCompleteSecession handlers
|
||||
// consult the LienAudit (CheckLiensCleared) which returns true only when
|
||||
// every lien on the Chapter has Cleared=true (or Amount=0). Cleared defaults
|
||||
// to false (additive — existing liens keep false until cleared).
|
||||
type Lien struct {
|
||||
Amount int64 `json:"amount" yaml:"amount"`
|
||||
CreditorReachID string `json:"creditor_reach_id" yaml:"creditor_reach_id"`
|
||||
SecuredAtFounding bool `json:"secured_at_founding" yaml:"secured_at_founding"`
|
||||
CoverPoolCovenantRef string `json:"cover_pool_covenant_ref,omitempty" yaml:"cover_pool_covenant_ref,omitempty"`
|
||||
Cleared bool `json:"cleared,omitempty" yaml:"cleared,omitempty"`
|
||||
}
|
||||
|
||||
// SecessionTerms is a Chapter's secession cooling terms (REQ-053, REQ-064).
|
||||
|
||||
@@ -579,3 +579,59 @@ func packageDir(t *testing.T, importPath string) string {
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
|
||||
// --- P5 locked-const + struct regression (REQ-064, REQ-059, D-074) -------------
|
||||
|
||||
// TestStandPierEscalationAnnualPassVolumeCentsLocalConst asserts the LOCAL
|
||||
// cross-documented Stand→Pier escalation threshold const (REQ-059, D-074)
|
||||
// holds its value + matches the canonical const in x/stand/types. The two
|
||||
// consts MUST stay in sync (the LOCAL const is the G-003 mirror of
|
||||
// x/stand/types.StandPierEscalationAnnualPassVolumeCents — the x/guild
|
||||
// keeper references the LOCAL const to avoid the cross-module struct
|
||||
// import).
|
||||
func TestStandPierEscalationAnnualPassVolumeCentsLocalConst(t *testing.T) {
|
||||
if types.StandPierEscalationAnnualPassVolumeCents != 10_000_000 {
|
||||
t.Errorf("StandPierEscalationAnnualPassVolumeCents (LOCAL) = %d, want 10000000 (D-074 — REQ-059)",
|
||||
types.StandPierEscalationAnnualPassVolumeCents)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGuildP5SecessionFields asserts the Guild struct carries the P5
|
||||
// additive secession-lifecycle fields (SecessionStartedAt + SecededAt) —
|
||||
// a compile-time + runtime regression firewall (removing either field
|
||||
// breaks this test). Both default to 0 (additive — existing Guild records
|
||||
// keep zero values until a secession is initiated/completed).
|
||||
func TestGuildP5SecessionFields(t *testing.T) {
|
||||
g := types.Guild{
|
||||
GuildID: "g1",
|
||||
SecessionStartedAt: 1000,
|
||||
SecededAt: 2000,
|
||||
}
|
||||
if g.SecessionStartedAt != 1000 {
|
||||
t.Errorf("SecessionStartedAt = %d, want 1000", g.SecessionStartedAt)
|
||||
}
|
||||
if g.SecededAt != 2000 {
|
||||
t.Errorf("SecededAt = %d, want 2000", g.SecededAt)
|
||||
}
|
||||
// Default zero-value (additive — existing Guild records unchanged).
|
||||
var g2 types.Guild
|
||||
if g2.SecessionStartedAt != 0 || g2.SecededAt != 0 {
|
||||
t.Error("zero-value Guild secession fields should be 0 (additive — existing records unchanged)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLienP5ClearedField asserts the Lien struct carries the P5 additive
|
||||
// Cleared field (REQ-064 secession lien-audit flag) — a compile-time +
|
||||
// runtime regression firewall. Cleared defaults to false (additive —
|
||||
// existing liens keep false until cleared).
|
||||
func TestLienP5ClearedField(t *testing.T) {
|
||||
l := types.Lien{Amount: 100, CreditorReachID: "c", SecuredAtFounding: true, Cleared: true}
|
||||
if !l.Cleared {
|
||||
t.Error("Lien Cleared = false, want true")
|
||||
}
|
||||
// Default zero-value (additive — existing liens unchanged).
|
||||
var l2 types.Lien
|
||||
if l2.Cleared {
|
||||
t.Error("zero-value Lien Cleared should be false (additive — existing liens unchanged until cleared)")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user