Novelty, HarmAwareness and ProgramEffect each get a 0..1 realism bound, served in ConfigBounds and enforced in ValidateConfig with field-named errors (the frontend matches on the JSON name to flag the right control). Reject/boundary test cases added.
76 lines
3.3 KiB
Go
76 lines
3.3 KiB
Go
package engine
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Bounds are realism limits agreed 2026-06-11 (e.g. a student does not
|
|
// have 150 close friendships, and a guaranteed forward is not a real
|
|
// base rate), enforced engine-side so the public API is covered too.
|
|
|
|
func TestConfigBoundsContainDefaultConfig(t *testing.T) {
|
|
if err := ValidateConfig(DefaultConfig()); err != nil {
|
|
t.Fatalf("ValidateConfig(DefaultConfig()) = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestValidateConfigRejectsOutOfBounds(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
mutate func(*Config)
|
|
wantMention string // the offending field's JSON name, for the UI's inline message
|
|
}{
|
|
{"too few students", func(c *Config) { c.NumStudents = 9 }, "numStudents"},
|
|
{"too many students", func(c *Config) { c.NumStudents = 501 }, "numStudents"},
|
|
{"no friendships", func(c *Config) { c.EdgesPerNode = 0 }, "edgesPerNode"},
|
|
{"implausibly many friendships", func(c *Config) { c.EdgesPerNode = 9 }, "edgesPerNode"},
|
|
{"negative triangle probability", func(c *Config) { c.TriangleProb = -0.1 }, "triangleProb"},
|
|
{"triangle probability above one", func(c *Config) { c.TriangleProb = 1.1 }, "triangleProb"},
|
|
{"negative forward probability", func(c *Config) { c.ForwardProb = -0.1 }, "forwardProb"},
|
|
{"guaranteed forwarding", func(c *Config) { c.ForwardProb = 0.95 }, "forwardProb"},
|
|
{"negative novelty", func(c *Config) { c.Novelty = -0.1 }, "novelty"},
|
|
{"novelty above one", func(c *Config) { c.Novelty = 1.1 }, "novelty"},
|
|
{"negative harm awareness", func(c *Config) { c.HarmAwareness = -0.1 }, "harmAwareness"},
|
|
{"harm awareness above one", func(c *Config) { c.HarmAwareness = 1.1 }, "harmAwareness"},
|
|
{"negative program effect", func(c *Config) { c.ProgramEffect = -0.1 }, "programEffect"},
|
|
{"program effect above one", func(c *Config) { c.ProgramEffect = 1.1 }, "programEffect"},
|
|
{"negative education budget", func(c *Config) { c.NumEducated = -1 }, "numEducated"},
|
|
{"educating more students than exist", func(c *Config) { c.NumEducated = 121 }, "numEducated"},
|
|
{"negative origin", func(c *Config) { c.Origin = -1 }, "origin"},
|
|
{"origin beyond the last student", func(c *Config) { c.Origin = 120 }, "origin"},
|
|
}
|
|
for _, testCase := range cases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
config := DefaultConfig()
|
|
testCase.mutate(&config)
|
|
err := ValidateConfig(config)
|
|
if err == nil {
|
|
t.Fatalf("ValidateConfig accepted %+v, want error", config)
|
|
}
|
|
if !strings.Contains(err.Error(), testCase.wantMention) {
|
|
t.Errorf("error %q does not name field %q", err, testCase.wantMention)
|
|
}
|
|
if _, err := RunScenario(config, StrategyNone); err == nil {
|
|
t.Error("RunScenario accepted the same config, want error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateConfigAcceptsBoundaryValues(t *testing.T) {
|
|
config := DefaultConfig()
|
|
bounds := ConfigBounds()
|
|
config.NumStudents = bounds.NumStudents.Max
|
|
config.EdgesPerNode = bounds.EdgesPerNode.Max
|
|
config.ForwardProb = bounds.ForwardProb.Max
|
|
config.TriangleProb = bounds.TriangleProb.Max
|
|
config.Novelty = bounds.Novelty.Max
|
|
config.HarmAwareness = bounds.HarmAwareness.Max
|
|
config.ProgramEffect = bounds.ProgramEffect.Max
|
|
config.NumEducated = config.NumStudents
|
|
config.Origin = config.NumStudents - 1
|
|
if err := ValidateConfig(config); err != nil {
|
|
t.Fatalf("ValidateConfig at the upper bounds = %v, want nil", err)
|
|
}
|
|
}
|