docs(P01): complete Orgs+Window foundation phase
---ci--- project: oy phase: 1 milestone: v0.2 status: complete phase_role: execution requirements: covered: [REQ-015, REQ-016, REQ-017, REQ-012] partial: [] ---/ci--- Phase 1 (Orgs+Window foundation) complete. 3 new modules (x/window, x/stand, x/guild) + lexicon meta-test scaffolding (G-004). 143 tests total (53 v0.1 baseline + 90 new), 100% coverage on new packages. Window = fullest primitive (lifecycle Open->Active->Revoked-> Expired, rate-limit, append-only audit log). 9-type Stand enum. Guild Hand-Pass @ 0% fee. G-003 by-ID-string import invariant test green. G-004/G-009 lexicon meta-test + self-test table green. Tagged v0.1.1.
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the stand
|
||||
// module (G-008 split). ValidateGenesis in types.go composes these helpers;
|
||||
// the security-engineer's test assertions live in genesis_test.go.
|
||||
//
|
||||
// The Stand genesis schema is a membership-set: Stands (the organizational
|
||||
// forms) + Memberships (the membership edges). The two top-level invariants
|
||||
// are stand-id uniqueness and member-reach uniqueness within a stand
|
||||
// (REQ-016, A-212 upgrade from v0.1's no-op ValidateGenesis).
|
||||
|
||||
// ValidateStands asserts stand-ids are present and unique.
|
||||
func ValidateStands(stands []Stand) error {
|
||||
seen := make(map[string]bool, len(stands))
|
||||
for i, s := range stands {
|
||||
if s.StandID == "" {
|
||||
return fmt.Errorf("stand [%d]: empty stand-id", i)
|
||||
}
|
||||
if seen[s.StandID] {
|
||||
return fmt.Errorf("stand: duplicate stand-id %q", s.StandID)
|
||||
}
|
||||
seen[s.StandID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateMemberships asserts the membership-set invariant: the (stand-id,
|
||||
// reach-id) pair is unique across the membership set — i.e. a reach can be
|
||||
// a member of a stand at most once. The same reach MAY be a member of
|
||||
// different stands (uniqueness is per-stand, not global).
|
||||
func ValidateMemberships(memberships []Membership) error {
|
||||
seen := make(map[string]bool, len(memberships))
|
||||
for i, m := range memberships {
|
||||
if m.StandID == "" {
|
||||
return fmt.Errorf("membership [%d]: empty stand-id", i)
|
||||
}
|
||||
if m.ReachID == "" {
|
||||
return fmt.Errorf("membership [%d]: empty reach-id", i)
|
||||
}
|
||||
key := m.StandID + "/" + m.ReachID
|
||||
if seen[key] {
|
||||
return fmt.Errorf("membership: duplicate member-reach %q in stand %q", m.ReachID, m.StandID)
|
||||
}
|
||||
seen[key] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/x/stand/types"
|
||||
)
|
||||
|
||||
// genesis_test.go holds the security-engineer's test assertions for the
|
||||
// data-engineer's genesis.go schema (G-008 split). The locked-const,
|
||||
// enum-coverage, and lexicon assertions live in types_test.go.
|
||||
|
||||
// TestValidateStandsRejectsDup asserts ValidateStands rejects duplicate
|
||||
// stand-ids (the membership-set's top-level invariant).
|
||||
func TestValidateStandsRejectsDup(t *testing.T) {
|
||||
stands := []types.Stand{
|
||||
{StandID: "s1"},
|
||||
{StandID: "s1"},
|
||||
}
|
||||
if err := types.ValidateStands(stands); err == nil {
|
||||
t.Error("ValidateStands should reject duplicate stand-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateStandsRejectsEmpty asserts empty stand-id is rejected.
|
||||
func TestValidateStandsRejectsEmpty(t *testing.T) {
|
||||
stands := []types.Stand{{StandID: ""}}
|
||||
if err := types.ValidateStands(stands); err == nil {
|
||||
t.Error("ValidateStands should reject empty stand-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateStandsAcceptsUnique asserts a clean stand set validates.
|
||||
func TestValidateStandsAcceptsUnique(t *testing.T) {
|
||||
stands := []types.Stand{{StandID: "s1"}, {StandID: "s2"}}
|
||||
if err := types.ValidateStands(stands); err != nil {
|
||||
t.Errorf("ValidateStands should accept unique ids, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMembershipsRejectsDupWithinStand asserts the membership-set
|
||||
// invariant: (stand-id, reach-id) pair must be unique.
|
||||
func TestValidateMembershipsRejectsDupWithinStand(t *testing.T) {
|
||||
m := []types.Membership{
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
{StandID: "s1", ReachID: "reach:a"}, // dup within stand
|
||||
}
|
||||
if err := types.ValidateMemberships(m); err == nil {
|
||||
t.Error("ValidateMemberships should reject duplicate (stand-id, reach-id)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMembershipsAcceptsSameReachDifferentStands asserts the same
|
||||
// reach can join different stands (uniqueness is per-stand, not global).
|
||||
func TestValidateMembershipsAcceptsSameReachDifferentStands(t *testing.T) {
|
||||
m := []types.Membership{
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
{StandID: "s2", ReachID: "reach:a"}, // ok
|
||||
}
|
||||
if err := types.ValidateMemberships(m); err != nil {
|
||||
t.Errorf("ValidateMemberships should accept same reach in different stands, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMembershipsRejectsEmptyFields asserts empty stand-id or
|
||||
// reach-id is rejected (every membership edge must be fully identified).
|
||||
func TestValidateMembershipsRejectsEmptyFields(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
m []types.Membership
|
||||
}{
|
||||
{"empty stand-id", []types.Membership{{StandID: "", ReachID: "reach:a"}}},
|
||||
{"empty reach-id", []types.Membership{{StandID: "s1", ReachID: ""}}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := types.ValidateMemberships(tc.m); err == nil {
|
||||
t.Error("ValidateMemberships should reject empty fields")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateMembershipsEmptyOK asserts an empty membership set validates.
|
||||
func TestValidateMembershipsEmptyOK(t *testing.T) {
|
||||
if err := types.ValidateMemberships(nil); err != nil {
|
||||
t.Errorf("ValidateMemberships(nil) should be nil, got: %v", err)
|
||||
}
|
||||
if err := types.ValidateMemberships([]types.Membership{}); err != nil {
|
||||
t.Errorf("ValidateMemberships([]) should be nil, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisComposesBoth asserts ValidateGenesis composes both
|
||||
// ValidateStands and ValidateMemberships.
|
||||
func TestValidateGenesisComposesBoth(t *testing.T) {
|
||||
// clean stands but dup membership — should fail
|
||||
gs := types.GenesisState{
|
||||
Stands: []types.Stand{{StandID: "s1"}},
|
||||
Memberships: []types.Membership{
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject dup membership even with clean stands")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisClean asserts a fully clean genesis validates.
|
||||
func TestValidateGenesisClean(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Stands: []types.Stand{{StandID: "s1"}, {StandID: "s2"}},
|
||||
Memberships: []types.Membership{
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
{StandID: "s2", ReachID: "reach:a"},
|
||||
{StandID: "s1", ReachID: "reach:b"},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "stand"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// StandTypeCount is the locked count of StandType enum values (vision §11).
|
||||
// A regression firewall: adding/removing/renaming a Stand type breaks this
|
||||
// const's test.
|
||||
StandTypeCount = 9
|
||||
)
|
||||
|
||||
// StandType enumerates the nine organizational forms (vision §11, REQ-016).
|
||||
// All nine are treated uniformly in v0.2 (A-213: the Shadow Stand behavioral
|
||||
// split is deferred to v0.3 design).
|
||||
type StandType string
|
||||
|
||||
const (
|
||||
StandHousehold StandType = "Household"
|
||||
StandCrew StandType = "Crew"
|
||||
StandEntity StandType = "Entity"
|
||||
StandCoop StandType = "Co-op"
|
||||
StandCircle StandType = "Circle"
|
||||
StandTrust StandType = "Trust"
|
||||
StandFoundation StandType = "Foundation"
|
||||
StandConfederation StandType = "Confederation"
|
||||
StandShadow StandType = "Shadow"
|
||||
)
|
||||
|
||||
// AllStandTypes returns all nine StandType values in vision §11 order.
|
||||
// Locked-const test asserts exactly 9 entries with these names (REQ-016).
|
||||
func AllStandTypes() []StandType {
|
||||
return []StandType{
|
||||
StandHousehold,
|
||||
StandCrew,
|
||||
StandEntity,
|
||||
StandCoop,
|
||||
StandCircle,
|
||||
StandTrust,
|
||||
StandFoundation,
|
||||
StandConfederation,
|
||||
StandShadow,
|
||||
}
|
||||
}
|
||||
|
||||
// Stand is a governed group holding a Vault (vision §11, REQ-016).
|
||||
// Modeled on Cosmos SDK x/group (a group of members with a decision policy
|
||||
// governing a Vault). admin-reach references a Reach ID (by-ID-string, G-003);
|
||||
// vault-id references x/vault by ID string (no struct import).
|
||||
type Stand struct {
|
||||
StandID string `json:"stand_id" yaml:"stand_id"`
|
||||
Type StandType `json:"type" yaml:"type"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
VaultID string `json:"vault_id" yaml:"vault_id"`
|
||||
AdminReach string `json:"admin_reach" yaml:"admin_reach"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
MemberCount uint32 `json:"member_count" yaml:"member_count"`
|
||||
}
|
||||
|
||||
// StandRole enumerates member roles within a Stand.
|
||||
type StandRole string
|
||||
|
||||
const (
|
||||
RoleMember StandRole = "Member"
|
||||
RoleAdmin StandRole = "Admin"
|
||||
RoleObserver StandRole = "Observer"
|
||||
)
|
||||
|
||||
// Membership is a Stand membership edge (REQ-016). stand-id references
|
||||
// x/stand by ID string; reach-id references x/identity Reach by string
|
||||
// (G-003 by-ID-string invariant).
|
||||
type Membership struct {
|
||||
StandID string `json:"stand_id" yaml:"stand_id"`
|
||||
ReachID string `json:"reach_id" yaml:"reach_id"`
|
||||
JoinedAt int64 `json:"joined_at" yaml:"joined_at"`
|
||||
Role StandRole `json:"role" yaml:"role"`
|
||||
}
|
||||
|
||||
// StandPolicy is a stub for a Stand's decision policy (A-205).
|
||||
// Mirrors x/group DecisionPolicy: threshold (N-of-M) OR weighted (sum of
|
||||
// weights >= threshold). The skeleton does not enforce the policy; v0.3
|
||||
// wires the live aggregation. Exactly one of Threshold/Weighted should be
|
||||
// non-zero in the live object; the skeleton keeps both as fields for
|
||||
// future-wiring symmetry with x/group.
|
||||
type StandPolicy struct {
|
||||
Threshold uint32 `json:"threshold" yaml:"threshold"`
|
||||
Weighted bool `json:"weighted" yaml:"weighted"`
|
||||
}
|
||||
|
||||
// Params for the stand module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the stand module genesis state (REQ-016).
|
||||
// Stands + Memberships are the two top-level sets; ValidateGenesis enforces
|
||||
// stand-id uniqueness and member-reach uniqueness within a stand.
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Stands []Stand `json:"stands" yaml:"stands"`
|
||||
Memberships []Membership `json:"memberships" yaml:"memberships"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Stands: []Stand{},
|
||||
Memberships: []Membership{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate stand-ids and duplicate (stand-id, reach-id)
|
||||
// membership pairs. The membership-set invariant is "a reach can be a
|
||||
// member of a stand at most once; the same reach may join different stands".
|
||||
// Validation is delegated 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("stand: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateStands(gs.Stands); err != nil {
|
||||
return fmt.Errorf("stand: %w", err)
|
||||
}
|
||||
if err := ValidateMemberships(gs.Memberships); err != nil {
|
||||
return fmt.Errorf("stand: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
"github.com/oy/openyield/x/stand/types"
|
||||
)
|
||||
|
||||
// TestStandTypeCountLockedConst asserts AllStandTypes() returns exactly 9
|
||||
// (vision §11). A regression firewall: adding/removing/renaming a Stand type
|
||||
// breaks this test (REQ-016).
|
||||
func TestStandTypeCountLockedConst(t *testing.T) {
|
||||
if types.StandTypeCount != 9 {
|
||||
t.Errorf("StandTypeCount = %d, expected 9 (vision §11 LOCKED)", types.StandTypeCount)
|
||||
}
|
||||
all := types.AllStandTypes()
|
||||
if len(all) != 9 {
|
||||
t.Errorf("AllStandTypes() len = %d, expected 9", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllStandTypesNames asserts the 9 vision §11 names in order with no
|
||||
// extras, no dups, no renames.
|
||||
func TestAllStandTypesNames(t *testing.T) {
|
||||
want := []string{
|
||||
"Household", "Crew", "Entity", "Co-op", "Circle",
|
||||
"Trust", "Foundation", "Confederation", "Shadow",
|
||||
}
|
||||
all := types.AllStandTypes()
|
||||
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("AllStandTypes()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate StandType %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestStandTypeValues asserts each named const matches its AllStandTypes entry.
|
||||
func TestStandTypeValues(t *testing.T) {
|
||||
if types.StandHousehold != "Household" {
|
||||
t.Errorf("StandHousehold = %q", types.StandHousehold)
|
||||
}
|
||||
if types.StandCrew != "Crew" {
|
||||
t.Errorf("StandCrew = %q", types.StandCrew)
|
||||
}
|
||||
if types.StandEntity != "Entity" {
|
||||
t.Errorf("StandEntity = %q", types.StandEntity)
|
||||
}
|
||||
if types.StandCoop != "Co-op" {
|
||||
t.Errorf("StandCoop = %q", types.StandCoop)
|
||||
}
|
||||
if types.StandCircle != "Circle" {
|
||||
t.Errorf("StandCircle = %q", types.StandCircle)
|
||||
}
|
||||
if types.StandTrust != "Trust" {
|
||||
t.Errorf("StandTrust = %q", types.StandTrust)
|
||||
}
|
||||
if types.StandFoundation != "Foundation" {
|
||||
t.Errorf("StandFoundation = %q", types.StandFoundation)
|
||||
}
|
||||
if types.StandConfederation != "Confederation" {
|
||||
t.Errorf("StandConfederation = %q", types.StandConfederation)
|
||||
}
|
||||
if types.StandShadow != "Shadow" {
|
||||
t.Errorf("StandShadow = %q", types.StandShadow)
|
||||
}
|
||||
}
|
||||
|
||||
// TestStandRoleEnumCoverage asserts the three StandRole values.
|
||||
func TestStandRoleEnumCoverage(t *testing.T) {
|
||||
roles := []types.StandRole{types.RoleMember, types.RoleAdmin, types.RoleObserver}
|
||||
if len(roles) != 3 {
|
||||
t.Errorf("expected 3 StandRole consts, got %d", len(roles))
|
||||
}
|
||||
seen := map[types.StandRole]bool{}
|
||||
for _, r := range roles {
|
||||
if r == "" {
|
||||
t.Error("empty StandRole")
|
||||
}
|
||||
if seen[r] {
|
||||
t.Errorf("duplicate StandRole %q", r)
|
||||
}
|
||||
seen[r] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestStandStructFields asserts Stand carries all required fields.
|
||||
func TestStandStructFields(t *testing.T) {
|
||||
s := types.Stand{
|
||||
StandID: "s1",
|
||||
Type: types.StandHousehold,
|
||||
Name: "Household A",
|
||||
VaultID: "v1",
|
||||
AdminReach: "reach:admin",
|
||||
CreatedAt: 100,
|
||||
MemberCount: 3,
|
||||
}
|
||||
if s.StandID != "s1" || s.Type != types.StandHousehold || s.Name != "Household A" ||
|
||||
s.VaultID != "v1" || s.AdminReach != "reach:admin" || s.CreatedAt != 100 ||
|
||||
s.MemberCount != 3 {
|
||||
t.Error("Stand fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMembershipStructFields asserts Membership carries all required fields.
|
||||
func TestMembershipStructFields(t *testing.T) {
|
||||
m := types.Membership{
|
||||
StandID: "s1",
|
||||
ReachID: "reach:member",
|
||||
JoinedAt: 200,
|
||||
Role: types.RoleMember,
|
||||
}
|
||||
if m.StandID != "s1" || m.ReachID != "reach:member" || m.JoinedAt != 200 ||
|
||||
m.Role != types.RoleMember {
|
||||
t.Error("Membership fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStandPolicyStub asserts StandPolicy carries threshold + weighted fields
|
||||
// (A-205 mirrors x/group DecisionPolicy).
|
||||
func TestStandPolicyStub(t *testing.T) {
|
||||
p := types.StandPolicy{Threshold: 5, Weighted: false}
|
||||
if p.Threshold != 5 || p.Weighted != false {
|
||||
t.Error("StandPolicy fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for Stands and Memberships.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := types.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Stands == nil || len(gs.Stands) != 0 {
|
||||
t.Errorf("Default Stands should be non-nil empty slice; got len=%d nil=%v", len(gs.Stands), gs.Stands == nil)
|
||||
}
|
||||
if gs.Memberships == nil || len(gs.Memberships) != 0 {
|
||||
t.Errorf("Default Memberships should be non-nil empty slice; got len=%d nil=%v", len(gs.Memberships), gs.Memberships == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupStandIDs asserts A-212: duplicate stand-ids
|
||||
// are rejected.
|
||||
func TestValidateGenesisRejectsDupStandIDs(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Stands: []types.Stand{
|
||||
{StandID: "s1"},
|
||||
{StandID: "s1"}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate stand-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupMemberReach asserts A-212: duplicate
|
||||
// (stand-id, reach-id) membership pairs are rejected.
|
||||
func TestValidateGenesisRejectsDupMemberReach(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Memberships: []types.Membership{
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
{StandID: "s1", ReachID: "reach:a"}, // dup within same stand
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate member-reach within a stand")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsSameReachInDifferentStands asserts the same
|
||||
// reach can be a member of two different stands (uniqueness is per-stand).
|
||||
func TestValidateGenesisAcceptsSameReachInDifferentStands(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Memberships: []types.Membership{
|
||||
{StandID: "s1", ReachID: "reach:a"},
|
||||
{StandID: "s2", ReachID: "reach:a"}, // ok — different stand
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept same reach in different stands, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyStandID asserts empty stand-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyStandID(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Stands: []types.Stand{{StandID: ""}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty stand-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := types.ValidateGenesis(json.RawMessage(`{bad`)); err == nil {
|
||||
t.Error("ValidateGenesis should reject malformed JSON")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisAcceptsClean asserts a clean genesis validates.
|
||||
func TestValidateGenesisAcceptsClean(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Stands: []types.Stand{{StandID: "s1"}, {StandID: "s2"}},
|
||||
Memberships: []types.Membership{{StandID: "s1", ReachID: "reach:a"}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if types.ModuleName != "stand" {
|
||||
t.Errorf("ModuleName = %q", types.ModuleName)
|
||||
}
|
||||
if types.StoreKey != "stand" {
|
||||
t.Errorf("StoreKey = %q", types.StoreKey)
|
||||
}
|
||||
if types.RouterKey != "stand" {
|
||||
t.Errorf("RouterKey = %q", types.RouterKey)
|
||||
}
|
||||
if types.QuerierRoute != "stand" {
|
||||
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) -------------------------------------------------
|
||||
|
||||
// TestLexiconNoBannedTermsInStandPackage scans every non-test .go file in
|
||||
// the stand/types package directory for the 9 banned terms (case-insensitive).
|
||||
// Production files only — the test file contains the banned terms as the list
|
||||
// of things to forbid (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInStandPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/stand/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 stand/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// packageDir resolves a Go import path to its filesystem directory.
|
||||
func packageDir(t *testing.T, importPath string) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
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