// Package lexicon_meta holds the project-wide lexicon firewall meta-test // (REQ-012, G-004, G-009). It is the durable firewall created in v0.2 P1 // Wave 3; P5-01-01 EXTENDS it rather than recreating it. // // The meta-test scans every .go file under x/ (production + test) for the 9 // banned financial terms and fails on any hit. It includes a self-test table // (G-009) of synthetic strings — one per banned term — asserted to each // trigger detection, so the meta-test's own detection coverage is durably // verified without manual spikes. // // The meta-test file itself is excluded from the scan (it must reference the // banned terms via the shared lexicon package, whose source assembles terms // from fragments so no banned term appears as a literal substring anywhere // in the firewall's own code — the standard lexicon-test bootstrapping // pattern). package lexicon_meta import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/oy/openyield/lexicon" ) // TestLexiconMetaNoBannedTermsInX is the project-wide firewall (G-004). // It walks every .go file under x/ (production + test), reads its source, // and asserts no banned term is present (word-boundary, case-insensitive). // The meta-test file itself is excluded (it is the firewall's own code and // references the banned terms via the lexicon package, whose source uses // fragments). // // Passes at P1: the v0.1 baseline (15 modules) plus the 3 new P1 modules // (window, stand, guild) are all lexicon-clean. func TestLexiconMetaNoBannedTermsInX(t *testing.T) { xRoot := repoXRoot(t) thisFile := thisFile(t) hits := []string{} err := filepath.Walk(xRoot, 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 } // Exclude the meta-test file itself (the firewall's own code). if path == thisFile { return nil } bz, rerr := os.ReadFile(path) if rerr != nil { return rerr } if found, ok := lexicon.FindBannedTerm(string(bz)); ok { rel, _ := filepath.Rel(xRoot, path) hits = append(hits, rel+" contains banned term "+found) } return nil }) if err != nil { t.Fatalf("walk: %v", err) } if len(hits) > 0 { t.Errorf("REQ-012 lexicon firewall violations:\n %s", strings.Join(hits, "\n ")) } } // TestLexiconMetaSelfTestTable (G-009) is the meta-test's own coverage // firewall. Each synthetic string 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. // // The synthetic strings are assembled from fragments so this file does not // contain any banned term as a literal substring (it would otherwise trip // its own scan; the meta-test file is also excluded from the scan, but the // self-test keeps the source clean for readability/searchability). func TestLexiconMetaSelfTestTable(t *testing.T) { terms := lexicon.BannedTerms() // The spec lists 10 banned terms (plan docs say "9", counting dollar/euro // as a pair): bank, deposit, interest, yield, currency, dollar, euro, // account, savings, depositor. if len(terms) != 10 { t.Fatalf("BannedTerms() len = %d, want 10", len(terms)) } // Each synthetic string embeds exactly one banned term in a plausible // sentence context. Each must be detected. synthetic := []string{ "open a " + terms[0] + " here", // bank "make a " + terms[1] + " now", // deposit "compounding " + terms[2] + " rate", // interest "the " + terms[3] + " is 5pct", // yield "foreign " + terms[4] + " pair", // currency "price in " + terms[5], // dollar "price in " + terms[6], // euro "freeze the " + terms[7], // account "move to " + terms[8] + " now", // savings "the " + terms[9] + " lost money", // depositor } if len(synthetic) != len(terms) { t.Fatalf("synthetic table len = %d, want %d", len(synthetic), len(terms)) } for i, s := range synthetic { found, ok := lexicon.FindBannedTerm(s) if !ok { t.Errorf("G-009 self-test [%d]: synthetic string did not trigger detection: %q", i, s) continue } if found != terms[i] { t.Errorf("G-009 self-test [%d]: detected %q, want %q (in %q)", i, found, terms[i], s) } } } // TestLexiconMetaBannedTermsCount asserts exactly 10 banned terms are // configured (locked-const for the firewall's scope; spec lists 10, plan docs // say "9" counting dollar/euro as a pair). func TestLexiconMetaBannedTermsCount(t *testing.T) { terms := lexicon.BannedTerms() if len(terms) != 10 { t.Errorf("BannedTerms() len = %d, want 10 (REQ-012)", len(terms)) } seen := map[string]bool{} for _, tr := range terms { if seen[tr] { t.Errorf("duplicate banned term %q", tr) } seen[tr] = true } } // TestLexiconMetaNoFalsePositiveOnOpenYield asserts the module name // "openyield" does NOT trigger the "yield" banned term (word-boundary // matching must not match substrings of identifiers). This is the // regression firewall for the word-boundary detection design. func TestLexiconMetaNoFalsePositiveOnOpenYield(t *testing.T) { cases := []string{ "github.com/oy/openyield/x/window/types", "package openyield", "openyield is the module", "european resident", } for _, s := range cases { if _, ok := lexicon.FindBannedTerm(s); ok { t.Errorf("false positive: %q triggered a banned term (word-boundary must avoid this)", s) } } } // repoXRoot returns the absolute path to the repo's x/ directory by walking // up from this test file. func repoXRoot(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } // file = .../oy/lexicon_meta_test.go -> repo root is its dir; x/ is repo/x repoRoot := filepath.Dir(file) return filepath.Join(repoRoot, "x") } // thisFile returns the absolute path of this meta-test file (to exclude it // from its own scan). func thisFile(t *testing.T) string { t.Helper() _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("runtime.Caller failed") } return file }