Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63042285e8 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"phase": 3,
|
||||
"phase": 4,
|
||||
"stage": "execute",
|
||||
"milestone": "v0.2",
|
||||
"milestone_type": "feature",
|
||||
@@ -7,5 +7,5 @@
|
||||
"phase_role": "execution",
|
||||
"project": "oy",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-08-17T21:40:00Z"
|
||||
"updated_at": "2026-08-17T21:50:00Z"
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
# Phase P4 — Bonds + Bearers + L2 — Ship Verification
|
||||
|
||||
> Milestone **v0.2 (The Mesh)** — Phase 4 (P4 — Bonds+Bearers+L2).
|
||||
> Branch: `oy/phase/04-bonds-bearers-l2`.
|
||||
> Tag: **NOT created** (per executor instructions — do NOT merge/tag/push).
|
||||
|
||||
## Verification Summary
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `go build ./...` | ✅ green |
|
||||
| `go test ./...` | ✅ green (303 PASS, 0 FAIL across 21 packages with tests) |
|
||||
| `go test -cover ./x/bond/types/...` | ✅ 96.8% (≥80%) |
|
||||
| `go test -cover ./x/bearers/types/...` | ✅ 100.0% (≥80%) |
|
||||
| `go test -cover ./x/satellite/types/...` | ✅ 100.0% (≥80%) |
|
||||
| Existing v0.1 tests (no regression) | ✅ all green (15+10=25 packages incl. 4 no-test) |
|
||||
| Lexicon meta-test (`TestLexiconMetaNoBannedTermsInX`) | ✅ green |
|
||||
| Bond lexicon (A-210 coupon-only) | ✅ green (`TestLexiconNoBannedTermsInBondPackage`) |
|
||||
| Satellite lexicon (Holder/Reach, not banned terms) | ✅ green (`TestLexiconNoBannedTermsInSatellitePackage`) |
|
||||
| Bearers extension lexicon | ✅ green (`TestLexiconNoBannedTermsInBearersPackage`) |
|
||||
| AllBearers() == 6 (no regression) | ✅ green (`TestBearerCount`, `TestOYLRStillInAllBearers`) |
|
||||
| Git tag `v0.1.4` | ⛔ NOT created (per executor instructions — do NOT tag/push) |
|
||||
|
||||
## Tasks Executed (8/8 committed)
|
||||
|
||||
| Task | File(s) | Commit | Persona |
|
||||
|---|---|---|---|
|
||||
| P4-01-01 | `x/bond/types/types.go`, `x/bond/types/genesis.go` | `242ebcc` | backend-engineer |
|
||||
| P4-02-01 | `x/bearers/types/types.go` (extended) | `0727219` | cosmos-engineer |
|
||||
| P4-03-01 | `x/satellite/types/types.go`, `x/satellite/types/genesis.go` | `0979015` | cosmos-engineer |
|
||||
| P4-01-02 | `x/bond/types/types_test.go` | `70f1ddf` | security-engineer |
|
||||
| P4-01-03 | `x/bond/types/genesis_test.go` (genesis.go committed in 01-01) | `e18c323` | data-engineer |
|
||||
| P4-02-02 | `x/bearers/types/types_test.go` (extended) | `faf0508` | security-engineer |
|
||||
| P4-03-02 | `x/satellite/types/types_test.go` | `9ee2d11` | security-engineer |
|
||||
| P4-04-01 | `.ciagent/oy/P4_SHIP_VERIFICATION.md` | (this commit) | lead-developer |
|
||||
|
||||
## Must-Haves (P4 checklist)
|
||||
|
||||
- [x] `x/bond` (new), `x/bearers` (extended), `x/satellite` (new) each have `types/types.go` + `types/types_test.go`.
|
||||
- [x] `go build ./...` and `go test ./...` green — including all v0.1 baseline tests (no regression).
|
||||
- [x] ≥80% coverage on `x/bond/types` (96.8%), `x/bearers/types` (100%), `x/satellite/types` (100%).
|
||||
- [x] Bond clamp invariant: `CouponCapBps == 800`, `CouponFloorBps == 0`; clamp below→floor, above→cap, in-range→unchanged.
|
||||
- [x] Bond lexicon: "coupon" exclusively, no banned terms (A-210).
|
||||
- [x] Bearers: `BearerTransport` interface compiles; `OYLRLink` + `BeaconFrame` stubs; existing `AllBearers()` (6) unchanged.
|
||||
- [x] Satellite: `L2Chain` exactly 5 (Polygon active + 4 stubs); `Packet` pinned to ICS-20 v1 shape; zero external deps.
|
||||
- [x] Lexicon assertion in all 3 test files (bond, bearers-ext, satellite).
|
||||
- [x] `ValidateGenesis` ID-uniqueness (all 3) + genesis clamp (Bond).
|
||||
- [ ] Git tag `v0.1.4` — ⛔ NOT created (executor instructed NOT to merge/tag/push).
|
||||
|
||||
## Deliverable Detail
|
||||
|
||||
### P4-01-01 — Bond types (backend-engineer, REQ-021, D-028)
|
||||
- `CouponCapBps = 800` (8%), `CouponFloorBps = 0` (0%) — LOCKED `const`.
|
||||
- `Bond` struct: id, issuer-stand-id (by-ID-string ref to x/stand per G-003), principal-grain, coupon-bps, term-days, issued-at, maturity, status.
|
||||
- `BondStatus` enum (5): Issued, Active, Matured, Defaulted, Repaid.
|
||||
- `Issue(...)` stub: constructs Bond with coupon clamped, status BondIssued.
|
||||
- `Clamp(couponBps)` mirrors `x/feecovenant` Clamp shape: `min(cap, max(floor, coupon))`.
|
||||
- `AllBondStatuses()` returns 5.
|
||||
- `DefaultParams`, `GenesisState` (bonds), `DefaultGenesisState`, `ValidateGenesis` (rejects dup bond-ids).
|
||||
|
||||
### P4-02-01 — Bearers extension (cosmos-engineer, D-029, A-209)
|
||||
- EXTENDED existing `x/bearers/types/types.go` (NOT a new module).
|
||||
- `BearerTransport` Go interface: `Send`, `Receive`, `Status` — no impl.
|
||||
- `OYLRLink` struct: gateway-id, range-meters, frequency-mhz, surveillance-resistant=true.
|
||||
- `BeaconFrame` struct: beacon-id, ephemeral-id, payload-bytes, ttl.
|
||||
- PRESERVED existing `BearerType` enum + `AllBearers()` (OY-LR still in the 6).
|
||||
- `DefaultParams`/`GenesisState` unchanged (no break).
|
||||
|
||||
### P4-03-01 — Satellite types (cosmos-engineer, REQ-009, D-021, A-215)
|
||||
- `L2Chain` enum (5): Polygon active; Base, Arbitrum, Optimism, Solana StatusPending (D-021).
|
||||
- `TransferChannel` struct: port-id, channel-id, counterparty, status.
|
||||
- `ChannelStatus` enum (4): Init, TryOpen, Open, Closed (ICS-20 handshake).
|
||||
- `WrappedBreadDenom` struct: denom, trace-path (IBC trace encoding).
|
||||
- `Packet` stub struct: sequence, source-port, source-channel, dest-port, dest-channel, data, timeout-height, timeout-timestamp (ICS-20 v1 shape).
|
||||
- NO ibc-go import (zero external deps — A-201).
|
||||
- `AllL2Chains()` returns 5; `AllChannelStatuses()` returns 4.
|
||||
- `DefaultParams`, `GenesisState` (channels + denoms), `DefaultGenesisState`, `ValidateGenesis` (rejects dup channel-ids + dup denoms).
|
||||
|
||||
### P4-01-02 — Bond tests (security-engineer, REQ-021)
|
||||
- Clamp invariant tests: below floor → floor, above cap → cap, in range → unchanged.
|
||||
- `CouponCapBps == 800` locked-const; `CouponFloorBps == 0` locked-const.
|
||||
- `BondStatus` enum coverage (5); `Issue` stub callable + clamps above cap.
|
||||
- `ValidateGenesis` rejects dup bond-id, unknown status, coupon above cap.
|
||||
- Lexicon assertion (lexicon helpers, no banned literals — A-210 coupon-only).
|
||||
|
||||
### P4-01-03 — Bond genesis (data-engineer, REQ-021)
|
||||
- `ValidateBonds` enforces coupon-bps within [floor, cap] at genesis load (D-028 clamp).
|
||||
- `genesis_test.go`: boundary tests (at floor, at cap, just above cap, just below cap).
|
||||
|
||||
### P4-02-02 — Bearers tests extension (security-engineer, D-029)
|
||||
- `BearerTransport` interface signature test (stub impl satisfies it).
|
||||
- `OYLRLink` non-empty + surveillance-resistant == true; `BeaconFrame` non-empty + ttl > 0.
|
||||
- OY-LR still in AllBearers() (REGRESSION: existing v0.1 tests pass).
|
||||
- Lexicon assertion (extends existing test file).
|
||||
|
||||
### P4-03-02 — Satellite tests (security-engineer, REQ-009)
|
||||
- `L2Chain` exactly 5 (Polygon + 4 stubs); Polygon only active (D-021).
|
||||
- `ChannelStatus` coverage (4); `Packet` fields match ICS-20 v1 (JSON tags).
|
||||
- `WrappedBreadDenom` trace-path encoding; `ValidateGenesis` rejects dup channel-id + dup denom.
|
||||
- Lexicon assertion (no banned terms — use Holder/Reach).
|
||||
|
||||
### P4-04-01 — Phase ship verification (lead-developer)
|
||||
- This document. Full build/test/coverage verification.
|
||||
|
||||
## Test Counts
|
||||
- **Total `--- PASS`: 303** (leaf tests; some names repeat across packages).
|
||||
- **Total `--- FAIL`: 0**.
|
||||
- **Packages with tests: 21** (4 packages have no test files: identity, processing, rootpool, vault — same as v0.1 baseline).
|
||||
|
||||
## Notes
|
||||
- The bond `genesis.go` was created in P4-01-01's commit (needed for `go build` — `ValidateBonds` is referenced by `ValidateGenesis` in types.go). P4-01-03 adds the dedicated `genesis_test.go` clamp assertions and owns the data-engineer's genesis-schema deliverable.
|
||||
- Pre-existing LSP errors in `x/watcher/` (cosmos-sdk imports not vendored) are unchanged and do not affect `go build ./...` or `go test ./...` (the watcher module builds under the v0.1 baseline; these are stale LSP diagnostics, not build errors).
|
||||
- No merge, no tag, no push performed (per executor instructions).
|
||||
@@ -48,6 +48,51 @@ type UnifiedBearerLayer struct {
|
||||
FirstToDeliver bool `json:"first_to_deliver" yaml:"first_to_deliver"`
|
||||
}
|
||||
|
||||
// BearerTransport is the transport interface for a bearer (D-029, vision
|
||||
// §14). A bearer implementation provides Send (dispatch a payload), Receive
|
||||
// (accept an inbound payload), and Status (report the bearer's current
|
||||
// reachability). This is a Go interface stub — no implementation is provided
|
||||
// in v0.2; the OY-LR and Beacon transports are typed stubs only (no
|
||||
// hardware/RF integration per D-029). The interface is the v0.2 hook for the
|
||||
// Phase 3 processing-mesh runtime.
|
||||
type BearerTransport interface {
|
||||
// Send dispatches a payload via the bearer. Returns an error if the
|
||||
// bearer cannot accept the payload. The stub implementations do not
|
||||
// actually transmit; the interface contract is the v0.2 deliverable.
|
||||
Send(payload []byte) error
|
||||
// Receive accepts an inbound payload from the bearer. Returns the
|
||||
// payload and an error if the bearer has no inbound payload.
|
||||
Receive() ([]byte, error)
|
||||
// Status reports the bearer's current reachability (true = reachable).
|
||||
Status() bool
|
||||
}
|
||||
|
||||
// OYLRLink is the OY-LR (LoRa, long-range 2-10km) transport link stub (D-029,
|
||||
// vision §14). OY-LR is surveillance-resistant (vision §14: differs from
|
||||
// Helium's public-coverage model). gateway-id is the LoRa gateway
|
||||
// identifier; range-meters is the link range (2-10km); frequency-mhz is the
|
||||
// operating frequency; surveillance-resistant is LOCKED true for OY-LR (the
|
||||
// bearer is designed to resist surveillance).
|
||||
type OYLRLink struct {
|
||||
GatewayID string `json:"gateway_id" yaml:"gateway_id"`
|
||||
RangeMeters int32 `json:"range_meters" yaml:"range_meters"`
|
||||
FrequencyMHz uint32 `json:"frequency_mhz" yaml:"frequency_mhz"`
|
||||
SurveillanceResistant bool `json:"surveillance_resistant" yaml:"surveillance_resistant"`
|
||||
}
|
||||
|
||||
// BeaconFrame is the OY-Beacon transport-mode beacon frame stub (D-029,
|
||||
// vision §14). A beacon is a transport-mode beacon (presence + small
|
||||
// payload), closest to Eddystone-EID (ephemeral identifier). beacon-id is
|
||||
// the beacon identifier; ephemeral-id is the rotating ephemeral identifier;
|
||||
// payload-bytes is the small payload; ttl is the time-to-live in seconds
|
||||
// (must be > 0 for a valid frame).
|
||||
type BeaconFrame struct {
|
||||
BeaconID string `json:"beacon_id" yaml:"beacon_id"`
|
||||
EphemeralID string `json:"ephemeral_id" yaml:"ephemeral_id"`
|
||||
PayloadBytes []byte `json:"payload_bytes" yaml:"payload_bytes"`
|
||||
TTL int64 `json:"ttl" yaml:"ttl"`
|
||||
}
|
||||
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
btypes "github.com/oy/openyield/x/bearers/types"
|
||||
ptypes "github.com/oy/openyield/x/processing/types"
|
||||
)
|
||||
@@ -56,3 +61,216 @@ func TestEmptyProcessorSelection(t *testing.T) {
|
||||
t.Error("Empty processor list should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
// --- v0.2 Bearers extension (P4-02-02, D-029) -----------------------------------
|
||||
// The following tests extend the existing v0.1 bearers tests with the v0.2
|
||||
// BearerTransport interface, OYLRLink, and BeaconFrame stubs (D-029). The
|
||||
// existing v0.1 tests above (TestBearerCount, TestSurveillanceResistantBearers,
|
||||
// TestProcessingModeFCFS, TestLightClientSize, TestProcessorSelectionByProximity,
|
||||
// TestEmptyProcessorSelection) MUST remain green — no regression.
|
||||
|
||||
// TestOYLRStillInAllBearers is the REGRESSION test (D-029): OY-LR must still
|
||||
// be in AllBearers() (the 6-bearer count is unchanged by the v0.2 extension).
|
||||
func TestOYLRStillInAllBearers(t *testing.T) {
|
||||
bearers := btypes.AllBearers()
|
||||
if len(bearers) != 6 {
|
||||
t.Errorf("AllBearers() len = %d, expected 6 (no regression — D-029)", len(bearers))
|
||||
}
|
||||
found := false
|
||||
for _, b := range bearers {
|
||||
if b.Type == btypes.BearerOYLR {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("OY-LR must still be in AllBearers() (no regression — D-029)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBearerTransportInterfaceSignature asserts the BearerTransport
|
||||
// interface is satisfiable by a stub implementation (D-029). The interface
|
||||
// has three methods: Send, Receive, Status — no implementation is provided
|
||||
// in v0.2; this test verifies the interface compiles and a stub satisfies it.
|
||||
func TestBearerTransportInterfaceSignature(t *testing.T) {
|
||||
// stubTransport is a minimal stub that satisfies BearerTransport.
|
||||
var _ btypes.BearerTransport = stubTransport{}
|
||||
}
|
||||
|
||||
// stubTransport is a minimal stub implementation of BearerTransport for the
|
||||
// interface-signature test. It does not actually transmit (no hardware/RF
|
||||
// integration per D-029); it exists only to verify the interface compiles.
|
||||
type stubTransport struct{}
|
||||
|
||||
func (stubTransport) Send(payload []byte) error { return nil }
|
||||
func (stubTransport) Receive() ([]byte, error) { return nil, nil }
|
||||
func (stubTransport) Status() bool { return true }
|
||||
|
||||
// TestBearerTransportInterfaceMethods asserts the interface methods have the
|
||||
// expected signatures by invoking them on the stub.
|
||||
func TestBearerTransportInterfaceMethods(t *testing.T) {
|
||||
s := stubTransport{}
|
||||
if err := s.Send([]byte("hi")); err != nil {
|
||||
t.Errorf("Send returned error: %v", err)
|
||||
}
|
||||
if _, err := s.Receive(); err != nil {
|
||||
t.Errorf("Receive returned error: %v", err)
|
||||
}
|
||||
if !s.Status() {
|
||||
t.Error("Status should return true for the stub")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYLRLinkStructNonEmpty asserts the OYLRLink struct is non-empty when
|
||||
// populated, and that surveillance-resistant is true (OY-LR is designed to
|
||||
// resist surveillance — vision §14).
|
||||
func TestOYLRLinkStructNonEmpty(t *testing.T) {
|
||||
link := btypes.OYLRLink{
|
||||
GatewayID: "gw-1",
|
||||
RangeMeters: 10000,
|
||||
FrequencyMHz: 915,
|
||||
SurveillanceResistant: true,
|
||||
}
|
||||
if link.GatewayID != "gw-1" {
|
||||
t.Errorf("GatewayID = %q", link.GatewayID)
|
||||
}
|
||||
if link.RangeMeters != 10000 {
|
||||
t.Errorf("RangeMeters = %d", link.RangeMeters)
|
||||
}
|
||||
if link.FrequencyMHz != 915 {
|
||||
t.Errorf("FrequencyMHz = %d", link.FrequencyMHz)
|
||||
}
|
||||
if !link.SurveillanceResistant {
|
||||
t.Error("SurveillanceResistant must be true for OY-LR (vision §14)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestOYLRLinkSurveillanceResistantTrue asserts the OYLRLink's surveillance-
|
||||
// resistant flag is the locked design property (OY-LR is surveillance-
|
||||
// resistant per vision §14). The zero-value is false; the constructor pattern
|
||||
// must set it true. This test asserts a populated link has it true.
|
||||
func TestOYLRLinkSurveillanceResistantTrue(t *testing.T) {
|
||||
link := btypes.OYLRLink{SurveillanceResistant: true}
|
||||
if !link.SurveillanceResistant {
|
||||
t.Error("OYLRLink.SurveillanceResistant must be true for OY-LR (§14)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBeaconFrameStructNonEmpty asserts the BeaconFrame struct is non-empty
|
||||
// when populated, and that ttl > 0 for a valid frame.
|
||||
func TestBeaconFrameStructNonEmpty(t *testing.T) {
|
||||
frame := btypes.BeaconFrame{
|
||||
BeaconID: "beacon-1",
|
||||
EphemeralID: "eph-abc",
|
||||
PayloadBytes: []byte{0x01, 0x02},
|
||||
TTL: 300,
|
||||
}
|
||||
if frame.BeaconID != "beacon-1" {
|
||||
t.Errorf("BeaconID = %q", frame.BeaconID)
|
||||
}
|
||||
if frame.EphemeralID != "eph-abc" {
|
||||
t.Errorf("EphemeralID = %q", frame.EphemeralID)
|
||||
}
|
||||
if len(frame.PayloadBytes) != 2 {
|
||||
t.Errorf("PayloadBytes len = %d", len(frame.PayloadBytes))
|
||||
}
|
||||
if frame.TTL <= 0 {
|
||||
t.Errorf("TTL = %d, must be > 0 for a valid frame", frame.TTL)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBeaconFrameTTLPositive asserts a valid BeaconFrame has TTL > 0.
|
||||
func TestBeaconFrameTTLPositive(t *testing.T) {
|
||||
cases := []int64{1, 60, 300, 3600}
|
||||
for _, ttl := range cases {
|
||||
f := btypes.BeaconFrame{TTL: ttl}
|
||||
if f.TTL <= 0 {
|
||||
t.Errorf("TTL = %d, must be > 0", f.TTL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultGenesisStateUnchanged asserts DefaultGenesisState is unchanged
|
||||
// by the v0.2 extension (no regression — the v0.1 GenesisState shape is
|
||||
// preserved).
|
||||
func TestDefaultGenesisStateUnchanged(t *testing.T) {
|
||||
gs := btypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisUnchanged asserts ValidateGenesis is unchanged (no
|
||||
// regression — v0.1 returned nil unconditionally; the extension preserves
|
||||
// this).
|
||||
func TestValidateGenesisUnchanged(t *testing.T) {
|
||||
if err := btypes.ValidateGenesis(nil); err != nil {
|
||||
t.Errorf("ValidateGenesis should return nil (no regression); got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
// The bearers extension must not introduce banned terms. The lexicon helpers
|
||||
// are used here — no banned literals are inlined in this test file.
|
||||
|
||||
// TestLexiconNoBannedTermsInBearersPackage scans every non-test .go file in
|
||||
// the bearers/types package directory for the banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInBearersPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/bearers/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 bearers/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 — D-029 extension)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInBearersTestFile 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 TestLexiconNoBannedTermsInBearersTestFile(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("bearers 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/bearers/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,54 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the bond
|
||||
// module (G-008 split). ValidateGenesis in types.go composes these helpers;
|
||||
// the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Bond genesis schema has one top-level set: Bonds (the issued bonds).
|
||||
// The invariants enforced at genesis load are (1) bond-id uniqueness, and
|
||||
// (2) the coupon clamp — each genesis bond's coupon-bps must be within
|
||||
// [CouponFloorBps, CouponCapBps]. The clamp invariant is the highest-severity
|
||||
// bond firewall (D-028): a genesis bond with a coupon above the cap or below
|
||||
// the floor is rejected at genesis load.
|
||||
|
||||
// ValidateBonds asserts bond-ids are present and unique, that each bond's
|
||||
// status is a known BondStatus, and that each bond's coupon-bps is within
|
||||
// the LOCKED bounds [CouponFloorBps, CouponCapBps] (the genesis-side clamp
|
||||
// enforcement — D-028). ValidateBonds is the data-engineer's schema
|
||||
// validator, composed by ValidateGenesis in types.go.
|
||||
func ValidateBonds(bonds []Bond) error {
|
||||
seen := make(map[string]bool, len(bonds))
|
||||
for i, b := range bonds {
|
||||
if b.BondID == "" {
|
||||
return fmt.Errorf("bond [%d]: empty bond-id", i)
|
||||
}
|
||||
if seen[b.BondID] {
|
||||
return fmt.Errorf("bond: duplicate bond-id %q", b.BondID)
|
||||
}
|
||||
seen[b.BondID] = true
|
||||
if !knownBondStatus(b.Status) {
|
||||
return fmt.Errorf("bond %q: unknown bond status %q", b.BondID, b.Status)
|
||||
}
|
||||
// Genesis-side clamp enforcement (D-028): a genesis bond's coupon
|
||||
// must be within the LOCKED [floor, cap] bounds. A bond with an
|
||||
// out-of-bounds coupon is rejected at genesis load rather than
|
||||
// silently clamped — the genesis schema is authoritative.
|
||||
if b.CouponBps < CouponFloorBps || b.CouponBps > CouponCapBps {
|
||||
return fmt.Errorf("bond %q: coupon-bps %d outside [%d, %d] (D-028 clamp at genesis load)",
|
||||
b.BondID, b.CouponBps, CouponFloorBps, CouponCapBps)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownBondStatus reports whether s is one of the five BondStatus values.
|
||||
func knownBondStatus(s BondStatus) bool {
|
||||
for _, ss := range AllBondStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
btypes "github.com/oy/openyield/x/bond/types"
|
||||
)
|
||||
|
||||
// genesis_test.go holds the security-engineer's genesis-clamp test assertions
|
||||
// for the bond module (G-008 — security-engineer owns ALL *_test.go files,
|
||||
// including genesis_test.go). These tests focus on the data-engineer's
|
||||
// genesis schema clamp enforcement (P4-01-03): ValidateGenesis rejects any
|
||||
// genesis bond whose coupon-bps is outside the LOCKED [floor, cap] bounds.
|
||||
// The clamp invariant (D-028) is the highest-severity bond firewall; the
|
||||
// genesis load is the first enforcement point.
|
||||
|
||||
// TestGenesisClampRejectsAboveCapForManyBonds asserts that multiple bonds,
|
||||
// each with a coupon above the cap, are all rejected. The genesis clamp
|
||||
// applies per-bond (not just the first).
|
||||
func TestGenesisClampRejectsAboveCapForManyBonds(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{
|
||||
{BondID: "b1", IssuerStandID: "s1", CouponBps: 801, Status: btypes.BondIssued},
|
||||
{BondID: "b2", IssuerStandID: "s1", CouponBps: 900, Status: btypes.BondActive},
|
||||
{BondID: "b3", IssuerStandID: "s1", CouponBps: 5000, Status: btypes.BondMatured},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject bonds with coupon-bps above cap")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisClampAcceptsAtBounds asserts bonds at the floor (0) and cap (800)
|
||||
// are accepted at genesis load (boundary inclusive).
|
||||
func TestGenesisClampAcceptsAtBounds(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{
|
||||
{BondID: "b-floor", IssuerStandID: "s1", CouponBps: 0, Status: btypes.BondIssued},
|
||||
{BondID: "b-cap", IssuerStandID: "s1", CouponBps: 800, Status: btypes.BondIssued},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept bonds at floor (0) and cap (800); got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisClampRejectsJustAboveCap asserts a coupon 1 bps above the cap is
|
||||
// rejected (off-by-one regression firewall).
|
||||
func TestGenesisClampRejectsJustAboveCap(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 801, Status: btypes.BondIssued}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject coupon-bps == 801 (just above cap 800)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisClampAcceptsJustBelowCap asserts a coupon 1 bps below the cap is
|
||||
// accepted.
|
||||
func TestGenesisClampAcceptsJustBelowCap(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 799, Status: btypes.BondIssued}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept coupon-bps == 799 (just below cap); got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisValidateBondsRejectsDup asserts the data-engineer's ValidateBonds
|
||||
// helper rejects duplicate bond-ids.
|
||||
func TestGenesisValidateBondsRejectsDup(t *testing.T) {
|
||||
bonds := []btypes.Bond{
|
||||
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
|
||||
{BondID: "b1", IssuerStandID: "s2", CouponBps: 200, Status: btypes.BondActive},
|
||||
}
|
||||
if err := btypes.ValidateBonds(bonds); err == nil {
|
||||
t.Error("ValidateBonds should reject duplicate bond-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGenesisValidateBondsAcceptsClean asserts ValidateBonds accepts a clean
|
||||
// set of bonds.
|
||||
func TestGenesisValidateBondsAcceptsClean(t *testing.T) {
|
||||
bonds := []btypes.Bond{
|
||||
{BondID: "b1", IssuerStandID: "s1", CouponBps: 0, Status: btypes.BondIssued},
|
||||
{BondID: "b2", IssuerStandID: "s1", CouponBps: 500, Status: btypes.BondActive},
|
||||
{BondID: "b3", IssuerStandID: "s2", CouponBps: 800, Status: btypes.BondMatured},
|
||||
}
|
||||
if err := btypes.ValidateBonds(bonds); err != nil {
|
||||
t.Errorf("ValidateBonds should accept clean bonds; got: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "bond"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// CouponCapBps is the upper bound on a bond coupon in basis points
|
||||
// (vision §17, REQ-021, D-028). Mission-locked at 8pct (800 bps); no
|
||||
// Council vote can change it. The bond module is the highest lexicon-risk
|
||||
// package (A-210): the coupon vocabulary is used EXCLUSIVELY here — the
|
||||
// banned financial terms that are natural coupon-synonyms are NEVER used
|
||||
// in this package. The security-engineer's lexicon assertion in
|
||||
// types_test.go is the firewall gate.
|
||||
CouponCapBps = 800 // 8pct (cap, LOCKED — D-028)
|
||||
|
||||
// CouponFloorBps is the lower bound on a bond coupon in basis points
|
||||
// (vision §17, REQ-021, D-028). Mission-locked at 0pct (0 bps); no
|
||||
// Council vote can change it.
|
||||
CouponFloorBps = 0 // 0pct (floor, LOCKED — D-028)
|
||||
|
||||
// BondStatusCount is the locked count of BondStatus enum values (vision
|
||||
// §17, REQ-021). A regression firewall: adding/removing/renaming a bond
|
||||
// status breaks this const's test.
|
||||
BondStatusCount = 5
|
||||
)
|
||||
|
||||
// BondStatus enumerates the bond lifecycle states (vision §17, REQ-021).
|
||||
// The five statuses mirror a fixed-coupon commitment lifecycle: Issued
|
||||
// (created), Active (in good standing), Matured (term reached), Defaulted
|
||||
// (covenant breach), Repaid (principal returned).
|
||||
type BondStatus string
|
||||
|
||||
const (
|
||||
BondIssued BondStatus = "Issued" // created, not yet active
|
||||
BondActive BondStatus = "Active" // in good standing
|
||||
BondMatured BondStatus = "Matured" // term reached
|
||||
BondDefaulted BondStatus = "Defaulted" // covenant breach
|
||||
BondRepaid BondStatus = "Repaid" // principal returned
|
||||
)
|
||||
|
||||
// AllBondStatuses returns all five BondStatus values in REQ-021 lifecycle
|
||||
// order. Locked-const test asserts exactly 5 entries with these names.
|
||||
func AllBondStatuses() []BondStatus {
|
||||
return []BondStatus{
|
||||
BondIssued,
|
||||
BondActive,
|
||||
BondMatured,
|
||||
BondDefaulted,
|
||||
BondRepaid,
|
||||
}
|
||||
}
|
||||
|
||||
// Bond is a fixed-coupon commitment issued by a Stand (vision §17, REQ-021).
|
||||
// issuer-stand-id references x/stand by ID string (G-003 by-ID-string ref —
|
||||
// P1-02-01 stand-id-ref; no struct import of x/stand). principal-grain is the
|
||||
// principal in Grain (the OY internal unit, cross-ref x/bread). coupon-bps is
|
||||
// the coupon rate in basis points, clamped to [CouponFloorBps, CouponCapBps]
|
||||
// by Clamp at issuance and at genesis load. term-days is the term length.
|
||||
// issued-at and maturity are unix timestamps. status is the lifecycle state.
|
||||
type Bond struct {
|
||||
BondID string `json:"bond_id" yaml:"bond_id"`
|
||||
IssuerStandID string `json:"issuer_stand_id" yaml:"issuer_stand_id"`
|
||||
PrincipalGrain int64 `json:"principal_grain" yaml:"principal_grain"`
|
||||
CouponBps uint32 `json:"coupon_bps" yaml:"coupon_bps"`
|
||||
TermDays uint32 `json:"term_days" yaml:"term_days"`
|
||||
IssuedAt int64 `json:"issued_at" yaml:"issued_at"`
|
||||
Maturity int64 `json:"maturity" yaml:"maturity"`
|
||||
Status BondStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// Issue is the bond issuance stub (REQ-021, D-028). It constructs a Bond with
|
||||
// the coupon clamped to [CouponFloorBps, CouponCapBps]. The stub does not
|
||||
// persist or enforce referential integrity of issuer-stand-id (that is a
|
||||
// v0.3 keeper concern); it only enforces the coupon clamp invariant at
|
||||
// construction time. The returned Bond has status BondIssued.
|
||||
func Issue(bondID, issuerStandID string, principalGrain int64, couponBps uint32, termDays uint32, issuedAt, maturity int64) Bond {
|
||||
return Bond{
|
||||
BondID: bondID,
|
||||
IssuerStandID: issuerStandID,
|
||||
PrincipalGrain: principalGrain,
|
||||
CouponBps: Clamp(couponBps),
|
||||
TermDays: termDays,
|
||||
IssuedAt: issuedAt,
|
||||
Maturity: maturity,
|
||||
Status: BondIssued,
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp ensures a coupon is within the LOCKED bounds (vision §17, REQ-021,
|
||||
// D-028: never above the cap, never below the floor). This is automatic and
|
||||
// authoritative; no Council vote can change it. The shape mirrors
|
||||
// x/feecovenant's Clamp exactly (min(cap, max(floor, coupon))).
|
||||
func Clamp(couponBps uint32) uint32 {
|
||||
if couponBps > CouponCapBps {
|
||||
return CouponCapBps
|
||||
}
|
||||
if couponBps < CouponFloorBps {
|
||||
return CouponFloorBps
|
||||
}
|
||||
return couponBps
|
||||
}
|
||||
|
||||
// Params for the bond module (skeleton — no tunables in v0.2; the cap and
|
||||
// floor are LOCKED consts, not Params fields).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the bond module genesis state (REQ-021). Bonds is the
|
||||
// top-level set of issued bonds. ValidateGenesis enforces bond-id uniqueness
|
||||
// and the coupon clamp at genesis load (the data-engineer's genesis.go holds
|
||||
// the schema helpers per G-008).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Bonds []Bond `json:"bonds" yaml:"bonds"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Bonds: []Bond{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate bond-ids, and runs the coupon clamp at genesis
|
||||
// load (each genesis bond's coupon-bps must be within [floor, cap]). 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("bond: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateBonds(gs.Bonds); err != nil {
|
||||
return fmt.Errorf("bond: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
btypes "github.com/oy/openyield/x/bond/types"
|
||||
)
|
||||
|
||||
// --- Clamp invariant tests (highest-severity for bond) --------------------------
|
||||
// The Clamp invariant is the bond module's firewall (D-028): a bond coupon
|
||||
// can never exceed the cap (8pct) and can never fall below the floor (0pct).
|
||||
// These tests are the regression firewall — a change to CouponCapBps or
|
||||
// CouponFloorBps breaks them.
|
||||
|
||||
// TestCouponCapBpsLockedConst asserts CouponCapBps == 800 (8pct, D-028 LOCKED).
|
||||
// A regression firewall: changing the cap breaks this test.
|
||||
func TestCouponCapBpsLockedConst(t *testing.T) {
|
||||
if btypes.CouponCapBps != 800 {
|
||||
t.Errorf("CouponCapBps = %d, expected 800 (8pct — D-028 LOCKED)", btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCouponFloorBpsLockedConst asserts CouponFloorBps == 0 (0pct, D-028 LOCKED).
|
||||
// A regression firewall: changing the floor breaks this test.
|
||||
func TestCouponFloorBpsLockedConst(t *testing.T) {
|
||||
if btypes.CouponFloorBps != 0 {
|
||||
t.Errorf("CouponFloorBps = %d, expected 0 (0pct — D-028 LOCKED)", btypes.CouponFloorBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampBelowFloorReturnsFloor asserts a coupon below the floor is clamped
|
||||
// up to the floor.
|
||||
func TestClampBelowFloorReturnsFloor(t *testing.T) {
|
||||
// Negative coupons are not representable (uint32); the only "below floor"
|
||||
// case is impossible since the floor is 0 and the type is uint32. The test
|
||||
// asserts the floor value itself passes through (the in-range boundary).
|
||||
// A future floor > 0 would make this test assert negative-clamping; the
|
||||
// current floor == 0 means the below-floor case is type-prevented.
|
||||
got := btypes.Clamp(btypes.CouponFloorBps)
|
||||
if got != btypes.CouponFloorBps {
|
||||
t.Errorf("Clamp(floor) = %d, expected floor %d", got, btypes.CouponFloorBps)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampAboveCapReturnsCap asserts a coupon above the cap is clamped down
|
||||
// to the cap.
|
||||
func TestClampAboveCapReturnsCap(t *testing.T) {
|
||||
cases := []uint32{
|
||||
uint32(btypes.CouponCapBps) + 1,
|
||||
uint32(btypes.CouponCapBps) + 100,
|
||||
uint32(btypes.CouponCapBps) + 1000,
|
||||
900,
|
||||
1000,
|
||||
5000,
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := btypes.Clamp(c)
|
||||
if got != btypes.CouponCapBps {
|
||||
t.Errorf("Clamp(%d) = %d, expected cap %d (above-cap must clamp to cap)", c, got, btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampInRangeUnchanged asserts a coupon within [floor, cap] is unchanged.
|
||||
func TestClampInRangeUnchanged(t *testing.T) {
|
||||
cases := []uint32{
|
||||
0,
|
||||
1,
|
||||
100,
|
||||
400,
|
||||
500,
|
||||
799,
|
||||
uint32(btypes.CouponCapBps),
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := btypes.Clamp(c)
|
||||
if got != c {
|
||||
t.Errorf("Clamp(%d) = %d, expected %d (in-range must be unchanged)", c, got, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampMatchesFeeCovenantShape asserts the bond Clamp has the same shape
|
||||
// as x/feecovenant's Clamp: min(cap, max(floor, coupon)). The test verifies
|
||||
// the boundary semantics rather than importing feecovenant (no cross-module
|
||||
// struct imports per G-003, though cross-module const access is allowed).
|
||||
func TestClampMatchesFeeCovenantShape(t *testing.T) {
|
||||
// The shape is min(cap, max(floor, coupon)). For floor=0 and cap=800:
|
||||
// min(800, max(0, coupon))
|
||||
// In-range passes through; above-cap clamps to cap; below-floor clamps to
|
||||
// floor (here, floor=0, so type-prevented for uint32).
|
||||
if btypes.Clamp(0) != 0 {
|
||||
t.Error("Clamp(0) should be 0 (floor boundary)")
|
||||
}
|
||||
if btypes.Clamp(800) != 800 {
|
||||
t.Error("Clamp(800) should be 800 (cap boundary)")
|
||||
}
|
||||
if btypes.Clamp(801) != 800 {
|
||||
t.Error("Clamp(801) should be 800 (above-cap clamps to cap)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClampInvariantBreaksIfCapChanges is the regression-firewall meta-assert:
|
||||
// if CouponCapBps were changed, the above-cap test would break. This test
|
||||
// documents the invariant: Clamp(above-cap) == cap, for the current cap.
|
||||
func TestClampInvariantBreaksIfCapChanges(t *testing.T) {
|
||||
above := uint32(btypes.CouponCapBps) + 50
|
||||
if btypes.Clamp(above) != btypes.CouponCapBps {
|
||||
t.Errorf("Clamp(%d) = %d, expected CouponCapBps %d (invariant: above-cap clamps to cap)", above, btypes.Clamp(above), btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// --- BondStatus enum coverage (5) ----------------------------------------------
|
||||
|
||||
// TestBondStatusCountLockedConst asserts BondStatusCount == 5 and
|
||||
// AllBondStatuses() returns exactly 5 (REQ-021). A regression firewall.
|
||||
func TestBondStatusCountLockedConst(t *testing.T) {
|
||||
if btypes.BondStatusCount != 5 {
|
||||
t.Errorf("BondStatusCount = %d, expected 5 (REQ-021 LOCKED)", btypes.BondStatusCount)
|
||||
}
|
||||
all := btypes.AllBondStatuses()
|
||||
if len(all) != 5 {
|
||||
t.Errorf("AllBondStatuses() len = %d, expected 5", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllBondStatusesNames asserts the 5 REQ-021 names in order with no
|
||||
// extras, no dups, no renames.
|
||||
func TestAllBondStatusesNames(t *testing.T) {
|
||||
want := []string{"Issued", "Active", "Matured", "Defaulted", "Repaid"}
|
||||
all := btypes.AllBondStatuses()
|
||||
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("AllBondStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate BondStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestBondStatusValues asserts each named const matches its AllBondStatuses
|
||||
// entry.
|
||||
func TestBondStatusValues(t *testing.T) {
|
||||
if btypes.BondIssued != "Issued" {
|
||||
t.Errorf("BondIssued = %q", btypes.BondIssued)
|
||||
}
|
||||
if btypes.BondActive != "Active" {
|
||||
t.Errorf("BondActive = %q", btypes.BondActive)
|
||||
}
|
||||
if btypes.BondMatured != "Matured" {
|
||||
t.Errorf("BondMatured = %q", btypes.BondMatured)
|
||||
}
|
||||
if btypes.BondDefaulted != "Defaulted" {
|
||||
t.Errorf("BondDefaulted = %q", btypes.BondDefaulted)
|
||||
}
|
||||
if btypes.BondRepaid != "Repaid" {
|
||||
t.Errorf("BondRepaid = %q", btypes.BondRepaid)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Issue stub callable -------------------------------------------------------
|
||||
|
||||
// TestIssueStubCallable asserts the Issue stub is callable and returns a
|
||||
// Bond with the coupon clamped and status BondIssued.
|
||||
func TestIssueStubCallable(t *testing.T) {
|
||||
b := btypes.Issue("bond-1", "stand-abc", 1_000_000, 500, 365, 1000, 1365)
|
||||
if b.BondID != "bond-1" {
|
||||
t.Errorf("BondID = %q", b.BondID)
|
||||
}
|
||||
if b.IssuerStandID != "stand-abc" {
|
||||
t.Errorf("IssuerStandID = %q", b.IssuerStandID)
|
||||
}
|
||||
if b.PrincipalGrain != 1_000_000 {
|
||||
t.Errorf("PrincipalGrain = %d", b.PrincipalGrain)
|
||||
}
|
||||
if b.CouponBps != 500 {
|
||||
t.Errorf("CouponBps = %d, expected 500 (in-range, unchanged)", b.CouponBps)
|
||||
}
|
||||
if b.TermDays != 365 {
|
||||
t.Errorf("TermDays = %d", b.TermDays)
|
||||
}
|
||||
if b.IssuedAt != 1000 || b.Maturity != 1365 {
|
||||
t.Errorf("IssuedAt=%d Maturity=%d", b.IssuedAt, b.Maturity)
|
||||
}
|
||||
if b.Status != btypes.BondIssued {
|
||||
t.Errorf("Status = %q, expected Issued", b.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIssueStubClampsAboveCap asserts the Issue stub clamps an above-cap
|
||||
// coupon down to the cap.
|
||||
func TestIssueStubClampsAboveCap(t *testing.T) {
|
||||
b := btypes.Issue("bond-2", "stand-abc", 1_000_000, 1200, 365, 1000, 1365)
|
||||
if b.CouponBps != btypes.CouponCapBps {
|
||||
t.Errorf("CouponBps = %d, expected cap %d (Issue must clamp above-cap coupon)", b.CouponBps, btypes.CouponCapBps)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bond struct fields --------------------------------------------------------
|
||||
|
||||
// TestBondStructFields asserts the Bond struct carries all required fields
|
||||
// including the by-ID-string ref to x/stand (issuer-stand-id per G-003).
|
||||
func TestBondStructFields(t *testing.T) {
|
||||
b := btypes.Bond{
|
||||
BondID: "bond-3",
|
||||
IssuerStandID: "stand-xyz",
|
||||
PrincipalGrain: 500_000,
|
||||
CouponBps: 300,
|
||||
TermDays: 180,
|
||||
IssuedAt: 2000,
|
||||
Maturity: 2180,
|
||||
Status: btypes.BondActive,
|
||||
}
|
||||
if b.BondID != "bond-3" || b.IssuerStandID != "stand-xyz" || b.PrincipalGrain != 500_000 ||
|
||||
b.CouponBps != 300 || b.TermDays != 180 || b.IssuedAt != 2000 || b.Maturity != 2180 ||
|
||||
b.Status != btypes.BondActive {
|
||||
t.Error("Bond fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBondIssuerStandIDIsString asserts issuer-stand-id is string-typed
|
||||
// (G-003 by-ID-string ref to x/stand; no struct import).
|
||||
func TestBondIssuerStandIDIsString(t *testing.T) {
|
||||
b := btypes.Bond{IssuerStandID: "stand-abc"}
|
||||
if b.IssuerStandID != "stand-abc" {
|
||||
t.Errorf("IssuerStandID = %q", b.IssuerStandID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis -------------------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slice for Bonds.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := btypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Bonds == nil || len(gs.Bonds) != 0 {
|
||||
t.Errorf("Default Bonds should be non-nil empty slice; got len=%d nil=%v", len(gs.Bonds), gs.Bonds == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupBondIDs asserts A-212: duplicate bond-ids are
|
||||
// rejected.
|
||||
func TestValidateGenesisRejectsDupBondIDs(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{
|
||||
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
|
||||
{BondID: "b1", IssuerStandID: "s2", CouponBps: 200, Status: btypes.BondActive}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate bond-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyBondID asserts empty bond-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyBondID(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{{BondID: "", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty bond-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownBondStatus asserts an unknown BondStatus
|
||||
// is rejected.
|
||||
func TestValidateGenesisRejectsUnknownBondStatus(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown bond status")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsCouponAboveCap asserts the genesis-side clamp: a
|
||||
// genesis bond with coupon-bps above the cap is rejected (D-028).
|
||||
func TestValidateGenesisRejectsCouponAboveCap(t *testing.T) {
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: uint32(btypes.CouponCapBps) + 1, Status: btypes.BondIssued}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject coupon-bps above cap (D-028 clamp at genesis load)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsCouponBelowFloor asserts the genesis-side clamp:
|
||||
// a genesis bond with coupon-bps below the floor is rejected (D-028).
|
||||
func TestValidateGenesisRejectsCouponBelowFloor(t *testing.T) {
|
||||
// Floor is 0; a uint32 cannot be below 0, so this test asserts the
|
||||
// boundary: coupon-bps == 0 (the floor) is accepted. The below-floor case
|
||||
// is type-prevented. We assert the floor boundary passes.
|
||||
gs := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{{BondID: "b1", IssuerStandID: "s1", CouponBps: 0, Status: btypes.BondIssued}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept coupon-bps == floor (0); got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := btypes.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 := btypes.GenesisState{
|
||||
Bonds: []btypes.Bond{
|
||||
{BondID: "b1", IssuerStandID: "s1", CouponBps: 100, Status: btypes.BondIssued},
|
||||
{BondID: "b2", IssuerStandID: "s1", CouponBps: 800, Status: btypes.BondActive},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := btypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts -------------------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if btypes.ModuleName != "bond" {
|
||||
t.Errorf("ModuleName = %q", btypes.ModuleName)
|
||||
}
|
||||
if btypes.StoreKey != "bond" {
|
||||
t.Errorf("StoreKey = %q", btypes.StoreKey)
|
||||
}
|
||||
if btypes.RouterKey != "bond" {
|
||||
t.Errorf("RouterKey = %q", btypes.RouterKey)
|
||||
}
|
||||
if btypes.QuerierRoute != "bond" {
|
||||
t.Errorf("QuerierRoute = %q", btypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = btypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
// The bond module is the HIGHEST lexicon-risk package (A-210): the banned
|
||||
// terms that are natural coupon-synonyms ("intere"+"st", "yie"+"ld") must
|
||||
// NEVER appear. The coupon vocabulary is used EXCLUSIVELY. The lexicon
|
||||
// helpers are used here — no banned literals are inlined in this test file.
|
||||
|
||||
// TestLexiconNoBannedTermsInBondPackage scans every non-test .go file in the
|
||||
// bond/types package directory for the banned terms (case-insensitive).
|
||||
// Production files only — the test file references banned terms via the
|
||||
// lexicon package helpers (standard lexicon-test bootstrapping pattern).
|
||||
func TestLexiconNoBannedTermsInBondPackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/bond/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 bond/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 — A-210 coupon-only vocabulary)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInBondTestFile 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 TestLexiconNoBannedTermsInBondTestFile(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("bond test file contains banned term %q — use lexicon helpers, not literals (A-210)", 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/bond/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,59 @@
|
||||
package types
|
||||
|
||||
import "fmt"
|
||||
|
||||
// genesis.go holds the data-engineer's genesis schema helpers for the
|
||||
// satellite module (G-008 split). ValidateGenesis in types.go composes these
|
||||
// helpers; the security-engineer's test assertions live in types_test.go.
|
||||
//
|
||||
// The Satellite genesis schema has two top-level sets: Channels (the IBC
|
||||
// transfer channels between OY Chain and L2 satellites) and Denoms (the
|
||||
// wrapped Bread denoms). The invariants enforced at genesis load are
|
||||
// (1) channel-id uniqueness, (2) denom uniqueness, and (3) each channel's
|
||||
// status is a known ChannelStatus.
|
||||
|
||||
// ValidateChannels asserts channel-ids are present and unique, and that
|
||||
// each channel's status is a known ChannelStatus. ValidateChannels is the
|
||||
// data-engineer's schema validator, composed by ValidateGenesis in types.go.
|
||||
func ValidateChannels(channels []TransferChannel) error {
|
||||
seen := make(map[string]bool, len(channels))
|
||||
for i, c := range channels {
|
||||
if c.ChannelID == "" {
|
||||
return fmt.Errorf("channel [%d]: empty channel-id", i)
|
||||
}
|
||||
if seen[c.ChannelID] {
|
||||
return fmt.Errorf("channel: duplicate channel-id %q", c.ChannelID)
|
||||
}
|
||||
seen[c.ChannelID] = true
|
||||
if !knownChannelStatus(c.Status) {
|
||||
return fmt.Errorf("channel %q: unknown channel status %q", c.ChannelID, c.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateDenoms asserts denoms are present and unique. ValidateDenoms is
|
||||
// the data-engineer's schema validator for the wrapped Bread denom set.
|
||||
func ValidateDenoms(denoms []WrappedBreadDenom) error {
|
||||
seen := make(map[string]bool, len(denoms))
|
||||
for i, d := range denoms {
|
||||
if d.Denom == "" {
|
||||
return fmt.Errorf("denom [%d]: empty denom", i)
|
||||
}
|
||||
if seen[d.Denom] {
|
||||
return fmt.Errorf("denom: duplicate denom %q", d.Denom)
|
||||
}
|
||||
seen[d.Denom] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// knownChannelStatus reports whether s is one of the four ChannelStatus values.
|
||||
func knownChannelStatus(s ChannelStatus) bool {
|
||||
for _, ss := range AllChannelStatuses() {
|
||||
if s == ss {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "satellite"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// L2ChainCount is the locked count of L2Chain enum values (vision §10,
|
||||
// REQ-009, D-021). Five L2 satellite chains: Polygon (the one active
|
||||
// representative in v0.2) plus Base, Arbitrum, Optimism, Solana (four
|
||||
// StatusPending enum placeholders). A regression firewall:
|
||||
// adding/removing/renaming a chain breaks this const's test.
|
||||
L2ChainCount = 5
|
||||
|
||||
// ChannelStatusCount is the locked count of ChannelStatus enum values
|
||||
// (ICS-20 handshake): Init, TryOpen, Open, Closed. A regression firewall
|
||||
// for the ICS-20 handshake shape (A-215).
|
||||
ChannelStatusCount = 4
|
||||
)
|
||||
|
||||
// L2Chain enumerates the L2 satellite chains (vision §10, REQ-009, D-021).
|
||||
// Polygon is the one active representative in v0.2 (D-021 scopes v0.2 to ONE
|
||||
// representative chain). Base, Arbitrum, Optimism, and Solana are
|
||||
// StatusPending enum placeholders (the full 5-chain IBC rollout is Phase 3
|
||||
// per D-021). Solana lacks native IBC (RESEARCH §1.1) and is stubbed as
|
||||
// StatusPending — no Solana light-client logic in v0.2.
|
||||
type L2Chain string
|
||||
|
||||
const (
|
||||
ChainPolygon L2Chain = "Polygon" // active representative (D-021)
|
||||
ChainBase L2Chain = "Base" // StatusPending placeholder
|
||||
ChainArbitrum L2Chain = "Arbitrum" // StatusPending placeholder
|
||||
ChainOptimism L2Chain = "Optimism" // StatusPending placeholder
|
||||
ChainSolana L2Chain = "Solana" // StatusPending placeholder (no native IBC)
|
||||
)
|
||||
|
||||
// ChainActivation is the activation state of an L2 chain (D-021): Active
|
||||
// (Polygon in v0.2) or StatusPending (the four stubs).
|
||||
type ChainActivation string
|
||||
|
||||
const (
|
||||
ChainActive ChainActivation = "Active" // chain is live for IBC transfer
|
||||
ChainStatusPending ChainActivation = "StatusPending" // chain is a placeholder (Phase 3 rollout)
|
||||
)
|
||||
|
||||
// ChainInfo describes an L2 chain's properties (REQ-009, D-021).
|
||||
type ChainInfo struct {
|
||||
Chain L2Chain `json:"chain" yaml:"chain"`
|
||||
Activation ChainActivation `json:"activation" yaml:"activation"`
|
||||
}
|
||||
|
||||
// AllL2Chains returns all five L2Chain values (Polygon + 4 stubs) with their
|
||||
// activation states (D-021). Locked-const test asserts exactly 5 entries.
|
||||
// Polygon is the only ChainActive entry; the other four are StatusPending.
|
||||
func AllL2Chains() []ChainInfo {
|
||||
return []ChainInfo{
|
||||
{ChainPolygon, ChainActive},
|
||||
{ChainBase, ChainStatusPending},
|
||||
{ChainArbitrum, ChainStatusPending},
|
||||
{ChainOptimism, ChainStatusPending},
|
||||
{ChainSolana, ChainStatusPending},
|
||||
}
|
||||
}
|
||||
|
||||
// ChannelStatus enumerates the ICS-20 channel handshake states (A-215):
|
||||
// Init (channel initialized), TryOpen (counterparty trying to open), Open
|
||||
// (channel established), Closed (channel closed). The four-state handshake
|
||||
// mirrors ibc-go ICS-20 v1 channel state (stable, widely implemented).
|
||||
type ChannelStatus string
|
||||
|
||||
const (
|
||||
ChannelInit ChannelStatus = "Init" // channel initialized
|
||||
ChannelTryOpen ChannelStatus = "TryOpen" // counterparty trying to open
|
||||
ChannelOpen ChannelStatus = "Open" // channel established
|
||||
ChannelClosed ChannelStatus = "Closed" // channel closed
|
||||
)
|
||||
|
||||
// AllChannelStatuses returns all four ChannelStatus values in ICS-20
|
||||
// handshake order. Locked-const test asserts exactly 4 entries.
|
||||
func AllChannelStatuses() []ChannelStatus {
|
||||
return []ChannelStatus{
|
||||
ChannelInit,
|
||||
ChannelTryOpen,
|
||||
ChannelOpen,
|
||||
ChannelClosed,
|
||||
}
|
||||
}
|
||||
|
||||
// TransferChannel is an IBC transfer channel between OY Chain (L1) and an L2
|
||||
// satellite (REQ-009, A-215). port-id and channel-id are the ICS-20 port and
|
||||
// channel identifiers (e.g. "transfer" / "channel-0"). counterparty is the
|
||||
// counterparty port+channel on the L2. status is the handshake state.
|
||||
type TransferChannel struct {
|
||||
PortID string `json:"port_id" yaml:"port_id"`
|
||||
ChannelID string `json:"channel_id" yaml:"channel_id"`
|
||||
Counterparty string `json:"counterparty" yaml:"counterparty"`
|
||||
Status ChannelStatus `json:"status" yaml:"status"`
|
||||
}
|
||||
|
||||
// WrappedBreadDenom encodes an IBC-traced wrapped Bread denom (REQ-009,
|
||||
// A-215). When Bread propagates from OY Chain (L1) to an L2 via IBC, the
|
||||
// denom on the L2 is the original denom prefixed with the IBC trace path
|
||||
// (e.g. "transfer/channel-0/bread"). denom is the full traced denom on the
|
||||
// destination chain; trace-path is the IBC trace (the port/channel hops).
|
||||
type WrappedBreadDenom struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
TracePath string `json:"trace_path" yaml:"trace_path"`
|
||||
}
|
||||
|
||||
// Packet is the ICS-20 v1 packet shape stub (REQ-009, A-215). Pinned to the
|
||||
// ICS-20 v1 channel packet shape (stable, widely implemented) to minimize
|
||||
// churn if a different ibc-go version is chosen in Phase 3. Fields:
|
||||
// sequence, source-port, source-channel, dest-port, dest-channel, data,
|
||||
// timeout-height, timeout-timestamp. NO ibc-go import — zero external deps
|
||||
// (A-201); the type is a self-contained Go struct.
|
||||
type Packet struct {
|
||||
Sequence uint64 `json:"sequence" yaml:"sequence"`
|
||||
SourcePort string `json:"source_port" yaml:"source_port"`
|
||||
SourceChannel string `json:"source_channel" yaml:"source_channel"`
|
||||
DestPort string `json:"dest_port" yaml:"dest_port"`
|
||||
DestChannel string `json:"dest_channel" yaml:"dest_channel"`
|
||||
Data []byte `json:"data" yaml:"data"`
|
||||
TimeoutHeight uint64 `json:"timeout_height" yaml:"timeout_height"`
|
||||
TimeoutTimestamp uint64 `json:"timeout_timestamp" yaml:"timeout_timestamp"`
|
||||
}
|
||||
|
||||
// Params for the satellite module (skeleton — no tunables in v0.2).
|
||||
type Params struct{}
|
||||
|
||||
func DefaultParams() Params { return Params{} }
|
||||
|
||||
// GenesisState defines the satellite module genesis state (REQ-009).
|
||||
// Channels is the set of IBC transfer channels; Denoms is the set of wrapped
|
||||
// Bread denoms. ValidateGenesis enforces channel-id uniqueness and denom
|
||||
// uniqueness. The data-engineer's genesis.go holds the schema helpers (G-008).
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Channels []TransferChannel `json:"channels" yaml:"channels"`
|
||||
Denoms []WrappedBreadDenom `json:"denoms" yaml:"denoms"`
|
||||
}
|
||||
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Channels: []TransferChannel{},
|
||||
Denoms: []WrappedBreadDenom{},
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212 upgrade from v0.1
|
||||
// no-op): rejects duplicate channel-ids and duplicate denoms. 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("satellite: invalid genesis: %w", err)
|
||||
}
|
||||
if err := ValidateChannels(gs.Channels); err != nil {
|
||||
return fmt.Errorf("satellite: %w", err)
|
||||
}
|
||||
if err := ValidateDenoms(gs.Denoms); err != nil {
|
||||
return fmt.Errorf("satellite: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
stypes "github.com/oy/openyield/x/satellite/types"
|
||||
)
|
||||
|
||||
// --- L2Chain enum (exactly 5, Polygon active + 4 stubs) ------------------------
|
||||
|
||||
// TestL2ChainCountLockedConst asserts L2ChainCount == 5 and AllL2Chains()
|
||||
// returns exactly 5 (REQ-009, D-021). A regression firewall.
|
||||
func TestL2ChainCountLockedConst(t *testing.T) {
|
||||
if stypes.L2ChainCount != 5 {
|
||||
t.Errorf("L2ChainCount = %d, expected 5 (REQ-009, D-021 LOCKED)", stypes.L2ChainCount)
|
||||
}
|
||||
all := stypes.AllL2Chains()
|
||||
if len(all) != 5 {
|
||||
t.Errorf("AllL2Chains() len = %d, expected 5", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllL2ChainsNames asserts the 5 chain names in order with no extras, no
|
||||
// dups, no renames (D-021: Polygon + Base/Arbitrum/Optimism/Solana).
|
||||
func TestAllL2ChainsNames(t *testing.T) {
|
||||
want := []string{"Polygon", "Base", "Arbitrum", "Optimism", "Solana"}
|
||||
all := stypes.AllL2Chains()
|
||||
if len(all) != len(want) {
|
||||
t.Fatalf("len = %d, want %d", len(all), len(want))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for i, c := range all {
|
||||
if string(c.Chain) != want[i] {
|
||||
t.Errorf("AllL2Chains()[%d].Chain = %q, want %q", i, c.Chain, want[i])
|
||||
}
|
||||
if seen[string(c.Chain)] {
|
||||
t.Errorf("duplicate L2Chain %q", c.Chain)
|
||||
}
|
||||
seen[string(c.Chain)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestL2ChainValues asserts each named const matches its AllL2Chains entry.
|
||||
func TestL2ChainValues(t *testing.T) {
|
||||
if stypes.ChainPolygon != "Polygon" {
|
||||
t.Errorf("ChainPolygon = %q", stypes.ChainPolygon)
|
||||
}
|
||||
if stypes.ChainBase != "Base" {
|
||||
t.Errorf("ChainBase = %q", stypes.ChainBase)
|
||||
}
|
||||
if stypes.ChainArbitrum != "Arbitrum" {
|
||||
t.Errorf("ChainArbitrum = %q", stypes.ChainArbitrum)
|
||||
}
|
||||
if stypes.ChainOptimism != "Optimism" {
|
||||
t.Errorf("ChainOptimism = %q", stypes.ChainOptimism)
|
||||
}
|
||||
if stypes.ChainSolana != "Solana" {
|
||||
t.Errorf("ChainSolana = %q", stypes.ChainSolana)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPolygonOnlyActiveRep asserts Polygon is the only ChainActive entry in
|
||||
// AllL2Chains (D-021: v0.2 scopes to ONE representative chain). The other
|
||||
// four must be StatusPending.
|
||||
func TestPolygonOnlyActiveRep(t *testing.T) {
|
||||
all := stypes.AllL2Chains()
|
||||
activeCount := 0
|
||||
for _, c := range all {
|
||||
if c.Activation == stypes.ChainActive {
|
||||
activeCount++
|
||||
if c.Chain != stypes.ChainPolygon {
|
||||
t.Errorf("chain %q is active, expected only Polygon (D-021)", c.Chain)
|
||||
}
|
||||
}
|
||||
if c.Activation == stypes.ChainStatusPending {
|
||||
if c.Chain == stypes.ChainPolygon {
|
||||
t.Error("Polygon must be active, not StatusPending (D-021)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if activeCount != 1 {
|
||||
t.Errorf("expected exactly 1 active chain (Polygon, D-021), got %d", activeCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFourStubsAreStatusPending asserts Base, Arbitrum, Optimism, Solana are
|
||||
// all StatusPending (D-021 — the 4 stubs).
|
||||
func TestFourStubsAreStatusPending(t *testing.T) {
|
||||
stubs := []stypes.L2Chain{stypes.ChainBase, stypes.ChainArbitrum, stypes.ChainOptimism, stypes.ChainSolana}
|
||||
all := stypes.AllL2Chains()
|
||||
activationByChain := map[string]stypes.ChainActivation{}
|
||||
for _, c := range all {
|
||||
activationByChain[string(c.Chain)] = c.Activation
|
||||
}
|
||||
for _, s := range stubs {
|
||||
if activationByChain[string(s)] != stypes.ChainStatusPending {
|
||||
t.Errorf("chain %q activation = %q, expected StatusPending (D-021)", s, activationByChain[string(s)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ChannelStatus enum (4 states) ---------------------------------------------
|
||||
|
||||
// TestChannelStatusCountLockedConst asserts ChannelStatusCount == 4 and
|
||||
// AllChannelStatuses() returns exactly 4 (A-215 ICS-20 handshake).
|
||||
func TestChannelStatusCountLockedConst(t *testing.T) {
|
||||
if stypes.ChannelStatusCount != 4 {
|
||||
t.Errorf("ChannelStatusCount = %d, expected 4 (A-215 ICS-20)", stypes.ChannelStatusCount)
|
||||
}
|
||||
all := stypes.AllChannelStatuses()
|
||||
if len(all) != 4 {
|
||||
t.Errorf("AllChannelStatuses() len = %d, expected 4", len(all))
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllChannelStatusesNames asserts the 4 ICS-20 handshake names in order.
|
||||
func TestAllChannelStatusesNames(t *testing.T) {
|
||||
want := []string{"Init", "TryOpen", "Open", "Closed"}
|
||||
all := stypes.AllChannelStatuses()
|
||||
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("AllChannelStatuses()[%d] = %q, want %q", i, s, want[i])
|
||||
}
|
||||
if seen[string(s)] {
|
||||
t.Errorf("duplicate ChannelStatus %q", s)
|
||||
}
|
||||
seen[string(s)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestChannelStatusValues asserts each named const.
|
||||
func TestChannelStatusValues(t *testing.T) {
|
||||
if stypes.ChannelInit != "Init" {
|
||||
t.Errorf("ChannelInit = %q", stypes.ChannelInit)
|
||||
}
|
||||
if stypes.ChannelTryOpen != "TryOpen" {
|
||||
t.Errorf("ChannelTryOpen = %q", stypes.ChannelTryOpen)
|
||||
}
|
||||
if stypes.ChannelOpen != "Open" {
|
||||
t.Errorf("ChannelOpen = %q", stypes.ChannelOpen)
|
||||
}
|
||||
if stypes.ChannelClosed != "Closed" {
|
||||
t.Errorf("ChannelClosed = %q", stypes.ChannelClosed)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Packet struct fields (ICS-20 v1 shape — A-215) ---------------------------
|
||||
|
||||
// TestPacketFieldsMatchICS20v1 asserts the Packet struct has exactly the 8
|
||||
// ICS-20 v1 fields with the expected names. A-215 pins the packet shape to
|
||||
// ICS-20 v1 to minimize churn. Cross-check field names via JSON tags.
|
||||
func TestPacketFieldsMatchICS20v1(t *testing.T) {
|
||||
p := stypes.Packet{
|
||||
Sequence: 42,
|
||||
SourcePort: "transfer",
|
||||
SourceChannel: "channel-0",
|
||||
DestPort: "transfer",
|
||||
DestChannel: "channel-1",
|
||||
Data: []byte("payload"),
|
||||
TimeoutHeight: 1000,
|
||||
TimeoutTimestamp: 9999999999,
|
||||
}
|
||||
if p.Sequence != 42 || p.SourcePort != "transfer" || p.SourceChannel != "channel-0" ||
|
||||
p.DestPort != "transfer" || p.DestChannel != "channel-1" ||
|
||||
len(p.Data) != 7 || p.TimeoutHeight != 1000 || p.TimeoutTimestamp != 9999999999 {
|
||||
t.Error("Packet fields not set correctly")
|
||||
}
|
||||
// ICS-20 v1 field-name parity: marshal and check JSON tags.
|
||||
bz, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
js := string(bz)
|
||||
wantTags := []string{
|
||||
`"sequence"`, `"source_port"`, `"source_channel"`, `"dest_port"`,
|
||||
`"dest_channel"`, `"data"`, `"timeout_height"`, `"timeout_timestamp"`,
|
||||
}
|
||||
for _, tag := range wantTags {
|
||||
if !strings.Contains(js, tag) {
|
||||
t.Errorf("Packet JSON missing tag %s (ICS-20 v1 shape parity A-215)", tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPacketICS20v1FieldCount asserts the Packet struct has exactly 8 fields
|
||||
// (the ICS-20 v1 shape). A regression firewall for packet-shape drift.
|
||||
func TestPacketICS20v1FieldCount(t *testing.T) {
|
||||
// The 8 ICS-20 v1 fields: sequence, source_port, source_channel,
|
||||
// dest_port, dest_channel, data, timeout_height, timeout_timestamp.
|
||||
// We verify by constructing a Packet with all 8 fields and asserting
|
||||
// each is independently settable to a non-zero value.
|
||||
p := stypes.Packet{
|
||||
Sequence: 1,
|
||||
SourcePort: "sp",
|
||||
SourceChannel: "sc",
|
||||
DestPort: "dp",
|
||||
DestChannel: "dc",
|
||||
Data: []byte{0x01},
|
||||
TimeoutHeight: 1,
|
||||
TimeoutTimestamp: 1,
|
||||
}
|
||||
if p.Sequence != 1 || p.SourcePort != "sp" || p.SourceChannel != "sc" ||
|
||||
p.DestPort != "dp" || p.DestChannel != "dc" || len(p.Data) != 1 ||
|
||||
p.TimeoutHeight != 1 || p.TimeoutTimestamp != 1 {
|
||||
t.Error("Packet does not have all 8 ICS-20 v1 fields independently settable")
|
||||
}
|
||||
}
|
||||
|
||||
// --- WrappedBreadDenom trace-path encoding ------------------------------------
|
||||
|
||||
// TestWrappedBreadDenomStruct asserts the WrappedBreadDenom struct carries
|
||||
// the denom and trace-path fields.
|
||||
func TestWrappedBreadDenomStruct(t *testing.T) {
|
||||
d := stypes.WrappedBreadDenom{
|
||||
Denom: "transfer/channel-0/bread",
|
||||
TracePath: "transfer/channel-0",
|
||||
}
|
||||
if d.Denom != "transfer/channel-0/bread" {
|
||||
t.Errorf("Denom = %q", d.Denom)
|
||||
}
|
||||
if d.TracePath != "transfer/channel-0" {
|
||||
t.Errorf("TracePath = %q", d.TracePath)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrappedBreadDenomTracePathEncoding asserts the IBC trace-path encoding
|
||||
// (REQ-009): the denom is the trace-path + "/" + original-denom.
|
||||
func TestWrappedBreadDenomTracePathEncoding(t *testing.T) {
|
||||
cases := []struct {
|
||||
trace string
|
||||
orig string
|
||||
}{
|
||||
{"transfer/channel-0", "bread"},
|
||||
{"transfer/channel-5", "bread"},
|
||||
{"transfer/channel-0/transfer/channel-3", "bread"}, // multi-hop
|
||||
}
|
||||
for _, c := range cases {
|
||||
full := c.trace + "/" + c.orig
|
||||
d := stypes.WrappedBreadDenom{Denom: full, TracePath: c.trace}
|
||||
if !strings.HasPrefix(d.Denom, d.TracePath) {
|
||||
t.Errorf("denom %q must start with trace-path %q", d.Denom, d.TracePath)
|
||||
}
|
||||
if !strings.HasSuffix(d.Denom, c.orig) {
|
||||
t.Errorf("denom %q must end with original denom %q", d.Denom, c.orig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- TransferChannel ----------------------------------------------------------
|
||||
|
||||
// TestTransferChannelStruct asserts the TransferChannel struct carries all
|
||||
// required fields.
|
||||
func TestTransferChannelStruct(t *testing.T) {
|
||||
ch := stypes.TransferChannel{
|
||||
PortID: "transfer",
|
||||
ChannelID: "channel-0",
|
||||
Counterparty: "transfer/channel-0",
|
||||
Status: stypes.ChannelOpen,
|
||||
}
|
||||
if ch.PortID != "transfer" || ch.ChannelID != "channel-0" ||
|
||||
ch.Counterparty != "transfer/channel-0" || ch.Status != stypes.ChannelOpen {
|
||||
t.Error("TransferChannel fields not set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Genesis -------------------------------------------------------------------
|
||||
|
||||
// TestDefaultGenesisStateEmpty asserts DefaultGenesisState returns non-nil
|
||||
// empty slices for Channels and Denoms.
|
||||
func TestDefaultGenesisStateEmpty(t *testing.T) {
|
||||
gs := stypes.DefaultGenesisState()
|
||||
if gs == nil {
|
||||
t.Fatal("DefaultGenesisState returned nil")
|
||||
}
|
||||
if gs.Channels == nil || len(gs.Channels) != 0 {
|
||||
t.Errorf("Default Channels should be non-nil empty slice; got len=%d nil=%v", len(gs.Channels), gs.Channels == nil)
|
||||
}
|
||||
if gs.Denoms == nil || len(gs.Denoms) != 0 {
|
||||
t.Errorf("Default Denoms should be non-nil empty slice; got len=%d nil=%v", len(gs.Denoms), gs.Denoms == nil)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupChannelIDs asserts A-212: duplicate
|
||||
// channel-ids are rejected.
|
||||
func TestValidateGenesisRejectsDupChannelIDs(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{
|
||||
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelOpen},
|
||||
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelInit}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate channel-ids")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyChannelID asserts empty channel-id is rejected.
|
||||
func TestValidateGenesisRejectsEmptyChannelID(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{{PortID: "transfer", ChannelID: "", Status: stypes.ChannelInit}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty channel-id")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsUnknownChannelStatus asserts an unknown
|
||||
// ChannelStatus is rejected.
|
||||
func TestValidateGenesisRejectsUnknownChannelStatus(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelStatus("Bogus")}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject unknown channel status")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsDupDenom asserts duplicate denoms are rejected.
|
||||
func TestValidateGenesisRejectsDupDenom(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Denoms: []stypes.WrappedBreadDenom{
|
||||
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"},
|
||||
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"}, // dup
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject duplicate denoms")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsEmptyDenom asserts empty denom is rejected.
|
||||
func TestValidateGenesisRejectsEmptyDenom(t *testing.T) {
|
||||
gs := stypes.GenesisState{
|
||||
Denoms: []stypes.WrappedBreadDenom{{Denom: "", TracePath: "transfer/channel-0"}},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err == nil {
|
||||
t.Error("ValidateGenesis should reject empty denom")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateGenesisRejectsBadJSON asserts malformed JSON is rejected.
|
||||
func TestValidateGenesisRejectsBadJSON(t *testing.T) {
|
||||
if err := stypes.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 := stypes.GenesisState{
|
||||
Channels: []stypes.TransferChannel{
|
||||
{PortID: "transfer", ChannelID: "channel-0", Status: stypes.ChannelOpen},
|
||||
{PortID: "transfer", ChannelID: "channel-1", Status: stypes.ChannelInit},
|
||||
},
|
||||
Denoms: []stypes.WrappedBreadDenom{
|
||||
{Denom: "transfer/channel-0/bread", TracePath: "transfer/channel-0"},
|
||||
},
|
||||
}
|
||||
bz, _ := json.Marshal(gs)
|
||||
if err := stypes.ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("ValidateGenesis should accept clean genesis, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module consts -------------------------------------------------------------
|
||||
|
||||
// TestModuleConsts asserts the four Cosmos-convention module consts.
|
||||
func TestModuleConsts(t *testing.T) {
|
||||
if stypes.ModuleName != "satellite" {
|
||||
t.Errorf("ModuleName = %q", stypes.ModuleName)
|
||||
}
|
||||
if stypes.StoreKey != "satellite" {
|
||||
t.Errorf("StoreKey = %q", stypes.StoreKey)
|
||||
}
|
||||
if stypes.RouterKey != "satellite" {
|
||||
t.Errorf("RouterKey = %q", stypes.RouterKey)
|
||||
}
|
||||
if stypes.QuerierRoute != "satellite" {
|
||||
t.Errorf("QuerierRoute = %q", stypes.QuerierRoute)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultParams asserts DefaultParams returns a zero-value Params.
|
||||
func TestDefaultParams(t *testing.T) {
|
||||
_ = stypes.DefaultParams() // no panics
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012) -------------------------------------------------
|
||||
// The satellite module must avoid the banned financial holder terms (the
|
||||
// lexicon firewall's banned list). Use "Holder"/"Reach" instead. The lexicon
|
||||
// helpers are used here — no banned literals are inlined.
|
||||
|
||||
// TestLexiconNoBannedTermsInSatellitePackage scans every non-test .go file in
|
||||
// the satellite/types package directory for the banned terms (case-
|
||||
// insensitive). Production files only — the test file references banned
|
||||
// terms via the lexicon package helpers.
|
||||
func TestLexiconNoBannedTermsInSatellitePackage(t *testing.T) {
|
||||
pkgDir := packageDir(t, "github.com/oy/openyield/x/satellite/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 satellite/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 — use Holder/Reach, not banned financial terms)", filepath.Base(f), found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInSatelliteTestFile asserts this test file itself
|
||||
// does not contain any banned term as a literal.
|
||||
func TestLexiconNoBannedTermsInSatelliteTestFile(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("satellite 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/satellite/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