Config is the single source of truth for parameters (TS types will be generated from these structs in milestone 2); all randomness flows from its three seeds, so identical configs give identical results. Golden test pins the default world: none=99/120 (82%), random=70/120 (58%), most-connected=7/120 (6%). Same story as the prototype's 85/64/8 with different dice; the ordering and the collapse are asserted explicitly, exact Python numbers are out of scope by design. 'go run ./cmd/spreadlab' prints the three-scenario comparison. This completes milestone 1 (engine ported, parameterised, tested).
87 lines
3.2 KiB
Go
87 lines
3.2 KiB
Go
package engine
|
|
|
|
import "testing"
|
|
|
|
// Golden regression tests, per the handoff: exact reach values are pinned
|
|
// for the default (fixed-seed) config so any change to engine behaviour
|
|
// shows up as a diff, and the qualitative ordering vs the prototype is
|
|
// asserted explicitly. The exact numbers intentionally differ from the
|
|
// Python figure (different RNG); the SHAPE of the result must match:
|
|
// no program >> random >> most-connected, with most-connected collapsing.
|
|
|
|
func runAllStrategies(t *testing.T) map[Strategy]Result {
|
|
t.Helper()
|
|
results := make(map[Strategy]Result)
|
|
for _, strategy := range []Strategy{StrategyNone, StrategyRandom, StrategyMostConnected} {
|
|
result, err := RunScenario(DefaultConfig(), strategy)
|
|
if err != nil {
|
|
t.Fatalf("RunScenario(%q): %v", strategy, err)
|
|
}
|
|
results[strategy] = result
|
|
}
|
|
return results
|
|
}
|
|
|
|
func TestRunScenarioGoldenReachValues(t *testing.T) {
|
|
// Pinned from the first verified run (2026-06-10). The prototype's
|
|
// figure showed 102/77/10 out of 120 (85%/64%/8%); ours is the same
|
|
// story with different dice.
|
|
wantReached := map[Strategy]int{
|
|
StrategyNone: 99, // 82%
|
|
StrategyRandom: 70, // 58%
|
|
StrategyMostConnected: 7, // 6%
|
|
}
|
|
results := runAllStrategies(t)
|
|
for strategy, result := range results {
|
|
t.Logf("%-15s educated=%2d reached=%3d/120 (%.0f%%) rounds=%d",
|
|
strategy, len(result.Educated), result.NumReached, result.ReachedPct, result.NumRounds)
|
|
if want, pinned := wantReached[strategy]; pinned && result.NumReached != want {
|
|
t.Errorf("%s: NumReached = %d, want %d", strategy, result.NumReached, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunScenarioQualitativeOrdering(t *testing.T) {
|
|
results := runAllStrategies(t)
|
|
noProgram := results[StrategyNone].NumReached
|
|
random := results[StrategyRandom].NumReached
|
|
mostConnected := results[StrategyMostConnected].NumReached
|
|
|
|
if noProgram <= random || random <= mostConnected {
|
|
t.Errorf("want noProgram > random > mostConnected, got %d, %d, %d",
|
|
noProgram, random, mostConnected)
|
|
}
|
|
if noProgram < 60 { // an unchecked cascade must reach most of the school
|
|
t.Errorf("no program reached only %d/120, expected a majority", noProgram)
|
|
}
|
|
if mostConnected > 30 { // educating the hubs must collapse the spread
|
|
t.Errorf("most-connected still reached %d/120, expected a collapse", mostConnected)
|
|
}
|
|
}
|
|
|
|
func TestRunScenarioRejectsInvalidInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*Config)
|
|
}{
|
|
{name: "forward probability above one", mutate: func(c *Config) { c.ForwardProb = 1.5 }},
|
|
{name: "origin out of range", mutate: func(c *Config) { c.Origin = 120 }},
|
|
{name: "negative educated count", mutate: func(c *Config) { c.NumEducated = -1 }},
|
|
{name: "edges per node too large", mutate: func(c *Config) { c.EdgesPerNode = 120 }},
|
|
}
|
|
for _, testCase := range tests {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
config := DefaultConfig()
|
|
testCase.mutate(&config)
|
|
if _, err := RunScenario(config, StrategyNone); err == nil {
|
|
t.Error("RunScenario accepted an invalid config")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("unknown strategy", func(t *testing.T) {
|
|
if _, err := RunScenario(DefaultConfig(), Strategy("telepathy")); err == nil {
|
|
t.Error("RunScenario accepted an unknown strategy")
|
|
}
|
|
})
|
|
}
|