docs(P02): complete Pacts+Partners phase

---ci---
project: oy
phase: 2
milestone: v0.2
status: complete
phase_role: execution
requirements:
  covered: [REQ-020, REQ-018]
  partial: []
---/ci---

Phase 2 (Pacts+Partners) complete. 2 new modules: x/pact (6-PactType enum + Mission Lock,
G-005 one module not six), x/partner (4-tier Partner Spectrum). 207 tests total (143 prev
+ 64 new). Coverage: x/pact 95.9%, x/partner 100%. Lexicon + G-003 invariants green.
Tagged v0.1.2.
This commit is contained in:
2026-08-17 21:23:29 +00:00
parent 93a8a3b311
commit 0fefd88668
7 changed files with 1511 additions and 2 deletions
+195
View File
@@ -0,0 +1,195 @@
package types
import (
"encoding/json"
"fmt"
"sync"
)
const (
ModuleName = "partner"
StoreKey = ModuleName
RouterKey = ModuleName
QuerierRoute = ModuleName
// PartnerTierCount is the locked count of PartnerTier enum values
// (vision §13, D-026). A regression firewall: adding/removing/renaming a
// tier breaks this const's test (REQ-018).
PartnerTierCount = 4
)
// PartnerTier enumerates the four partner tiers (vision §13, REQ-018, D-026).
// Op processes Pass-Acts; MasterOp is a senior Op; Pier is a credential /
// identity provider (e-Residency/biometrics); Anchor is institutional.
// "Op" is used (not "operator" — the latter implies a banned financial term
// per RESEARCH §1.6; "Op" is vision-§13 lexicon-clean).
type PartnerTier string
const (
TierOp PartnerTier = "Op" // processes Pass-Acts
TierMasterOp PartnerTier = "MasterOp" // senior Op
TierPier PartnerTier = "Pier" // credential / identity provider
TierAnchor PartnerTier = "Anchor" // institutional
)
// AllPartnerTiers returns all four PartnerTier values in vision §13 order.
// Locked-const test asserts exactly 4 entries with these names (REQ-018).
func AllPartnerTiers() []PartnerTier {
return []PartnerTier{
TierOp,
TierMasterOp,
TierPier,
TierAnchor,
}
}
// PartnerStatus enumerates the lifecycle states of a Partner (REQ-018).
type PartnerStatus string
const (
StatusPending PartnerStatus = "Pending" // partner registered, not yet active
StatusActive PartnerStatus = "Active" // partner is live
StatusSuspended PartnerStatus = "Suspended" // partner temporarily halted
StatusRevoked PartnerStatus = "Revoked" // partner permanently revoked
)
// PartnerStatusCount is the locked count of PartnerStatus enum values.
const PartnerStatusCount = 4
// CredentialType enumerates the kinds of credentials a Pier can reference
// (REQ-018). The ref-uri is opaque; Pier credential routing is deferred per
// Q5 (v0.3 will wire the live routing). The skeleton defines the type enum
// so genesis / registry entries carry a typed credential kind.
type CredentialType string
const (
CredentialEresidency CredentialType = "Eresidency" // e-Residency-style identity
CredentialBiometric CredentialType = "Biometric" // biometric identity
CredentialVouch CredentialType = "Vouch" // vouch-based attestation
CredentialCustom CredentialType = "Custom" // opaque custom credential
)
// CredentialRef references an external credential provider (REQ-018).
// provider-id references a Partner (typically a Pier) by ID string
// (G-003 by-ID-string invariant). ref-uri is an opaque URI; Pier credential
// routing is deferred per Q5, so the skeleton keeps the ref opaque.
type CredentialRef struct {
ProviderID string `json:"provider_id" yaml:"provider_id"`
CredentialType CredentialType `json:"credential_type" yaml:"credential_type"`
RefURI string `json:"ref_uri" yaml:"ref_uri"`
}
// Partner is a registered actor on the Partner Spectrum (vision §13, REQ-018).
// reach-id references x/identity Reach by string (G-003 by-ID-string
// invariant). credential-ref references a credential provider (typically a
// Pier) by ID string. region is a free-form locale tag.
type Partner struct {
PartnerID string `json:"partner_id" yaml:"partner_id"`
Tier PartnerTier `json:"tier" yaml:"tier"`
Name string `json:"name" yaml:"name"`
ReachID string `json:"reach_id" yaml:"reach_id"`
Region string `json:"region" yaml:"region"`
CredentialRef CredentialRef `json:"credential_ref" yaml:"credential_ref"`
Status PartnerStatus `json:"status" yaml:"status"`
}
// Keeper is a registry keeper stub for Partners (REQ-018). The skeleton
// provides in-memory add/get/list/by-tier operations; v0.3 wires the live
// keeper backed by the store. It is safe for concurrent use (the live keeper
// will use the SDK store, which is single-threaded per-block; the stub uses
// a mutex so the skeleton's tests can exercise concurrent paths).
type Keeper struct {
mu sync.Mutex
partners map[string]Partner
}
// NewKeeper returns an empty registry keeper stub.
func NewKeeper() *Keeper {
return &Keeper{partners: make(map[string]Partner)}
}
// AddPartner registers a Partner by ID. Returns an error if the ID is empty
// or already registered.
func (k *Keeper) AddPartner(p Partner) error {
if p.PartnerID == "" {
return fmt.Errorf("partner: empty partner-id")
}
k.mu.Lock()
defer k.mu.Unlock()
if _, exists := k.partners[p.PartnerID]; exists {
return fmt.Errorf("partner: duplicate partner-id %q", p.PartnerID)
}
k.partners[p.PartnerID] = p
return nil
}
// GetPartner returns a Partner by ID and true, or zero-value and false.
func (k *Keeper) GetPartner(id string) (Partner, bool) {
k.mu.Lock()
defer k.mu.Unlock()
p, ok := k.partners[id]
return p, ok
}
// ListPartners returns all registered Partners (unordered).
func (k *Keeper) ListPartners() []Partner {
k.mu.Lock()
defer k.mu.Unlock()
out := make([]Partner, 0, len(k.partners))
for _, p := range k.partners {
out = append(out, p)
}
return out
}
// ListByTier returns all registered Partners matching the given tier.
func (k *Keeper) ListByTier(tier PartnerTier) []Partner {
k.mu.Lock()
defer k.mu.Unlock()
out := []Partner{}
for _, p := range k.partners {
if p.Tier == tier {
out = append(out, p)
}
}
return out
}
// Params for the partner module (skeleton — no tunables in v0.2).
type Params struct{}
func DefaultParams() Params { return Params{} }
// GenesisState defines the partner module genesis state (REQ-018).
// Partners is the top-level set; ValidateGenesis enforces partner-id uniqueness.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
Partners []Partner `json:"partners" yaml:"partners"`
}
func DefaultGenesisState() *GenesisState {
return &GenesisState{
Params: DefaultParams(),
Partners: []Partner{},
}
}
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
// no-op): rejects duplicate partner-ids.
func ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
if err := json.Unmarshal(bz, &gs); err != nil {
return fmt.Errorf("partner: invalid genesis: %w", err)
}
seen := make(map[string]bool, len(gs.Partners))
for _, p := range gs.Partners {
if p.PartnerID == "" {
return fmt.Errorf("partner: empty partner-id")
}
if seen[p.PartnerID] {
return fmt.Errorf("partner: duplicate partner-id %q", p.PartnerID)
}
seen[p.PartnerID] = true
}
return nil
}
+426
View File
@@ -0,0 +1,426 @@
package types_test
import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"github.com/oy/openyield/lexicon"
"github.com/oy/openyield/x/partner/types"
)
// TestPartnerTierCountLockedConst asserts PartnerTierCount is exactly 4
// and AllPartnerTiers() returns exactly 4 (vision §13, REQ-018, D-026). A
// regression firewall: adding/removing/renaming a tier breaks this test.
func TestPartnerTierCountLockedConst(t *testing.T) {
if types.PartnerTierCount != 4 {
t.Errorf("PartnerTierCount = %d, expected 4 (vision §13 LOCKED)", types.PartnerTierCount)
}
all := types.AllPartnerTiers()
if len(all) != 4 {
t.Errorf("AllPartnerTiers() len = %d, expected 4", len(all))
}
}
// TestAllPartnerTiersNames asserts the 4 vision §13 names in order with no
// extras, no dups, no renames. "Op" (not "operator") per vision §13 — the
// latter implies a banned financial term per RESEARCH §1.6; "Op" is
// lexicon-clean.
func TestAllPartnerTiersNames(t *testing.T) {
want := []string{"Op", "MasterOp", "Pier", "Anchor"}
all := types.AllPartnerTiers()
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("AllPartnerTiers()[%d] = %q, want %q", i, s, want[i])
}
if seen[string(s)] {
t.Errorf("duplicate PartnerTier %q", s)
}
seen[string(s)] = true
}
}
// TestPartnerTierValues asserts each named const matches its AllPartnerTiers
// entry.
func TestPartnerTierValues(t *testing.T) {
if types.TierOp != "Op" {
t.Errorf("TierOp = %q", types.TierOp)
}
if types.TierMasterOp != "MasterOp" {
t.Errorf("TierMasterOp = %q", types.TierMasterOp)
}
if types.TierPier != "Pier" {
t.Errorf("TierPier = %q", types.TierPier)
}
if types.TierAnchor != "Anchor" {
t.Errorf("TierAnchor = %q", types.TierAnchor)
}
}
// TestPartnerStatusCountLockedConst asserts PartnerStatusCount is exactly 4.
func TestPartnerStatusCountLockedConst(t *testing.T) {
if types.PartnerStatusCount != 4 {
t.Errorf("PartnerStatusCount = %d, expected 4", types.PartnerStatusCount)
}
}
// TestPartnerStatusEnumCoverage asserts all four PartnerStatus values are
// distinct and non-empty (REQ-018 lifecycle: Pending, Active, Suspended, Revoked).
func TestPartnerStatusEnumCoverage(t *testing.T) {
statuses := []types.PartnerStatus{
types.StatusPending, types.StatusActive,
types.StatusSuspended, types.StatusRevoked,
}
if len(statuses) != 4 {
t.Errorf("expected 4 PartnerStatus consts, got %d", len(statuses))
}
seen := map[types.PartnerStatus]bool{}
for _, s := range statuses {
if s == "" {
t.Error("empty PartnerStatus")
}
if seen[s] {
t.Errorf("duplicate PartnerStatus %q", s)
}
seen[s] = true
}
}
// TestCredentialTypeEnumCoverage asserts the CredentialType values are
// distinct and non-empty (Pier credential routing deferred per Q5; the
// enum is the typed kind for genesis / registry entries).
func TestCredentialTypeEnumCoverage(t *testing.T) {
cts := []types.CredentialType{
types.CredentialEresidency, types.CredentialBiometric,
types.CredentialVouch, types.CredentialCustom,
}
if len(cts) != 4 {
t.Errorf("expected 4 CredentialType consts, got %d", len(cts))
}
seen := map[types.CredentialType]bool{}
for _, c := range cts {
if c == "" {
t.Error("empty CredentialType")
}
if seen[c] {
t.Errorf("duplicate CredentialType %q", c)
}
seen[c] = true
}
}
// TestCredentialRefStruct asserts CredentialRef carries all required fields
// (provider-id, credential-type, ref-uri — opaque URI).
func TestCredentialRefStruct(t *testing.T) {
c := types.CredentialRef{
ProviderID: "pier-1",
CredentialType: types.CredentialEresidency,
RefURI: "oy:cred:pier-1/eresidency/abc123",
}
if c.ProviderID != "pier-1" || c.CredentialType != types.CredentialEresidency ||
c.RefURI != "oy:cred:pier-1/eresidency/abc123" {
t.Error("CredentialRef fields not set correctly")
}
}
// TestPartnerStructFields asserts Partner carries all required fields
// including the by-ID-string reach-id (G-003).
func TestPartnerStructFields(t *testing.T) {
p := types.Partner{
PartnerID: "pt1",
Tier: types.TierPier,
Name: "Pier One",
ReachID: "reach:pier-1",
Region: "EU",
CredentialRef: types.CredentialRef{
ProviderID: "pier-1",
CredentialType: types.CredentialBiometric,
RefURI: "oy:cred:bio/x",
},
Status: types.StatusActive,
}
if p.PartnerID != "pt1" || p.Tier != types.TierPier || p.Name != "Pier One" ||
p.ReachID != "reach:pier-1" || p.Region != "EU" ||
p.CredentialRef.ProviderID != "pier-1" || p.Status != types.StatusActive {
t.Error("Partner fields not set correctly")
}
}
// --- Registry keeper stub tests (REQ-018) ---------------------------------------
// TestKeeperAddGetRoundTrip asserts AddPartner + GetPartner round-trips a
// Partner by ID.
func TestKeeperAddGetRoundTrip(t *testing.T) {
k := types.NewKeeper()
p := types.Partner{
PartnerID: "pt1",
Tier: types.TierOp,
Name: "Op One",
ReachID: "reach:op-1",
Status: types.StatusActive,
}
if err := k.AddPartner(p); err != nil {
t.Fatalf("AddPartner: %v", err)
}
got, ok := k.GetPartner("pt1")
if !ok {
t.Fatal("GetPartner: not found")
}
if got.PartnerID != "pt1" || got.Tier != types.TierOp {
t.Errorf("GetPartner returned wrong Partner: %+v", got)
}
}
// TestKeeperAddPartnerRejectsEmptyID asserts AddPartner rejects an empty id.
func TestKeeperAddPartnerRejectsEmptyID(t *testing.T) {
k := types.NewKeeper()
if err := k.AddPartner(types.Partner{PartnerID: ""}); err == nil {
t.Error("AddPartner should reject empty partner-id")
}
}
// TestKeeperAddPartnerRejectsDup asserts AddPartner rejects a duplicate id.
func TestKeeperAddPartnerRejectsDup(t *testing.T) {
k := types.NewKeeper()
p := types.Partner{PartnerID: "pt1", Tier: types.TierOp}
if err := k.AddPartner(p); err != nil {
t.Fatalf("first AddPartner: %v", err)
}
if err := k.AddPartner(p); err == nil {
t.Error("AddPartner should reject duplicate partner-id")
}
}
// TestKeeperGetPartnerMissing asserts GetPartner returns false for an
// unregistered id.
func TestKeeperGetPartnerMissing(t *testing.T) {
k := types.NewKeeper()
if _, ok := k.GetPartner("nope"); ok {
t.Error("GetPartner should return false for unregistered id")
}
}
// TestKeeperListPartners asserts ListPartners returns all registered Partners.
func TestKeeperListPartners(t *testing.T) {
k := types.NewKeeper()
_ = k.AddPartner(types.Partner{PartnerID: "a", Tier: types.TierOp})
_ = k.AddPartner(types.Partner{PartnerID: "b", Tier: types.TierAnchor})
list := k.ListPartners()
if len(list) != 2 {
t.Errorf("ListPartners len = %d, want 2", len(list))
}
}
// TestKeeperListPartnersEmpty asserts ListPartners on an empty keeper returns
// a non-nil empty slice (or a usable slice).
func TestKeeperListPartnersEmpty(t *testing.T) {
k := types.NewKeeper()
list := k.ListPartners()
if list == nil {
t.Fatal("ListPartners returned nil")
}
if len(list) != 0 {
t.Errorf("ListPartners len = %d, want 0", len(list))
}
}
// TestKeeperListByTier asserts ListByTier returns only Partners matching the
// given tier (REQ-018 round-trip).
func TestKeeperListByTier(t *testing.T) {
k := types.NewKeeper()
_ = k.AddPartner(types.Partner{PartnerID: "op1", Tier: types.TierOp})
_ = k.AddPartner(types.Partner{PartnerID: "op2", Tier: types.TierOp})
_ = k.AddPartner(types.Partner{PartnerID: "mop1", Tier: types.TierMasterOp})
_ = k.AddPartner(types.Partner{PartnerID: "pier1", Tier: types.TierPier})
_ = k.AddPartner(types.Partner{PartnerID: "anc1", Tier: types.TierAnchor})
tt := []struct {
tier types.PartnerTier
wantN int
}{
{types.TierOp, 2},
{types.TierMasterOp, 1},
{types.TierPier, 1},
{types.TierAnchor, 1},
}
for _, tc := range tt {
t.Run(string(tc.tier), func(t *testing.T) {
got := k.ListByTier(tc.tier)
if len(got) != tc.wantN {
t.Errorf("ListByTier(%q) len = %d, want %d", tc.tier, len(got), tc.wantN)
}
for _, p := range got {
if p.Tier != tc.tier {
t.Errorf("ListByTier(%q) returned Partner with tier %q", tc.tier, p.Tier)
}
}
})
}
}
// TestKeeperListByTierEmpty asserts ListByTier returns an empty (non-nil)
// slice when no Partners match.
func TestKeeperListByTierEmpty(t *testing.T) {
k := types.NewKeeper()
got := k.ListByTier(types.TierAnchor)
if got == nil {
t.Fatal("ListByTier returned nil")
}
if len(got) != 0 {
t.Errorf("ListByTier len = %d, want 0", len(got))
}
}
// --- Genesis tests (REQ-018, A-212) ----------------------------------------------
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns a non-nil
// empty slice for Partners.
func TestDefaultGenesisStateEmpty(t *testing.T) {
gs := types.DefaultGenesisState()
if gs == nil {
t.Fatal("DefaultGenesisState returned nil")
}
if gs.Partners == nil || len(gs.Partners) != 0 {
t.Errorf("Default Partners should be non-nil empty slice; got len=%d nil=%v", len(gs.Partners), gs.Partners == nil)
}
}
// TestValidateGenesisRejectsDupPartnerIDs asserts A-212: duplicate partner-ids
// are rejected.
func TestValidateGenesisRejectsDupPartnerIDs(t *testing.T) {
gs := types.GenesisState{
Partners: []types.Partner{
{PartnerID: "pt1", Tier: types.TierOp},
{PartnerID: "pt1", Tier: types.TierAnchor}, // dup
},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject duplicate partner-ids")
}
}
// TestValidateGenesisRejectsEmptyPartnerID asserts empty partner-id is rejected.
func TestValidateGenesisRejectsEmptyPartnerID(t *testing.T) {
gs := types.GenesisState{
Partners: []types.Partner{{PartnerID: "", Tier: types.TierOp}},
}
bz, _ := json.Marshal(gs)
if err := types.ValidateGenesis(bz); err == nil {
t.Error("ValidateGenesis should reject empty partner-id")
}
}
// 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{
Partners: []types.Partner{
{PartnerID: "pt1", Tier: types.TierOp, Status: types.StatusActive},
{PartnerID: "pt2", Tier: types.TierPier, Status: types.StatusPending},
},
}
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 != "partner" {
t.Errorf("ModuleName = %q", types.ModuleName)
}
if types.StoreKey != "partner" {
t.Errorf("StoreKey = %q", types.StoreKey)
}
if types.RouterKey != "partner" {
t.Errorf("RouterKey = %q", types.RouterKey)
}
if types.QuerierRoute != "partner" {
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) -------------------------------------------------
// TestLexiconNoBannedTermsInPartnerPackage scans every non-test .go file in
// the partner/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 TestLexiconNoBannedTermsInPartnerPackage(t *testing.T) {
pkgDir := packageDir(t, "github.com/oy/openyield/x/partner/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 partner/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)
}
}
}
// TestLexiconNoBannedTermsInPartnerTestFile 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).
func TestLexiconNoBannedTermsInPartnerTestFile(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("partner 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/partner/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)
}