diff options
Diffstat (limited to 'internal/game/hardening_casino_entropy_wiring_test.go')
| -rw-r--r-- | internal/game/hardening_casino_entropy_wiring_test.go | 72 |
1 files changed, 72 insertions, 0 deletions
diff --git a/internal/game/hardening_casino_entropy_wiring_test.go b/internal/game/hardening_casino_entropy_wiring_test.go new file mode 100644 index 0000000..4f7c75e --- /dev/null +++ b/internal/game/hardening_casino_entropy_wiring_test.go @@ -0,0 +1,72 @@ +package game + +// Closure regressions for the casino RNG hardening: the entropy-seeded +// constructor must be the one PRODUCTION wires, not merely available. + +import ( + "io/fs" + "os" + "path/filepath" + "strings" + "testing" +) + +// TestProductionCasinoSeedIsEntropy pins the production wiring: game.New must +// build its casino manager with casino.NewManagerEntropy, and no production +// file may call casino.NewManager(seed). That constructor exists only as the +// deterministic test seam, because a wall-clock seed makes every shoe, reel, +// and spin stream reconstructible from the startup nanosecond. +// +// The manager's seed is unexported and deliberately not observable through the +// casino API (NewManagerEntropy and NewManager are otherwise +// indistinguishable), so this is a source-level guard: it is the only place +// the choice of constructor can be pinned. It fails on a revert to +// NewManager(time.Now().UnixNano()). +func TestProductionCasinoSeedIsEntropy(t *testing.T) { + root := filepath.Join("..", "..") + sawEntropyCall := false + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + name := d.Name() + // Skip VCS internals, generated/test data, and the persisted + // game data (which is not Go source at all). + if path != root && (strings.HasPrefix(name, ".") || name == "testdata" || name == "vendor") { + return fs.SkipDir + } + return nil + } + if !strings.HasSuffix(d.Name(), ".go") || strings.HasSuffix(d.Name(), "_test.go") { + return nil + } + data, readErr := os.ReadFile(path) + if readErr != nil { + return readErr + } + rel, relErr := filepath.Rel(root, path) + if relErr != nil { + rel = path + } + for i, line := range strings.Split(string(data), "\n") { + trimmed := strings.TrimSpace(line) + if strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "*") { + continue // prose about the seam, not a call + } + if strings.Contains(trimmed, "casino.NewManager(") { + t.Errorf("%s:%d calls the deterministic casino.NewManager(seed) outside tests: %s", rel, i+1, trimmed) + } + if strings.Contains(trimmed, "casino.NewManagerEntropy()") { + sawEntropyCall = true + } + } + return nil + }) + if err != nil { + t.Fatalf("walk module: %v", err) + } + if !sawEntropyCall { + t.Fatal("no production call to casino.NewManagerEntropy() found: the casino RNG entropy fix is not wired") + } +} |
