docs(milestone): complete OpenYield v0.2 (The Mesh)
---ci--- project: oy phase: 5 milestone: v0.2 status: complete phase_role: final requirements: covered: [REQ-009, REQ-011, REQ-015, REQ-016, REQ-017, REQ-018, REQ-020, REQ-021, REQ-012] partial: [] ---/ci--- Milestone v0.2 (The Mesh) complete. Skeleton+tests layer for 9 new modules + 1 extension: x/window (REQ-015, fullest), x/stand (REQ-016, 9 types), x/guild (REQ-017, HandPass 0%), x/pact (REQ-020, 6 types + Mission Lock), x/partner (REQ-018, 4-tier), x/council (REQ-011, 3 councils + Mission Lock const), x/forex (Forex v1, Bread/Asset pairs), x/bond (REQ-021, 8% cap Clamp), x/satellite (REQ-009, 5-chain L2 + ICS-20 v1), x/bearers EXTENDED (OY-LR/Beacon). 303 tests total (53 v0.1 baseline + 250 new). Coverage >=95.9% on all new/extended packages (8 at 100%). Lexicon firewall (REQ-012) project-wide + per-module. G-003 by-ID-string import invariant. 5 phases: P0 (v0.1.0) + P1-P4 (v0.1.1..v0.1.4) + P5 final (v0.1.5 = this milestone release). Per run.md patch-line model: no separate minor tag. 14 clarification decisions (D-020..D-033), 15 research assumptions (A-201..A-215), 31 tasks across 5 phases, 10 grill binding decisions (G-001..G-010) all applied. Next milestone: v0.3 (The Bearers) per ROADMAP Phase 3.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the
|
||||
// council module (G-008 split). ValidateGenesis in types.go composes these
|
||||
// helpers; the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Council genesis schema has two top-level sets: Councils (the three
|
||||
// governance councils — Mesh/Guild/Stand) and Voices (the Voice-tally
|
||||
// set). The invariants enforced at genesis load are (1) council-id
|
||||
// uniqueness, (2) voice-id uniqueness, (3) referential integrity (each
|
||||
// Voice's council-id references an existing Council), and (4) the
|
||||
// Mission-Lock check (the global MissionLockAmendable const bool is the
|
||||
// firewall — this helper is the genesis-side echo).
|
||||
|
||||
// ValidateCouncils asserts council-ids are present and unique, and that
|
||||
// each Council's kind is a known CouncilKind. A Stand Council must populate
|
||||
// stand-id-ref (by-ID-string ref to x/stand); a Guild Council must populate
|
||||
// guild-id-ref (by-ID-string ref to x/guild). A Mesh Council leaves both
|
||||
// refs empty. ValidateCouncils is the data-engineer's schema validator,
|
||||
// composed by ValidateGenesis in types.go.
|
||||
func ValidateCouncils(councils []Council) error {
|
||||
seen := make(map[string]bool, len(councils))
|
||||
for i, c := range councils {
|
||||
if c.CouncilID == "" {
|
||||
return fmt.Errorf("council [%d]: empty council-id", i)
|
||||
}
|
||||
if seen[c.CouncilID] {
|
||||
return fmt.Errorf("council: duplicate council-id %q", c.CouncilID)
|
||||
}
|
||||
seen[c.CouncilID] = true
|
||||
if !knownCouncilKind(c.Kind) {
|
||||
return fmt.Errorf("council %q: unknown council kind %q", c.CouncilID, c.Kind)
|
||||
}
|
||||
// A Stand Council must reference a Stand by-ID-string (P1-02-01 ref).
|
||||
if c.Kind == CouncilStand && c.StandIDRef == "" {
|
||||
return fmt.Errorf("council %q: Stand Council missing stand-id-ref", c.CouncilID)
|
||||
}
|
||||
// A Guild Council must reference a Guild by-ID-string (P1-03-01 ref).
|
||||
if c.Kind == CouncilGuild && c.GuildIDRef == "" {
|
||||
return fmt.Errorf("council %q: Guild Council missing guild-id-ref", c.CouncilID)
|
||||
}
|
||||
}
|
||||
if err := MissionLockCheck(councils); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateVoices asserts voice-ids are present and unique, and that each
|
||||
// Voice's council-id references an existing Council in the genesis set
|
||||
// (referential integrity — the P3-01-03 deliverable: each Voice tally's
|
||||
// council-id must resolve to a genesis Council). signal-kind must be a
|
||||
// known SignalKind (the four Freeholder signals, cross-ref REQ-005). The
|
||||
// referential-integrity check is the data-engineer's genesis invariant: a
|
||||
// Voice tally pointing at a non-existent Council is rejected at genesis
|
||||
// load (no orphan tallies).
|
||||
func ValidateVoices(voices []Voice, councils []Council) error {
|
||||
councilIDs := make(map[string]bool, len(councils))
|
||||
for _, c := range councils {
|
||||
councilIDs[c.CouncilID] = true
|
||||
}
|
||||
seen := make(map[string]bool, len(voices))
|
||||
for i, v := range voices {
|
||||
if v.VoiceID == "" {
|
||||
return fmt.Errorf("voice [%d]: empty voice-id", i)
|
||||
}
|
||||
if seen[v.VoiceID] {
|
||||
return fmt.Errorf("voice: duplicate voice-id %q", v.VoiceID)
|
||||
}
|
||||
seen[v.VoiceID] = true
|
||||
if !councilIDs[v.CouncilID] {
|
||||
return fmt.Errorf("voice %q: council-id %q does not reference an existing council", v.VoiceID, v.CouncilID)
|
||||
}
|
||||
if !knownSignalKind(v.SignalKind) {
|
||||
return fmt.Errorf("voice %q: unknown signal-kind %q", v.VoiceID, v.SignalKind)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownCouncilKind reports whether k is one of the three CouncilKind values.
|
||||
func knownCouncilKind(k CouncilKind) bool {
|
||||
for _, kk := range AllCouncilKinds() {
|
||||
if k == kk {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// knownSignalKind reports whether s is one of the four SignalKind values.
|
||||
func knownSignalKind(s SignalKind) bool {
|
||||
for _, kk := range AllSignalKinds() {
|
||||
if s == kk {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MissionLockCheck asserts the Mission-Lock invariant on a slice of
|
||||
// Councils (vision §19, REQ-011). Because MissionLockAmendable is a compile-
|
||||
// time const bool == false, this check always passes — it exists as the
|
||||
// data-engineer's genesis-side assertion that the Mission-Lock firewall is
|
||||
// intact. If the const ever flipped to true (which the test suite rejects),
|
||||
// the genesis load would surface it here. The helper is the genesis hook
|
||||
// for v0.3 keeper logic to extend with live per-council Mission-Lock
|
||||
// enforcement.
|
||||
func MissionLockCheck(councils []Council) error {
|
||||
// The global MissionLockAmendable const is the firewall: if it were ever
|
||||
// flipped to true (which the test suite rejects), the genesis load would
|
||||
// surface it here. The per-council loop is the hook for v0.3 live logic.
|
||||
if MissionLockAmendable {
|
||||
return fmt.Errorf("council: Mission Lock amendable (MissionLockAmendable == true) — firewall breach")
|
||||
}
|
||||
for range councils {
|
||||
// No per-council runtime data to verify in the skeleton — the const
|
||||
// is the source of truth. The loop preserves the hook point.
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "council"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// CouncilKindCount is the locked count of CouncilKind enum values
|
||||
// (vision §13 / REQ-011). A regression firewall: adding/removing/renaming
|
||||
// a Council kind breaks this const's test.
|
||||
CouncilKindCount = 3
|
||||
|
||||
// MissionLockAmendable is the Mission-Lock invariant (vision §19, REQ-011):
|
||||
// the Six Principles + Fee Covenant + no-amend covenant can NEVER be
|
||||
// amended by any council. This is a locked const bool — the highest-
|
||||
// severity regression firewall in the council module. The const can
|
||||
// NEVER be set true; the test asserts it is false and that no code path
|
||||
// can flip it (the compile-time const is the firewall, not runtime data).
|
||||
MissionLockAmendable = false
|
||||
|
||||
// SignalKindCount is the locked count of SignalKind enum values — the
|
||||
// four Freeholder signals (vision §9.1 / REQ-005) plus Capital (REQ-011
|
||||
// multi-source Voice). Cross-ref v0.1 x/standing FreeholderSignals.
|
||||
SignalKindCount = 4
|
||||
)
|
||||
|
||||
// CouncilKind enumerates the three governance councils (vision §13, REQ-011):
|
||||
// Mesh Council (whole-mesh), Guild Council (guild-level), Stand Council
|
||||
// (Stand-level). Each uses multi-source Voice. Mission Lock (the Six
|
||||
// Principles + fee covenant + no-amend covenant) cannot be amended by any
|
||||
// council — enforced by the compile-time MissionLockAmendable const bool.
|
||||
type CouncilKind string
|
||||
|
||||
const (
|
||||
CouncilMesh CouncilKind = "MeshCouncil" // whole-mesh council
|
||||
CouncilGuild CouncilKind = "GuildCouncil" // guild-level council
|
||||
CouncilStand CouncilKind = "StandCouncil" // Stand-level council
|
||||
)
|
||||
|
||||
// AllCouncilKinds returns all three CouncilKind values in REQ-011 order.
|
||||
// Locked-const test asserts exactly 3 entries with these names (REQ-011).
|
||||
func AllCouncilKinds() []CouncilKind {
|
||||
return []CouncilKind{
|
||||
CouncilMesh,
|
||||
CouncilGuild,
|
||||
CouncilStand,
|
||||
}
|
||||
}
|
||||
|
||||
// Council is one of three governance councils (REQ-011). kind picks the
|
||||
// tier (Mesh/Guild/Stand). stand-id-ref references x/stand by ID string
|
||||
// (optional — only Stand Councils populate it; P1-02-01 by-ID-string ref).
|
||||
// guild-id-ref references x/guild by ID string (optional — only Guild
|
||||
// Councils populate it; P1-03-01 by-ID-string ref). Both refs are by-ID-
|
||||
// string per G-003 (no struct imports of x/stand or x/guild). members is
|
||||
// the voice-holder set; voice-threshold is the tally pass threshold.
|
||||
type Council struct {
|
||||
CouncilID string `json:"council_id" yaml:"council_id"`
|
||||
Kind CouncilKind `json:"kind" yaml:"kind"`
|
||||
StandIDRef string `json:"stand_id_ref,omitempty" yaml:"stand_id_ref,omitempty"`
|
||||
GuildIDRef string `json:"guild_id_ref,omitempty" yaml:"guild_id_ref,omitempty"`
|
||||
Members []CouncilMember `json:"members" yaml:"members"`
|
||||
VoiceThreshold uint32 `json:"voice_threshold" yaml:"voice_threshold"`
|
||||
}
|
||||
|
||||
// CouncilMember is a voice-holder in a Council (REQ-011). reach-id
|
||||
// references x/identity Reach by string (G-003 — the lexicon-clean holder
|
||||
// identifier; the banned financial holder term is NOT used here). voice-
|
||||
// weight is the member's Voice weight in the tally; joined-at is the join
|
||||
// timestamp.
|
||||
type CouncilMember struct {
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
VoiceWeight uint32 `json:"voice_weight" yaml:"voice_weight"`
|
||||
JoinedAt int64 `json:"joined_at" yaml:"joined_at"`
|
||||
}
|
||||
|
||||
// Voice is a single Voice signal cast on a Council proposal (REQ-011).
|
||||
// council-id references the Council by ID string (G-003). proposer-reach
|
||||
// references x/identity Reach by string (lexicon-clean holder identifier;
|
||||
// the banned financial holder term is NOT used).
|
||||
// signal-kind picks the multi-source Voice input (Stash/Standing/Vouch/
|
||||
// Capital — the four Freeholder signals, cross-ref v0.1 REQ-005
|
||||
// FreeholderSignals). target-ref is the proposal/option the Voice targets
|
||||
// (opaque string ref). tally is the running tally result; timestamp is the
|
||||
// cast time.
|
||||
type Voice struct {
|
||||
VoiceID string `json:"voice_id" yaml:"voice_id"`
|
||||
CouncilID string `json:"council_id" yaml:"council_id"`
|
||||
ProposerReach string `json:"proposer_reach" yaml:"proposer_reach"`
|
||||
SignalKind SignalKind `json:"signal_kind" yaml:"signal_kind"`
|
||||
TargetRef string `json:"target_ref" yaml:"target_ref"`
|
||||
Tally TallyResult `json:"tally" yaml:"tally"`
|
||||
Timestamp int64 `json:"timestamp" yaml:"timestamp"`
|
||||
}
|
||||
|
||||
// SignalKind enumerates the multi-source Voice inputs (REQ-011). The four
|
||||
// Freeholder signals (vision §9.1 / REQ-005, cross-ref x/standing
|
||||
// FreeholderSignals): Stash, Standing, Vouch, Capital. No "Freeholder"
|
||||
// SignalKind — the four signals are the inputs a Freeholder-eligible Reach
|
||||
// casts; the eligibility is upstream (x/standing). Capital is the committed-
|
||||
// capital signal (vision §9.1 committed_capital).
|
||||
type SignalKind string
|
||||
|
||||
const (
|
||||
SignalStash SignalKind = "Stash" // Stash-maturity signal (vision §9.1)
|
||||
SignalStanding SignalKind = "Standing" // multi-domain Standing signal (§9.1)
|
||||
SignalVouch SignalKind = "Vouch" // community endorsement / Vouch (§9.1)
|
||||
SignalCapital SignalKind = "Capital" // committed-capital signal (§9.1)
|
||||
)
|
||||
|
||||
// AllSignalKinds returns all four SignalKind values in REQ-005 / vision §9.1
|
||||
// order. Locked-const test asserts exactly 4 entries (cross-ref v0.1
|
||||
// x/standing FreeholderSignals: StashMaturity, MultiDomainStanding,
|
||||
// CommittedCapital, CommunityEndorsement — the four signals map to
|
||||
// Stash/Standing/Capital/Vouch here).
|
||||
func AllSignalKinds() []SignalKind {
|
||||
return []SignalKind{
|
||||
SignalStash,
|
||||
SignalStanding,
|
||||
SignalVouch,
|
||||
SignalCapital,
|
||||
}
|
||||
}
|
||||
|
||||
// TallyResult mirrors Cosmos SDK x/gov TallyResult shape (A-204) for
|
||||
// future wiring of Council governance to x/gov. Fields: yes, no, abstain
|
||||
// (no "no-with-veto" — anti-greed, vision §19), nowithveto (kept as a
|
||||
// zero-locked field for x/gov shape parity — always 0 in OY since the
|
||||
// VoteOption enum has no veto option), total (total Voice cast). The
|
||||
// quorum-met flag is the tally pass indicator. The field names (yes, no,
|
||||
// abstain) match x/gov exactly so a future x/gov wiring is mechanical.
|
||||
type TallyResult struct {
|
||||
Yes uint64 `json:"yes" yaml:"yes"`
|
||||
No uint64 `json:"no" yaml:"no"`
|
||||
Abstain uint64 `json:"abstain" yaml:"abstain"`
|
||||
NoWithVeto uint64 `json:"nowithveto" yaml:"nowithveto"` // always 0 — no veto option (anti-greed)
|
||||
Total uint64 `json:"total" yaml:"total"`
|
||||
QuorumMet bool `json:"quorum_met" yaml:"quorum_met"`
|
||||
}
|
||||
|
||||
// Params for the council module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the council module genesis state (REQ-011).
|
||||
// Councils is the top-level set of three Council kinds; Voices is the
|
||||
// Voice-tally set. ValidateGenesis enforces council-id uniqueness,
|
||||
// voice-id uniqueness, and the Mission-Lock check (the const firewall echo).
|
||||
// The data-engineer's genesis.go holds the schema helpers (G-008).
|
||||
type GenesisState struct {
|
||||
Councils []Council `json:"councils" yaml:"councils"`
|
||||
Voices []Voice `json:"voices" yaml:"voices"`
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Councils: []Council{},
|
||||
Voices: []Voice{},
|
||||
Params: DefaultParams(),
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate council-ids and duplicate voice-ids, and runs
|
||||
// the Mission-Lock check. Delegates to the data-engineer's genesis.go
|
||||
// helpers (G-008).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("council: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateCouncils(gs.Councils); err != nil {
|
||||
return fmt.Errorf("council: %w", err)
|
||||
}
|
||||
if err := ValidateVoices(gs.Voices, gs.Councils); err != nil {
|
||||
return fmt.Errorf("council: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
"github.com/oy/openyield/x/council/types"
|
||||
)
|
||||
|
||||
// TestCouncilKindCountLockedConst asserts CouncilKindCount is exactly 3
|
||||
// and AllCouncilKinds() returns exactly 3 (REQ-011). A regression firewall:
|
||||
// adding/removing/renaming a Council kind breaks this test.
|
||||
func TestCouncilKindCountLockedConst(t *testing.T) {
|
||||
if types.CouncilKindCount != 3 {
|
||||
t.Errorf("CouncilKindCount = %d, expected 3 (REQ-011 LOCKED)", types.CouncilKindCount)
|
||||
}
|
||||
all := types.AllCouncilKinds()
|
||||
if len(all) != 3 {
|
||||
t.Errorf("AllCouncilKinds() len = %d, expected 3", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllCouncilKindsNames asserts the 3 REQ-011 names in order with no
|
||||
// extras, no dups, no renames.
|
||||
func TestAllCouncilKindsNames(t *testing.T) {
|
||||
want := []string{"MeshCouncil", "GuildCouncil", "StandCouncil"}
|
||||
all := types.AllCouncilKinds()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, k := range all {
|
||||
if string(k) != want[i] {
|
||||
t.Errorf("AllCouncilKinds()[%d] = %q, want %q", i, k, want[i])
|
||||
}
|
||||
if seen[string(k)] {
|
||||
t.Errorf("duplicate CouncilKind %q", k)
|
||||
}
|
||||
seen[string(k)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestCouncilKindValues asserts each named const matches its AllCouncilKinds
|
||||
// entry.
|
||||
func TestCouncilKindValues(t *testing.T) {
|
||||
if types.CouncilMesh != "MeshCouncil" {
|
||||
t.Errorf("CouncilMesh = %q", types.CouncilMesh)
|
||||
}
|
||||
if types.CouncilGuild != "GuildCouncil" {
|
||||
t.Errorf("CouncilGuild = %q", types.CouncilGuild)
|
||||
}
|
||||
if types.CouncilStand != "StandCouncil" {
|
||||
t.Errorf("CouncilStand = %q", types.CouncilStand)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissionLockAmendableConstFalse asserts the global Mission-Lock const
|
||||
// is false (vision §19, REQ-011): the Mission Lock can NEVER be amended.
|
||||
// This is the highest-severity regression firewall for the council module.
|
||||
// The const can NEVER be set true; this test is the firewall that breaks if
|
||||
// anyone flips the const.
|
||||
func TestMissionLockAmendableConstFalse(t *testing.T) {
|
||||
if types.MissionLockAmendable != false {
|
||||
t.Fatalf("MissionLockAmendable = %v, expected false (Mission Lock non-amendable — vision §19)", types.MissionLockAmendable)
|
||||
}
|
||||
// Re-assert via a bool-typed comparison so the test fails to compile if
|
||||
// the const is ever changed to a non-bool type (defence in depth).
|
||||
var isFalse bool = types.MissionLockAmendable == false
|
||||
if !isFalse {
|
||||
t.Fatal("MissionLockAmendable must equal false")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissionLockAmendableCannotBeSetTrue asserts the const cannot be set
|
||||
// true — it is a compile-time const, not a runtime variable. The test
|
||||
// constructs an expression that would fail to compile if the const were a
|
||||
// mutable var (the const-ness is the firewall). This is the regression
|
||||
// firewall the spec mandates: "a test asserting it can never be set true".
|
||||
func TestMissionLockAmendableCannotBeSetTrue(t *testing.T) {
|
||||
// The const is declared as `const MissionLockAmendable = false`. Go
|
||||
// consts cannot be reassigned at runtime. The test below would be a
|
||||
// compile error if it tried to assign to the const:
|
||||
// types.MissionLockAmendable = true // cannot assign to const
|
||||
// So the firewall IS the compile-time const-ness. We assert the value
|
||||
// is false and the type is bool (so a future change to a string or int
|
||||
// would break the typed comparison above). The regression guard is that
|
||||
// any PR flipping the const to true breaks TestMissionLockAmendableConstFalse
|
||||
// AND any PR changing it to a var breaks the `const` declaration (Go
|
||||
// compiler rejects assignment to a var-typed const in other code paths).
|
||||
if types.MissionLockAmendable {
|
||||
t.Fatal("MissionLockAmendable must be false; the const is the firewall — flipping it to true is a Mission Lock breach")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignalKindCountLockedConst asserts SignalKindCount is exactly 4
|
||||
// (the four Freeholder signals, cross-ref v0.1 REQ-005 / vision §9.1).
|
||||
func TestSignalKindCountLockedConst(t *testing.T) {
|
||||
if types.SignalKindCount != 4 {
|
||||
t.Errorf("SignalKindCount = %d, expected 4 (REQ-005 four Freeholder signals)", types.SignalKindCount)
|
||||
}
|
||||
all := types.AllSignalKinds()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllSignalKinds() len = %d, expected 4", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllSignalKindsNames asserts the 4 signal names (Stash, Standing,
|
||||
// Vouch, Capital) cross-ref v0.1 x/standing FreeholderSignals (StashMaturity,
|
||||
// MultiDomainStanding, CommunityEndorsement, CommittedCapital).
|
||||
func TestAllSignalKindsNames(t *testing.T) {
|
||||
want := []string{"Stash", "Standing", "Vouch", "Capital"}
|
||||
all := types.AllSignalKinds()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, s := range all {
|
||||
if string(s) != want[i] {
|
||||
t.Errorf("AllSignalKinds()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate SignalKind %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestSignalKindValues asserts each named const matches its AllSignalKinds
|
||||
// entry.
|
||||
func TestSignalKindValues(t *testing.T) {
|
||||
if types.SignalStash != "Stash" {
|
||||
t.Errorf("SignalStash = %q", types.SignalStash)
|
||||
}
|
||||
if types.SignalStanding != "Standing" {
|
||||
t.Errorf("SignalStanding = %q", types.SignalStanding)
|
||||
}
|
||||
if types.SignalVouch != "Vouch" {
|
||||
t.Errorf("SignalVouch = %q", types.SignalVouch)
|
||||
}
|
||||
if types.SignalCapital != "Capital" {
|
||||
t.Errorf("SignalCapital = %q", types.SignalCapital)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTallyResultStructShape asserts TallyResult mirrors x/gov shape (A-204):
|
||||
// fields yes, no, abstain, nowithveto, total, quorum_met. The no-with-veto
|
||||
// field is kept for x/gov parity but always 0 (OY has no veto option —
|
||||
// anti-greed, vision §19). The test asserts the field names via JSON tags
|
||||
// and that NoWithVeto is zero by default.
|
||||
func TestTallyResultStructShape(t *testing.T) {
|
||||
tr := types.TallyResult{
|
||||
Yes: 10,
|
||||
No: 3,
|
||||
Abstain: 1,
|
||||
NoWithVeto: 0, // always 0 — no veto option
|
||||
Total: 14,
|
||||
QuorumMet: true,
|
||||
}
|
||||
if tr.Yes != 10 || tr.No != 3 || tr.Abstain != 1 || tr.NoWithVeto != 0 ||
|
||||
tr.Total != 14 || tr.QuorumMet != true {
|
||||
t.Error("TallyResult fields not set correctly")
|
||||
}
|
||||
// x/gov field-name parity: marshal and check JSON tags.
|
||||
bz, err := json.Marshal(tr)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
js := string(bz)
|
||||
for _, tag := range []string{`"yes"`, `"no"`, `"abstain"`, `"nowithveto"`, `"total"`, `"quorum_met"`} {
|
||||
if !strings.Contains(js, tag) {
|
||||
t.Errorf("TallyResult JSON missing tag %s (x/gov shape parity A-204)", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTallyResultNoWithVetoAlwaysZero asserts the default TallyResult has
|
||||
// NoWithVeto == 0 (the anti-greed invariant — no veto option in OY).
|
||||
func TestTallyResultNoWithVetoAlwaysZero(t *testing.T) {
|
||||
var tr types.TallyResult
|
||||
if tr.NoWithVeto != 0 {
|
||||
t.Errorf("default TallyResult.NoWithVeto = %d, expected 0 (no veto option — anti-greed)", tr.NoWithVeto)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCouncilStructFields asserts Council carries all required fields
|
||||
// including the by-ID-string refs (stand-id-ref, guild-id-ref per G-003).
|
||||
func TestCouncilStructFields(t *testing.T) {
|
||||
c := types.Council{
|
||||
CouncilID: "c1",
|
||||
Kind: types.CouncilStand,
|
||||
StandIDRef: "stand-xyz",
|
||||
GuildIDRef: "",
|
||||
Members: []types.CouncilMember{{ReachID: "reach:a", VoiceWeight: 5, JoinedAt: 100}},
|
||||
VoiceThreshold: 3,
|
||||
}
|
||||
if c.CouncilID != "c1" || c.Kind != types.CouncilStand || c.StandIDRef != "stand-xyz" ||
|
||||
c.GuildIDRef != "" || len(c.Members) != 1 || c.VoiceThreshold != 3 {
|
||||
t.Error("Council fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCouncilStructRefsAreStrings asserts stand-id-ref and guild-id-ref are
|
||||
// string-typed (G-003 by-ID-string invariant; the G-003 import invariant is
|
||||
// enforced project-wide by P1-01-02's go/parser scan, so this test only
|
||||
// asserts the field types at the struct level, not cross-module imports).
|
||||
func TestCouncilStructRefsAreStrings(t *testing.T) {
|
||||
c := types.Council{StandIDRef: "stand-abc", GuildIDRef: "guild-def"}
|
||||
if c.StandIDRef != "stand-abc" {
|
||||
t.Errorf("StandIDRef = %q", c.StandIDRef)
|
||||
}
|
||||
if c.GuildIDRef != "guild-def" {
|
||||
t.Errorf("GuildIDRef = %q", c.GuildIDRef)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCouncilMemberStructFields asserts CouncilMember uses reach-id (NOT
|
||||
// the banned financial holder term — lexicon-clean).
|
||||
func TestCouncilMemberStructFields(t *testing.T) {
|
||||
m := types.CouncilMember{ReachID: "reach:a", VoiceWeight: 7, JoinedAt: 200}
|
||||
if m.ReachID != "reach:a" || m.VoiceWeight != 7 || m.JoinedAt != 200 {
|
||||
t.Error("CouncilMember fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVoiceStructFields asserts Voice carries all required fields.
|
||||
func TestVoiceStructFields(t *testing.T) {
|
||||
v := types.Voice{
|
||||
VoiceID: "v1",
|
||||
CouncilID: "c1",
|
||||
ProposerReach: "reach:prop",
|
||||
SignalKind: types.SignalStash,
|
||||
TargetRef: "proposal:p1",
|
||||
Tally: types.TallyResult{Yes: 1, Total: 1, QuorumMet: true},
|
||||
Timestamp: 999,
|
||||
}
|
||||
if v.VoiceID != "v1" || v.CouncilID != "c1" || v.ProposerReach != "reach:prop" ||
|
||||
v.SignalKind != types.SignalStash || v.TargetRef != "proposal:p1" ||
|
||||
v.Tally.Yes != 1 || v.Tally.Total != 1 || v.Tally.QuorumMet != true || v.Timestamp != 999 {
|
||||
t.Error("Voice fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for Councils and Voices.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := types.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Councils == nil || len(gs.Councils) != 0 {
|
||||
t.Errorf("Default Councils should be non-nil empty slice; got len=%d nil=%v", len(gs.Councils), gs.Councils == nil)
|
||||
}
|
||||
if gs.Voices == nil || len(gs.Voices) != 0 {
|
||||
t.Errorf("Default Voices should be non-nil empty slice; got len=%d nil=%v", len(gs.Voices), gs.Voices == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupCouncilIDs asserts A-212: duplicate
|
||||
// council-ids are rejected.
|
||||
func TestValidateGenesisRejectsDupCouncilIDs(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{
|
||||
{CouncilID: "c1", Kind: types.CouncilMesh},
|
||||
{CouncilID: "c1", Kind: types.CouncilGuild, GuildIDRef: "g1"}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate council-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupVoiceIDs asserts A-212: duplicate voice-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupVoiceIDs(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
|
||||
Voices: []types.Voice{
|
||||
{VoiceID: "v1", CouncilID: "c1", SignalKind: types.SignalStash},
|
||||
{VoiceID: "v1", CouncilID: "c1", SignalKind: types.SignalVouch}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate voice-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyCouncilID asserts empty council-id is
|
||||
// rejected.
|
||||
func TestValidateGenesisRejectsEmptyCouncilID(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "", Kind: types.CouncilMesh}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty council-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyVoiceID asserts empty voice-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyVoiceID(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
|
||||
Voices: []types.Voice{{VoiceID: "", CouncilID: "c1", SignalKind: types.SignalStash}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty voice-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownCouncilKind asserts an unknown
|
||||
// CouncilKind is rejected (data-engineer schema validation).
|
||||
func TestValidateGenesisRejectsUnknownCouncilKind(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilKind("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown council kind")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownSignalKind asserts an unknown SignalKind
|
||||
// is rejected.
|
||||
func TestValidateGenesisRejectsUnknownSignalKind(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
|
||||
Voices: []types.Voice{{VoiceID: "v1", CouncilID: "c1", SignalKind: types.SignalKind("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown signal-kind")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := types.ValidateGenesis(json.RawMessage(`{not json`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{
|
||||
{CouncilID: "cm", Kind: types.CouncilMesh},
|
||||
{CouncilID: "cg", Kind: types.CouncilGuild, GuildIDRef: "g1"},
|
||||
{CouncilID: "cs", Kind: types.CouncilStand, StandIDRef: "s1"},
|
||||
},
|
||||
Voices: []types.Voice{
|
||||
{VoiceID: "v1", CouncilID: "cm", SignalKind: types.SignalStash},
|
||||
{VoiceID: "v2", CouncilID: "cs", SignalKind: types.SignalCapital},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsStandCouncilWithoutStandIDRef asserts a Stand
|
||||
// Council without stand-id-ref is rejected (by-ID-string ref to x/stand).
|
||||
func TestValidateGenesisRejectsStandCouncilWithoutStandIDRef(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "cs", Kind: types.CouncilStand, StandIDRef: ""}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject Stand Council without stand-id-ref")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsGuildCouncilWithoutGuildIDRef asserts a Guild
|
||||
// Council without guild-id-ref is rejected (by-ID-string ref to x/guild).
|
||||
func TestValidateGenesisRejectsGuildCouncilWithoutGuildIDRef(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "cg", Kind: types.CouncilGuild, GuildIDRef: ""}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject Guild Council without guild-id-ref")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsVoiceWithUnknownCouncil asserts referential
|
||||
// integrity: a Voice whose council-id does not reference an existing
|
||||
// Council is rejected (P3-01-03 deliverable).
|
||||
func TestValidateGenesisRejectsVoiceWithUnknownCouncil(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Councils: []types.Council{{CouncilID: "c1", Kind: types.CouncilMesh}},
|
||||
Voices: []types.Voice{{VoiceID: "v1", CouncilID: "no-such-council", SignalKind: types.SignalStash}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject Voice with unknown council-id (referential integrity)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissionLockCheckIsNoOp asserts the genesis-side MissionLockCheck helper
|
||||
// is a no-op (the const is the true firewall). It must return nil for any
|
||||
// slice of Councils.
|
||||
func TestMissionLockCheckIsNoOp(t *testing.T) {
|
||||
councils := []types.Council{
|
||||
{CouncilID: "c1", Kind: types.CouncilMesh},
|
||||
{CouncilID: "c2", Kind: types.CouncilGuild, GuildIDRef: "g1"},
|
||||
{CouncilID: "c3", Kind: types.CouncilStand, StandIDRef: "s1"},
|
||||
}
|
||||
if err := types.MissionLockCheck(councils); err != nil {
|
||||
t.Errorf("MissionLockCheck should be a no-op (const is the firewall), got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if types.ModuleName != "council" {
|
||||
t.Errorf("ModuleName = %q", types.ModuleName)
|
||||
}
|
||||
if types.StoreKey != "council" {
|
||||
t.Errorf("StoreKey = %q", types.StoreKey)
|
||||
}
|
||||
if types.RouterKey != "council" {
|
||||
t.Errorf("RouterKey = %q", types.RouterKey)
|
||||
}
|
||||
if types.QuerierRoute != "council" {
|
||||
t.Errorf("QuerierRoute = %q", types.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = types.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
|
||||
// TestLexiconNoBannedTermsInCouncilPackage scans every non-test .go file in
|
||||
// the council/types package directory for the 9 banned terms
|
||||
// (case-insensitive). Production files only — the test file references
|
||||
// banned terms via the lexicon package helpers (standard lexicon-test
|
||||
// bootstrapping pattern; no banned literals are inlined in this test file).
|
||||
func TestLexiconNoBannedTermsInCouncilPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/council/types")
|
||||
files, err := filepath.Glob(filepath.Join(pkgDir, "*.go"))
|
||||
if err != nil {
|
||||
t.Fatalf("glob: %v", err)
|
||||
}
|
||||
prodFiles := []string{}
|
||||
for _, f := range files {
|
||||
if strings.HasSuffix(f, "_test.go") {
|
||||
continue
|
||||
}
|
||||
prodFiles = append(prodFiles, f)
|
||||
}
|
||||
if len(prodFiles) == 0 {
|
||||
t.Fatal("no production .go files found in council/types")
|
||||
}
|
||||
for _, f := range prodFiles {
|
||||
bz, err := os.ReadFile(f)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", f, err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Errorf("%s: banned term %q (REQ-012 lexicon firewall)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInCouncilTestFile asserts this test file itself
|
||||
// does not contain any banned term as a literal (the firewall scans test
|
||||
// files too; the lexicon helpers must be used rather than inlining banned
|
||||
// terms). This is the self-bootstrapping check.
|
||||
func TestLexiconNoBannedTermsInCouncilTestFile(t *testing.T) {
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
bz, err := os.ReadFile(thisFile)
|
||||
if err != nil {
|
||||
t.Fatalf("read self: %v", err)
|
||||
}
|
||||
if found, ok := lexicon.FindBannedTerm(string(bz)); ok {
|
||||
t.Fatalf("council test file contains banned term %q — use lexicon helpers, not literals", found)
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory by
|
||||
// walking up from this test file (v0.2 skeleton has zero external deps).
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/council/types/types_test.go -> repoRoot = .../oy (4 dirs up)
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(file))))
|
||||
rel := strings.TrimPrefix(importPath, "github.com/oy/openyield/")
|
||||
return filepath.Join(repoRoot, rel)
|
||||
}
|
||||
Reference in New Issue
Block a user