Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a00131cf0 | |||
| 6d63482c48 | |||
| 463e11e8d2 |
@@ -123,3 +123,86 @@ func SyntheticBannedStrings() []string {
|
||||
"the " + terms[9] + " lost money", // depositor
|
||||
}
|
||||
}
|
||||
|
||||
// coverFragments holds the 4 Cover-specific banned terms (D-088, REQ-055
|
||||
// lexicon scope) as (a, b) halves. Neither half alone is a banned term, and
|
||||
// concatenation produces the banned term at runtime — the same fragment-
|
||||
// assembly bootstrapping pattern as the project-wide fragments above so this
|
||||
// package's source does not contain any banned term as a literal substring.
|
||||
// These are the four terms the Cover module's vocabulary MUST NOT use: the
|
||||
// safe vision names are "Cover", "Cover-Fee", "Cover Call", "Cover-Charter",
|
||||
// "Cover Pool", "Cover Claims Voucher" (D-088); the four terms below are the
|
||||
// banned synonyms enforced by lexicon_meta_cover.
|
||||
var coverFragments = []term{
|
||||
{"insur", "ance"}, // insurance
|
||||
{"prem", "ium"}, // premium
|
||||
{"cla", "im"}, // claim
|
||||
{"pol", "icy"}, // policy
|
||||
}
|
||||
|
||||
// CoverBannedTerms returns the 4 Cover-specific banned terms (D-088): the
|
||||
// four terms the Cover module's vocabulary MUST NOT use. The terms are
|
||||
// assembled at runtime from coverFragments so this package's source does not
|
||||
// contain any banned term as a literal substring (the standard lexicon-test
|
||||
// bootstrapping pattern). These are ADDITIVE to the project-wide
|
||||
// BannedTerms() — the project-wide 10 terms also apply to x/cover; this list
|
||||
// is the Cover-specific superset layer enforced by lexicon_meta_cover.
|
||||
func CoverBannedTerms() []string {
|
||||
out := make([]string, len(coverFragments))
|
||||
for i, t := range coverFragments {
|
||||
out[i] = t.a + t.b
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// coverBannedTermRegexes are the compiled word-boundary regexes for the 4
|
||||
// Cover-specific banned terms. Word boundaries prevent false positives (a
|
||||
// Cover-Call's "claimant" must NOT trip the banned "claim" — the regex bans
|
||||
// the word as a concept, not as an arbitrary substring). The regexes are
|
||||
// case-insensitive. Mirrors bannedTermRegexes for the Cover-specific list.
|
||||
var coverBannedTermRegexes = func() []*regexp.Regexp {
|
||||
terms := CoverBannedTerms()
|
||||
out := make([]*regexp.Regexp, len(terms))
|
||||
for i, t := range terms {
|
||||
out[i] = regexp.MustCompile(`\b` + regexp.QuoteMeta(t) + `\b`)
|
||||
}
|
||||
return out
|
||||
}()
|
||||
|
||||
// FindCoverBannedTerm returns the first Cover-specific banned term found in
|
||||
// s (case-insensitive, word-boundary match) and true, or "" and false if
|
||||
// none. Mirrors FindBannedTerm but uses the Cover-specific 4-term list
|
||||
// (D-088). Used by the lexicon_meta_cover meta-test (the 4th lexicon meta-
|
||||
// test) and the per-package lexicon assertion in x/cover/types/types_test.go.
|
||||
// A Cover source file that contains a Cover-specific banned term triggers
|
||||
// this helper; the project-wide FindBannedTerm is NOT consulted here (the
|
||||
// two firewalls are layered: project-wide + Cover-specific).
|
||||
func FindCoverBannedTerm(s string) (string, bool) {
|
||||
lower := strings.ToLower(s)
|
||||
terms := CoverBannedTerms()
|
||||
for i, re := range coverBannedTermRegexes {
|
||||
if re.MatchString(lower) {
|
||||
return terms[i], true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// SyntheticCoverBannedStrings returns one synthetic string per Cover-specific
|
||||
// banned term, each embedding exactly one banned term in a plausible Cover-
|
||||
// module sentence context. This is the single source of truth (G-014) for
|
||||
// the synthetic self-test table consumed by lexicon_meta_cover ::
|
||||
// TestLexiconMetaCoverSelfTestTable. Mirrors SyntheticBannedStrings for the
|
||||
// 4-term Cover-specific list. The strings are built from CoverBannedTerms()
|
||||
// (already fragment-assembled), so this package's own source stays lexicon-
|
||||
// clean. The returned slice is indexed positionally against CoverBannedTerms():
|
||||
// the i-th synthetic string embeds the i-th Cover-specific banned term.
|
||||
func SyntheticCoverBannedStrings() []string {
|
||||
terms := CoverBannedTerms()
|
||||
return []string{
|
||||
"buy " + terms[0] + " now", // insurance
|
||||
"pay the " + terms[1] + " fee", // premium
|
||||
"file a " + terms[2] + " today", // claim
|
||||
"the " + terms[3] + " expires", // policy
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
// Package lexicon_meta_cover holds the Cover lexicon firewall (REQ-055,
|
||||
// D-088) — the 4th lexicon meta-test.
|
||||
//
|
||||
// It is a NEW sibling meta-test created in v0.7 P1 that MIRRORS the v0.6
|
||||
// web firewall (lexicon_meta_web/lexicon_meta_web_test.go, package
|
||||
// lexicon_meta_web) but scans the Cover module surface (x/cover/**/*.go)
|
||||
// for BOTH the 10 project-wide banned terms (lexicon.FindBannedTerm) AND
|
||||
// the 4 Cover-specific banned terms (lexicon.FindCoverBannedTerm — D-088).
|
||||
// It uses the SAME lexicon.FindBannedTerm + lexicon.FindCoverBannedTerm
|
||||
// (word-boundary, case-insensitive) — NO detection reimplementation — so
|
||||
// the four firewalls (x/*.go project-wide, docs, web, cover) share a
|
||||
// single source of truth for the banned terms. The Cover-specific 4 terms
|
||||
// (insurance, premium, claim, policy — assembled from fragments by
|
||||
// lexicon.CoverBannedTerms) are the Cover-module superset layer: the
|
||||
// project-wide 10 terms ALSO apply to x/cover; this firewall adds the 4
|
||||
// Cover-specific terms on top.
|
||||
//
|
||||
// Placement: this file lives in lexicon_meta_cover/ (a subdirectory of the
|
||||
// repo root) because Go does not permit two distinct packages in the same
|
||||
// directory; the v0.2 firewall is package lexicon_meta at the repo root,
|
||||
// the v0.3 firewall is package lexicon_meta_docs in lexicon_meta_docs/,
|
||||
// and the v0.6 firewall is package lexicon_meta_web in lexicon_meta_web/.
|
||||
// The invocation `go test ./lexicon_meta_cover/...` (PLANS v0.7 P1)
|
||||
// resolves to this package. Run via `go test ./...` from the repo root.
|
||||
//
|
||||
// G-013 walk-coverage: TestLexiconMetaCoverWalkCoverage injects synthetic
|
||||
// banned-term .go files into a temp x/cover/ subtree and asserts the walk
|
||||
// FINDS them — one for a project-wide term, one for a Cover-specific term.
|
||||
// This closes the "silently scans nothing and reports green" failure mode
|
||||
// that the G-009 self-test table (detection) alone does not cover.
|
||||
//
|
||||
// G-014 self-test drift: the self-test tables reuse
|
||||
// lexicon.SyntheticBannedStrings() (project-wide) +
|
||||
// lexicon.SyntheticCoverBannedStrings() (Cover-specific) — the single
|
||||
// sources of truth shared with the other three meta-tests.
|
||||
//
|
||||
// G-024: this test file stays stdlib + lexicon-only (no cosmos-sdk import).
|
||||
package lexicon_meta_cover
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
)
|
||||
|
||||
// repoRoot returns the absolute path to the repo root by walking up from
|
||||
// this test file (the test lives at <repoRoot>/lexicon_meta_cover/).
|
||||
func repoRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/lexicon_meta_cover/lexicon_meta_cover_test.go
|
||||
// repo root = filepath.Dir(filepath.Dir(file))
|
||||
return filepath.Dir(filepath.Dir(file))
|
||||
}
|
||||
|
||||
// coverRoot returns the absolute path to the repo's x/cover directory.
|
||||
func coverRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
return filepath.Join(repoRoot(t), "x", "cover")
|
||||
}
|
||||
|
||||
// thisFile returns the absolute path of this meta-test file (to exclude it
|
||||
// from its own scan — it references banned terms via the lexicon package,
|
||||
// whose source assembles terms from fragments, so no banned-term literal
|
||||
// appears in the firewall's own code).
|
||||
func thisFile(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// isCoverTarget reports whether path (relative to repo root) is a .go file
|
||||
// under x/cover/ (production + test). Non-.go files under x/cover/ are
|
||||
// skipped.
|
||||
func isCoverTarget(rel string) bool {
|
||||
prefix := strings.Join([]string{"x", "cover", ""}, string(filepath.Separator))
|
||||
if !strings.HasPrefix(rel, prefix) {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(rel, ".go")
|
||||
}
|
||||
|
||||
// TestLexiconMetaCoverNoBannedTerms is the Cover firewall (D-088). It walks
|
||||
// x/cover/**/*.go (production + test), reads each file's source, and
|
||||
// asserts no banned term (project-wide OR Cover-specific) is present
|
||||
// (word-boundary, case-insensitive). Excludes this test file itself
|
||||
// (self-exclusion via runtime.Caller(0) — though this file lives outside
|
||||
// x/cover/, the exclusion is belt-and-suspenders in case the walk root is
|
||||
// ever broadened).
|
||||
//
|
||||
// Passes at P1 with the x/cover module lexicon-clean by construction. The
|
||||
// x/cover/types/types_test.go per-package lexicon assertion
|
||||
// (TestLexiconNoBannedTermsInCover) is the in-module firewall; this
|
||||
// meta-test is the repo-wide Cover firewall (run via `go test ./...`).
|
||||
func TestLexiconMetaCoverNoBannedTerms(t *testing.T) {
|
||||
root := coverRoot(t)
|
||||
this := thisFile(t)
|
||||
hits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
// Self-exclusion: skip this meta-test file (belt-and-suspenders;
|
||||
// this file lives outside x/cover/ so the walk would not reach it
|
||||
// anyway, but the exclusion is robust to a future walk-root change).
|
||||
if path == this {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
src := string(bz)
|
||||
// Project-wide 10 terms.
|
||||
if found, ok := lexicon.FindBannedTerm(src); ok {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
hits = append(hits, rel+" contains project-wide banned term "+found)
|
||||
}
|
||||
// Cover-specific 4 terms.
|
||||
if found, ok := lexicon.FindCoverBannedTerm(src); ok {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
hits = append(hits, rel+" contains Cover-specific banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
if len(hits) > 0 {
|
||||
t.Errorf("REQ-055/D-088 Cover lexicon firewall violations:\n %s",
|
||||
strings.Join(hits, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaCoverSelfTestTable (G-009 for cover) is the firewall's own
|
||||
// detection-coverage guard. Each synthetic string embeds exactly one
|
||||
// banned term in a plausible sentence context and is asserted to trigger
|
||||
// detection, so the firewall's detection logic is durably verified — if
|
||||
// detection ever breaks, this test fails before the firewall silently
|
||||
// passes a real violation in a Cover source file.
|
||||
//
|
||||
// This test exercises BOTH the project-wide terms (lexicon.SyntheticBannedStrings
|
||||
// + lexicon.FindBannedTerm) AND the Cover-specific terms
|
||||
// (lexicon.SyntheticCoverBannedStrings + lexicon.FindCoverBannedTerm),
|
||||
// so both layers of the Cover firewall are durably verified.
|
||||
func TestLexiconMetaCoverSelfTestTable(t *testing.T) {
|
||||
// Project-wide layer.
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) != 10 {
|
||||
t.Fatalf("BannedTerms() len = %d, want 10", len(terms))
|
||||
}
|
||||
synthetic := lexicon.SyntheticBannedStrings()
|
||||
if len(synthetic) != len(terms) {
|
||||
t.Fatalf("SyntheticBannedStrings() len = %d, want %d", len(synthetic), len(terms))
|
||||
}
|
||||
for i, s := range synthetic {
|
||||
found, ok := lexicon.FindBannedTerm(s)
|
||||
if !ok {
|
||||
t.Errorf("G-009 cover self-test (project-wide) [%d]: synthetic string did not trigger detection: %q", i, s)
|
||||
continue
|
||||
}
|
||||
if found != terms[i] {
|
||||
t.Errorf("G-009 cover self-test (project-wide) [%d]: detected %q, want %q (in %q)", i, found, terms[i], s)
|
||||
}
|
||||
}
|
||||
|
||||
// Cover-specific layer.
|
||||
coverTerms := lexicon.CoverBannedTerms()
|
||||
if len(coverTerms) != 4 {
|
||||
t.Fatalf("CoverBannedTerms() len = %d, want 4 (D-088)", len(coverTerms))
|
||||
}
|
||||
coverSynthetic := lexicon.SyntheticCoverBannedStrings()
|
||||
if len(coverSynthetic) != len(coverTerms) {
|
||||
t.Fatalf("SyntheticCoverBannedStrings() len = %d, want %d (must match CoverBannedTerms())", len(coverSynthetic), len(coverTerms))
|
||||
}
|
||||
for i, s := range coverSynthetic {
|
||||
found, ok := lexicon.FindCoverBannedTerm(s)
|
||||
if !ok {
|
||||
t.Errorf("G-009 cover self-test (Cover-specific) [%d]: synthetic string did not trigger detection: %q", i, s)
|
||||
continue
|
||||
}
|
||||
if found != coverTerms[i] {
|
||||
t.Errorf("G-009 cover self-test (Cover-specific) [%d]: detected %q, want %q (in %q)", i, found, coverTerms[i], s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaCoverBannedTermsCount asserts exactly 10 project-wide
|
||||
// banned terms + 4 Cover-specific banned terms are configured (locked-const
|
||||
// for the firewall's scope). Derived from lexicon.BannedTerms() +
|
||||
// lexicon.CoverBannedTerms() — the single sources — so a count change
|
||||
// breaks the firewalls (G-014 drift prevention).
|
||||
func TestLexiconMetaCoverBannedTermsCount(t *testing.T) {
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) != 10 {
|
||||
t.Errorf("BannedTerms() len = %d, want 10 (REQ-012)", len(terms))
|
||||
}
|
||||
coverTerms := lexicon.CoverBannedTerms()
|
||||
if len(coverTerms) != 4 {
|
||||
t.Errorf("CoverBannedTerms() len = %d, want 4 (D-088)", len(coverTerms))
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, tr := range terms {
|
||||
if seen[tr] {
|
||||
t.Errorf("duplicate project-wide banned term %q", tr)
|
||||
}
|
||||
seen[tr] = true
|
||||
}
|
||||
for _, tr := range coverTerms {
|
||||
if seen[tr] {
|
||||
t.Errorf("Cover-specific banned term %q duplicates a project-wide term", tr)
|
||||
}
|
||||
seen[tr] = true
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaCoverNoFalsePositiveOnClaimant asserts the field name
|
||||
// "ClaimantReachID" (used by types.CoverCall) does NOT trigger the
|
||||
// Cover-specific banned term that looks like a substring of "Claimant"
|
||||
// (word-boundary matching must not match substrings of identifiers). This
|
||||
// is the regression firewall for the word-boundary detection design on the
|
||||
// Cover-specific layer — mirrors the project-wide
|
||||
// TestLexiconMetaNoFalsePositiveOnOpenYield.
|
||||
func TestLexiconMetaCoverNoFalsePositiveOnClaimant(t *testing.T) {
|
||||
cases := []string{
|
||||
"ClaimantReachID",
|
||||
"ClaimantReachID string",
|
||||
"the ClaimantReachID field",
|
||||
"c.ClaimantReachID",
|
||||
}
|
||||
for _, s := range cases {
|
||||
if _, ok := lexicon.FindCoverBannedTerm(s); ok {
|
||||
t.Errorf("false positive: %q triggered a Cover-specific banned term (word-boundary must avoid this)", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestLexiconMetaCoverWalkCoverage (G-013) is the walk-coverage firewall
|
||||
// for the Cover meta-test. The G-009 self-test table (above) verifies
|
||||
// DETECTION (FindBannedTerm / FindCoverBannedTerm on synthetic strings)
|
||||
// but NOT the WALK (which files are scanned). A walk bug — e.g. wrong path
|
||||
// prefix, missing x/cover/ recursion — would silently scan nothing and
|
||||
// report green on zero files. This test closes that gap by injecting
|
||||
// synthetic banned-term .go files into a fixture dir under the real
|
||||
// x/cover/ path the walk scans and asserting the walk FINDS them — one
|
||||
// fixture for a project-wide term, one for a Cover-specific term.
|
||||
//
|
||||
// The fixtures are created under x/cover/.lexicon_fixture/ (a real x/cover/
|
||||
// subtree the walk reaches) and removed via defer so they never leak into
|
||||
// the repo. If the walk logic misses either fixture, this test fails loudly
|
||||
// instead of letting a broken walk pass the firewall green on zero files
|
||||
// scanned.
|
||||
func TestLexiconMetaCoverWalkCoverage(t *testing.T) {
|
||||
root := coverRoot(t)
|
||||
|
||||
// Build synthetic banned terms from fragments so THIS file does not
|
||||
// contain banned-term literals.
|
||||
terms := lexicon.BannedTerms()
|
||||
if len(terms) == 0 {
|
||||
t.Fatal("BannedTerms() returned no terms — cannot run walk-coverage")
|
||||
}
|
||||
coverTerms := lexicon.CoverBannedTerms()
|
||||
if len(coverTerms) == 0 {
|
||||
t.Fatal("CoverBannedTerms() returned no terms — cannot run walk-coverage")
|
||||
}
|
||||
// Project-wide fixture: use the first banned term ("bank") reassembled.
|
||||
pwTerm := terms[0][:2] + terms[0][2:]
|
||||
// Cover-specific fixture: use the first Cover term reassembled.
|
||||
coverTerm := coverTerms[0][:len(coverTerms[0])/2] + coverTerms[0][len(coverTerms[0])/2:]
|
||||
|
||||
fixtureDir := filepath.Join(root, ".lexicon_fixture")
|
||||
if err := os.MkdirAll(fixtureDir, 0o755); err != nil {
|
||||
t.Fatalf("mkdir fixture: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(fixtureDir)
|
||||
|
||||
// Project-wide fixture .go file.
|
||||
pwFixture := filepath.Join(fixtureDir, "bad_pw_fixture.go")
|
||||
pwContent := []byte("// fixture\n// this file contains a project-wide banned term: " + pwTerm + "\npackage lexicon_fixture\n")
|
||||
if err := os.WriteFile(pwFixture, pwContent, 0o644); err != nil {
|
||||
t.Fatalf("write pw fixture: %v", err)
|
||||
}
|
||||
// Cover-specific fixture .go file.
|
||||
coverFixture := filepath.Join(fixtureDir, "bad_cover_fixture.go")
|
||||
coverContent := []byte("// fixture\n// this file contains a Cover-specific banned term: " + coverTerm + "\npackage lexicon_fixture\n")
|
||||
if err := os.WriteFile(coverFixture, coverContent, 0o644); err != nil {
|
||||
t.Fatalf("write cover fixture: %v", err)
|
||||
}
|
||||
|
||||
// Run the SAME walk logic as TestLexiconMetaCoverNoBannedTerms and
|
||||
// assert it FINDS both fixtures' banned terms. A walk that returns zero
|
||||
// hits here proves the walk logic is broken.
|
||||
pwHits := []string{}
|
||||
coverHits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
src := string(bz)
|
||||
if found, ok := lexicon.FindBannedTerm(src); ok {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
pwHits = append(pwHits, rel+":"+found)
|
||||
}
|
||||
if found, ok := lexicon.FindCoverBannedTerm(src); ok {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
coverHits = append(coverHits, rel+":"+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
|
||||
// Assert the project-wide fixture was found.
|
||||
foundPW := false
|
||||
for _, h := range pwHits {
|
||||
if strings.Contains(h, "bad_pw_fixture.go") && strings.Contains(h, pwTerm) {
|
||||
foundPW = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundPW {
|
||||
t.Errorf("G-013 walk-coverage (project-wide): the walk did NOT find the synthetic project-wide banned-term fixture at %s — the Cover firewall walk logic is broken (it would silently scan nothing and report green). pwHits=%v", pwFixture, pwHits)
|
||||
}
|
||||
|
||||
// Assert the Cover-specific fixture was found.
|
||||
foundCover := false
|
||||
for _, h := range coverHits {
|
||||
if strings.Contains(h, "bad_cover_fixture.go") && strings.Contains(h, coverTerm) {
|
||||
foundCover = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundCover {
|
||||
t.Errorf("G-013 walk-coverage (Cover-specific): the walk did NOT find the synthetic Cover-specific banned-term fixture at %s — the Cover firewall walk logic is broken. coverHits=%v", coverFixture, coverHits)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Package firewall holds the Anti-Crowding-Out firewall (D-079, D-088).
|
||||
//
|
||||
// The firewall is the enforcement mechanism for RightNoTaxOnPersonalStash —
|
||||
// the Bill of Rights right that prohibits routing Cover-Fees OUT of
|
||||
// contributor-pool semantics. A Cover-Fee is the annual contrib that funds
|
||||
// a Cover Pool's reserve; it MUST route into the Pool's ReserveAccount (a
|
||||
// contributor-pool reserve holder), never into a Root-Pool operating-
|
||||
// expenses holder (the Anti-Crowding-Out case: routing Cover-Fees to Root-
|
||||
// Pool operating expenses would let the protocol crowding-out the
|
||||
// contributor pool's reserve).
|
||||
//
|
||||
// The firewall is an ALLOW-LIST of permitted routing destinations (D-088(2)
|
||||
// — the concrete simtest-enforceable shape). The RouteCoverFee handler
|
||||
// passes the destination holder string to CheckCoverFeeRouting; the
|
||||
// firewall checks the destination is non-empty AND not a known bad
|
||||
// destination. For P1 simtest-grade, the firewall rejects the specific
|
||||
// string "root-pool-operating-expenses" (the Anti-Crowding-Out case) and
|
||||
// accepts any other non-empty string. The full destination-match check
|
||||
// (the destination must EXACTLY match the Pool's ReserveAccount) is
|
||||
// enforced at the call site (the handler compares the destination to
|
||||
// pool.ReserveAccount BEFORE calling the firewall; the firewall is the
|
||||
// second-layer defense).
|
||||
//
|
||||
// Defense in depth (D-079): the runtime firewall (this package) rejects
|
||||
// code paths; the lexicon_meta_cover meta-test rejects doc drift. The two
|
||||
// layers together close the Anti-Crowding-Out failure mode: a code path
|
||||
// that routes a Cover-Fee to a Root-Pool holder is rejected by the
|
||||
// firewall; a doc that drifts to describing Cover-Fees as routing to
|
||||
// Root-Pool is rejected by the meta-test.
|
||||
//
|
||||
// This package is a LEAF checker: it does NOT import x/cover/types (the
|
||||
// handler passes strings in). It is stdlib-only (G-024 — the firewall has
|
||||
// no cosmos-sdk dependency; it is a pure string check). This keeps the
|
||||
// firewall testable in isolation + import-cycle-free.
|
||||
package firewall
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ErrAntiCrowdingOut is returned by CheckCoverFeeRouting when the
|
||||
// destination is a known bad destination (the Anti-Crowding-Out case). The
|
||||
// RouteCoverFee handler wraps this in a cover-specific error message.
|
||||
var ErrAntiCrowdingOut = errors.New("cover-fee routing outside contributor-pool semantics (Anti-Crowding-Out firewall)")
|
||||
|
||||
// badDestination is the known bad destination the firewall rejects (the
|
||||
// Anti-Crowding-Out case). Built from fragments so this source file does
|
||||
// not contain the literal bad destination as a searchable string (mirrors
|
||||
// the lexicon fragment-assembly pattern; the firewall's own code is
|
||||
// allowed to name the destination it bans, but the fragment assembly keeps
|
||||
// the source grep-clean for "root-pool" drift auditing). P1 simtest-grade:
|
||||
// the firewall rejects exactly this one destination; the full destination-
|
||||
// match check (destination must EXACTLY match the Pool's ReserveAccount)
|
||||
// is enforced at the call site.
|
||||
var badDestination = string([]byte{
|
||||
'r', 'o', 'o', 't', '-', 'p', 'o', 'o', 'l',
|
||||
'-', 'o', 'p', 'e', 'r', 'a', 't', 'i', 'n', 'g',
|
||||
'-', 'e', 'x', 'p', 'e', 'n', 's', 'e', 's',
|
||||
})
|
||||
|
||||
// CheckCoverFeeRouting is the Anti-Crowding-Out firewall (D-079, D-088).
|
||||
// It returns nil if the destination is a permitted routing destination (a
|
||||
// non-empty holder string that is NOT the known bad destination), or
|
||||
// ErrAntiCrowdingOut if the destination is the known bad destination (the
|
||||
// Root-Pool operating-expenses holder — the Anti-Crowding-Out case).
|
||||
//
|
||||
// The RouteCoverFee handler calls this AFTER loading the pool + BEFORE
|
||||
// persisting the Cover-Fee routing. The handler passes the pool's
|
||||
// ReserveAccount (the destination the fee routes into); the firewall is
|
||||
// the second-layer defense (the first layer is the handler's own
|
||||
// destination-match check — the destination must be the pool's
|
||||
// ReserveAccount; the firewall catches the case where the destination IS
|
||||
// the pool's ReserveAccount but that holder is itself the bad destination,
|
||||
// i.e. a pool misconfigured to route to Root-Pool operating expenses).
|
||||
//
|
||||
// P1 simtest-grade: the firewall rejects exactly the one known bad
|
||||
// destination + the empty-string case. The full destination-match check
|
||||
// is enforced at the call site (the handler compares the destination to
|
||||
// pool.ReserveAccount).
|
||||
func CheckCoverFeeRouting(destinationAccount string) error {
|
||||
if destinationAccount == "" {
|
||||
return errors.New("cover-fee routing: empty destination (Anti-Crowding-Out firewall)")
|
||||
}
|
||||
if strings.EqualFold(destinationAccount, badDestination) {
|
||||
return ErrAntiCrowdingOut
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package firewall
|
||||
|
||||
// firewall_test.go holds the unit tests for the Anti-Crowding-Out firewall
|
||||
// (D-079, D-088). The firewall is a leaf checker (stdlib-only); these tests
|
||||
// exercise CheckCoverFeeRouting in isolation. The keeper simtest also
|
||||
// exercises the firewall via the RouteCoverFee handler (integration
|
||||
// coverage), but this in-package test gives the firewall package its own
|
||||
// coverage number >=80%.
|
||||
//
|
||||
// Lexicon self-exclusion (D-088): this test file must NOT contain the
|
||||
// banned project-wide or Cover-specific terms as literals. The bad
|
||||
// destination string is assembled from bytes (not a literal) so the
|
||||
// firewall's own bad-destination constant is not re-inlined here as a
|
||||
// searchable literal.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// badDest reassembles the firewall's bad destination from bytes so this
|
||||
// test file does not contain the literal bad string as a searchable
|
||||
// substring (mirrors the firewall's own byte assembly). Matches the
|
||||
// firewall's badDestination byte-for-byte.
|
||||
func badDest() string {
|
||||
return string([]byte{
|
||||
'r', 'o', 'o', 't', '-', 'p', 'o', 'o', 'l',
|
||||
'-', 'o', 'p', 'e', 'r', 'a', 't', 'i', 'n', 'g',
|
||||
'-', 'e', 'x', 'p', 'e', 'n', 's', 'e', 's',
|
||||
})
|
||||
}
|
||||
|
||||
// TestCheckCoverFeeRoutingAcceptsPermitted asserts the firewall accepts a
|
||||
// non-empty permitted destination (returns nil).
|
||||
func TestCheckCoverFeeRoutingAcceptsPermitted(t *testing.T) {
|
||||
cases := []string{
|
||||
"acc-1",
|
||||
"oy:reserve:pool-1",
|
||||
"contributor-pool-reserve",
|
||||
"some-other-destination",
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := CheckCoverFeeRouting(c); err != nil {
|
||||
t.Errorf("CheckCoverFeeRouting(%q) = %v, want nil", c, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckCoverFeeRoutingRejectsEmpty asserts the firewall rejects an
|
||||
// empty destination.
|
||||
func TestCheckCoverFeeRoutingRejectsEmpty(t *testing.T) {
|
||||
err := CheckCoverFeeRouting("")
|
||||
if err == nil {
|
||||
t.Fatal("CheckCoverFeeRouting(empty) should error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "empty") {
|
||||
t.Errorf("empty-destination error = %q, want 'empty'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckCoverFeeRoutingRejectsBadDestination asserts the firewall
|
||||
// rejects the known bad destination (the Anti-Crowding-Out case) with
|
||||
// ErrAntiCrowdingOut.
|
||||
func TestCheckCoverFeeRoutingRejectsBadDestination(t *testing.T) {
|
||||
err := CheckCoverFeeRouting(badDest())
|
||||
if err == nil {
|
||||
t.Fatal("CheckCoverFeeRouting(bad destination) should error")
|
||||
}
|
||||
if err != ErrAntiCrowdingOut {
|
||||
t.Errorf("error = %v, want ErrAntiCrowdingOut", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Anti-Crowding-Out") {
|
||||
t.Errorf("error = %q, want 'Anti-Crowding-Out'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestCheckCoverFeeRoutingCaseInsensitive asserts the firewall rejects the
|
||||
// bad destination case-insensitively (the Root-Pool operating-expenses
|
||||
// holder in any case is the Anti-Crowding-Out case).
|
||||
func TestCheckCoverFeeRoutingCaseInsensitive(t *testing.T) {
|
||||
upper := strings.ToUpper(badDest())
|
||||
if err := CheckCoverFeeRouting(upper); err == nil {
|
||||
t.Error("CheckCoverFeeRouting(upper-case bad destination) should error (case-insensitive)")
|
||||
}
|
||||
if err := CheckCoverFeeRouting(strings.ToLower(badDest())); err == nil {
|
||||
t.Error("CheckCoverFeeRouting(lower-case bad destination) should error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrAntiCrowdingOutIsSentinel asserts ErrAntiCrowdingOut is a non-nil
|
||||
// sentinel error (the handler wraps it; the simtest asserts on the
|
||||
// message substring).
|
||||
func TestErrAntiCrowdingOutIsSentinel(t *testing.T) {
|
||||
if ErrAntiCrowdingOut == nil {
|
||||
t.Fatal("ErrAntiCrowdingOut should be non-nil")
|
||||
}
|
||||
if !strings.Contains(ErrAntiCrowdingOut.Error(), "Anti-Crowding-Out") {
|
||||
t.Errorf("ErrAntiCrowdingOut Error = %q, want 'Anti-Crowding-Out'", ErrAntiCrowdingOut.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package keeper
|
||||
|
||||
// keeper.go holds the store-backed Keeper for the cover module's Cover Pool
|
||||
// runtime (REQ-046, REQ-047, REQ-049, REQ-050, REQ-055, D-077, D-086,
|
||||
// D-088, D-089).
|
||||
//
|
||||
// The Keeper wraps an sdk.KVStore via a storeKey. It holds:
|
||||
// - the CoverPool records (pool-id -> CoverPool);
|
||||
// - the CoverCall records (call-id -> CoverCall; the FileCoverCall
|
||||
// handler persists here; P4 adds the Voucher adjudication).
|
||||
//
|
||||
// The Cover-Fee routing (RouteCoverFee) does NOT persist a separate record
|
||||
// in P1 — the routing is the event (the reserve balance update is a
|
||||
// simtest-grade stub). P2 may add a CoverFeeRouting record; P1 ships the
|
||||
// event-only path.
|
||||
//
|
||||
// The Keeper also holds the FOUR expected-keeper shims (StandingKeeper for
|
||||
// the D-077 gate; WatcherKeeper for the launch attestation; BondKeeper for
|
||||
// the P4 MAB check; StillKeeper for the below-floor auto-pause). The shims
|
||||
// are interfaces (G-003 — no struct import of x/standing/types,
|
||||
// x/watcher/types, x/bond/types, x/still/types); the concrete keepers (or
|
||||
// simtest stubs) satisfy them structurally.
|
||||
//
|
||||
// State-machine ordering (vision §7, enforced in every handler):
|
||||
// ValidateBasic -> handler authz/gate -> state mutation -> ctx.EventManager().EmitEvent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/oy/openyield/x/cover/types"
|
||||
)
|
||||
|
||||
// Keeper is the store-backed cover Cover-Pool keeper.
|
||||
type Keeper struct {
|
||||
cdc codec.Codec
|
||||
storeKey storetypes.StoreKey
|
||||
standingKeeper types.StandingKeeper
|
||||
watcherKeeper types.WatcherKeeper
|
||||
bondKeeper types.BondKeeper
|
||||
stillKeeper types.StillKeeper
|
||||
}
|
||||
|
||||
// NewKeeper constructs a new store-backed cover Keeper. The four expected-
|
||||
// keeper shims are injected (all nil-able for partial tests; the handlers
|
||||
// guard nil shims and skip the corresponding check, still mutating state —
|
||||
// the simtest wiring documents this). The StandingKeeper gates the launch
|
||||
// (D-077); the WatcherKeeper attests the launch (REQ-046); the BondKeeper
|
||||
// is held for P4 (the P1 handlers do not call it); the StillKeeper records
|
||||
// the below-floor auto-pause (D-089(1)).
|
||||
func NewKeeper(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandingKeeper, wk types.WatcherKeeper, bk types.BondKeeper, stK types.StillKeeper) Keeper {
|
||||
return Keeper{
|
||||
cdc: cdc,
|
||||
storeKey: storeKey,
|
||||
standingKeeper: sk,
|
||||
watcherKeeper: wk,
|
||||
bondKeeper: bk,
|
||||
stillKeeper: stK,
|
||||
}
|
||||
}
|
||||
|
||||
// SetStandingKeeper sets the StandingKeeper expected-keeper shim (for
|
||||
// post-construction wiring, e.g., app wiring or test setup).
|
||||
func (k *Keeper) SetStandingKeeper(sk types.StandingKeeper) { k.standingKeeper = sk }
|
||||
|
||||
// SetWatcherKeeper sets the WatcherKeeper expected-keeper shim.
|
||||
func (k *Keeper) SetWatcherKeeper(wk types.WatcherKeeper) { k.watcherKeeper = wk }
|
||||
|
||||
// SetBondKeeper sets the BondKeeper expected-keeper shim.
|
||||
func (k *Keeper) SetBondKeeper(bk types.BondKeeper) { k.bondKeeper = bk }
|
||||
|
||||
// SetStillKeeper sets the StillKeeper expected-keeper shim.
|
||||
func (k *Keeper) SetStillKeeper(stK types.StillKeeper) { k.stillKeeper = stK }
|
||||
|
||||
// StoreKey returns the keeper's store key (exported for simtest access to
|
||||
// the underlying KVStore, e.g. to inject corrupt bytes for marshal-error
|
||||
// coverage). Mirrors the x/hub simtest pattern (the simtest reaches the
|
||||
// store via ctx.KVStore(k.StoreKey())).
|
||||
func (k Keeper) StoreKey() storetypes.StoreKey { return k.storeKey }
|
||||
|
||||
// --- CoverPool store ----------------------------------------------------------
|
||||
|
||||
var poolKeyPrefix = []byte("pool/")
|
||||
|
||||
func poolKey(poolID string) []byte {
|
||||
return append(poolKeyPrefix, []byte(poolID)...)
|
||||
}
|
||||
|
||||
// GetCoverPool loads a CoverPool by pool-id. Returns the pool and true if
|
||||
// found, or zero value + false if not.
|
||||
func (k Keeper) GetCoverPool(ctx sdk.Context, poolID string) (types.CoverPool, bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(poolKey(poolID))
|
||||
if bz == nil {
|
||||
return types.CoverPool{}, false
|
||||
}
|
||||
var p types.CoverPool
|
||||
if err := json.Unmarshal(bz, &p); err != nil {
|
||||
return types.CoverPool{}, false
|
||||
}
|
||||
return p, true
|
||||
}
|
||||
|
||||
// SetCoverPool persists a CoverPool by pool-id.
|
||||
func (k Keeper) SetCoverPool(ctx sdk.Context, p types.CoverPool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("cover: marshal pool %q: %v", p.PoolID, err))
|
||||
}
|
||||
store.Set(poolKey(p.PoolID), bz)
|
||||
}
|
||||
|
||||
// AllCoverPools returns all persisted CoverPool records (iteration helper,
|
||||
// unordered).
|
||||
func (k Keeper) AllCoverPools(ctx sdk.Context) []types.CoverPool {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := store.Iterator(poolKeyPrefix, prefixEnd(poolKeyPrefix))
|
||||
defer iterator.Close()
|
||||
out := []types.CoverPool{}
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var p types.CoverPool
|
||||
if err := json.Unmarshal(iterator.Value(), &p); err == nil {
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// --- CoverCall store ----------------------------------------------------------
|
||||
|
||||
var callKeyPrefix = []byte("call/")
|
||||
|
||||
func callKey(callID string) []byte {
|
||||
return append(callKeyPrefix, []byte(callID)...)
|
||||
}
|
||||
|
||||
// GetCoverCall loads a CoverCall by call-id. Returns the call and true if
|
||||
// found, or zero value + false if not.
|
||||
func (k Keeper) GetCoverCall(ctx sdk.Context, callID string) (types.CoverCall, bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz := store.Get(callKey(callID))
|
||||
if bz == nil {
|
||||
return types.CoverCall{}, false
|
||||
}
|
||||
var c types.CoverCall
|
||||
if err := json.Unmarshal(bz, &c); err != nil {
|
||||
return types.CoverCall{}, false
|
||||
}
|
||||
return c, true
|
||||
}
|
||||
|
||||
// SetCoverCall persists a CoverCall by call-id.
|
||||
func (k Keeper) SetCoverCall(ctx sdk.Context, c types.CoverCall) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
bz, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("cover: marshal call %q: %v", c.CallID, err))
|
||||
}
|
||||
store.Set(callKey(c.CallID), bz)
|
||||
}
|
||||
|
||||
// AllCoverCalls returns all persisted CoverCall records (iteration helper,
|
||||
// unordered).
|
||||
func (k Keeper) AllCoverCalls(ctx sdk.Context) []types.CoverCall {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := store.Iterator(callKeyPrefix, prefixEnd(callKeyPrefix))
|
||||
defer iterator.Close()
|
||||
out := []types.CoverCall{}
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
var c types.CoverCall
|
||||
if err := json.Unmarshal(iterator.Value(), &c); err == nil {
|
||||
out = append(out, c)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// --- prefixEnd helper ---------------------------------------------------------
|
||||
|
||||
// prefixEnd returns the key that sorts immediately after all keys sharing
|
||||
// the given prefix (the standard prefix-iteration end key: increment the
|
||||
// last byte, drop overflow). Used for store.Iterator(start, prefixEnd(start))
|
||||
// prefix scans. Mirrors x/hub/keeper/keeper.go.
|
||||
func prefixEnd(prefix []byte) []byte {
|
||||
if len(prefix) == 0 {
|
||||
return nil
|
||||
}
|
||||
end := make([]byte, len(prefix))
|
||||
copy(end, prefix)
|
||||
for i := len(end) - 1; i >= 0; i-- {
|
||||
end[i]++
|
||||
if end[i] != 0 {
|
||||
return end
|
||||
}
|
||||
}
|
||||
// All bytes were 0xFF; return nil (iterate to end of store).
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,354 @@
|
||||
package keeper
|
||||
|
||||
// msg_server.go implements the cover module's MsgServer (REQ-046, REQ-047,
|
||||
// REQ-049, REQ-050, REQ-055, D-077, D-079, D-086, D-088, D-089). The
|
||||
// MsgServer wraps the Keeper + the four expected-keeper shims (already on
|
||||
// the Keeper: StandingKeeper, WatcherKeeper, BondKeeper, StillKeeper).
|
||||
//
|
||||
// Each method returns a (*Response, error). Handler state-machine ordering
|
||||
// is enforced: ValidateBasic -> handler authz/gate -> state mutation ->
|
||||
// ctx.EventManager().EmitEvent.
|
||||
//
|
||||
// Handler set:
|
||||
// - LaunchCoverPool: D-086 category phase check + D-077 Standing gate +
|
||||
// reserve floor + Watcher attestation; persists the CoverPool.
|
||||
// - RouteCoverFee: D-079 Anti-Crowding-Out firewall + category-tag match +
|
||||
// below-floor auto-pause + StillKeeper invocation; emits the routing
|
||||
// event.
|
||||
// - FileCoverCall: P1 scaffold — persists the CoverCall + emits an event;
|
||||
// P4 adds the Voucher adjudication + no-self-adjudication + slashing.
|
||||
//
|
||||
// Nil-shim behavior (simtest wiring): a nil StandingKeeper skips the D-077
|
||||
// gate (the handler still mutates state — the simtest documents the wiring
|
||||
// contract); a nil WatcherKeeper skips the launch attestation; a nil
|
||||
// StillKeeper skips the auto-Still recording (the pool's PoolPaused flag is
|
||||
// still set, just the Still event is not recorded in a still store); a nil
|
||||
// BondKeeper is the P1 default (the P4 handler will reject a nil shim as a
|
||||
// wiring error when the P4 MAB check is wired).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/oy/openyield/x/cover/firewall"
|
||||
"github.com/oy/openyield/x/cover/types"
|
||||
)
|
||||
|
||||
// msgServer is the concrete MsgServer implementation wrapping the Keeper.
|
||||
type msgServer struct {
|
||||
Keeper
|
||||
}
|
||||
|
||||
// NewMsgServerImpl returns the cover MsgServer for the provided Keeper.
|
||||
func NewMsgServerImpl(k Keeper) types.MsgServer {
|
||||
return &msgServer{Keeper: k}
|
||||
}
|
||||
|
||||
var _ types.MsgServer = msgServer{}
|
||||
|
||||
// unwrapCtx extracts the sdk.Context from the interface-typed ctx.
|
||||
func unwrapCtx(ctx interface{}) sdk.Context {
|
||||
if c, ok := ctx.(sdk.Context); ok {
|
||||
return c
|
||||
}
|
||||
panic(fmt.Sprintf("cover: expected sdk.Context, got %T", ctx))
|
||||
}
|
||||
|
||||
// gateForCategory returns the locked Standing gate floor for a Cover
|
||||
// category (D-077). HealthMCS demands the Preferred gate (4.5); Travel +
|
||||
// IncomePause use the Trusted gate (4.0) as the default. Other Phase2
|
||||
// categories (none in P1) would also use the Trusted gate; the handler
|
||||
// rejects out-of-phase categories BEFORE reaching this helper (the D-086
|
||||
// phase check runs first), so this helper is only called for in-phase
|
||||
// categories.
|
||||
func gateForCategory(cat types.CoverCategory) float64 {
|
||||
if cat == types.CatHealthMCS {
|
||||
return types.CoverStandingGatePreferred
|
||||
}
|
||||
return types.CoverStandingGateTrusted
|
||||
}
|
||||
|
||||
// bucketMeetsGate reports whether a Standing bucket string + score meet the
|
||||
// locked gate floor (D-077). The bucket string is one of "New", "Trusted",
|
||||
// "Preferred", "Top", "Slashed" (cross-doc to x/standing.StandingBucket).
|
||||
// "Trusted" or higher ("Preferred", "Top") meets a Trusted gate; "Preferred"
|
||||
// or higher ("Top") meets a Preferred gate. The score is a secondary check
|
||||
// (defense in depth: the bucket is the primary gate, the score confirms).
|
||||
// "New" or "Slashed" never meets either gate.
|
||||
func bucketMeetsGate(bucket string, score float64, gate float64) bool {
|
||||
switch bucket {
|
||||
case "Top":
|
||||
return true
|
||||
case "Preferred":
|
||||
return gate <= types.CoverStandingGatePreferred && score >= gate
|
||||
case "Trusted":
|
||||
return gate <= types.CoverStandingGateTrusted && score >= gate
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- LaunchCoverPool ----------------------------------------------------------
|
||||
|
||||
// LaunchCoverPool launches a Cover Pool (REQ-046, REQ-047, REQ-049, D-077,
|
||||
// D-086). The handler enforces:
|
||||
// 1. ValidateBasic (stateless — floor check on ReserveAnnualContribRatio).
|
||||
// 2. Idempotency: pool-id must not already exist.
|
||||
// 3. D-086 category phase check: each category's phase must be in the
|
||||
// pool's FactoryAllowedPhases (P1 default = [Phase2] only — so only
|
||||
// Travel/HealthMCS/IncomePause allowed in P1; Phase3/Phase4 categories
|
||||
// REJECTED).
|
||||
// 4. D-090(3) dual gate check: the Params.PoolStandingGate >= the protocol
|
||||
// minimum (CoverStandingGateTrusted) — a pool may tighten the gate but
|
||||
// never lower it.
|
||||
// 5. D-077 Standing gate: for each category, query
|
||||
// StandingKeeper.GetStandingBucket(hostReachID, category). Compare the
|
||||
// returned bucket + score against the locked gate (Trusted for Travel/
|
||||
// IncomePause; Preferred for HealthMCS). A nil StandingKeeper skips
|
||||
// the gate check (simtest wiring).
|
||||
// 6. Reserve floor re-check (REQ-047 defense in depth):
|
||||
// ReserveAnnualContribRatio >= CoverReserveFloorAnnualContribX.
|
||||
// 7. Watcher attestation (REQ-046): WatcherKeeper.Attest(poolID, payload).
|
||||
// A nil WatcherKeeper skips (simtest).
|
||||
// 8. Persist the CoverPool (PoolPaused = false, FactoryAllowedPhases +
|
||||
// PoolStandingGate from Params).
|
||||
//
|
||||
// On success an event is emitted.
|
||||
func (s msgServer) LaunchCoverPool(ctx interface{}, msg *types.MsgLaunchCoverPool) (*types.MsgLaunchCoverPoolResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
// Idempotency: pool-id must not already exist.
|
||||
if _, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID); ok {
|
||||
return nil, fmt.Errorf("cover: pool %q already exists", msg.PoolID)
|
||||
}
|
||||
|
||||
// Load the Params (P1: DefaultParams — the live Params store is deferred;
|
||||
// the handler uses DefaultParams for the FactoryAllowedPhases + the
|
||||
// PoolStandingGate floor). A future P2 will load the Params from the
|
||||
// params store; P1 ships the default.
|
||||
params := types.DefaultParams()
|
||||
if err := params.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("cover: params invalid: %w", err)
|
||||
}
|
||||
|
||||
// D-086 category phase check: each category's phase must be in the
|
||||
// FactoryAllowedPhases (P1 default = [Phase2] only).
|
||||
allowed := make(map[types.CoverCategoryPhase]bool, len(params.FactoryAllowedPhases))
|
||||
for _, ph := range params.FactoryAllowedPhases {
|
||||
allowed[ph] = true
|
||||
}
|
||||
for _, cat := range msg.Categories {
|
||||
ph := types.CoverCategoryPhaseFor(cat)
|
||||
if ph == "" {
|
||||
return nil, fmt.Errorf("cover: unknown category %q (D-086 phase check)", cat)
|
||||
}
|
||||
if !allowed[ph] {
|
||||
return nil, fmt.Errorf("cover: category %q is phase %q, not in FactoryAllowedPhases %v (D-086: P1 allows %v only)", cat, ph, params.FactoryAllowedPhases, params.FactoryAllowedPhases)
|
||||
}
|
||||
}
|
||||
|
||||
// D-077 Standing gate: for each category, query the host's Standing
|
||||
// bucket + score and compare against the locked gate. A nil
|
||||
// StandingKeeper skips the gate check (simtest wiring — documented).
|
||||
if s.Keeper.standingKeeper != nil {
|
||||
for _, cat := range msg.Categories {
|
||||
gate := gateForCategory(cat)
|
||||
bucket, score, err := s.Keeper.standingKeeper.GetStandingBucket(msg.HostReachID, string(cat))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cover: Standing lookup for host %q category %q: %w (D-077 gate)", msg.HostReachID, cat, err)
|
||||
}
|
||||
if !bucketMeetsGate(bucket, score, gate) {
|
||||
return nil, fmt.Errorf("cover: host %q Standing bucket %q score %.2f for category %q does not meet the locked gate %.2f (D-077)", msg.HostReachID, bucket, score, cat, gate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve floor re-check (defense in depth — ValidateBasic already
|
||||
// checked this statelessly).
|
||||
if msg.ReserveAnnualContribRatio < types.CoverReserveFloorAnnualContribX {
|
||||
return nil, fmt.Errorf("cover: ReserveAnnualContribRatio %.2f < floor %.2f (REQ-047 handler re-check)", msg.ReserveAnnualContribRatio, types.CoverReserveFloorAnnualContribX)
|
||||
}
|
||||
|
||||
// Watcher attestation (REQ-046). A nil WatcherKeeper skips (simtest).
|
||||
if s.Keeper.watcherKeeper != nil {
|
||||
payload := []byte(fmt.Sprintf("cover.launch:%s:%s:%v:%.2f", msg.PoolID, msg.HostReachID, msg.Categories, msg.ReserveAnnualContribRatio))
|
||||
if _, err := s.Keeper.watcherKeeper.Attest(msg.PoolID, payload); err != nil {
|
||||
return nil, fmt.Errorf("cover: Watcher attestation for pool %q: %w (REQ-046)", msg.PoolID, err)
|
||||
}
|
||||
}
|
||||
|
||||
pool := types.CoverPool{
|
||||
PoolID: msg.PoolID,
|
||||
HostReachID: msg.HostReachID,
|
||||
Categories: msg.Categories,
|
||||
ReserveAnnualContribRatio: msg.ReserveAnnualContribRatio,
|
||||
ReserveAccount: msg.ReserveAccount,
|
||||
PoolPaused: false,
|
||||
CharterHash: msg.CharterHash,
|
||||
FactoryAllowedPhases: params.FactoryAllowedPhases,
|
||||
PoolStandingGate: params.PoolStandingGate,
|
||||
CreatedAt: sdkCtx.BlockHeight(),
|
||||
}
|
||||
s.Keeper.SetCoverPool(sdkCtx, pool)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"cover.pool_launched",
|
||||
sdk.NewAttribute("pool_id", msg.PoolID),
|
||||
sdk.NewAttribute("host_reach_id", msg.HostReachID),
|
||||
sdk.NewAttribute("reserve_annual_contrib_ratio", fmt.Sprintf("%.2f", msg.ReserveAnnualContribRatio)),
|
||||
))
|
||||
return &types.MsgLaunchCoverPoolResponse{}, nil
|
||||
}
|
||||
|
||||
// --- RouteCoverFee ------------------------------------------------------------
|
||||
|
||||
// RouteCoverFee routes a Cover-Fee into a pool's reserve (REQ-050, D-079
|
||||
// firewall, REQ-047 below-floor auto-pause). The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. Load the CoverPool. If not found, REJECT.
|
||||
// 3. Below-floor pause check (REQ-047): if pool.PoolPaused == true, REJECT
|
||||
// with "pool paused (below reserve floor)".
|
||||
// 4. D-079 Anti-Crowding-Out firewall: call
|
||||
// firewall.CheckCoverFeeRouting(pool.ReserveAccount). If the firewall
|
||||
// rejects (the destination is NOT permitted — e.g. the pool's
|
||||
// ReserveAccount is the Root-Pool operating-expenses holder), REJECT.
|
||||
// 5. Category-tag validation (REQ-050, FR-COVER-11): the CategoryTag must
|
||||
// match one of the Pool's Categories. Mismatch -> REJECT.
|
||||
// 6. Reserve floor check (REQ-047): if pool.ReserveAnnualContribRatio <
|
||||
// floor, REJECT the routing AND set pool.PoolPaused = true (auto-pause)
|
||||
// AND invoke StillKeeper.Still(poolID, "below reserve floor") (D-089(1)
|
||||
// — nil StillKeeper skips). Persist the paused pool. Emit
|
||||
// cover.pool_below_floor.
|
||||
// 7. Otherwise: emit cover.cover_fee_routed (the routing is the event; the
|
||||
// reserve balance update is a simtest-grade stub).
|
||||
func (s msgServer) RouteCoverFee(ctx interface{}, msg *types.MsgRouteCoverFee) (*types.MsgRouteCoverFeeResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cover: pool %q not found (RouteCoverFee rejected)", msg.PoolID)
|
||||
}
|
||||
|
||||
// Below-floor pause check: a paused pool rejects all routing.
|
||||
if pool.PoolPaused {
|
||||
return nil, fmt.Errorf("cover: pool %q paused (below reserve floor) — routing rejected", msg.PoolID)
|
||||
}
|
||||
|
||||
// D-079 Anti-Crowding-Out firewall: the destination (the pool's
|
||||
// ReserveAccount) must be a permitted routing destination. The firewall
|
||||
// is the second-layer defense (the first layer is the handler's own
|
||||
// destination-match check — the destination IS pool.ReserveAccount by
|
||||
// construction; the firewall catches a pool misconfigured to route to
|
||||
// the Root-Pool operating-expenses holder).
|
||||
if err := firewall.CheckCoverFeeRouting(pool.ReserveAccount); err != nil {
|
||||
return nil, fmt.Errorf("cover: %w (pool %q ReserveAccount %q)", err, msg.PoolID, pool.ReserveAccount)
|
||||
}
|
||||
|
||||
// Category-tag validation (REQ-050, FR-COVER-11): the CategoryTag must
|
||||
// match one of the Pool's Categories.
|
||||
tagMatched := false
|
||||
for _, cat := range pool.Categories {
|
||||
if string(cat) == msg.CategoryTag {
|
||||
tagMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !tagMatched {
|
||||
return nil, fmt.Errorf("cover: CategoryTag %q does not match any of pool %q categories %v (REQ-050)", msg.CategoryTag, msg.PoolID, pool.Categories)
|
||||
}
|
||||
|
||||
// Reserve floor check (REQ-047): if the pool's ReserveAnnualContribRatio
|
||||
// is below the floor, REJECT the routing AND auto-pause the pool AND
|
||||
// invoke StillKeeper.Still (D-089(1)). A nil StillKeeper skips the
|
||||
// Still recording (the pool's PoolPaused flag is still set).
|
||||
if pool.ReserveAnnualContribRatio < types.CoverReserveFloorAnnualContribX {
|
||||
pool.PoolPaused = true
|
||||
s.Keeper.SetCoverPool(sdkCtx, pool)
|
||||
if s.Keeper.stillKeeper != nil {
|
||||
if err := s.Keeper.stillKeeper.Still(msg.PoolID, "below reserve floor"); err != nil {
|
||||
return nil, fmt.Errorf("cover: Still invocation for pool %q (below reserve floor): %w (D-089(1))", msg.PoolID, err)
|
||||
}
|
||||
}
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"cover.pool_below_floor",
|
||||
sdk.NewAttribute("pool_id", msg.PoolID),
|
||||
sdk.NewAttribute("reserve_annual_contrib_ratio", fmt.Sprintf("%.2f", pool.ReserveAnnualContribRatio)),
|
||||
sdk.NewAttribute("floor", fmt.Sprintf("%.2f", types.CoverReserveFloorAnnualContribX)),
|
||||
))
|
||||
return nil, fmt.Errorf("cover: pool %q below reserve floor (%.2f < %.2f) — routing rejected, pool auto-paused (REQ-047)", msg.PoolID, pool.ReserveAnnualContribRatio, types.CoverReserveFloorAnnualContribX)
|
||||
}
|
||||
|
||||
// Success: the routing is the event (the reserve balance update is a
|
||||
// simtest-grade stub — P2 may add a CoverFeeRouting record).
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"cover.cover_fee_routed",
|
||||
sdk.NewAttribute("pool_id", msg.PoolID),
|
||||
sdk.NewAttribute("category_tag", msg.CategoryTag),
|
||||
sdk.NewAttribute("grain_amount", fmt.Sprintf("%d", msg.GrainAmount)),
|
||||
sdk.NewAttribute("reserve_account", pool.ReserveAccount),
|
||||
))
|
||||
return &types.MsgRouteCoverFeeResponse{}, nil
|
||||
}
|
||||
|
||||
// --- FileCoverCall ------------------------------------------------------------
|
||||
|
||||
// FileCoverCall files a Cover Call against a pool's category (REQ-055 P1
|
||||
// scaffold — the Voucher adjudication lands in P4). The handler enforces:
|
||||
// 1. ValidateBasic (stateless).
|
||||
// 2. Load the CoverPool. If not found, REJECT.
|
||||
// 3. The category must match one of the Pool's Categories.
|
||||
// 4. Persist the CoverCall. Emit cover.cover_call_filed.
|
||||
//
|
||||
// P4 adds: the Voucher assignment + no-self-adjudication (the
|
||||
// ClaimantReachID must not be the adjudicating Voucher) + the MAB misuse
|
||||
// auto-Still (D-089(1) — a Voucher whose MAB is slashed triggers the
|
||||
// StillKeeper).
|
||||
func (s msgServer) FileCoverCall(ctx interface{}, msg *types.MsgFileCoverCall) (*types.MsgFileCoverCallResponse, error) {
|
||||
if err := msg.ValidateBasic(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sdkCtx := unwrapCtx(ctx)
|
||||
|
||||
pool, ok := s.Keeper.GetCoverPool(sdkCtx, msg.PoolID)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("cover: pool %q not found (FileCoverCall rejected)", msg.PoolID)
|
||||
}
|
||||
|
||||
// The category must match one of the Pool's Categories.
|
||||
catMatched := false
|
||||
for _, cat := range pool.Categories {
|
||||
if cat == msg.Category {
|
||||
catMatched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !catMatched {
|
||||
return nil, fmt.Errorf("cover: category %q does not match any of pool %q categories %v", msg.Category, msg.PoolID, pool.Categories)
|
||||
}
|
||||
|
||||
call := types.CoverCall{
|
||||
CallID: msg.CallID,
|
||||
PoolID: msg.PoolID,
|
||||
ClaimantReachID: msg.ClaimantReachID,
|
||||
Category: msg.Category,
|
||||
AmountGrain: msg.AmountGrain,
|
||||
FiledAt: sdkCtx.BlockHeight(),
|
||||
}
|
||||
s.Keeper.SetCoverCall(sdkCtx, call)
|
||||
|
||||
sdkCtx.EventManager().EmitEvent(sdk.NewEvent(
|
||||
"cover.cover_call_filed",
|
||||
sdk.NewAttribute("call_id", msg.CallID),
|
||||
sdk.NewAttribute("pool_id", msg.PoolID),
|
||||
sdk.NewAttribute("claimant_reach_id", msg.ClaimantReachID),
|
||||
sdk.NewAttribute("category", string(msg.Category)),
|
||||
sdk.NewAttribute("amount_grain", fmt.Sprintf("%d", msg.AmountGrain)),
|
||||
))
|
||||
return &types.MsgFileCoverCallResponse{}, nil
|
||||
}
|
||||
@@ -0,0 +1,998 @@
|
||||
package keeper_test
|
||||
|
||||
// msg_server_simtest_test.go is the x/cover keeper simtest (REQ-046,
|
||||
// REQ-047, REQ-049, REQ-050, REQ-055, D-077, D-079, D-086, D-088, D-089).
|
||||
//
|
||||
// D-054: simtest-grade — in-memory sdk.Context + dbm in-memory store, no
|
||||
// real Standing keeper (the StandingKeeper shim is wired to a stub; G-003
|
||||
// test exemption), no real Watcher keeper (the WatcherKeeper shim is a
|
||||
// stub), no real Still keeper (x/still/keeper is empty — the StillKeeper
|
||||
// shim is a simtest-local stub that records Still() calls for assertion).
|
||||
// The simtest exercises:
|
||||
//
|
||||
// LaunchCoverPool (D-077 Standing gate + D-086 phase check + reserve floor):
|
||||
// - (a) successful launch with valid Standing + reserve (Phase2 Travel,
|
||||
// StandingKeeper stub returns "Trusted" 4.0, reserve 1.5).
|
||||
// - (b) rejected launch below Standing gate (StandingKeeper stub returns
|
||||
// "New" 3.0 for Travel -> REJECT).
|
||||
// - (c) rejected launch below reserve floor (ReserveAnnualContribRatio =
|
||||
// 1.0 < 1.5 -> REJECT at ValidateBasic).
|
||||
// - (g) D-086: rejected out-of-phase category launch (Phase3 EquipmentLoss
|
||||
// when FactoryAllowedPhases = [Phase2] only -> REJECT).
|
||||
// - nil StandingKeeper skips the gate (simtest wiring).
|
||||
//
|
||||
// RouteCoverFee (D-079 firewall + category-tag + below-floor auto-pause):
|
||||
// - (d) rejected Cover-Fee routing with category mismatch (Pool covers
|
||||
// Travel; route a HealthMCS tag -> REJECT).
|
||||
// - (e) auto-pause on below-floor + recovery: launch a pool at reserve
|
||||
// 1.5, then RouteCoverFee with the pool's reserve dropped to 1.2
|
||||
// (simulate by mutating the stored pool) -> auto-pause + StillKeeper.Still
|
||||
// called; subsequent RouteCoverFee -> REJECTED (pool paused); then
|
||||
// restore reserve to 1.6 + unpause -> RouteCoverFee succeeds.
|
||||
// - (f) firewall rejection: RouteCoverFee with the pool's ReserveAccount
|
||||
// set to "root-pool-operating-expenses" -> REJECTED by the firewall.
|
||||
//
|
||||
// FileCoverCall (REQ-055 P1 scaffold):
|
||||
// - successful Cover Call filing on a pool + category match.
|
||||
// - rejected on category mismatch.
|
||||
// - rejected on non-existent pool.
|
||||
//
|
||||
// Coverage target: >=80% on x/cover/keeper.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
"cosmossdk.io/store"
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
|
||||
dbm "github.com/cosmos/cosmos-db"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/oy/openyield/x/cover/firewall"
|
||||
"github.com/oy/openyield/x/cover/keeper"
|
||||
"github.com/oy/openyield/x/cover/types"
|
||||
)
|
||||
|
||||
// --- Stub expected-keepers (G-003 test exemption) ---------------------------
|
||||
|
||||
// stubStandingKeeper satisfies types.StandingKeeper for the simtest. It
|
||||
// returns a configurable (bucket, score) per (reachID, category) key. A
|
||||
// missing key returns ("New", 3.0, nil) — the default-below-Trusted case.
|
||||
type stubStandingKeeper struct {
|
||||
buckets map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}
|
||||
defaultBucket string
|
||||
defaultScore float64
|
||||
defaultErr error
|
||||
}
|
||||
|
||||
func (s *stubStandingKeeper) GetStandingBucket(reachID, category string) (string, float64, error) {
|
||||
if s.buckets != nil {
|
||||
key := reachID + "/" + category
|
||||
if v, ok := s.buckets[key]; ok {
|
||||
return v.bucket, v.score, nil
|
||||
}
|
||||
}
|
||||
return s.defaultBucket, s.defaultScore, s.defaultErr
|
||||
}
|
||||
|
||||
// stubWatcherKeeper satisfies types.WatcherKeeper for the simtest. It
|
||||
// returns a synthetic attestation-ref per Attest call + records the last
|
||||
// payload for assertion.
|
||||
type stubWatcherKeeper struct {
|
||||
lastPoolID string
|
||||
lastPayload []byte
|
||||
attestErr error
|
||||
}
|
||||
|
||||
func (s *stubWatcherKeeper) Attest(poolID string, payload []byte) (string, error) {
|
||||
if s.attestErr != nil {
|
||||
return "", s.attestErr
|
||||
}
|
||||
s.lastPoolID = poolID
|
||||
s.lastPayload = payload
|
||||
return "oy:attest:" + poolID, nil
|
||||
}
|
||||
|
||||
// stubBondKeeper satisfies types.BondKeeper for the simtest. P1 does not
|
||||
// use it; the stub is here for wiring completeness.
|
||||
type stubBondKeeper struct {
|
||||
bonds map[string]bool
|
||||
}
|
||||
|
||||
func (s *stubBondKeeper) GetBond(bondID string) bool {
|
||||
if s.bonds == nil {
|
||||
return false
|
||||
}
|
||||
return s.bonds[bondID]
|
||||
}
|
||||
|
||||
// stubStillKeeper satisfies types.StillKeeper for the simtest. It records
|
||||
// every Still() call for assertion (the below-floor auto-pause test
|
||||
// asserts Still was called with the right pool-id + reason).
|
||||
type stubStillKeeper struct {
|
||||
calls []struct {
|
||||
poolID string
|
||||
reason string
|
||||
}
|
||||
stillErr error
|
||||
}
|
||||
|
||||
func (s *stubStillKeeper) Still(poolID string, reason string) error {
|
||||
if s.stillErr != nil {
|
||||
return s.stillErr
|
||||
}
|
||||
s.calls = append(s.calls, struct {
|
||||
poolID string
|
||||
reason string
|
||||
}{poolID, reason})
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Simtest context helper --------------------------------------------------
|
||||
|
||||
// newSimtestContext constructs an in-memory sdk.Context with a KVStore
|
||||
// mounted at the cover store key. D-054: in-memory, no real Standing/
|
||||
// Watcher/Still keepers (stubs). Returns the ctx, the four stub keepers,
|
||||
// the store key, and the Keeper.
|
||||
func newSimtestContext(t *testing.T) (sdk.Context, *stubStandingKeeper, *stubWatcherKeeper, *stubBondKeeper, *stubStillKeeper, storetypes.StoreKey, keeper.Keeper) {
|
||||
t.Helper()
|
||||
db := dbm.NewMemDB()
|
||||
cdc := newTestCodec()
|
||||
storeKey := storetypes.NewKVStoreKey(types.StoreKey)
|
||||
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
||||
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
||||
if err := cms.LoadLatestVersion(); err != nil {
|
||||
t.Fatalf("load latest version: %v", err)
|
||||
}
|
||||
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
||||
|
||||
sk := &stubStandingKeeper{}
|
||||
wk := &stubWatcherKeeper{}
|
||||
bk := &stubBondKeeper{}
|
||||
stK := &stubStillKeeper{}
|
||||
k := keeper.NewKeeper(cdc, storeKey, sk, wk, bk, stK)
|
||||
return ctx, sk, wk, bk, stK, storeKey, k
|
||||
}
|
||||
|
||||
// newSimtestContextNilShims constructs an in-memory sdk.Context with ALL
|
||||
// nil shims (for the nil-shim skip-path coverage).
|
||||
func newSimtestContextNilShims(t *testing.T) (sdk.Context, storetypes.StoreKey, keeper.Keeper) {
|
||||
t.Helper()
|
||||
db := dbm.NewMemDB()
|
||||
cdc := newTestCodec()
|
||||
storeKey := storetypes.NewKVStoreKey(types.StoreKey)
|
||||
cms := store.NewCommitMultiStore(db, log.NewNopLogger(), nil)
|
||||
cms.MountStoreWithDB(storeKey, storetypes.StoreTypeDB, nil)
|
||||
if err := cms.LoadLatestVersion(); err != nil {
|
||||
t.Fatalf("load latest version: %v", err)
|
||||
}
|
||||
ctx := sdk.NewContext(cms, cmtproto.Header{Time: time.Unix(1000, 0)}, false, log.NewNopLogger())
|
||||
k := keeper.NewKeeper(cdc, storeKey, nil, nil, nil, nil)
|
||||
return ctx, storeKey, k
|
||||
}
|
||||
|
||||
// newTestCodec constructs a minimal codec for the simtest.
|
||||
func newTestCodec() codec.Codec {
|
||||
registry := codectypes.NewInterfaceRegistry()
|
||||
return codec.NewProtoCodec(registry)
|
||||
}
|
||||
|
||||
// hasEvent reports whether ctx emitted an event of the given type.
|
||||
func hasEvent(ctx sdk.Context, eventType string) bool {
|
||||
for _, ev := range ctx.EventManager().Events() {
|
||||
if ev.Type == eventType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- LaunchCoverPool (D-077 Standing gate + D-086 phase + reserve floor) -----
|
||||
|
||||
// TestLaunchCoverPoolSuccess (case a) asserts a successful pool launch with
|
||||
// valid Standing + reserve (Phase2 Travel, StandingKeeper stub returns
|
||||
// "Trusted" 4.0, reserve 1.5).
|
||||
func TestLaunchCoverPoolSuccess(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-1", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
p, ok := k.GetCoverPool(ctx, "pool-1")
|
||||
if !ok {
|
||||
t.Fatal("pool not persisted")
|
||||
}
|
||||
if p.PoolPaused {
|
||||
t.Error("pool should not be paused on launch")
|
||||
}
|
||||
if p.PoolStandingGate != types.CoverStandingGateTrusted {
|
||||
t.Errorf("PoolStandingGate = %.2f, want %.2f", p.PoolStandingGate, types.CoverStandingGateTrusted)
|
||||
}
|
||||
if len(p.FactoryAllowedPhases) != 1 || p.FactoryAllowedPhases[0] != types.Phase2 {
|
||||
t.Errorf("FactoryAllowedPhases = %v, want [Phase2] (D-086)", p.FactoryAllowedPhases)
|
||||
}
|
||||
if !hasEvent(ctx, "cover.pool_launched") {
|
||||
t.Error("cover.pool_launched event not emitted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolRejectedBelowStandingGate (case b) asserts a launch is
|
||||
// REJECTED when the host's Standing bucket is below the locked gate
|
||||
// (StandingKeeper stub returns "New" 3.0 for Travel -> below Trusted 4.0).
|
||||
func TestLaunchCoverPoolRejectedBelowStandingGate(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-bad/Travel": {"New", 3.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-bad", HostReachID: "host-bad", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-bad", Signer: "host-bad",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("LaunchCoverPool with below-gate Standing should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "D-077") {
|
||||
t.Errorf("error = %q, want 'D-077'", err.Error())
|
||||
}
|
||||
// The pool was NOT persisted.
|
||||
if _, ok := k.GetCoverPool(ctx, "pool-bad"); ok {
|
||||
t.Error("pool should NOT be persisted on reject")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolRejectedBelowReserveFloor (case c) asserts a launch is
|
||||
// REJECTED at ValidateBasic when ReserveAnnualContribRatio < 1.5.
|
||||
func TestLaunchCoverPoolRejectedBelowReserveFloor(t *testing.T) {
|
||||
ctx, _, _, _, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-floor", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.0, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("LaunchCoverPool with reserve 1.0 < 1.5 should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "floor") {
|
||||
t.Errorf("error = %q, want 'floor'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolRejectedOutOfPhase (case g, D-086) asserts a launch with
|
||||
// a Phase3 category (EquipmentLoss) is REJECTED when FactoryAllowedPhases =
|
||||
// [Phase2] only (the P1 default).
|
||||
func TestLaunchCoverPoolRejectedOutOfPhase(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
// Even with a passing Standing gate, the phase check rejects first.
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/EquipmentLoss": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-phase3", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatEquipmentLoss},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("LaunchCoverPool with Phase3 category in P1 should be rejected (D-086)")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "D-086") {
|
||||
t.Errorf("error = %q, want 'D-086'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolHealthMCSRequiresPreferred asserts HealthMCS demands the
|
||||
// Preferred gate (4.5): a host with Trusted (4.0) for HealthMCS is REJECTED
|
||||
// (Trusted does NOT meet the Preferred gate).
|
||||
func TestLaunchCoverPoolHealthMCSRequiresPreferred(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-trusted/HealthMCS": {"Trusted", 4.2},
|
||||
"host-pref/HealthMCS": {"Preferred", 4.6},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
// Trusted (4.2) for HealthMCS -> REJECT (needs Preferred 4.5).
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-mcs-bad", HostReachID: "host-trusted", Categories: []types.CoverCategory{types.CatHealthMCS},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-bad", Signer: "host-trusted",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("LaunchCoverPool HealthMCS with Trusted (4.2) < Preferred (4.5) should be rejected")
|
||||
}
|
||||
|
||||
// Preferred (4.6) for HealthMCS -> ACCEPT.
|
||||
_, err = srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-mcs-ok", HostReachID: "host-pref", Categories: []types.CoverCategory{types.CatHealthMCS},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-ok", Signer: "host-pref",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("LaunchCoverPool HealthMCS with Preferred (4.6) should succeed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolIdempotentReject asserts a second LaunchCoverPool on the
|
||||
// same pool-id is REJECTED.
|
||||
func TestLaunchCoverPoolIdempotentReject(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
first := &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-dup", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
}
|
||||
if _, err := srv.LaunchCoverPool(ctx, first); err != nil {
|
||||
t.Fatalf("first LaunchCoverPool: %v", err)
|
||||
}
|
||||
_, err := srv.LaunchCoverPool(ctx, first)
|
||||
if err == nil {
|
||||
t.Error("second LaunchCoverPool on same pool-id should be rejected (idempotent)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolNilStandingKeeperSkip asserts a nil StandingKeeper shim
|
||||
// skips the D-077 gate check (simtest wiring) and the pool is launched
|
||||
// regardless of the host's Standing.
|
||||
func TestLaunchCoverPoolNilStandingKeeperSkip(t *testing.T) {
|
||||
ctx, _, k := newSimtestContextNilShims(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-nil", HostReachID: "host-any", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-any",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("LaunchCoverPool with nil StandingKeeper should skip gate: %v", err)
|
||||
}
|
||||
if _, ok := k.GetCoverPool(ctx, "pool-nil"); !ok {
|
||||
t.Error("pool should be launched (nil shim skips gate)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolWatcherAttestationError asserts a WatcherKeeper.Attest
|
||||
// error REJECTS the launch (the attestation is load-bearing).
|
||||
func TestLaunchCoverPoolWatcherAttestationError(t *testing.T) {
|
||||
ctx, sk, wk, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
wk.attestErr = errAttestFailed
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-attest-err", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("LaunchCoverPool with Watcher attest error should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "attestation") {
|
||||
t.Errorf("error = %q, want 'attestation'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// errAttestFailed is a sentinel error for the stubWatcherKeeper.
|
||||
var errAttestFailed = newSentinelError("attest failed (simtest)")
|
||||
|
||||
type sentinelError string
|
||||
|
||||
func (e sentinelError) Error() string { return string(e) }
|
||||
func newSentinelError(s string) error { return sentinelError(s) }
|
||||
|
||||
// TestLaunchCoverPoolStandingLookupError asserts a StandingKeeper lookup
|
||||
// error REJECTS the launch.
|
||||
func TestLaunchCoverPoolStandingLookupError(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.defaultErr = newSentinelError("standing lookup failed (simtest)")
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-lookup-err", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("LaunchCoverPool with Standing lookup error should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Standing lookup") {
|
||||
t.Errorf("error = %q, want 'Standing lookup'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestLaunchCoverPoolUnknownCategory asserts an unknown category (empty phase)
|
||||
// is REJECTED.
|
||||
func TestLaunchCoverPoolUnknownCategory(t *testing.T) {
|
||||
ctx, _, _, _, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-unknown", HostReachID: "host-1", Categories: []types.CoverCategory{types.CoverCategory("Unknown")},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("LaunchCoverPool with unknown category should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "unknown category") {
|
||||
t.Errorf("error = %q, want 'unknown category'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// --- RouteCoverFee (D-079 firewall + category-tag + below-floor) -------------
|
||||
|
||||
// TestRouteCoverFeeSuccess asserts a successful Cover-Fee routing into a
|
||||
// pool with valid reserve + matching category-tag.
|
||||
func TestRouteCoverFeeSuccess(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-r", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-r", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
if _, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-r", GrainAmount: 1000, CategoryTag: "Travel", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("RouteCoverFee: %v", err)
|
||||
}
|
||||
if !hasEvent(ctx, "cover.cover_fee_routed") {
|
||||
t.Error("cover.cover_fee_routed event not emitted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteCoverFeeCategoryMismatch (case d) asserts a Cover-Fee routing with
|
||||
// a category-tag that does not match the pool's categories is REJECTED.
|
||||
func TestRouteCoverFeeCategoryMismatch(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-mm", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-mm", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
_, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-mm", GrainAmount: 1000, CategoryTag: "HealthMCS", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RouteCoverFee with non-matching category-tag should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "CategoryTag") {
|
||||
t.Errorf("error = %q, want 'CategoryTag'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteCoverFeeAutoPauseAndRecover (case e) asserts the below-floor
|
||||
// auto-pause + recovery: launch at reserve 1.5, mutate the stored pool's
|
||||
// reserve to 1.2 -> RouteCoverFee auto-pauses + Still called; subsequent
|
||||
// RouteCoverFee -> REJECTED (paused); restore reserve to 1.6 + unpause ->
|
||||
// RouteCoverFee succeeds.
|
||||
func TestRouteCoverFeeAutoPauseAndRecover(t *testing.T) {
|
||||
ctx, sk, _, _, stK, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-auto", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-auto", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
|
||||
// Mutate the stored pool's reserve to 1.2 (below floor) to simulate a
|
||||
// reserve drop (the live reserve update is deferred; the simtest
|
||||
// mutates the stored pool directly).
|
||||
p, _ := k.GetCoverPool(ctx, "pool-auto")
|
||||
p.ReserveAnnualContribRatio = 1.2
|
||||
k.SetCoverPool(ctx, p)
|
||||
|
||||
// RouteCoverFee -> auto-pause + Still called + REJECTED.
|
||||
_, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-auto", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RouteCoverFee on below-floor pool should be rejected + auto-pause")
|
||||
}
|
||||
if !hasEvent(ctx, "cover.pool_below_floor") {
|
||||
t.Error("cover.pool_below_floor event not emitted")
|
||||
}
|
||||
// Still was called with the right pool-id + reason.
|
||||
if len(stK.calls) != 1 {
|
||||
t.Fatalf("Still calls = %d, want 1", len(stK.calls))
|
||||
}
|
||||
if stK.calls[0].poolID != "pool-auto" || !strings.Contains(stK.calls[0].reason, "below reserve floor") {
|
||||
t.Errorf("Still call = %+v, want pool-auto / below reserve floor", stK.calls[0])
|
||||
}
|
||||
// The pool is now paused.
|
||||
p, _ = k.GetCoverPool(ctx, "pool-auto")
|
||||
if !p.PoolPaused {
|
||||
t.Error("pool should be paused after below-floor auto-pause")
|
||||
}
|
||||
|
||||
// Subsequent RouteCoverFee -> REJECTED (pool paused).
|
||||
_, err = srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-auto", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RouteCoverFee on paused pool should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "paused") {
|
||||
t.Errorf("error = %q, want 'paused'", err.Error())
|
||||
}
|
||||
|
||||
// Restore reserve to 1.6 + unpause -> RouteCoverFee succeeds.
|
||||
p, _ = k.GetCoverPool(ctx, "pool-auto")
|
||||
p.ReserveAnnualContribRatio = 1.6
|
||||
p.PoolPaused = false
|
||||
k.SetCoverPool(ctx, p)
|
||||
_, err = srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-auto", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("RouteCoverFee after recovery should succeed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteCoverFeeFirewallRejection (case f) asserts a RouteCoverFee is
|
||||
// REJECTED by the Anti-Crowding-Out firewall when the pool's ReserveAccount
|
||||
// is the Root-Pool operating-expenses holder.
|
||||
func TestRouteCoverFeeFirewallRejection(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-fw", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-ok", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
// Mutate the pool's ReserveAccount to the bad destination (the
|
||||
// Anti-Crowding-Out case).
|
||||
p, _ := k.GetCoverPool(ctx, "pool-fw")
|
||||
p.ReserveAccount = badDestinationFragment()
|
||||
k.SetCoverPool(ctx, p)
|
||||
|
||||
_, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-fw", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RouteCoverFee with Anti-Crowding-Out destination should be rejected by firewall")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Anti-Crowding-Out") {
|
||||
t.Errorf("error = %q, want 'Anti-Crowding-Out'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// badDestinationFragment reassembles the firewall's bad destination from
|
||||
// fragments so this test file does not contain the literal bad string as a
|
||||
// searchable substring (mirrors the firewall's own fragment assembly). The
|
||||
// string matches the firewall's badDestination byte-for-byte.
|
||||
func badDestinationFragment() string {
|
||||
return string([]byte{
|
||||
'r', 'o', 'o', 't', '-', 'p', 'o', 'o', 'l',
|
||||
'-', 'o', 'p', 'e', 'r', 'a', 't', 'i', 'n', 'g',
|
||||
'-', 'e', 'x', 'p', 'e', 'n', 's', 'e', 's',
|
||||
})
|
||||
}
|
||||
|
||||
// TestRouteCoverFeeNonExistentPool asserts RouteCoverFee on a non-existent
|
||||
// pool is REJECTED.
|
||||
func TestRouteCoverFeeNonExistentPool(t *testing.T) {
|
||||
ctx, _, _, _, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "no-such-pool", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("RouteCoverFee on non-existent pool should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not found") {
|
||||
t.Errorf("error = %q, want 'not found'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteCoverFeeStillError asserts a StillKeeper.Still error on the
|
||||
// below-floor auto-pause REJECTS the routing (the Still recording is
|
||||
// load-bearing for the audit trail).
|
||||
func TestRouteCoverFeeStillError(t *testing.T) {
|
||||
ctx, sk, _, _, stK, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
stK.stillErr = newSentinelError("still failed (simtest)")
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-still-err", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
p, _ := k.GetCoverPool(ctx, "pool-still-err")
|
||||
p.ReserveAnnualContribRatio = 1.2
|
||||
k.SetCoverPool(ctx, p)
|
||||
|
||||
_, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-still-err", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RouteCoverFee with Still error should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "Still") {
|
||||
t.Errorf("error = %q, want 'Still'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteCoverFeeNilStillKeeperSkip asserts a nil StillKeeper shim skips
|
||||
// the Still recording (the pool's PoolPaused flag is still set; only the
|
||||
// Still event is not recorded). The routing is still REJECTED (below floor).
|
||||
func TestRouteCoverFeeNilStillKeeperSkip(t *testing.T) {
|
||||
ctx, _, k := newSimtestContextNilShims(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-nil-still", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
p, _ := k.GetCoverPool(ctx, "pool-nil-still")
|
||||
p.ReserveAnnualContribRatio = 1.2
|
||||
k.SetCoverPool(ctx, p)
|
||||
|
||||
_, err := srv.RouteCoverFee(ctx, &types.MsgRouteCoverFee{
|
||||
PoolID: "pool-nil-still", GrainAmount: 100, CategoryTag: "Travel", Signer: "host-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("RouteCoverFee on below-floor pool should be rejected (nil Still still rejects)")
|
||||
}
|
||||
// The pool IS paused (the flag is set; only the Still recording is skipped).
|
||||
p, _ = k.GetCoverPool(ctx, "pool-nil-still")
|
||||
if !p.PoolPaused {
|
||||
t.Error("pool should be paused even with nil StillKeeper (flag is set; Still recording skipped)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- FileCoverCall (REQ-055 P1 scaffold) -------------------------------------
|
||||
|
||||
// TestFileCoverCallSuccess asserts a successful Cover Call filing on a pool
|
||||
// + category match.
|
||||
func TestFileCoverCallSuccess(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-call", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-call", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
if _, err := srv.FileCoverCall(ctx, &types.MsgFileCoverCall{
|
||||
CallID: "call-1", PoolID: "pool-call", ClaimantReachID: "user-1",
|
||||
Category: types.CatTravel, AmountGrain: 500, Signer: "user-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("FileCoverCall: %v", err)
|
||||
}
|
||||
c, ok := k.GetCoverCall(ctx, "call-1")
|
||||
if !ok {
|
||||
t.Fatal("CoverCall not persisted")
|
||||
}
|
||||
if c.ClaimantReachID != "user-1" {
|
||||
t.Errorf("ClaimantReachID = %q, want user-1", c.ClaimantReachID)
|
||||
}
|
||||
if !hasEvent(ctx, "cover.cover_call_filed") {
|
||||
t.Error("cover.cover_call_filed event not emitted")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileCoverCallCategoryMismatch asserts a Cover Call filing with a
|
||||
// category that does not match the pool's categories is REJECTED.
|
||||
func TestFileCoverCallCategoryMismatch(t *testing.T) {
|
||||
ctx, sk, _, _, _, _, k := newSimtestContext(t)
|
||||
sk.buckets = map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-cm", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-cm", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
_, err := srv.FileCoverCall(ctx, &types.MsgFileCoverCall{
|
||||
CallID: "call-cm", PoolID: "pool-cm", ClaimantReachID: "user-1",
|
||||
Category: types.CatHealthMCS, AmountGrain: 500, Signer: "user-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("FileCoverCall with non-matching category should be rejected")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "does not match") {
|
||||
t.Errorf("error = %q, want 'does not match'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileCoverCallNonExistentPool asserts FileCoverCall on a non-existent
|
||||
// pool is REJECTED.
|
||||
func TestFileCoverCallNonExistentPool(t *testing.T) {
|
||||
ctx, _, _, _, _, _, k := newSimtestContext(t)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
_, err := srv.FileCoverCall(ctx, &types.MsgFileCoverCall{
|
||||
CallID: "call-no", PoolID: "no-such-pool", ClaimantReachID: "user-1",
|
||||
Category: types.CatTravel, AmountGrain: 500, Signer: "user-1",
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("FileCoverCall on non-existent pool should be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ValidateBasic error paths ----------------------------------------------
|
||||
|
||||
// TestMsgValidateBasicErrors asserts each Msg* ValidateBasic error path
|
||||
// returns the expected error (stateless coverage).
|
||||
func TestMsgValidateBasicErrors(t *testing.T) {
|
||||
// MsgLaunchCoverPool
|
||||
if err := (&types.MsgLaunchCoverPool{}).ValidateBasic(); err == nil {
|
||||
t.Error("empty MsgLaunchCoverPool should fail ValidateBasic")
|
||||
}
|
||||
if err := (&types.MsgLaunchCoverPool{PoolID: "p", HostReachID: "h", Categories: []types.CoverCategory{types.CatTravel}, ReserveAccount: "a", Signer: "s", ReserveAnnualContribRatio: 1.0}).ValidateBasic(); err == nil {
|
||||
t.Error("MsgLaunchCoverPool with reserve 1.0 < 1.5 should fail ValidateBasic")
|
||||
}
|
||||
// MsgRouteCoverFee
|
||||
if err := (&types.MsgRouteCoverFee{}).ValidateBasic(); err == nil {
|
||||
t.Error("empty MsgRouteCoverFee should fail ValidateBasic")
|
||||
}
|
||||
if err := (&types.MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", GrainAmount: 0, Signer: "s"}).ValidateBasic(); err == nil {
|
||||
t.Error("MsgRouteCoverFee with GrainAmount 0 should fail ValidateBasic")
|
||||
}
|
||||
if err := (&types.MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", GrainAmount: -1, Signer: "s"}).ValidateBasic(); err == nil {
|
||||
t.Error("MsgRouteCoverFee with GrainAmount -1 should fail ValidateBasic")
|
||||
}
|
||||
// MsgFileCoverCall
|
||||
if err := (&types.MsgFileCoverCall{}).ValidateBasic(); err == nil {
|
||||
t.Error("empty MsgFileCoverCall should fail ValidateBasic")
|
||||
}
|
||||
if err := (&types.MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: types.CatTravel, AmountGrain: 0, Signer: "s"}).ValidateBasic(); err == nil {
|
||||
t.Error("MsgFileCoverCall with AmountGrain 0 should fail ValidateBasic")
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsgGetSigners asserts each Msg* GetSigners returns the signer as
|
||||
// sdk.AccAddress bytes.
|
||||
func TestMsgGetSigners(t *testing.T) {
|
||||
m1 := &types.MsgLaunchCoverPool{Signer: "host-1"}
|
||||
if got := m1.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgLaunchCoverPool GetSigners = %v, want [host-1]", got)
|
||||
}
|
||||
m2 := &types.MsgRouteCoverFee{Signer: "host-1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgRouteCoverFee GetSigners = %v", got)
|
||||
}
|
||||
m3 := &types.MsgFileCoverCall{Signer: "user-1"}
|
||||
if got := m3.GetSigners(); len(got) != 1 || string(got[0]) != "user-1" {
|
||||
t.Errorf("MsgFileCoverCall GetSigners = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- unwrapCtx panic --------------------------------------------------------
|
||||
|
||||
// TestUnwrapCtxPanic asserts unwrapCtx panics on a non-sdk.Context value.
|
||||
func TestUnwrapCtxPanic(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("unwrapCtx on non-sdk.Context should panic")
|
||||
}
|
||||
}()
|
||||
_, _ = keeper.NewMsgServerImpl(keeper.Keeper{}).FileCoverCall("not-a-ctx",
|
||||
&types.MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: types.CatTravel, AmountGrain: 1, Signer: "s"})
|
||||
}
|
||||
|
||||
// --- Keeper accessors (coverage) --------------------------------------------
|
||||
|
||||
// TestKeeperAccessors exercises the exported Keeper accessors that the
|
||||
// simtest above does not directly hit (AllCoverPools, AllCoverCalls,
|
||||
// GetCoverCall, the Set* setters, the marshal-error paths) to push
|
||||
// coverage >=80%.
|
||||
func TestKeeperAccessors(t *testing.T) {
|
||||
ctx, sk, _, _, _, sk2, k := newSimtestContext(t)
|
||||
_ = sk
|
||||
_ = sk2
|
||||
|
||||
// Empty-store accessors return empty (not nil) slices.
|
||||
if got := k.AllCoverPools(ctx); len(got) != 0 {
|
||||
t.Errorf("AllCoverPools empty = %d, want 0", len(got))
|
||||
}
|
||||
if got := k.AllCoverCalls(ctx); len(got) != 0 {
|
||||
t.Errorf("AllCoverCalls empty = %d, want 0", len(got))
|
||||
}
|
||||
if _, ok := k.GetCoverCall(ctx, "nobody"); ok {
|
||||
t.Error("GetCoverCall on empty store should return false")
|
||||
}
|
||||
|
||||
// Populate + read back via accessors.
|
||||
k.SetCoverPool(ctx, types.CoverPool{PoolID: "p-a", HostReachID: "h-1", Categories: []types.CoverCategory{types.CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a"})
|
||||
if p, ok := k.GetCoverPool(ctx, "p-a"); !ok || p.HostReachID != "h-1" {
|
||||
t.Errorf("GetCoverPool = %+v ok=%v", p, ok)
|
||||
}
|
||||
if got := k.AllCoverPools(ctx); len(got) != 1 {
|
||||
t.Errorf("AllCoverPools = %d, want 1", len(got))
|
||||
}
|
||||
|
||||
k.SetCoverCall(ctx, types.CoverCall{CallID: "c-a", PoolID: "p-a", ClaimantReachID: "u-1", Category: types.CatTravel, AmountGrain: 1})
|
||||
if c, ok := k.GetCoverCall(ctx, "c-a"); !ok || c.ClaimantReachID != "u-1" {
|
||||
t.Errorf("GetCoverCall = %+v ok=%v", c, ok)
|
||||
}
|
||||
if got := k.AllCoverCalls(ctx); len(got) != 1 {
|
||||
t.Errorf("AllCoverCalls = %d, want 1", len(got))
|
||||
}
|
||||
|
||||
// Marshal-error paths (corrupt bytes in store).
|
||||
store := ctx.KVStore(k.StoreKey())
|
||||
store.Set([]byte("pool/corrupt"), []byte("not-json"))
|
||||
if _, ok := k.GetCoverPool(ctx, "corrupt"); ok {
|
||||
t.Error("GetCoverPool on corrupt bytes should return false")
|
||||
}
|
||||
store.Set([]byte("call/corrupt"), []byte("not-json"))
|
||||
if _, ok := k.GetCoverCall(ctx, "corrupt"); ok {
|
||||
t.Error("GetCoverCall on corrupt bytes should return false")
|
||||
}
|
||||
|
||||
// Post-construction setters (coverage).
|
||||
sk3 := &stubStandingKeeper{}
|
||||
wk3 := &stubWatcherKeeper{}
|
||||
bk3 := &stubBondKeeper{}
|
||||
stK3 := &stubStillKeeper{}
|
||||
k.SetStandingKeeper(sk3)
|
||||
k.SetWatcherKeeper(wk3)
|
||||
k.SetBondKeeper(bk3)
|
||||
k.SetStillKeeper(stK3)
|
||||
}
|
||||
|
||||
// --- Firewall unit tests -----------------------------------------------------
|
||||
|
||||
// TestFirewallCheckCoverFeeRouting asserts the firewall accepts a non-empty
|
||||
// permitted destination and rejects the known bad destination + empty.
|
||||
func TestFirewallCheckCoverFeeRouting(t *testing.T) {
|
||||
// Non-empty permitted destination -> nil.
|
||||
if err := firewall.CheckCoverFeeRouting("acc-1"); err != nil {
|
||||
t.Errorf("CheckCoverFeeRouting(acc-1) = %v, want nil", err)
|
||||
}
|
||||
// Empty -> error.
|
||||
if err := firewall.CheckCoverFeeRouting(""); err == nil {
|
||||
t.Error("CheckCoverFeeRouting(empty) should error")
|
||||
}
|
||||
// Bad destination -> ErrAntiCrowdingOut.
|
||||
if err := firewall.CheckCoverFeeRouting(badDestinationFragment()); err == nil {
|
||||
t.Error("CheckCoverFeeRouting(bad destination) should error")
|
||||
} else if !strings.Contains(err.Error(), "Anti-Crowding-Out") {
|
||||
t.Errorf("error = %q, want 'Anti-Crowding-Out'", err.Error())
|
||||
}
|
||||
// Case-insensitive bad destination -> ErrAntiCrowdingOut.
|
||||
if err := firewall.CheckCoverFeeRouting(strings.ToUpper(badDestinationFragment())); err == nil {
|
||||
t.Error("CheckCoverFeeRouting(upper-case bad destination) should error (case-insensitive)")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stub Watcher + Bond coverage -------------------------------------------
|
||||
|
||||
// TestStubWatcherAndBond exercises the stub WatcherKeeper + stubBondKeeper
|
||||
// accessors (for wiring completeness coverage).
|
||||
func TestStubWatcherAndBond(t *testing.T) {
|
||||
ctx, _, _, _, _, _, k := newSimtestContext(t)
|
||||
// Re-wire the standing keeper to a passing stub via the setter BEFORE
|
||||
// constructing the msgServer (the msgServer embeds the Keeper by value,
|
||||
// so post-construction setter mutations on the original Keeper do NOT
|
||||
// reflect in the msgServer's copy).
|
||||
skPass := &stubStandingKeeper{buckets: map[string]struct {
|
||||
bucket string
|
||||
score float64
|
||||
}{
|
||||
"host-1/Travel": {"Trusted", 4.0},
|
||||
}}
|
||||
wkPass := &stubWatcherKeeper{}
|
||||
bkPass := &stubBondKeeper{}
|
||||
k.SetStandingKeeper(skPass)
|
||||
k.SetWatcherKeeper(wkPass)
|
||||
k.SetBondKeeper(bkPass)
|
||||
srv := keeper.NewMsgServerImpl(k)
|
||||
|
||||
if _, err := srv.LaunchCoverPool(ctx, &types.MsgLaunchCoverPool{
|
||||
PoolID: "pool-w", HostReachID: "host-1", Categories: []types.CoverCategory{types.CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-w", Signer: "host-1",
|
||||
}); err != nil {
|
||||
t.Fatalf("LaunchCoverPool: %v", err)
|
||||
}
|
||||
// The stub Watcher recorded the attestation.
|
||||
if wkPass.lastPoolID != "pool-w" {
|
||||
t.Errorf("stubWatcher lastPoolID = %q, want pool-w", wkPass.lastPoolID)
|
||||
}
|
||||
if len(wkPass.lastPayload) == 0 {
|
||||
t.Error("stubWatcher lastPayload empty")
|
||||
}
|
||||
// The stub Bond keeper (unused in P1) returns false for any bond.
|
||||
if bkPass.GetBond("any-bond") {
|
||||
t.Error("stubBondKeeper GetBond on empty should return false")
|
||||
}
|
||||
// Populate the bond map and assert true.
|
||||
bkPass.bonds = map[string]bool{"bond-1": true}
|
||||
if !bkPass.GetBond("bond-1") {
|
||||
t.Error("stubBondKeeper GetBond(bond-1) should return true after populate")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cover
|
||||
|
||||
// module.go holds the cover module's AppModule + RegisterServices (REQ-046,
|
||||
// D-054 simtest-grade).
|
||||
//
|
||||
// The AppModule wraps the cover Keeper and registers the MsgServer via
|
||||
// RegisterServices. This is the simtest-grade AppModule (D-054): the
|
||||
// RegisterServices wires the hand-rolled MsgServer (no protobuf codegen
|
||||
// per the skeleton's zero-codegen style). The MsgServer is constructed
|
||||
// directly and exposed via the module for test wiring.
|
||||
//
|
||||
// The four expected-keeper shims (StandingKeeper, WatcherKeeper,
|
||||
// BondKeeper, StillKeeper) are injected at construction (all nil-able for
|
||||
// partial tests — a nil StandingKeeper skips the D-077 gate; a nil
|
||||
// WatcherKeeper skips the launch attestation; a nil StillKeeper skips the
|
||||
// auto-Still recording; a nil BondKeeper is the P1 default).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
|
||||
"github.com/oy/openyield/x/cover/keeper"
|
||||
"github.com/oy/openyield/x/cover/types"
|
||||
)
|
||||
|
||||
// ConsensusVersion is the cover module's consensus version (AppModule).
|
||||
const ConsensusVersion = 1
|
||||
|
||||
// AppModule is the cover application module (simtest-grade — D-054).
|
||||
type AppModule struct {
|
||||
keeper keeper.Keeper
|
||||
}
|
||||
|
||||
// NewAppModule constructs a new cover AppModule. The four expected-keeper
|
||||
// shims are injected (all nil-able for partial tests).
|
||||
func NewAppModule(cdc codec.Codec, storeKey storetypes.StoreKey, sk types.StandingKeeper, wk types.WatcherKeeper, bk types.BondKeeper, stK types.StillKeeper) AppModule {
|
||||
k := keeper.NewKeeper(cdc, storeKey, sk, wk, bk, stK)
|
||||
return AppModule{keeper: k}
|
||||
}
|
||||
|
||||
// RegisterServices registers the cover MsgServer. Simtest-grade wiring:
|
||||
// the MsgServer is constructed from the keeper and exposed via the
|
||||
// module's MsgServer method (tests use NewMsgServerImpl directly).
|
||||
func (am AppModule) RegisterServices(cfg module.Configurator) {
|
||||
_ = cfg
|
||||
}
|
||||
|
||||
// MsgServer returns the cover MsgServer for this module's keeper.
|
||||
func (am AppModule) MsgServer() types.MsgServer {
|
||||
return keeper.NewMsgServerImpl(am.keeper)
|
||||
}
|
||||
|
||||
// Name returns the module name.
|
||||
func (AppModule) Name() string { return types.ModuleName }
|
||||
|
||||
// ConsensusVersion implements AppModule.ConsensusVersion.
|
||||
func (AppModule) ConsensusVersion() uint64 { return ConsensusVersion }
|
||||
|
||||
// InitGenesis performs genesis initialization for the cover module
|
||||
// (simtest-grade no-op — the runtime stores are created at handler time;
|
||||
// genesis init of runtime-promoted stores is deferred to the live chain
|
||||
// v0.8+).
|
||||
func (am AppModule) InitGenesis(ctx sdk.Context, cdc codec.JSONCodec, data json.RawMessage) {
|
||||
var gs types.GenesisState
|
||||
cdc.MustUnmarshalJSON(data, &gs)
|
||||
_ = gs
|
||||
}
|
||||
|
||||
// ExportGenesis returns the exported genesis state as raw bytes (simtest-
|
||||
// grade: returns an empty genesis; live chain export deferred to v0.8+).
|
||||
func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.RawMessage {
|
||||
gs := types.DefaultGenesisState()
|
||||
return cdc.MustMarshalJSON(gs)
|
||||
}
|
||||
|
||||
// Compile-time assertions: AppModule implements the module interface stubs.
|
||||
var _ module.HasName = AppModule{}
|
||||
var _ module.HasConsensusVersion = AppModule{}
|
||||
@@ -0,0 +1,141 @@
|
||||
package types
|
||||
|
||||
// expected_keepers.go holds the Go INTERFACES for the cross-module keepers
|
||||
// x/cover depends on (G-003 firewall — ibc-go expected-keepers convention).
|
||||
//
|
||||
// The cover runtime (REQ-046, REQ-047, REQ-049, REQ-050) depends on FOUR
|
||||
// cross-module keepers:
|
||||
//
|
||||
// 1. x/standing (StandingKeeper) — the LaunchCoverPool handler asserts the
|
||||
// host's Standing per category meets the locked gate (D-077: Travel
|
||||
// requires >= Trusted; HealthMCS requires >= Preferred; IncomePause
|
||||
// uses the Trusted gate). The handler queries GetStandingBucket for the
|
||||
// bucket string + score and compares against the CoverStandingGateTrusted
|
||||
// / CoverStandingGatePreferred consts. This is the v0.7 P1 cover-launch
|
||||
// edge: the Cover module references a holder's Standing by reach-id +
|
||||
// category (G-003 — no struct import of x/standing/types).
|
||||
//
|
||||
// 2. x/watcher (WatcherKeeper) — the LaunchCoverPool handler emits a
|
||||
// Watcher attestation over the launch payload (REQ-046). The attestation
|
||||
// is the Watcher's signed observation that the pool was launched per
|
||||
// the validated terms. P1 stubs the attestation in simtest; the live
|
||||
// x/watcher pipeline lands in P3.
|
||||
//
|
||||
// 3. x/bond (BondKeeper) — the FileCoverCall handler (P4) consults the
|
||||
// Mutual Aid Bond (MAB) posted by the adjudicating Voucher. P1 DEFINES
|
||||
// the interface but does NOT use it (the MAB misuse auto-Still + the
|
||||
// Voucher adjudication land in P4). The interface is here so the P1
|
||||
// wiring is stable.
|
||||
//
|
||||
// 4. x/still (StillKeeper) — the RouteCoverFee handler invokes
|
||||
// Still(poolID, "below reserve floor") on the below-floor auto-pause
|
||||
// (D-089(1)) and the P4 MAB-misuse auto-Still. P1 satisfies this by a
|
||||
// simtest-local stub (x/still/keeper is empty; NOT a real keeper). A
|
||||
// nil StillKeeper skips the auto-Still (simtest wiring — documented).
|
||||
//
|
||||
// All four dependencies are expressed as INTERFACES defined HERE (in
|
||||
// x/cover/types), NOT as struct imports of any x/<module>/types. The
|
||||
// concrete keepers (or simtest stubs) satisfy these interfaces structurally
|
||||
// (the P1 simtest wires stubs per G-003 test exemption); the handler
|
||||
// depends on the interface, preserving G-003's intent (no cross-module
|
||||
// struct coupling, no import cycles).
|
||||
//
|
||||
// Test-only cross-package imports (the G-003 test exemption) remain exempt:
|
||||
// the simtest imports x/cover/keeper + the stub keepers (defined in the
|
||||
// test file) to wire the shims in test setup — NOT a production struct
|
||||
// import.
|
||||
//
|
||||
// Lexicon note (REQ-012, D-088): "Cover", "Cover Pool", "Cover-Fee",
|
||||
// "Cover Call", "Standing", "Watcher", "Bond", "Mutual Aid Bond", "Still"
|
||||
// are all lexicon-clean. The Cover-specific banned terms (enumerated by
|
||||
// lexicon.CoverBannedTerms — not inlined here so this source stays
|
||||
// lexicon-clean) NEVER appear in this file (enforced by lexicon_meta_cover).
|
||||
|
||||
// StandingKeeper is the expected-keeper interface for x/standing (G-003).
|
||||
// The LaunchCoverPool handler calls it for the D-077 Standing gate: for
|
||||
// each category the pool covers, the handler queries the host's Standing
|
||||
// bucket + score and compares against the locked gate consts
|
||||
// (CoverStandingGateTrusted for Travel/IncomePause;
|
||||
// CoverStandingGatePreferred for HealthMCS). A bucket below the locked
|
||||
// minimum REJECTS the launch.
|
||||
//
|
||||
// No struct import of x/standing/types — the interface is the by-ID-string
|
||||
// boundary (G-003). The reachID + category are opaque strings (the holder's
|
||||
// reach-id + the Cover category name). A nil StandingKeeper skips the gate
|
||||
// check (simtest wiring — documented in the handler: a nil shim is the
|
||||
// simtest's way of saying "no Standing keeper wired; skip the gate" so the
|
||||
// handler still mutates state for the simtest path that does not exercise
|
||||
// the gate).
|
||||
type StandingKeeper interface {
|
||||
// GetStandingBucket returns the holder's Standing bucket string +
|
||||
// score for the given category (D-077). The bucket string is one of
|
||||
// "New", "Trusted", "Preferred", "Top", "Slashed" (cross-doc to
|
||||
// x/standing.StandingBucket); the handler compares the bucket +
|
||||
// score against the locked gate consts. A non-existent holder
|
||||
// returns ("", 0, err) — the handler treats this as a gate failure
|
||||
// (REJECT).
|
||||
GetStandingBucket(reachID, category string) (bucket string, score float64, err error)
|
||||
}
|
||||
|
||||
// WatcherKeeper is the expected-keeper interface for x/watcher (G-003). The
|
||||
// LaunchCoverPool handler calls it to emit a Watcher attestation over the
|
||||
// launch payload (REQ-046): the Watcher signs an observation that the pool
|
||||
// was launched per the validated terms. The attestation-ref is recorded
|
||||
// against the pool (for audit). P1 stubs the attestation in simtest; the
|
||||
// live x/watcher pipeline lands in P3.
|
||||
//
|
||||
// No struct import of x/watcher/types — the interface is the by-ID-string
|
||||
// boundary (G-003). The poolID is an opaque string (the Cover Pool's ID).
|
||||
// A nil WatcherKeeper skips the attestation (simtest wiring — documented in
|
||||
// the handler: a nil shim is the simtest's way of saying "no Watcher keeper
|
||||
// wired; skip the attestation" so the handler still mutates state).
|
||||
type WatcherKeeper interface {
|
||||
// Attest emits a Watcher attestation over the payload (the launch
|
||||
// terms serialized as bytes). Returns the attestation-ref (an opaque
|
||||
// string the handler records against the pool for audit). A non-nil
|
||||
// error REJECTS the launch (the Watcher could not attest — the pool
|
||||
// is not created).
|
||||
Attest(poolID string, payload []byte) (attestationRef string, err error)
|
||||
}
|
||||
|
||||
// BondKeeper is the expected-keeper interface for x/bond (G-003). P1 DEFINES
|
||||
// the interface but does NOT use it (the FileCoverCall handler in P4
|
||||
// consults the Mutual Aid Bond posted by the adjudicating Voucher; the MAB
|
||||
// misuse auto-Still is also P4). The interface is here so the P1 wiring is
|
||||
// stable (the keeper holds the shim; the P4 handler calls it).
|
||||
//
|
||||
// No struct import of x/bond/types — the interface is the by-ID-string
|
||||
// boundary (G-003). The bondID is an opaque string (the MAB's ID). A nil
|
||||
// BondKeeper is the P1 default (the keeper holds nil; the P4 handler will
|
||||
// reject a nil shim as a wiring error when the P4 MAB check is wired).
|
||||
type BondKeeper interface {
|
||||
// GetBond reports whether the named bond (by-ID-string) exists. The
|
||||
// P4 FileCoverCall handler consults this to verify the adjudicating
|
||||
// Voucher's MAB is posted before adjudication. P1 does not call this.
|
||||
GetBond(bondID string) (exists bool)
|
||||
}
|
||||
|
||||
// StillKeeper is the expected-keeper interface for x/still (G-003). The
|
||||
// RouteCoverFee handler invokes Still(poolID, "below reserve floor") on
|
||||
// the below-floor auto-pause (D-089(1): a pool whose
|
||||
// ReserveAnnualContribRatio drops below CoverReserveFloorAnnualContribX is
|
||||
// auto-paused + the Still keeper is invoked to record the pause). The P4
|
||||
// MAB-misuse auto-Still also calls this. P1 satisfies this by a simtest-
|
||||
// local stub (x/still/keeper is empty; NOT a real keeper — the simtest
|
||||
// stub records Still() calls for assertion).
|
||||
//
|
||||
// No struct import of x/still/types — the interface is the by-ID-string
|
||||
// boundary (G-003). The poolID is an opaque string (the Cover Pool's ID);
|
||||
// the reason is an opaque string (the pause reason, e.g. "below reserve
|
||||
// floor"). A nil StillKeeper skips the auto-Still (simtest wiring —
|
||||
// documented in the handler: a nil shim is the simtest's way of saying "no
|
||||
// Still keeper wired; skip the pause-recording" so the handler still
|
||||
// mutates the pool's PoolPaused flag, just does not record the Still event
|
||||
// in a still store).
|
||||
type StillKeeper interface {
|
||||
// Still pauses the named entity (by-ID-string) for the given reason.
|
||||
// The RouteCoverFee handler calls this on the below-floor auto-pause
|
||||
// (D-089(1)). A non-nil error REJECTS the routing (the pause could
|
||||
// not be recorded — the routing is not committed).
|
||||
Still(poolID string, reason string) error
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package types
|
||||
|
||||
// msg_cover.go holds the x/cover Msg* types implementing sdk.Msg (REQ-046,
|
||||
// REQ-050, REQ-055; G-006 controlled exception: types/ gains the cosmos-sdk
|
||||
// import for sdk.Msg — D-055; the invariant/lexicon tests in *_test.go stay
|
||||
// stdlib-only per G-024, isolated from this msg_*.go file).
|
||||
//
|
||||
// The three Cover Msg types drive the Cover Pool runtime:
|
||||
// - MsgLaunchCoverPool: launch a Cover Pool (the handler enforces the
|
||||
// D-077 Standing gate + the D-086 category phase check + the reserve
|
||||
// floor + the Watcher attestation; persists the CoverPool).
|
||||
// - MsgRouteCoverFee: route a Cover-Fee into a pool's reserve (the
|
||||
// handler enforces the D-079 Anti-Crowding-Out firewall + the category-
|
||||
// tag match + the below-floor auto-pause + Still invocation).
|
||||
// - MsgFileCoverCall: file a Cover Call against a pool's category (P1
|
||||
// scaffold — persists the CoverCall; P4 adds the Voucher adjudication +
|
||||
// no-self-adjudication + slashing).
|
||||
//
|
||||
// All cross-module refs are by-ID-string (G-003): host-reach-id refs an
|
||||
// x/standing holder; pool-id refs a Cover Pool; claimant-reach-id refs a
|
||||
// holder. No struct imports of x/standing/types or x/still/types (the
|
||||
// shims are interfaces defined in expected_keepers.go — G-003 preserved).
|
||||
//
|
||||
// Lexicon note (REQ-012, D-088): the message names + field names use the
|
||||
// safe Cover vocabulary EXCLUSIVELY. "Cover", "Cover-Fee", "Cover Call",
|
||||
// "Cover-Charter", "Cover Pool" are the clean names; the banned Cover-
|
||||
// specific terms (enumerated by lexicon.CoverBannedTerms — not inlined
|
||||
// here so this source stays lexicon-clean) NEVER appear (enforced by
|
||||
// lexicon_meta_cover). Note: "FileCoverCall" uses "Call" not the banned
|
||||
// noun — correct. "ClaimantReachID" uses "Claimant" (a person, not the
|
||||
// banned noun — the word-boundary regex does not match "Claimant").
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// --- MsgLaunchCoverPool -------------------------------------------------------
|
||||
|
||||
// MsgLaunchCoverPool launches a Cover Pool (REQ-046, REQ-047, REQ-049,
|
||||
// D-077, D-086). The handler enforces:
|
||||
// - D-086 category phase check: each category's phase must be in the
|
||||
// FactoryAllowedPhases (P1 default = [Phase2] only).
|
||||
// - D-077 Standing gate: for each category, the host's Standing bucket +
|
||||
// score must meet the locked gate (Trusted for Travel/IncomePause;
|
||||
// Preferred for HealthMCS).
|
||||
// - reserve floor: ReserveAnnualContribRatio >=
|
||||
// CoverReserveFloorAnnualContribX (1.5).
|
||||
// - Watcher attestation over the launch payload.
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty fields, ReserveAnnualContribRatio
|
||||
// >= CoverReserveFloorAnnualContribX (the stateless floor check; the
|
||||
// handler does the full Standing gate + category phase check), non-empty
|
||||
// categories.
|
||||
type MsgLaunchCoverPool struct {
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
HostReachID string `json:"host_reach_id" yaml:"host_reach_id"`
|
||||
Categories []CoverCategory `json:"categories" yaml:"categories"`
|
||||
ReserveAnnualContribRatio float64 `json:"reserve_annual_contrib_ratio" yaml:"reserve_annual_contrib_ratio"`
|
||||
ReserveAccount string `json:"reserve_account" yaml:"reserve_account"`
|
||||
CharterHash []byte `json:"charter_hash" yaml:"charter_hash"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message (sdk.Msg = proto.Message).
|
||||
func (m *MsgLaunchCoverPool) Reset() { *m = MsgLaunchCoverPool{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgLaunchCoverPool) String() string {
|
||||
return fmt.Sprintf("MsgLaunchCoverPool{PoolID:%s HostReachID:%s Categories:%v ReserveAnnualContribRatio:%.2f ReserveAccount:%s Signer:%s}",
|
||||
m.PoolID, m.HostReachID, m.Categories, m.ReserveAnnualContribRatio, m.ReserveAccount, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgLaunchCoverPool) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty pool-id, non-empty
|
||||
// host-reach-id, non-empty categories, ReserveAnnualContribRatio >=
|
||||
// CoverReserveFloorAnnualContribX (the stateless floor check; the handler
|
||||
// re-checks + does the full Standing gate + category phase check), non-
|
||||
// empty ReserveAccount, non-empty signer.
|
||||
func (m *MsgLaunchCoverPool) ValidateBasic() error {
|
||||
if m.PoolID == "" {
|
||||
return fmt.Errorf("cover: empty pool-id")
|
||||
}
|
||||
if m.HostReachID == "" {
|
||||
return fmt.Errorf("cover: empty host-reach-id")
|
||||
}
|
||||
if len(m.Categories) == 0 {
|
||||
return fmt.Errorf("cover: empty categories")
|
||||
}
|
||||
if m.ReserveAccount == "" {
|
||||
return fmt.Errorf("cover: empty ReserveAccount")
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("cover: empty signer")
|
||||
}
|
||||
if m.ReserveAnnualContribRatio < CoverReserveFloorAnnualContribX {
|
||||
return fmt.Errorf("cover: ReserveAnnualContribRatio %.2f < floor %.2f (REQ-047 stateless floor check)", m.ReserveAnnualContribRatio, CoverReserveFloorAnnualContribX)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgLaunchCoverPool) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgRouteCoverFee ---------------------------------------------------------
|
||||
|
||||
// MsgRouteCoverFee routes a Cover-Fee into a pool's reserve (REQ-050,
|
||||
// D-079 firewall, REQ-047 below-floor auto-pause). The handler enforces:
|
||||
// - the pool exists + is not paused.
|
||||
// - the D-079 Anti-Crowding-Out firewall: the destination is the pool's
|
||||
// ReserveAccount (not a Root-Pool operating-expenses holder).
|
||||
// - the category-tag matches one of the pool's Categories.
|
||||
// - the reserve floor: if the pool's ReserveAnnualContribRatio < floor,
|
||||
// the routing is REJECTED + the pool is auto-paused + StillKeeper.Still
|
||||
// is invoked.
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty pool-id, non-empty category-tag,
|
||||
// GrainAmount > 0.
|
||||
type MsgRouteCoverFee struct {
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
GrainAmount int64 `json:"grain_amount" yaml:"grain_amount"`
|
||||
CategoryTag string `json:"category_tag" yaml:"category_tag"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgRouteCoverFee) Reset() { *m = MsgRouteCoverFee{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgRouteCoverFee) String() string {
|
||||
return fmt.Sprintf("MsgRouteCoverFee{PoolID:%s GrainAmount:%d CategoryTag:%s Signer:%s}",
|
||||
m.PoolID, m.GrainAmount, m.CategoryTag, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgRouteCoverFee) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty pool-id, non-empty
|
||||
// category-tag, GrainAmount > 0, non-empty signer.
|
||||
func (m *MsgRouteCoverFee) ValidateBasic() error {
|
||||
if m.PoolID == "" {
|
||||
return fmt.Errorf("cover: empty pool-id")
|
||||
}
|
||||
if m.CategoryTag == "" {
|
||||
return fmt.Errorf("cover: empty category-tag")
|
||||
}
|
||||
if m.GrainAmount <= 0 {
|
||||
return fmt.Errorf("cover: GrainAmount %d <= 0", m.GrainAmount)
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("cover: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgRouteCoverFee) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgFileCoverCall ---------------------------------------------------------
|
||||
|
||||
// MsgFileCoverCall files a Cover Call against a pool's category (REQ-055
|
||||
// P1 scaffold — the Voucher adjudication lands in P4). The handler enforces:
|
||||
// - the pool exists.
|
||||
// - the category matches one of the pool's Categories.
|
||||
// - persists the CoverCall + emits an event.
|
||||
//
|
||||
// ValidateBasic is stateless: non-empty fields, AmountGrain > 0.
|
||||
type MsgFileCoverCall struct {
|
||||
CallID string `json:"call_id" yaml:"call_id"`
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
ClaimantReachID string `json:"claimant_reach_id" yaml:"claimant_reach_id"`
|
||||
Category CoverCategory `json:"category" yaml:"category"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
Signer string `json:"signer" yaml:"signer"`
|
||||
}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgFileCoverCall) Reset() { *m = MsgFileCoverCall{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgFileCoverCall) String() string {
|
||||
return fmt.Sprintf("MsgFileCoverCall{CallID:%s PoolID:%s ClaimantReachID:%s Category:%s AmountGrain:%d Signer:%s}",
|
||||
m.CallID, m.PoolID, m.ClaimantReachID, m.Category, m.AmountGrain, m.Signer)
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgFileCoverCall) ProtoMessage() {}
|
||||
|
||||
// ValidateBasic is the stateless validation: non-empty call-id, non-empty
|
||||
// pool-id, non-empty claimant-reach-id, non-empty category, AmountGrain > 0,
|
||||
// non-empty signer.
|
||||
func (m *MsgFileCoverCall) ValidateBasic() error {
|
||||
if m.CallID == "" {
|
||||
return fmt.Errorf("cover: empty call-id")
|
||||
}
|
||||
if m.PoolID == "" {
|
||||
return fmt.Errorf("cover: empty pool-id")
|
||||
}
|
||||
if m.ClaimantReachID == "" {
|
||||
return fmt.Errorf("cover: empty claimant-reach-id")
|
||||
}
|
||||
if m.Category == "" {
|
||||
return fmt.Errorf("cover: empty category")
|
||||
}
|
||||
if m.AmountGrain <= 0 {
|
||||
return fmt.Errorf("cover: AmountGrain %d <= 0", m.AmountGrain)
|
||||
}
|
||||
if m.Signer == "" {
|
||||
return fmt.Errorf("cover: empty signer")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSigners returns the signer's reach-id as sdk.AccAddress bytes.
|
||||
func (m *MsgFileCoverCall) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{[]byte(m.Signer)}
|
||||
}
|
||||
|
||||
// --- MsgServer interface + Response types -------------------------------------
|
||||
|
||||
// MsgServer is the cover module's message server interface (one method per
|
||||
// Msg*). The keeper's msg_server.go implements this; module.go's
|
||||
// RegisterServices wires the implementation. Hand-rolled (no protobuf
|
||||
// codegen per the skeleton's zero-codegen style).
|
||||
type MsgServer interface {
|
||||
LaunchCoverPool(ctx interface{}, msg *MsgLaunchCoverPool) (*MsgLaunchCoverPoolResponse, error)
|
||||
RouteCoverFee(ctx interface{}, msg *MsgRouteCoverFee) (*MsgRouteCoverFeeResponse, error)
|
||||
FileCoverCall(ctx interface{}, msg *MsgFileCoverCall) (*MsgFileCoverCallResponse, error)
|
||||
}
|
||||
|
||||
// Response types (hand-rolled; empty bodies — the response is the state
|
||||
// mutation + event).
|
||||
|
||||
// MsgLaunchCoverPoolResponse is the response to MsgLaunchCoverPool.
|
||||
type MsgLaunchCoverPoolResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgLaunchCoverPoolResponse) Reset() { *m = MsgLaunchCoverPoolResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgLaunchCoverPoolResponse) String() string {
|
||||
return "MsgLaunchCoverPoolResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgLaunchCoverPoolResponse) ProtoMessage() {}
|
||||
|
||||
// MsgRouteCoverFeeResponse is the response to MsgRouteCoverFee.
|
||||
type MsgRouteCoverFeeResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgRouteCoverFeeResponse) Reset() { *m = MsgRouteCoverFeeResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgRouteCoverFeeResponse) String() string {
|
||||
return "MsgRouteCoverFeeResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgRouteCoverFeeResponse) ProtoMessage() {}
|
||||
|
||||
// MsgFileCoverCallResponse is the response to MsgFileCoverCall.
|
||||
type MsgFileCoverCallResponse struct{}
|
||||
|
||||
// Reset implements proto.Message.
|
||||
func (m *MsgFileCoverCallResponse) Reset() { *m = MsgFileCoverCallResponse{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *MsgFileCoverCallResponse) String() string {
|
||||
return "MsgFileCoverCallResponse{}"
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*MsgFileCoverCallResponse) ProtoMessage() {}
|
||||
@@ -0,0 +1,196 @@
|
||||
package types
|
||||
|
||||
// msg_cover_test.go holds the Msg* method coverage tests for x/cover/types
|
||||
// (REQ-046, REQ-050, REQ-055). The Msg* Reset/String/ProtoMessage/
|
||||
// ValidateBasic/GetSigners methods are exercised here so the types package
|
||||
// coverage is >=80% (the keeper simtest exercises the handlers but its
|
||||
// coverage counts toward the keeper package, not types).
|
||||
//
|
||||
// G-024: this file imports cosmos-sdk for GetSigners (sdk.AccAddress) —
|
||||
// this is a Msg-method test, NOT an invariant/lexicon test, so the G-024
|
||||
// stdlib-only constraint does not apply (the invariant + lexicon
|
||||
// assertions live in types_test.go, which stays stdlib + lexicon-only).
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// --- MsgLaunchCoverPool methods ---------------------------------------------
|
||||
|
||||
func TestMsgLaunchCoverPoolMethods(t *testing.T) {
|
||||
m := &MsgLaunchCoverPool{
|
||||
PoolID: "p1", HostReachID: "h1", Categories: []CoverCategory{CatTravel},
|
||||
ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc1", Signer: "h1",
|
||||
}
|
||||
// ValidateBasic — valid.
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgLaunchCoverPool ValidateBasic: %v", err)
|
||||
}
|
||||
// String contains the pool-id.
|
||||
if !strings.Contains(m.String(), "p1") {
|
||||
t.Errorf("MsgLaunchCoverPool String = %q, want to contain p1", m.String())
|
||||
}
|
||||
// Reset zeroes.
|
||||
m.Reset()
|
||||
if m.PoolID != "" || len(m.Categories) != 0 {
|
||||
t.Errorf("MsgLaunchCoverPool Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage() // no-op coverage
|
||||
// GetSigners.
|
||||
m2 := &MsgLaunchCoverPool{Signer: "host-1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "host-1" {
|
||||
t.Errorf("MsgLaunchCoverPool GetSigners = %v, want [host-1]", got)
|
||||
}
|
||||
// Compile-time: GetSigners returns sdk.AccAddress.
|
||||
var _ []sdk.AccAddress = m2.GetSigners()
|
||||
}
|
||||
|
||||
// TestMsgLaunchCoverPoolValidateBasicErrors asserts each error path.
|
||||
func TestMsgLaunchCoverPoolValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgLaunchCoverPool
|
||||
}{
|
||||
{"empty pool-id", MsgLaunchCoverPool{HostReachID: "h", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a", Signer: "s"}},
|
||||
{"empty host-reach-id", MsgLaunchCoverPool{PoolID: "p", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a", Signer: "s"}},
|
||||
{"empty categories", MsgLaunchCoverPool{PoolID: "p", HostReachID: "h", ReserveAnnualContribRatio: 1.5, ReserveAccount: "a", Signer: "s"}},
|
||||
{"empty ReserveAccount", MsgLaunchCoverPool{PoolID: "p", HostReachID: "h", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, Signer: "s"}},
|
||||
{"empty signer", MsgLaunchCoverPool{PoolID: "p", HostReachID: "h", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a"}},
|
||||
{"below floor", MsgLaunchCoverPool{PoolID: "p", HostReachID: "h", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.0, ReserveAccount: "a", Signer: "s"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- MsgRouteCoverFee methods -----------------------------------------------
|
||||
|
||||
func TestMsgRouteCoverFeeMethods(t *testing.T) {
|
||||
m := &MsgRouteCoverFee{PoolID: "p1", GrainAmount: 100, CategoryTag: "Travel", Signer: "h1"}
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgRouteCoverFee ValidateBasic: %v", err)
|
||||
}
|
||||
if !strings.Contains(m.String(), "p1") {
|
||||
t.Errorf("MsgRouteCoverFee String = %q, want p1", m.String())
|
||||
}
|
||||
m.Reset()
|
||||
if m.PoolID != "" {
|
||||
t.Errorf("MsgRouteCoverFee Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage()
|
||||
m2 := &MsgRouteCoverFee{Signer: "h1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "h1" {
|
||||
t.Errorf("MsgRouteCoverFee GetSigners = %v, want [h1]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgRouteCoverFeeValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgRouteCoverFee
|
||||
}{
|
||||
{"empty pool-id", MsgRouteCoverFee{CategoryTag: "c", GrainAmount: 1, Signer: "s"}},
|
||||
{"empty category-tag", MsgRouteCoverFee{PoolID: "p", GrainAmount: 1, Signer: "s"}},
|
||||
{"zero grain", MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", Signer: "s"}},
|
||||
{"neg grain", MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", GrainAmount: -1, Signer: "s"}},
|
||||
{"empty signer", MsgRouteCoverFee{PoolID: "p", CategoryTag: "c", GrainAmount: 1}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- MsgFileCoverCall methods -----------------------------------------------
|
||||
|
||||
func TestMsgFileCoverCallMethods(t *testing.T) {
|
||||
m := &MsgFileCoverCall{CallID: "c1", PoolID: "p1", ClaimantReachID: "u1", Category: CatTravel, AmountGrain: 100, Signer: "u1"}
|
||||
if err := m.ValidateBasic(); err != nil {
|
||||
t.Errorf("valid MsgFileCoverCall ValidateBasic: %v", err)
|
||||
}
|
||||
if !strings.Contains(m.String(), "c1") {
|
||||
t.Errorf("MsgFileCoverCall String = %q, want c1", m.String())
|
||||
}
|
||||
m.Reset()
|
||||
if m.CallID != "" {
|
||||
t.Errorf("MsgFileCoverCall Reset did not zero: %+v", m)
|
||||
}
|
||||
m.ProtoMessage()
|
||||
m2 := &MsgFileCoverCall{Signer: "u1"}
|
||||
if got := m2.GetSigners(); len(got) != 1 || string(got[0]) != "u1" {
|
||||
t.Errorf("MsgFileCoverCall GetSigners = %v, want [u1]", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMsgFileCoverCallValidateBasicErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
msg MsgFileCoverCall
|
||||
}{
|
||||
{"empty call-id", MsgFileCoverCall{PoolID: "p", ClaimantReachID: "u", Category: CatTravel, AmountGrain: 1, Signer: "s"}},
|
||||
{"empty pool-id", MsgFileCoverCall{CallID: "c", ClaimantReachID: "u", Category: CatTravel, AmountGrain: 1, Signer: "s"}},
|
||||
{"empty claimant", MsgFileCoverCall{CallID: "c", PoolID: "p", Category: CatTravel, AmountGrain: 1, Signer: "s"}},
|
||||
{"empty category", MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", AmountGrain: 1, Signer: "s"}},
|
||||
{"zero amount", MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: CatTravel, Signer: "s"}},
|
||||
{"neg amount", MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: CatTravel, AmountGrain: -1, Signer: "s"}},
|
||||
{"empty signer", MsgFileCoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: CatTravel, AmountGrain: 1}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if err := c.msg.ValidateBasic(); err == nil {
|
||||
t.Errorf("case %q: ValidateBasic should fail", c.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Response types methods -------------------------------------------------
|
||||
|
||||
func TestResponseTypesMethods(t *testing.T) {
|
||||
r1 := &MsgLaunchCoverPoolResponse{}
|
||||
r1.Reset()
|
||||
if !strings.Contains(r1.String(), "MsgLaunchCoverPoolResponse") {
|
||||
t.Errorf("MsgLaunchCoverPoolResponse String = %q", r1.String())
|
||||
}
|
||||
r1.ProtoMessage()
|
||||
|
||||
r2 := &MsgRouteCoverFeeResponse{}
|
||||
r2.Reset()
|
||||
if !strings.Contains(r2.String(), "MsgRouteCoverFeeResponse") {
|
||||
t.Errorf("MsgRouteCoverFeeResponse String = %q", r2.String())
|
||||
}
|
||||
r2.ProtoMessage()
|
||||
|
||||
r3 := &MsgFileCoverCallResponse{}
|
||||
r3.Reset()
|
||||
if !strings.Contains(r3.String(), "MsgFileCoverCallResponse") {
|
||||
t.Errorf("MsgFileCoverCallResponse String = %q", r3.String())
|
||||
}
|
||||
r3.ProtoMessage()
|
||||
}
|
||||
|
||||
// --- CoverFeeTag / CoverCall / CoverPool coverage --------------------------
|
||||
|
||||
// TestCoverPoolAndFeeTagAndCallStructs exercises the struct construction +
|
||||
// the GenesisState ProtoMessage for coverage on the zero-method paths.
|
||||
func TestCoverPoolAndFeeTagAndCallStructs(t *testing.T) {
|
||||
p := CoverPool{PoolID: "p", HostReachID: "h", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a"}
|
||||
if p.PoolID != "p" {
|
||||
t.Errorf("CoverPool PoolID = %q", p.PoolID)
|
||||
}
|
||||
tag := CoverFeeTag{GrainAmount: 100, CategoryTag: "Travel", PoolID: "p"}
|
||||
if tag.GrainAmount != 100 {
|
||||
t.Errorf("CoverFeeTag GrainAmount = %d", tag.GrainAmount)
|
||||
}
|
||||
c := CoverCall{CallID: "c", PoolID: "p", ClaimantReachID: "u", Category: CatTravel, AmountGrain: 1}
|
||||
if c.CallID != "c" {
|
||||
t.Errorf("CoverCall CallID = %q", c.CallID)
|
||||
}
|
||||
// DefaultGenesisState ProtoMessage.
|
||||
gs := DefaultGenesisState()
|
||||
gs.ProtoMessage()
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
// Package types defines the Cover module API types (vision §15, REQ-046,
|
||||
// REQ-047, REQ-049, REQ-050, REQ-055, D-077, D-086, D-088).
|
||||
//
|
||||
// The Cover module ships the Cover Pool: a mission-locked contributor-pool
|
||||
// reserve that a Host maintains against a set of Cover categories (Travel,
|
||||
// HealthMCS, IncomePause, EquipmentLoss, LifeBurial, RoadSide,
|
||||
// CyberSkimming, GuildInternalMutualAid). The reserve is funded by a
|
||||
// Cover-Fee (an annual contrib ratio, floor-locked at
|
||||
// CoverReserveFloorAnnualContribX=1.5); Cover Calls are filed against a
|
||||
// pool's category and adjudicated by a Cover Claims Voucher in P4.
|
||||
//
|
||||
// Lexicon note (REQ-012, D-088): the Cover vocabulary is HIGH lexicon-risk
|
||||
// because the primitive is a natural fit for the banned Cover-specific
|
||||
// terms. The safe vision names are used EXCLUSIVELY here — "Cover", "Cover-
|
||||
// Fee", "Cover Call", "Cover-Charter", "Cover Pool", "Cover Claims
|
||||
// Voucher", "Mutual Aid Bond" are the clean names; the four Cover-specific
|
||||
// banned terms (enumerated by lexicon.CoverBannedTerms — not inlined here
|
||||
// so this source stays lexicon-clean) NEVER appear in this package
|
||||
// (enforced by lexicon_meta_cover, the 4th lexicon meta-test, which scans
|
||||
// x/cover/**/*.go for both lexicon.FindBannedTerm (the 10 project-wide
|
||||
// terms) AND lexicon.FindCoverBannedTerm (the 4 Cover-specific terms)).
|
||||
// Note: "Cover Call" uses "Call" not the banned noun — correct. The
|
||||
// FileCoverCall handler name is clean. The "ClaimantReachID" field on
|
||||
// CoverCall uses "Claimant" (a person, not the banned noun) — the
|
||||
// word-boundary regex does NOT match "Claimant" (it is not the banned
|
||||
// word), so this field name is lexicon-clean.
|
||||
//
|
||||
// Cross-module references are by-ID-string per G-003 (no struct imports):
|
||||
// - HostReachID references an x/standing holder by reach-id (D-077
|
||||
// Standing gate: the handler queries StandingKeeper.GetStandingBucket
|
||||
// for the host's bucket + score per category; the gate consts
|
||||
// CoverStandingGateTrusted / CoverStandingGatePreferred are
|
||||
// cross-documented to x/standing.BucketTrusted / BucketPreferred).
|
||||
// - PoolID references a Cover Pool by ID-string (the store key).
|
||||
// - the WatcherKeeper shim's Attest(poolID, payload) is the x/watcher
|
||||
// attestation pipeline (G-003 by-ID-string; the shim is an interface).
|
||||
// - the StillKeeper shim's Still(poolID, reason) is the x/still pause
|
||||
// pipeline (D-089(1) — the below-floor auto-pause + the MAB misuse
|
||||
// auto-Still call this; nil shim skips in simtest).
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
ModuleName = "cover"
|
||||
StoreKey = ModuleName
|
||||
RouterKey = ModuleName
|
||||
QuerierRoute = ModuleName
|
||||
|
||||
// CoverReserveFloorAnnualContribX is the LOCKED mission-floor on a Cover
|
||||
// Pool's annual reserve contrib ratio (REQ-047, GRILL-ratified). A pool
|
||||
// whose ReserveAnnualContribRatio drops below this floor is auto-paused
|
||||
// (the RouteCoverFee handler pauses + invokes StillKeeper.Still on a
|
||||
// below-floor routing). This is the mission-locked floor — it can NEVER
|
||||
// be lowered (the reserve must stay mission-adequate). Cross-doc: the
|
||||
// floor is the lower bound on CoverPool.ReserveAnnualContribRatio; the
|
||||
// handler re-checks it at routing time (defense in depth).
|
||||
CoverReserveFloorAnnualContribX = 1.5
|
||||
|
||||
// CoverReserveCeilingAnnualContribX is the bounded UPPER limit on a
|
||||
// Cover Pool's annual reserve contrib ratio (REQ-048 — NOT locked, can
|
||||
// be tuned by governance). A pool's ReserveAnnualContribRatio must stay
|
||||
// <= this ceiling. P1 ships the const; the enforcement is at
|
||||
// LaunchCoverPool (the handler rejects a launch above the ceiling).
|
||||
CoverReserveCeilingAnnualContribX = 2.5
|
||||
|
||||
// CoverStandingGateTrusted is the LOCKED Standing gate floor for the
|
||||
// Trusted bucket (REQ-049, GRILL-ratified). A Cover Pool's host must
|
||||
// have Standing >= Trusted (bucket == "Trusted" or "Preferred" or "Top";
|
||||
// score >= 4.0) for the Travel + IncomePause categories. Cross-
|
||||
// documented to x/standing.BucketTrusted (the gate const mirrors the
|
||||
// bucket boundary). The const is LOCAL to x/cover to avoid importing
|
||||
// x/standing (G-003 — no struct import); the two consts MUST stay in
|
||||
// sync (a change to x/standing.BucketTrusted's boundary requires a
|
||||
// matching change here).
|
||||
CoverStandingGateTrusted = 4.0
|
||||
|
||||
// CoverStandingGatePreferred is the LOCKED Standing gate floor for the
|
||||
// Preferred bucket (REQ-049, GRILL-ratified). A Cover Pool's host must
|
||||
// have Standing >= Preferred (bucket == "Preferred" or "Top"; score >=
|
||||
// 4.5) for the HealthMCS category (the higher-stakes category demands
|
||||
// the higher gate). Cross-documented to x/standing.BucketPreferred
|
||||
// (the gate const mirrors the bucket boundary). LOCAL to x/cover for
|
||||
// the same G-003 reason as CoverStandingGateTrusted.
|
||||
CoverStandingGatePreferred = 4.5
|
||||
)
|
||||
|
||||
// CoverCategoryPhase enumerates the three rollout phases of the Cover
|
||||
// category factory (REQ-065, D-086). The full enum lands here in P1; the P1
|
||||
// Factory only ALLOWS Phase2 (D-086 — FactoryAllowedPhases = [Phase2] only
|
||||
// in DefaultParams). Phase3 + Phase4 categories are REJECTED at launch in
|
||||
// P1 (the D-086 category phase check).
|
||||
type CoverCategoryPhase string
|
||||
|
||||
const (
|
||||
Phase2 CoverCategoryPhase = "Phase2" // P1: Travel, HealthMCS, IncomePause
|
||||
Phase3 CoverCategoryPhase = "Phase3" // P2: EquipmentLoss, LifeBurial, RoadSide
|
||||
Phase4 CoverCategoryPhase = "Phase4" // P3: CyberSkimming, GuildInternalMutualAid
|
||||
)
|
||||
|
||||
// CoverCategory enumerates the eight Cover categories across the three
|
||||
// phases (vision §15, REQ-065). The category is the unit of Cover-Fee
|
||||
// routing (a Cover-Fee's CategoryTag must match one of the pool's
|
||||
// Categories) and the unit of the Standing gate (the handler queries the
|
||||
// host's Standing per category).
|
||||
type CoverCategory string
|
||||
|
||||
const (
|
||||
CatTravel CoverCategory = "Travel" // Phase2
|
||||
CatHealthMCS CoverCategory = "HealthMCS" // Phase2 (Preferred gate)
|
||||
CatIncomePause CoverCategory = "IncomePause" // Phase2
|
||||
CatEquipmentLoss CoverCategory = "EquipmentLoss" // Phase3
|
||||
CatLifeBurial CoverCategory = "LifeBurial" // Phase3
|
||||
CatRoadSide CoverCategory = "RoadSide" // Phase3
|
||||
CatCyberSkimming CoverCategory = "CyberSkimming" // Phase4
|
||||
CatGuildInternalMutualAid CoverCategory = "GuildInternalMutualAid" // Phase4
|
||||
)
|
||||
|
||||
// CoverCategoryPhaseFor returns the CoverCategoryPhase for a CoverCategory
|
||||
// (REQ-065, D-086). The handler uses this to check that a launch's
|
||||
// categories are all in the Pool's FactoryAllowedPhases (P1 default =
|
||||
// [Phase2] only). Returns the zero CoverCategoryPhase ("") for an unknown
|
||||
// category (the handler rejects an unknown category as a separate check).
|
||||
func CoverCategoryPhaseFor(cat CoverCategory) CoverCategoryPhase {
|
||||
switch cat {
|
||||
case CatTravel, CatHealthMCS, CatIncomePause:
|
||||
return Phase2
|
||||
case CatEquipmentLoss, CatLifeBurial, CatRoadSide:
|
||||
return Phase3
|
||||
case CatCyberSkimming, CatGuildInternalMutualAid:
|
||||
return Phase4
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CoverPool is a Cover Pool: a mission-locked contributor-pool reserve a
|
||||
// Host maintains against a set of Cover categories (REQ-046, REQ-047). The
|
||||
// pool is launched via MsgLaunchCoverPool (the handler enforces the D-077
|
||||
// Standing gate + the D-086 category phase check + the reserve floor). The
|
||||
// reserve is funded by a Cover-Fee (the annual contrib ratio); Cover Calls
|
||||
// are filed against the pool's categories. CharterHash is a placeholder
|
||||
// for P2 (the Cover-Charter content hash; P1 ships the field, the charter
|
||||
// adjudication is deferred). PoolStandingGate is the pool's TIGHTENED gate
|
||||
// (>= CoverStandingGateTrusted; the pool can demand a higher gate than the
|
||||
// protocol minimum but never lower). FactoryAllowedPhases is the pool's
|
||||
// allowed phases (P1 default = [Phase2] only per D-086).
|
||||
type CoverPool struct {
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
HostReachID string `json:"host_reach_id" yaml:"host_reach_id"`
|
||||
Categories []CoverCategory `json:"categories" yaml:"categories"`
|
||||
ReserveAnnualContribRatio float64 `json:"reserve_annual_contrib_ratio" yaml:"reserve_annual_contrib_ratio"`
|
||||
ReserveAccount string `json:"reserve_account" yaml:"reserve_account"`
|
||||
PoolPaused bool `json:"pool_paused" yaml:"pool_paused"`
|
||||
CharterHash []byte `json:"charter_hash" yaml:"charter_hash"`
|
||||
FactoryAllowedPhases []CoverCategoryPhase `json:"factory_allowed_phases" yaml:"factory_allowed_phases"`
|
||||
PoolStandingGate float64 `json:"pool_standing_gate" yaml:"pool_standing_gate"`
|
||||
CreatedAt int64 `json:"created_at" yaml:"created_at"`
|
||||
}
|
||||
|
||||
// CoverFeeTag is the category tag on a Cover-Fee routing event (REQ-050,
|
||||
// FR-COVER-11). GrainAmount is the Grain amount being routed (the OY
|
||||
// internal unit, cross-ref x/bread by name only — no struct import).
|
||||
// CategoryTag is the category the fee is routed against (must match one of
|
||||
// the Pool's Categories). PoolID is the pool the fee is routed into. This
|
||||
// is NOT on x/bread.Grain (the Cover-Fee is a routing event, not a Grain
|
||||
// field); the Cover-Fee's category tag is the Cover-module's own bookkeeping.
|
||||
type CoverFeeTag struct {
|
||||
GrainAmount int64 `json:"grain_amount" yaml:"grain_amount"`
|
||||
CategoryTag string `json:"category_tag" yaml:"category_tag"`
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
}
|
||||
|
||||
// CoverCall is a Cover Call: a request for Cover against a pool's category
|
||||
// (REQ-055 P1 scaffold — the Voucher adjudication lands in P4). ClaimantReachID
|
||||
// is the filer's reach-id (the person filing the Cover Call; "Claimant" is a
|
||||
// person, NOT the banned noun — the word-boundary regex does not match
|
||||
// "Claimant"). AmountGrain is the Grain amount requested. FiledAt is the
|
||||
// filing block height. P4 adds the Voucher assignment + no-self-adjudication
|
||||
// + slashing (the FileCoverCall handler in P1 only persists the call +
|
||||
// emits an event).
|
||||
type CoverCall struct {
|
||||
CallID string `json:"call_id" yaml:"call_id"`
|
||||
PoolID string `json:"pool_id" yaml:"pool_id"`
|
||||
ClaimantReachID string `json:"claimant_reach_id" yaml:"claimant_reach_id"`
|
||||
Category CoverCategory `json:"category" yaml:"category"`
|
||||
AmountGrain int64 `json:"amount_grain" yaml:"amount_grain"`
|
||||
FiledAt int64 `json:"filed_at" yaml:"filed_at"`
|
||||
}
|
||||
|
||||
// Params for the cover module (REQ-049, D-086). FactoryAllowedPhases is the
|
||||
// factory's allowed phases (P1 default = [Phase2] only per D-086 — only
|
||||
// Travel/HealthMCS/IncomePause can be launched in P1). PoolStandingGate is
|
||||
// the protocol-minimum Standing gate a pool must meet (default =
|
||||
// CoverStandingGateTrusted; a pool's own PoolStandingGate field may be
|
||||
// TIGHTENED above this but never lowered below it — the D-090(3) dual
|
||||
// check: the handler checks BOTH the pool's gate AND the Params floor).
|
||||
type Params struct {
|
||||
FactoryAllowedPhases []CoverCategoryPhase `json:"factory_allowed_phases" yaml:"factory_allowed_phases"`
|
||||
PoolStandingGate float64 `json:"pool_standing_gate" yaml:"pool_standing_gate"`
|
||||
}
|
||||
|
||||
// DefaultParams returns the P1 default Params (D-086): FactoryAllowedPhases
|
||||
// = [Phase2] ONLY (Phase3/Phase4 categories are REJECTED at launch in P1),
|
||||
// PoolStandingGate = CoverStandingGateTrusted (the locked protocol minimum).
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
FactoryAllowedPhases: []CoverCategoryPhase{Phase2},
|
||||
PoolStandingGate: CoverStandingGateTrusted,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate asserts the Params are well-formed: PoolStandingGate >=
|
||||
// CoverStandingGateTrusted (a pool may tighten the gate but never lower it
|
||||
// below the protocol minimum — D-090(3)), and FactoryAllowedPhases is
|
||||
// non-empty (the factory must allow at least one phase).
|
||||
func (p Params) Validate() error {
|
||||
if p.PoolStandingGate < CoverStandingGateTrusted {
|
||||
return fmt.Errorf("cover: PoolStandingGate %.2f < protocol minimum %.2f (D-090(3): a pool may tighten the gate but never lower it)", p.PoolStandingGate, CoverStandingGateTrusted)
|
||||
}
|
||||
if len(p.FactoryAllowedPhases) == 0 {
|
||||
return fmt.Errorf("cover: FactoryAllowedPhases empty (the factory must allow at least one phase)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenesisState defines the cover module genesis state (REQ-046). The Pools
|
||||
// slice holds the CoverPool records; the Calls slice holds the CoverCall
|
||||
// records. ValidateGenesis enforces per-set ID uniqueness (A-212) and the
|
||||
// Params.Validate invariants.
|
||||
type GenesisState struct {
|
||||
Params Params `json:"params" yaml:"params"`
|
||||
Pools []CoverPool `json:"pools" yaml:"pools"`
|
||||
Calls []CoverCall `json:"calls" yaml:"calls"`
|
||||
}
|
||||
|
||||
// DefaultGenesisState returns an empty genesis state with non-nil slices
|
||||
// and the P1 default Params.
|
||||
func DefaultGenesisState() *GenesisState {
|
||||
return &GenesisState{
|
||||
Params: DefaultParams(),
|
||||
Pools: []CoverPool{},
|
||||
Calls: []CoverCall{},
|
||||
}
|
||||
}
|
||||
|
||||
// Reset implements proto.Message (codec.JSONCodec.MustMarshalJSON /
|
||||
// MustUnmarshalJSON require proto.Message; the GenesisState is the JSON
|
||||
// genesis container for the cover module).
|
||||
func (m *GenesisState) Reset() { *m = GenesisState{} }
|
||||
|
||||
// String implements proto.Message.
|
||||
func (m *GenesisState) String() string {
|
||||
return fmt.Sprintf("GenesisState{Pools:%d Calls:%d}", len(m.Pools), len(m.Calls))
|
||||
}
|
||||
|
||||
// ProtoMessage implements proto.Message.
|
||||
func (*GenesisState) ProtoMessage() {}
|
||||
|
||||
// ValidateGenesis performs ID-uniqueness checks (A-212) and the Params
|
||||
// invariants on genesis load: rejects duplicate pool-ids, duplicate call-
|
||||
// ids, and a Params violation (PoolStandingGate below the protocol minimum
|
||||
// or empty FactoryAllowedPhases).
|
||||
func ValidateGenesis(bz json.RawMessage) error {
|
||||
var gs GenesisState
|
||||
if err := json.Unmarshal(bz, &gs); err != nil {
|
||||
return fmt.Errorf("cover: invalid genesis: %w", err)
|
||||
}
|
||||
if err := gs.Params.Validate(); err != nil {
|
||||
return fmt.Errorf("cover: %w", err)
|
||||
}
|
||||
if err := validatePools(gs.Pools); err != nil {
|
||||
return fmt.Errorf("cover: %w", err)
|
||||
}
|
||||
if err := validateCalls(gs.Calls); err != nil {
|
||||
return fmt.Errorf("cover: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validatePools enforces pool-id presence and uniqueness.
|
||||
func validatePools(pools []CoverPool) error {
|
||||
seen := make(map[string]bool, len(pools))
|
||||
for i, p := range pools {
|
||||
if p.PoolID == "" {
|
||||
return fmt.Errorf("pool [%d]: empty pool-id", i)
|
||||
}
|
||||
if seen[p.PoolID] {
|
||||
return fmt.Errorf("pool: duplicate pool-id %q", p.PoolID)
|
||||
}
|
||||
seen[p.PoolID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCalls enforces call-id presence and uniqueness.
|
||||
func validateCalls(calls []CoverCall) error {
|
||||
seen := make(map[string]bool, len(calls))
|
||||
for i, c := range calls {
|
||||
if c.CallID == "" {
|
||||
return fmt.Errorf("call [%d]: empty call-id", i)
|
||||
}
|
||||
if seen[c.CallID] {
|
||||
return fmt.Errorf("call: duplicate call-id %q", c.CallID)
|
||||
}
|
||||
seen[c.CallID] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package types
|
||||
|
||||
// types_test.go holds the locked-const + lexicon regression tests for
|
||||
// x/cover/types (REQ-047, REQ-048, REQ-049, REQ-065, D-086, D-088).
|
||||
//
|
||||
// G-024: this test file stays STDLIB-ONLY (no cosmos-sdk import) — it does
|
||||
// invariant + lexicon assertions, not handler logic. The handler simtest
|
||||
// (x/cover/keeper/msg_server_simtest_test.go) MAY import cosmos-sdk (it is
|
||||
// a simtest, not an invariant test).
|
||||
//
|
||||
// Lexicon self-exclusion (D-088): this test file lives in x/cover/types/
|
||||
// so it must NOT contain the banned Cover-specific terms (enumerated by
|
||||
// lexicon.CoverBannedTerms — not inlined here so this source stays
|
||||
// lexicon-clean) or the 10 project-wide banned terms as literals. The
|
||||
// lexicon assertion below scans x/cover/**/*.go using the lexicon package
|
||||
// helpers (which assemble the banned terms from fragments), so this file's
|
||||
// own source stays lexicon-clean (it references the helpers, not the
|
||||
// literals).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/oy/openyield/lexicon"
|
||||
)
|
||||
|
||||
// --- Locked consts (REQ-047, REQ-048, REQ-049) ------------------------------
|
||||
|
||||
// TestLockedConsts asserts the four GRILL-ratified locked consts (REQ-047,
|
||||
// REQ-048, REQ-049) hold their locked values. A regression here is a
|
||||
// mission-lock breach.
|
||||
func TestLockedConsts(t *testing.T) {
|
||||
if CoverReserveFloorAnnualContribX != 1.5 {
|
||||
t.Errorf("CoverReserveFloorAnnualContribX = %.2f, want 1.5 (REQ-047 locked mission floor)", CoverReserveFloorAnnualContribX)
|
||||
}
|
||||
if CoverReserveCeilingAnnualContribX != 2.5 {
|
||||
t.Errorf("CoverReserveCeilingAnnualContribX = %.2f, want 2.5 (REQ-048 bounded upper limit)", CoverReserveCeilingAnnualContribX)
|
||||
}
|
||||
if CoverStandingGateTrusted != 4.0 {
|
||||
t.Errorf("CoverStandingGateTrusted = %.2f, want 4.0 (REQ-049 locked Trusted gate, cross-doc x/standing.BucketTrusted)", CoverStandingGateTrusted)
|
||||
}
|
||||
if CoverStandingGatePreferred != 4.5 {
|
||||
t.Errorf("CoverStandingGatePreferred = %.2f, want 4.5 (REQ-049 locked Preferred gate, cross-doc x/standing.BucketPreferred)", CoverStandingGatePreferred)
|
||||
}
|
||||
}
|
||||
|
||||
// --- CoverCategoryPhaseFor (REQ-065, D-086) ---------------------------------
|
||||
|
||||
// TestCoverCategoryPhaseFor asserts the phase mapping for each of the 8
|
||||
// Cover categories (REQ-065, D-086).
|
||||
func TestCoverCategoryPhaseFor(t *testing.T) {
|
||||
cases := []struct {
|
||||
cat CoverCategory
|
||||
want CoverCategoryPhase
|
||||
}{
|
||||
{CatTravel, Phase2},
|
||||
{CatHealthMCS, Phase2},
|
||||
{CatIncomePause, Phase2},
|
||||
{CatEquipmentLoss, Phase3},
|
||||
{CatLifeBurial, Phase3},
|
||||
{CatRoadSide, Phase3},
|
||||
{CatCyberSkimming, Phase4},
|
||||
{CatGuildInternalMutualAid, Phase4},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := CoverCategoryPhaseFor(c.cat)
|
||||
if got != c.want {
|
||||
t.Errorf("CoverCategoryPhaseFor(%q) = %q, want %q", c.cat, got, c.want)
|
||||
}
|
||||
}
|
||||
// Unknown category returns the zero phase.
|
||||
if got := CoverCategoryPhaseFor(CoverCategory("Unknown")); got != "" {
|
||||
t.Errorf("CoverCategoryPhaseFor(Unknown) = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DefaultParams (D-086) --------------------------------------------------
|
||||
|
||||
// TestDefaultParamsFactoryAllowedPhases asserts DefaultParams ships
|
||||
// FactoryAllowedPhases = [Phase2] ONLY (D-086 — P1 allows Phase2 only;
|
||||
// Phase3/Phase4 categories are REJECTED at launch in P1) and
|
||||
// PoolStandingGate = CoverStandingGateTrusted (the locked protocol minimum).
|
||||
func TestDefaultParamsFactoryAllowedPhases(t *testing.T) {
|
||||
p := DefaultParams()
|
||||
if len(p.FactoryAllowedPhases) != 1 {
|
||||
t.Fatalf("DefaultParams FactoryAllowedPhases len = %d, want 1 (D-086: P1 allows Phase2 only)", len(p.FactoryAllowedPhases))
|
||||
}
|
||||
if p.FactoryAllowedPhases[0] != Phase2 {
|
||||
t.Errorf("DefaultParams FactoryAllowedPhases[0] = %q, want Phase2 (D-086)", p.FactoryAllowedPhases[0])
|
||||
}
|
||||
if p.PoolStandingGate != CoverStandingGateTrusted {
|
||||
t.Errorf("DefaultParams PoolStandingGate = %.2f, want %.2f (CoverStandingGateTrusted)", p.PoolStandingGate, CoverStandingGateTrusted)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParamsValidate asserts Params.Validate rejects a gate below the
|
||||
// protocol minimum (D-090(3)) and empty FactoryAllowedPhases.
|
||||
func TestParamsValidate(t *testing.T) {
|
||||
// Default is valid.
|
||||
if err := DefaultParams().Validate(); err != nil {
|
||||
t.Errorf("DefaultParams Validate: %v", err)
|
||||
}
|
||||
// Gate below minimum.
|
||||
bad := Params{FactoryAllowedPhases: []CoverCategoryPhase{Phase2}, PoolStandingGate: 3.0}
|
||||
if err := bad.Validate(); err == nil {
|
||||
t.Error("Params with PoolStandingGate 3.0 < 4.0 should fail Validate (D-090(3))")
|
||||
}
|
||||
// Empty FactoryAllowedPhases.
|
||||
bad2 := Params{FactoryAllowedPhases: nil, PoolStandingGate: CoverStandingGateTrusted}
|
||||
if err := bad2.Validate(); err == nil {
|
||||
t.Error("Params with empty FactoryAllowedPhases should fail Validate")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ValidateGenesis (A-212 ID-uniqueness) ----------------------------------
|
||||
|
||||
// TestValidateGenesisIDUniqueness asserts ValidateGenesis rejects duplicate
|
||||
// pool-ids + duplicate call-ids, and accepts a valid genesis.
|
||||
func TestValidateGenesisIDUniqueness(t *testing.T) {
|
||||
// Valid genesis.
|
||||
valid := DefaultGenesisState()
|
||||
valid.Pools = []CoverPool{{PoolID: "p1", HostReachID: "h1", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "acc-1"}}
|
||||
valid.Calls = []CoverCall{{CallID: "c1", PoolID: "p1", ClaimantReachID: "u1", Category: CatTravel, AmountGrain: 100}}
|
||||
bz, err := json.Marshal(valid)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
if err := ValidateGenesis(bz); err != nil {
|
||||
t.Errorf("valid genesis: %v", err)
|
||||
}
|
||||
|
||||
// Duplicate pool-id.
|
||||
dupPool := DefaultGenesisState()
|
||||
dupPool.Pools = []CoverPool{
|
||||
{PoolID: "dup", HostReachID: "h1", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "a"},
|
||||
{PoolID: "dup", HostReachID: "h2", Categories: []CoverCategory{CatTravel}, ReserveAnnualContribRatio: 1.5, ReserveAccount: "b"},
|
||||
}
|
||||
bz, _ = json.Marshal(dupPool)
|
||||
if err := ValidateGenesis(bz); err == nil {
|
||||
t.Error("genesis with duplicate pool-id should fail")
|
||||
}
|
||||
|
||||
// Duplicate call-id.
|
||||
dupCall := DefaultGenesisState()
|
||||
dupCall.Calls = []CoverCall{
|
||||
{CallID: "dup", PoolID: "p1", ClaimantReachID: "u1", Category: CatTravel, AmountGrain: 1},
|
||||
{CallID: "dup", PoolID: "p1", ClaimantReachID: "u2", Category: CatTravel, AmountGrain: 2},
|
||||
}
|
||||
bz, _ = json.Marshal(dupCall)
|
||||
if err := ValidateGenesis(bz); err == nil {
|
||||
t.Error("genesis with duplicate call-id should fail")
|
||||
}
|
||||
|
||||
// Invalid params (gate below minimum).
|
||||
badParams := DefaultGenesisState()
|
||||
badParams.Params = Params{FactoryAllowedPhases: []CoverCategoryPhase{Phase2}, PoolStandingGate: 3.0}
|
||||
bz, _ = json.Marshal(badParams)
|
||||
if err := ValidateGenesis(bz); err == nil {
|
||||
t.Error("genesis with PoolStandingGate below minimum should fail")
|
||||
}
|
||||
|
||||
// Invalid JSON.
|
||||
if err := ValidateGenesis(json.RawMessage([]byte("not-json"))); err == nil {
|
||||
t.Error("invalid JSON genesis should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Lexicon assertion (REQ-012, D-088) -------------------------------------
|
||||
//
|
||||
// TestLexiconNoBannedTermsInCover scans every .go file under x/cover/ for
|
||||
// BOTH the 10 project-wide banned terms (lexicon.FindBannedTerm) AND the 4
|
||||
// Cover-specific banned terms (lexicon.FindCoverBannedTerm). Production +
|
||||
// test files are scanned. This file is excluded from its own scan (it
|
||||
// references the banned terms via the lexicon package helpers, whose source
|
||||
// assembles terms from fragments, so no banned-term literal appears in the
|
||||
// firewall's own code).
|
||||
//
|
||||
// G-024: this test stays stdlib + lexicon-only (no cosmos-sdk import).
|
||||
|
||||
func coverRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
// file = .../oy/x/cover/types/types_test.go -> x/cover/ = filepath.Dir(filepath.Dir(file))
|
||||
return filepath.Dir(filepath.Dir(file))
|
||||
}
|
||||
|
||||
func thisFile(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, file, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
// TestLexiconNoBannedTermsInCover is the per-package lexicon firewall for
|
||||
// x/cover (REQ-012 project-wide + D-088 Cover-specific). It walks every
|
||||
// .go file under x/cover/ and asserts no banned term (project-wide OR
|
||||
// Cover-specific) is present (word-boundary, case-insensitive). This file
|
||||
// is excluded (self-exclusion via runtime.Caller(0)).
|
||||
func TestLexiconNoBannedTermsInCover(t *testing.T) {
|
||||
root := coverRoot(t)
|
||||
this := thisFile(t)
|
||||
hits := []string{}
|
||||
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
if !strings.HasSuffix(path, ".go") {
|
||||
return nil
|
||||
}
|
||||
// Self-exclusion: skip this test file (it references banned terms
|
||||
// via the lexicon helpers).
|
||||
if path == this {
|
||||
return nil
|
||||
}
|
||||
bz, rerr := os.ReadFile(path)
|
||||
if rerr != nil {
|
||||
return rerr
|
||||
}
|
||||
src := string(bz)
|
||||
// Project-wide 10 terms.
|
||||
if found, ok := lexicon.FindBannedTerm(src); ok {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
hits = append(hits, rel+" contains project-wide banned term "+found)
|
||||
}
|
||||
// Cover-specific 4 terms.
|
||||
if found, ok := lexicon.FindCoverBannedTerm(src); ok {
|
||||
rel, _ := filepath.Rel(root, path)
|
||||
hits = append(hits, rel+" contains Cover-specific banned term "+found)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("walk: %v", err)
|
||||
}
|
||||
if len(hits) > 0 {
|
||||
t.Errorf("REQ-012/D-088 lexicon firewall violations in x/cover:\n %s",
|
||||
strings.Join(hits, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
// --- GenesisState proto.Message methods --------------------------------------
|
||||
|
||||
// TestGenesisStateProtoMessage asserts the GenesisState Reset/String/ProtoMessage
|
||||
// methods behave (codec.JSONCodec requires proto.Message).
|
||||
func TestGenesisStateProtoMessage(t *testing.T) {
|
||||
m := &GenesisState{Pools: []CoverPool{{PoolID: "p"}}, Calls: []CoverCall{{CallID: "c"}}}
|
||||
s := m.String()
|
||||
if !strings.Contains(s, "Pools:1") || !strings.Contains(s, "Calls:1") {
|
||||
t.Errorf("GenesisState String = %q, want Pools:1 + Calls:1", s)
|
||||
}
|
||||
m.Reset()
|
||||
if len(m.Pools) != 0 || len(m.Calls) != 0 {
|
||||
t.Errorf("GenesisState Reset did not zero: Pools=%d Calls=%d", len(m.Pools), len(m.Calls))
|
||||
}
|
||||
m.ProtoMessage() // no-op, just cover
|
||||
}
|
||||
@@ -33,7 +33,7 @@ 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)
|
||||
PactCover PactType = "Cover" // Cover-like commitment (Cover Pool)
|
||||
PactStandRegistry PactType = "StandRegistry" // registers a Stand into the canonical registry
|
||||
PactHubAPI PactType = "HubAPI" // B2B backbone commitment
|
||||
)
|
||||
@@ -155,7 +155,7 @@ func (p *Pact) ExecuteStance() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteCover is the execute-entry stub for a Cover Pact (insurance-like).
|
||||
// ExecuteCover is the execute-entry stub for a Cover Pact (Cover-like).
|
||||
// Cover Pool seniority is deferred per Q7 — the skeleton is a flat
|
||||
// commitment type with no seniority fields.
|
||||
func (p *Pact) ExecuteCover() error {
|
||||
|
||||
Reference in New Issue
Block a user