Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fefd88668 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"phase": 1,
|
||||
"phase": 2,
|
||||
"stage": "execute",
|
||||
"milestone": "v0.2",
|
||||
"milestone_type": "feature",
|
||||
@@ -7,5 +7,5 @@
|
||||
"phase_role": "execution",
|
||||
"project": "oy",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-17T21:15:00Z"
|
||||
"updated_at": "2026-08-17T21:30:00Z"
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
# P2 Ship Verification — v0.2 Phase 2 (Pacts + Partners)
|
||||
|
||||
**Branch**: `oy/phase/02-pacts-partners`
|
||||
**Phase**: P2 — Pacts + Partners (REQ-020, REQ-018)
|
||||
**Tag target**: `v0.1.2` (orchestrator ships; executor does NOT merge/tag/push)
|
||||
**Date**: 2026-08-17
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 2 ships two new Mesh modules — `x/pact` (6-Pact enum with Mission-Lock
|
||||
invariant) and `x/partner` (4-tier Partner Spectrum with registry keeper stub)
|
||||
— both consuming Window + Stand refs from P1 by-ID-string (G-003). All five
|
||||
P2 tasks executed atomically with per-task commits. Build green, tests green,
|
||||
coverage ≥80% on both new packages, lexicon firewall green.
|
||||
|
||||
## Must-Haves (from PLANS.md P2 Must-Haves)
|
||||
|
||||
| Must-Have | Status | Evidence |
|
||||
|---|---|---|
|
||||
| `x/pact`, `x/partner` each have `types/types.go` + `types/types_test.go` | ✅ | 4 files created (pact: types.go+types_test.go+genesis.go; partner: types.go+types_test.go) |
|
||||
| `go build ./...` and `go test ./...` green | ✅ | `go build ./...` → build OK; `go test ./...` → all ok (20 packages with tests) |
|
||||
| ≥80% coverage on `x/pact/types`, `x/partner/types` | ✅ | pact 95.9%, partner 100.0% |
|
||||
| Pact locked-const: exactly 6 types (vision §16 names) | ✅ | `PactTypeCount == 6`, `AllPactTypes()` returns Pause/Ground/Stance/Cover/StandRegistry/HubAPI; `TestPactTypeCountLockedConst` + `TestAllPactTypesNames` |
|
||||
| Partner locked-const: exactly 4 tiers (Op, MasterOp, Pier, Anchor) | ✅ | `PartnerTierCount == 4`, `AllPartnerTiers()`; `TestPartnerTierCountLockedConst` + `TestAllPartnerTiersNames` |
|
||||
| Mission-Lock invariant: Pause/Ground/Stance core terms non-amendable | ✅ | `MissionLockAmendable == false` const + per-type `AmendableCoreTermsPause/Ground/Stance == false` consts; `TestMissionLockAmendableConstFalse` + `TestMissionLockCoreTermsNonAmendable` (highest-severity regression firewall) |
|
||||
| Lexicon assertion in both new test files | ✅ | `TestLexiconNoBannedTermsInPactPackage` + `TestLexiconNoBannedTermsInPactTestFile`; `TestLexiconNoBannedTermsInPartnerPackage` + `TestLexiconNoBannedTermsInPartnerTestFile` |
|
||||
| `ValidateGenesis` ID-uniqueness checks | ✅ | pact rejects dup/empty pact-ids + unknown types; partner rejects dup/empty partner-ids (A-212 upgrade) |
|
||||
| Git tag `v0.1.2` | ⏸ DEFERRED | Orchestrator ships (executor does NOT tag/merge/push per instructions) |
|
||||
|
||||
## Tasks Committed (5)
|
||||
|
||||
| Task | Commit | Description |
|
||||
|---|---|---|
|
||||
| P2-01-01 | `d00d51d` | pact types — 6 PactType enum, Mission-Lock invariant, execute stubs |
|
||||
| P2-02-01 | `f74e4ae` | partner types — 4-tier Partner Spectrum, registry keeper stub |
|
||||
| P2-01-02 | `c050e52` | pact types tests — locked-const, Mission-Lock, execute stubs, lexicon |
|
||||
| P2-01-03 | `76d5f5d` | pact genesis schema — ValidatePacts rejects dup ids, Mission-Lock check |
|
||||
| P2-02-02 | `363b367` | partner types tests — locked-const, registry, ListByTier, lexicon |
|
||||
|
||||
## Build / Test / Coverage Results
|
||||
|
||||
### `go build ./...`
|
||||
```
|
||||
build OK
|
||||
```
|
||||
|
||||
### `go test ./... -count=1`
|
||||
- 20 packages with tests, all `ok` (no FAILs)
|
||||
- Total test count: **207** (up from 143 baseline → +64 new tests across pact + partner)
|
||||
- Packages with no test files: lexicon, x/identity/types, x/processing/types, x/rootpool/types, x/vault/types (unchanged from baseline)
|
||||
|
||||
### `go test -cover ./x/pact/types/... ./x/partner/types/...`
|
||||
| Package | Coverage | Target | Pass |
|
||||
|---|---|---|---|
|
||||
| `x/pact/types` | **95.9%** | ≥80% | ✅ |
|
||||
| `x/partner/types` | **100.0%** | ≥80% | ✅ |
|
||||
|
||||
### Lexicon meta-test (`go test -run TestLexiconMeta .`)
|
||||
- `TestLexiconMetaNoBannedTermsInX` — PASS (scans all `x/**/*.go` production + test for 10 banned terms)
|
||||
- `TestLexiconMetaSelfTestTable` — PASS (G-009 self-test table for all 10 banned terms)
|
||||
- `TestLexiconMetaBannedTermsCount` — PASS
|
||||
- `TestLexiconMetaNoFalsePositiveOnOpenYield` — PASS (word-boundary matcher, "openyield" not flagged)
|
||||
|
||||
### G-003 by-ID-string invariant (`go test -run TestG003 ./x/window/...`)
|
||||
- `TestG003NoCrossModuleStructImportsInProduction` — PASS (no production `.go` file under `x/` imports a foreign `x/<module>/types` package; pact + partner conform — refs are by-ID-string)
|
||||
|
||||
## Module Details
|
||||
|
||||
### x/pact (REQ-020, A-207: ONE module with enum)
|
||||
- **PactType enum**: Pause, Ground, Stance, Cover, StandRegistry, HubAPI — exactly 6 (vision §16)
|
||||
- **PactStatus enum**: Proposed, Active, Fulfilled, Voided
|
||||
- **Pact struct**: id, type, parties ([]string Reach IDs), terms ([]byte), status, execute-msg-ref, window-id-ref (string, G-003), stand-id-ref (string, G-003)
|
||||
- **Per-type Execute* stubs**: ExecutePause/Ground/Stance/Cover/StandRegistry/HubAPI — each transitions Proposed→Active, guards on type + status; ExecuteStandRegistry requires non-empty stand-id-ref
|
||||
- **Mission-Lock invariant**: `MissionLockAmendable` const bool false + per-type `AmendableCoreTermsPause/Ground/Stance` const flags false; Cover/StandRegistry/HubAPI amendable. `MissionLockAmendableCoreTerms(type)` helper
|
||||
- **AllPactTypes()** returns exactly 6 in vision §16 order
|
||||
- **Genesis**: `GenesisState{Pacts []Pact}`, `DefaultGenesisState()`, `ValidateGenesis` (rejects dup/empty pact-ids, unknown types, bad JSON); data-engineer's `ValidatePacts` + `MissionLockCheck` wired into the genesis load path (G-008)
|
||||
|
||||
### x/partner (REQ-018, D-026)
|
||||
- **PartnerTier enum**: Op, MasterOp, Pier, Anchor — exactly 4 (vision §13). "Op" not "operator" — lexicon-clean per RESEARCH §1.6
|
||||
- **PartnerStatus enum**: Pending, Active, Suspended, Revoked
|
||||
- **CredentialType enum**: Eresidency, Biometric, Vouch, Custom
|
||||
- **CredentialRef struct**: provider-id, credential-type, ref-uri (opaque URI; Pier credential routing deferred per Q5)
|
||||
- **Partner struct**: id, tier, name, reach-id (string, G-003), region, credential-ref, status
|
||||
- **Registry keeper stub**: `Keeper` with `NewKeeper`, `AddPartner`, `GetPartner`, `ListPartners`, `ListByTier` (in-memory, mutex-safe)
|
||||
- **AllPartnerTiers()** returns exactly 4 in vision §13 order
|
||||
- **Genesis**: `GenesisState{Partners []Partner}`, `DefaultGenesisState()`, `ValidateGenesis` (rejects dup/empty partner-ids, bad JSON)
|
||||
|
||||
## Deviation: genesis.go created in P2-01-01 alongside types.go
|
||||
|
||||
The plan ordered genesis.go as task P2-01-03 (after the test task P2-01-02),
|
||||
but `types.go` references `ValidatePacts` (the genesis helper) and the build
|
||||
must be green after each per-task commit. I therefore created `genesis.go`
|
||||
with `ValidatePacts` + `MissionLockCheck` in P2-01-01, and P2-01-03 then
|
||||
extended it (wiring `MissionLockCheck` INTO `ValidatePacts` so the genesis
|
||||
load path enforces the Mission-Lock check alongside id-uniqueness) and
|
||||
committed the extension as the P2-01-03 deliverable. Both tasks are
|
||||
individually committed; the deviation is structural only (genesis helper
|
||||
landed in the types task to keep the build green, then was refined in the
|
||||
genesis task). No semantic change to the plan's deliverables.
|
||||
|
||||
## Lexicon Compliance Notes
|
||||
|
||||
- **No banned literals** in any new `x/**/*.go` file (production or test). The 10 banned terms (bank, deposit, interest, yield, currency, dollar, euro, account, savings, depositor) are referenced only via the `lexicon` package helpers (`lexicon.FindBannedTerm`, `lexicon.BannedTerms`) in test files.
|
||||
- **Partner module** uses "Op"/"MasterOp"/"Pier"/"Anchor" (not "operator", which implies a banned financial term per RESEARCH §1.6). Verified lexicon-clean.
|
||||
- **Pact module** avoids "account" — uses "Holder"/"Reach" conventions. The term "parties" ([]string of Reach IDs) is used for the Pact's participating Reach IDs.
|
||||
- **Self-bootstrapping**: each test file has a `TestLexiconNoBannedTermsIn*TestFile` self-check that asserts the test file itself contains no banned literals (the lexicon helpers must be used, not inline strings).
|
||||
- **Project-wide meta-test** (`lexicon_meta_test.go`) scans ALL `x/**/*.go` including the new pact + partner files — PASS.
|
||||
|
||||
## Pre-existing LSP noise (not P2 scope)
|
||||
|
||||
The LSP reports errors in `x/watcher/` files (cosmos-sdk/codec imports) and
|
||||
`go.mod` (version "v2.0.1" invalid). These are **pre-existing** and **not in
|
||||
P2 scope** — `x/watcher` is a v0.1 module with stale cosmos-sdk references
|
||||
that are not part of the v0.2 skeleton (the v0.2 skeleton is zero-deps;
|
||||
`go build ./...` succeeds because the watcher files are excluded from the
|
||||
build path or compile cleanly via `go build`). `go build ./...` and
|
||||
`go test ./...` both PASS, confirming the LSP noise does not affect the
|
||||
build.
|
||||
|
||||
## Orchestrator Handoff
|
||||
|
||||
- **Do NOT merge/tag/push** — executor leaves the branch `oy/phase/02-pacts-partners` with 5 commits for the orchestrator to ship as tag `v0.1.2`.
|
||||
- All P2 must-haves pass except the git tag (deferred to orchestrator per instructions).
|
||||
- No regressions: all v0.1 baseline tests (143) + all v0.2-P1 tests + 64 new P2 tests = 207 total, all green.
|
||||
@@ -0,0 +1,80 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the pact
|
||||
// module (G-008 split). ValidateGenesis in types.go composes these helpers;
|
||||
// the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Pact genesis schema is a single top-level set: Pacts. The two
|
||||
// invariants enforced at genesis load are (1) pact-id uniqueness (A-212) and
|
||||
// (2) the Mission-Lock check (the per-type AmendableCoreTerms flags for
|
||||
// Pause/Ground/Stance must be false — the global MissionLockAmendable const
|
||||
// bool is the firewall). The Mission-Lock is enforced by compile-time consts;
|
||||
// the genesis-side MissionLockCheck is the data-engineer's hook that asserts
|
||||
// the const firewall is intact whenever genesis Pacts are loaded (so a
|
||||
// future change to the consts would surface here too).
|
||||
|
||||
// ValidatePacts asserts pact-ids are present and unique, and that each
|
||||
// Pact's type is a known PactType. It also runs the Mission-Lock check
|
||||
// (MissionLockCheck) so the genesis load path enforces both invariants.
|
||||
// ValidatePacts is the data-engineer's schema validator, composed by
|
||||
// ValidateGenesis in types.go.
|
||||
func ValidatePacts(pacts []Pact) error {
|
||||
seen := make(map[string]bool, len(pacts))
|
||||
for i, p := range pacts {
|
||||
if p.PactID == "" {
|
||||
return fmt.Errorf("pact [%d]: empty pact-id", i)
|
||||
}
|
||||
if seen[p.PactID] {
|
||||
return fmt.Errorf("pact: duplicate pact-id %q", p.PactID)
|
||||
}
|
||||
seen[p.PactID] = true
|
||||
if !knownPactType(p.Type) {
|
||||
return fmt.Errorf("pact %q: unknown pact type %q", p.PactID, p.Type)
|
||||
}
|
||||
}
|
||||
if err := MissionLockCheck(pacts); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownPactType reports whether t is one of the six vision §16 PactType values.
|
||||
func knownPactType(t PactType) bool {
|
||||
for _, kt := range AllPactTypes() {
|
||||
if t == kt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MissionLockCheck asserts the Mission-Lock invariant on a slice of Pacts:
|
||||
// every Pause/Ground/Stance Pact must have its AmendableCoreTerms flag false.
|
||||
// Because the flags are compile-time consts (AmendableCoreTermsPause/Ground/
|
||||
// Stance == false) and the global MissionLockAmendable const is false, this
|
||||
// check always passes — it exists as the data-engineer's genesis-side
|
||||
// assertion that the Mission-Lock firewall is intact. If the consts ever
|
||||
// changed to true, this check would still pass (the consts are the firewall,
|
||||
// not runtime data); the test in types_test.go is the true regression guard.
|
||||
// The helper is the genesis hook for v0.3 keeper logic to extend with live
|
||||
// per-pact Mission-Lock enforcement.
|
||||
func MissionLockCheck(pacts []Pact) 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-pact loop echoes the invariant for each
|
||||
// Mission-Locked Pact type so a future per-pact check has a hook point.
|
||||
if MissionLockAmendable {
|
||||
return fmt.Errorf("pact: Mission Lock amendable (MissionLockAmendable == true) — firewall breach")
|
||||
}
|
||||
for _, p := range pacts {
|
||||
if !MissionLockAmendableCoreTerms(p.Type) {
|
||||
// Non-amendable core terms: the const flags already guarantee this;
|
||||
// the genesis check is the echo. No per-pact runtime data to verify
|
||||
// in the skeleton — the const is the source of truth.
|
||||
continue
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "pact"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// PactTypeCount is the locked count of PactType enum values (vision §16).
|
||||
// A regression firewall: adding/removing/renaming a Pact type breaks this
|
||||
// const's test (REQ-020, A-207: ONE module with enum, not six micro-modules).
|
||||
PactTypeCount = 6
|
||||
|
||||
// MissionLockAmendable is the Mission-Lock invariant: the core terms of
|
||||
// Pause/Ground/Stance Pacts are non-amendable (vision §19). This is a
|
||||
// locked const bool: it can NEVER be set true. The regression test asserts
|
||||
// it is false and that the per-type AmendableCoreTerms flags for
|
||||
// Pause/Ground/Stance are all false.
|
||||
MissionLockAmendable = false
|
||||
)
|
||||
|
||||
// PactType enumerates the six commitment types (vision §16, REQ-020).
|
||||
// A-207: all six live in ONE x/pact module with a PactType enum + per-type
|
||||
// execute-entry stubs (NOT six micro-modules).
|
||||
type PactType string
|
||||
|
||||
const (
|
||||
PactPause PactType = "Pause" // circuit-breaker commitment (wraps x/still)
|
||||
PactGround PactType = "Ground" // earth-anchored collateral lock commitment
|
||||
PactStance PactType = "Stance" // public-position / attestation commitment
|
||||
PactCover PactType = "Cover" // insurance-like commitment (Cover Pool)
|
||||
PactStandRegistry PactType = "StandRegistry" // registers a Stand into the canonical registry
|
||||
PactHubAPI PactType = "HubAPI" // B2B backbone commitment
|
||||
)
|
||||
|
||||
// AllPactTypes returns all six PactType values in vision §16 order.
|
||||
// Locked-const test asserts exactly 6 entries with these names (REQ-020).
|
||||
func AllPactTypes() []PactType {
|
||||
return []PactType{
|
||||
PactPause,
|
||||
PactGround,
|
||||
PactStance,
|
||||
PactCover,
|
||||
PactStandRegistry,
|
||||
PactHubAPI,
|
||||
}
|
||||
}
|
||||
|
||||
// PactStatus enumerates the lifecycle states of a Pact (REQ-020).
|
||||
type PactStatus string
|
||||
|
||||
const (
|
||||
StatusProposed PactStatus = "Proposed" // pact created, not yet active
|
||||
StatusActive PactStatus = "Active" // pact is live and binding
|
||||
StatusFulfilled PactStatus = "Fulfilled" // pact completed successfully
|
||||
StatusVoided PactStatus = "Voided" // pact voided (cancelled / breached)
|
||||
)
|
||||
|
||||
// PactStatusCount is the locked count of PactStatus enum values.
|
||||
const PactStatusCount = 4
|
||||
|
||||
// Pact is a commitment of one of six types (vision §16, REQ-020). Each Pact
|
||||
// has a type, parties (Reach IDs by-ID-string per G-003), opaque terms-bytes,
|
||||
// a status, and per-type execute-message ref. window-id-ref references
|
||||
// x/window by ID string (G-003 by-ID-string invariant; P1-01-01 convention).
|
||||
// stand-id-ref references x/stand by ID string (P1-02-01 convention); only
|
||||
// StandRegistry Pacts populate it for non-empty, others leave it "".
|
||||
type Pact struct {
|
||||
PactID string `json:"pact_id" yaml:"pact_id"`
|
||||
Type PactType `json:"type" yaml:"type"`
|
||||
Parties []string `json:"parties" yaml:"parties"`
|
||||
Terms []byte `json:"terms" yaml:"terms"`
|
||||
Status PactStatus `json:"status" yaml:"status"`
|
||||
ExecuteMsgRef string `json:"execute_msg_ref" yaml:"execute_msg_ref"`
|
||||
WindowIDRef string `json:"window_id_ref" yaml:"window_id_ref"`
|
||||
StandIDRef string `json:"stand_id_ref" yaml:"stand_id_ref"`
|
||||
}
|
||||
|
||||
// MissionLockCoreTerms flags which Pact types have non-amendable core terms
|
||||
// under the Mission Lock (vision §19). Pause/Ground/Stance core terms are
|
||||
// non-amendable; the const flags below are the per-type invariant. The
|
||||
// module-level MissionLockAmendable const bool is the global firewall.
|
||||
const (
|
||||
// AmendableCoreTermsPause is false: Pause Pact core terms are
|
||||
// non-amendable under the Mission Lock.
|
||||
AmendableCoreTermsPause = false
|
||||
// AmendableCoreTermsGround is false: Ground Pact core terms are
|
||||
// non-amendable under the Mission Lock.
|
||||
AmendableCoreTermsGround = false
|
||||
// AmendableCoreTermsStance is false: Stance Pact core terms are
|
||||
// non-amendable under the Mission Lock.
|
||||
AmendableCoreTermsStance = false
|
||||
)
|
||||
|
||||
// MissionLockAmendableCoreTerms returns the per-type AmendableCoreTerms flag
|
||||
// for a PactType. Pause/Ground/Stance return false (non-amendable); Cover,
|
||||
// StandRegistry, HubAPI return true (amendable per the skeleton — these are
|
||||
// not Mission-Locked). The Mission-Lock invariant test asserts the three
|
||||
// core types return false.
|
||||
func MissionLockAmendableCoreTerms(t PactType) bool {
|
||||
switch t {
|
||||
case PactPause:
|
||||
return AmendableCoreTermsPause
|
||||
case PactGround:
|
||||
return AmendableCoreTermsGround
|
||||
case PactStance:
|
||||
return AmendableCoreTermsStance
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ExecutePause is the execute-entry stub for a Pause Pact (circuit-breaker).
|
||||
// The skeleton returns the proposed status transition; v0.3 wires the live
|
||||
// keeper that wraps x/still.
|
||||
func (p *Pact) ExecutePause() error {
|
||||
if p.Type != PactPause {
|
||||
return fmt.Errorf("ExecutePause: pact %q is type %q, not Pause", p.PactID, p.Type)
|
||||
}
|
||||
if p.Status != StatusProposed {
|
||||
return fmt.Errorf("ExecutePause: pact %q status %q, not Proposed", p.PactID, p.Status)
|
||||
}
|
||||
p.Status = StatusActive
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteGround is the execute-entry stub for a Ground Pact
|
||||
// (earth-anchored collateral lock).
|
||||
func (p *Pact) ExecuteGround() error {
|
||||
if p.Type != PactGround {
|
||||
return fmt.Errorf("ExecuteGround: pact %q is type %q, not Ground", p.PactID, p.Type)
|
||||
}
|
||||
if p.Status != StatusProposed {
|
||||
return fmt.Errorf("ExecuteGround: pact %q status %q, not Proposed", p.PactID, p.Status)
|
||||
}
|
||||
p.Status = StatusActive
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteStance is the execute-entry stub for a Stance Pact
|
||||
// (public-position / attestation).
|
||||
func (p *Pact) ExecuteStance() error {
|
||||
if p.Type != PactStance {
|
||||
return fmt.Errorf("ExecuteStance: pact %q is type %q, not Stance", p.PactID, p.Type)
|
||||
}
|
||||
if p.Status != StatusProposed {
|
||||
return fmt.Errorf("ExecuteStance: pact %q status %q, not Proposed", p.PactID, p.Status)
|
||||
}
|
||||
p.Status = StatusActive
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteCover is the execute-entry stub for a Cover Pact (insurance-like).
|
||||
// Cover Pool seniority is deferred per Q7 — the skeleton is a flat
|
||||
// commitment type with no seniority fields.
|
||||
func (p *Pact) ExecuteCover() error {
|
||||
if p.Type != PactCover {
|
||||
return fmt.Errorf("ExecuteCover: pact %q is type %q, not Cover", p.PactID, p.Type)
|
||||
}
|
||||
if p.Status != StatusProposed {
|
||||
return fmt.Errorf("ExecuteCover: pact %q status %q, not Proposed", p.PactID, p.Status)
|
||||
}
|
||||
p.Status = StatusActive
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteStandRegistry is the execute-entry stub for a StandRegistry Pact.
|
||||
// stand-id-ref references x/stand by ID string (G-003); the skeleton activates
|
||||
// the pact without a live keeper call.
|
||||
func (p *Pact) ExecuteStandRegistry() error {
|
||||
if p.Type != PactStandRegistry {
|
||||
return fmt.Errorf("ExecuteStandRegistry: pact %q is type %q, not StandRegistry", p.PactID, p.Type)
|
||||
}
|
||||
if p.Status != StatusProposed {
|
||||
return fmt.Errorf("ExecuteStandRegistry: pact %q status %q, not Proposed", p.PactID, p.Status)
|
||||
}
|
||||
if p.StandIDRef == "" {
|
||||
return fmt.Errorf("ExecuteStandRegistry: pact %q missing stand-id-ref", p.PactID)
|
||||
}
|
||||
p.Status = StatusActive
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteHubAPI is the execute-entry stub for a HubAPI Pact (B2B backbone).
|
||||
// The full Hub API suite is deferred to Phase 3; v0.2 = stub type only.
|
||||
func (p *Pact) ExecuteHubAPI() error {
|
||||
if p.Type != PactHubAPI {
|
||||
return fmt.Errorf("ExecuteHubAPI: pact %q is type %q, not HubAPI", p.PactID, p.Type)
|
||||
}
|
||||
if p.Status != StatusProposed {
|
||||
return fmt.Errorf("ExecuteHubAPI: pact %q status %q, not Proposed", p.PactID, p.Status)
|
||||
}
|
||||
p.Status = StatusActive
|
||||
return nil
|
||||
}
|
||||
|
||||
// Params for the pact module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the pact module genesis state (REQ-020).
|
||||
// Pacts is the top-level set; ValidateGenesis enforces pact-id uniqueness and
|
||||
// the Mission-Lock check (Mission-Locked types' AmendableCoreTerms flags must
|
||||
// be false). The data-engineer's genesis.go holds the schema helpers (G-008).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Pacts []Pact `json:"pacts" yaml:"pacts"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Pacts: []Pact{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate pact-ids, and runs the Mission-Lock check on
|
||||
// genesis Pacts. 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("pact: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidatePacts(gs.Pacts); err != nil {
|
||||
return fmt.Errorf("pact: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
"github.com/oy/openyield/x/pact/types"
|
||||
)
|
||||
|
||||
// TestPactTypeCountLockedConst asserts PactTypeCount is exactly 6 and
|
||||
// AllPactTypes() returns exactly 6 (vision §16, REQ-020, A-207). A regression
|
||||
// firewall: adding/removing/renaming a Pact type breaks this test.
|
||||
func TestPactTypeCountLockedConst(t *testing.T) {
|
||||
if types.PactTypeCount != 6 {
|
||||
t.Errorf("PactTypeCount = %d, expected 6 (vision §16 LOCKED)", types.PactTypeCount)
|
||||
}
|
||||
all := types.AllPactTypes()
|
||||
if len(all) != 6 {
|
||||
t.Errorf("AllPactTypes() len = %d, expected 6", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllPactTypesNames asserts the 6 vision §16 names in order with no
|
||||
// extras, no dups, no renames.
|
||||
func TestAllPactTypesNames(t *testing.T) {
|
||||
want := []string{
|
||||
"Pause", "Ground", "Stance", "Cover", "StandRegistry", "HubAPI",
|
||||
}
|
||||
all := types.AllPactTypes()
|
||||
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("AllPactTypes()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate PactType %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestPactTypeValues asserts each named const matches its AllPactTypes entry.
|
||||
func TestPactTypeValues(t *testing.T) {
|
||||
if types.PactPause != "Pause" {
|
||||
t.Errorf("PactPause = %q", types.PactPause)
|
||||
}
|
||||
if types.PactGround != "Ground" {
|
||||
t.Errorf("PactGround = %q", types.PactGround)
|
||||
}
|
||||
if types.PactStance != "Stance" {
|
||||
t.Errorf("PactStance = %q", types.PactStance)
|
||||
}
|
||||
if types.PactCover != "Cover" {
|
||||
t.Errorf("PactCover = %q", types.PactCover)
|
||||
}
|
||||
if types.PactStandRegistry != "StandRegistry" {
|
||||
t.Errorf("PactStandRegistry = %q", types.PactStandRegistry)
|
||||
}
|
||||
if types.PactHubAPI != "HubAPI" {
|
||||
t.Errorf("PactHubAPI = %q", types.PactHubAPI)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPactStatusCountLockedConst asserts PactStatusCount is exactly 4.
|
||||
func TestPactStatusCountLockedConst(t *testing.T) {
|
||||
if types.PactStatusCount != 4 {
|
||||
t.Errorf("PactStatusCount = %d, expected 4", types.PactStatusCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPactStatusEnumCoverage asserts all four PactStatus values are distinct
|
||||
// and non-empty (REQ-020 lifecycle: Proposed, Active, Fulfilled, Voided).
|
||||
func TestPactStatusEnumCoverage(t *testing.T) {
|
||||
statuses := []types.PactStatus{
|
||||
types.StatusProposed, types.StatusActive,
|
||||
types.StatusFulfilled, types.StatusVoided,
|
||||
}
|
||||
if len(statuses) != 4 {
|
||||
t.Errorf("expected 4 PactStatus consts, got %d", len(statuses))
|
||||
}
|
||||
seen := map[types.PactStatus]bool{}
|
||||
for _, s := range statuses {
|
||||
if s == "" {
|
||||
t.Error("empty PactStatus")
|
||||
}
|
||||
if seen[s] {
|
||||
t.Errorf("duplicate PactStatus %q", s)
|
||||
}
|
||||
seen[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissionLockAmendableConstFalse asserts the global Mission-Lock const
|
||||
// is false (vision §19): the Mission Lock can NEVER be amended. This is the
|
||||
// highest-severity regression firewall for the pact module.
|
||||
func TestMissionLockAmendableConstFalse(t *testing.T) {
|
||||
if types.MissionLockAmendable != false {
|
||||
t.Fatalf("MissionLockAmendable = %v, expected false (Mission Lock non-amendable)", types.MissionLockAmendable)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissionLockCoreTermsNonAmendable asserts the per-type AmendableCoreTerms
|
||||
// const flags for Pause/Ground/Stance are all false (Mission-Lock invariant).
|
||||
// Cover/StandRegistry/HubAPI return true (amendable — not Mission-Locked).
|
||||
func TestMissionLockCoreTermsNonAmendable(t *testing.T) {
|
||||
// Pause/Ground/Stance core terms MUST be non-amendable.
|
||||
if types.AmendableCoreTermsPause != false {
|
||||
t.Error("AmendableCoreTermsPause must be false (Mission Lock)")
|
||||
}
|
||||
if types.AmendableCoreTermsGround != false {
|
||||
t.Error("AmendableCoreTermsGround must be false (Mission Lock)")
|
||||
}
|
||||
if types.AmendableCoreTermsStance != false {
|
||||
t.Error("AmendableCoreTermsStance must be false (Mission Lock)")
|
||||
}
|
||||
// The MissionLockAmendableCoreTerms helper echoes the const flags.
|
||||
locked := []types.PactType{types.PactPause, types.PactGround, types.PactStance}
|
||||
for _, pt := range locked {
|
||||
if types.MissionLockAmendableCoreTerms(pt) != false {
|
||||
t.Errorf("MissionLockAmendableCoreTerms(%q) = true, want false (Mission Lock)", pt)
|
||||
}
|
||||
}
|
||||
// Cover/StandRegistry/HubAPI are amendable (not Mission-Locked).
|
||||
amendable := []types.PactType{types.PactCover, types.PactStandRegistry, types.PactHubAPI}
|
||||
for _, pt := range amendable {
|
||||
if types.MissionLockAmendableCoreTerms(pt) != true {
|
||||
t.Errorf("MissionLockAmendableCoreTerms(%q) = false, want true (amendable)", pt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPactStructFields asserts Pact carries all required fields including
|
||||
// the by-ID-string refs (window-id-ref, stand-id-ref per G-003).
|
||||
func TestPactStructFields(t *testing.T) {
|
||||
p := types.Pact{
|
||||
PactID: "p1",
|
||||
Type: types.PactPause,
|
||||
Parties: []string{"reach:a", "reach:b"},
|
||||
Terms: []byte("terms-bytes"),
|
||||
Status: types.StatusProposed,
|
||||
ExecuteMsgRef: "msg:pause:1",
|
||||
WindowIDRef: "w1",
|
||||
StandIDRef: "s1",
|
||||
}
|
||||
if p.PactID != "p1" || p.Type != types.PactPause || len(p.Parties) != 2 ||
|
||||
string(p.Terms) != "terms-bytes" || p.Status != types.StatusProposed ||
|
||||
p.ExecuteMsgRef != "msg:pause:1" || p.WindowIDRef != "w1" || p.StandIDRef != "s1" {
|
||||
t.Error("Pact fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPactStructRefsAreStrings asserts window-id-ref and stand-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 TestPactStructRefsAreStrings(t *testing.T) {
|
||||
// Construct a Pact and confirm the ref fields hold plain strings —
|
||||
// no struct imports of x/window or x/stand are needed.
|
||||
p := types.Pact{WindowIDRef: "window-abc", StandIDRef: "stand-xyz"}
|
||||
if p.WindowIDRef != "window-abc" {
|
||||
t.Errorf("WindowIDRef = %q", p.WindowIDRef)
|
||||
}
|
||||
if p.StandIDRef != "stand-xyz" {
|
||||
t.Errorf("StandIDRef = %q", p.StandIDRef)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteStubsCallable asserts each per-type Execute* stub is callable
|
||||
// and transitions a Proposed Pact to Active (REQ-020).
|
||||
func TestExecuteStubsCallable(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
pact types.Pact
|
||||
execFn func(*types.Pact) error
|
||||
}{
|
||||
{"Pause", types.Pact{PactID: "p1", Type: types.PactPause, Status: types.StatusProposed}, (*types.Pact).ExecutePause},
|
||||
{"Ground", types.Pact{PactID: "p2", Type: types.PactGround, Status: types.StatusProposed}, (*types.Pact).ExecuteGround},
|
||||
{"Stance", types.Pact{PactID: "p3", Type: types.PactStance, Status: types.StatusProposed}, (*types.Pact).ExecuteStance},
|
||||
{"Cover", types.Pact{PactID: "p4", Type: types.PactCover, Status: types.StatusProposed}, (*types.Pact).ExecuteCover},
|
||||
{"HubAPI", types.Pact{PactID: "p6", Type: types.PactHubAPI, Status: types.StatusProposed}, (*types.Pact).ExecuteHubAPI},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := tc.pact
|
||||
if err := tc.execFn(&p); err != nil {
|
||||
t.Fatalf("Execute%s: %v", tc.name, err)
|
||||
}
|
||||
if p.Status != types.StatusActive {
|
||||
t.Errorf("after Execute%s, status = %q, want Active", tc.name, p.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteStandRegistryRequiresStandIDRef asserts ExecuteStandRegistry
|
||||
// requires a non-empty stand-id-ref (the by-ID-string ref to x/stand).
|
||||
func TestExecuteStandRegistryRequiresStandIDRef(t *testing.T) {
|
||||
p := types.Pact{PactID: "p5", Type: types.PactStandRegistry, Status: types.StatusProposed, StandIDRef: ""}
|
||||
if err := p.ExecuteStandRegistry(); err == nil {
|
||||
t.Error("ExecuteStandRegistry should error on empty stand-id-ref")
|
||||
}
|
||||
p.StandIDRef = "s1"
|
||||
if err := p.ExecuteStandRegistry(); err != nil {
|
||||
t.Errorf("ExecuteStandRegistry with stand-id-ref: %v", err)
|
||||
}
|
||||
if p.Status != types.StatusActive {
|
||||
t.Errorf("status = %q, want Active", p.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteStubsRejectWrongType asserts each Execute* stub rejects a Pact
|
||||
// of the wrong type (type guard).
|
||||
func TestExecuteStubsRejectWrongType(t *testing.T) {
|
||||
p := types.Pact{PactID: "p", Type: types.PactCover, Status: types.StatusProposed}
|
||||
if err := p.ExecutePause(); err == nil {
|
||||
t.Error("ExecutePause on a Cover pact should error")
|
||||
}
|
||||
if err := p.ExecuteGround(); err == nil {
|
||||
t.Error("ExecuteGround on a Cover pact should error")
|
||||
}
|
||||
if err := p.ExecuteStance(); err == nil {
|
||||
t.Error("ExecuteStance on a Cover pact should error")
|
||||
}
|
||||
if err := p.ExecuteStandRegistry(); err == nil {
|
||||
t.Error("ExecuteStandRegistry on a Cover pact should error")
|
||||
}
|
||||
if err := p.ExecuteHubAPI(); err == nil {
|
||||
t.Error("ExecuteHubAPI on a Cover pact should error")
|
||||
}
|
||||
// ExecuteCover should succeed (matches type).
|
||||
if err := p.ExecuteCover(); err != nil {
|
||||
t.Errorf("ExecuteCover on a Cover pact: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteStubsRejectNonProposed asserts each Execute* stub rejects a
|
||||
// Pact not in the Proposed status.
|
||||
func TestExecuteStubsRejectNonProposed(t *testing.T) {
|
||||
tt := []struct {
|
||||
name string
|
||||
pact types.Pact
|
||||
execFn func(*types.Pact) error
|
||||
}{
|
||||
{"Pause-active", types.Pact{PactID: "p", Type: types.PactPause, Status: types.StatusActive}, (*types.Pact).ExecutePause},
|
||||
{"Ground-fulfilled", types.Pact{PactID: "p", Type: types.PactGround, Status: types.StatusFulfilled}, (*types.Pact).ExecuteGround},
|
||||
{"Stance-voided", types.Pact{PactID: "p", Type: types.PactStance, Status: types.StatusVoided}, (*types.Pact).ExecuteStance},
|
||||
{"Cover-active", types.Pact{PactID: "p", Type: types.PactCover, Status: types.StatusActive}, (*types.Pact).ExecuteCover},
|
||||
{"HubAPI-voided", types.Pact{PactID: "p", Type: types.PactHubAPI, Status: types.StatusVoided}, (*types.Pact).ExecuteHubAPI},
|
||||
}
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
p := tc.pact
|
||||
if err := tc.execFn(&p); err == nil {
|
||||
t.Errorf("Execute%s on %q-status pact should error", tc.name, p.Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteStandRegistryNonProposed asserts ExecuteStandRegistry rejects
|
||||
// a non-Proposed StandRegistry pact even when stand-id-ref is set.
|
||||
func TestExecuteStandRegistryNonProposed(t *testing.T) {
|
||||
p := types.Pact{PactID: "p", Type: types.PactStandRegistry, Status: types.StatusActive, StandIDRef: "s1"}
|
||||
if err := p.ExecuteStandRegistry(); err == nil {
|
||||
t.Error("ExecuteStandRegistry on Active pact should error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns a
|
||||
// non-nil empty slice for Pacts.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := types.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Pacts == nil || len(gs.Pacts) != 0 {
|
||||
t.Errorf("Default Pacts should be non-nil empty slice; got len=%d nil=%v", len(gs.Pacts), gs.Pacts == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupPactIDs asserts A-212: duplicate pact-ids
|
||||
// are rejected (upgrade from v0.1's no-op ValidateGenesis).
|
||||
func TestValidateGenesisRejectsDupPactIDs(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Pacts: []types.Pact{
|
||||
{PactID: "p1", Type: types.PactPause},
|
||||
{PactID: "p1", Type: types.PactGround}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate pact-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyPactID asserts empty pact-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyPactID(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Pacts: []types.Pact{{PactID: "", Type: types.PactPause}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty pact-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownType asserts an unknown PactType is
|
||||
// rejected (data-engineer schema validation).
|
||||
func TestValidateGenesisRejectsUnknownType(t *testing.T) {
|
||||
gs := types.GenesisState{
|
||||
Pacts: []types.Pact{{PactID: "p1", Type: types.PactType("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown pact type")
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
Pacts: []types.Pact{
|
||||
{PactID: "p1", Type: types.PactPause, Status: types.StatusProposed},
|
||||
{PactID: "p2", Type: types.PactCover, Status: types.StatusActive},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := types.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMissionLockCheckIsNoOp asserts the genesis-side MissionLockCheck helper
|
||||
// is a no-op (the const flags are the true firewall). It must return nil for
|
||||
// any slice of Pacts — the Mission Lock is enforced at compile time by the
|
||||
// const bools, not at genesis load.
|
||||
func TestMissionLockCheckIsNoOp(t *testing.T) {
|
||||
pacts := []types.Pact{
|
||||
{PactID: "p1", Type: types.PactPause},
|
||||
{PactID: "p2", Type: types.PactGround},
|
||||
{PactID: "p3", Type: types.PactStance},
|
||||
{PactID: "p4", Type: types.PactCover},
|
||||
}
|
||||
if err := types.MissionLockCheck(pacts); err != nil {
|
||||
t.Errorf("MissionLockCheck should be a no-op (const flags are the firewall), got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if types.ModuleName != "pact" {
|
||||
t.Errorf("ModuleName = %q", types.ModuleName)
|
||||
}
|
||||
if types.StoreKey != "pact" {
|
||||
t.Errorf("StoreKey = %q", types.StoreKey)
|
||||
}
|
||||
if types.RouterKey != "pact" {
|
||||
t.Errorf("RouterKey = %q", types.RouterKey)
|
||||
}
|
||||
if types.QuerierRoute != "pact" {
|
||||
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) -------------------------------------------------
|
||||
|
||||
// TestLexiconNoBannedTermsInPactPackage scans every non-test .go file in
|
||||
// the pact/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 TestLexiconNoBannedTermsInPactPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/pact/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 pact/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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInPactTestFile 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 TestLexiconNoBannedTermsInPactTestFile(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("pact 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/pact/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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user