engine: realistic config bounds, enforced and served

ConfigBounds (students 10-500, edgesPerNode 1-8, forwardProb 0-0.9,
triangleProb 0-1) caps what RunScenario accepts: a student does not
keep 150 close friendships and a guaranteed forward is not a real
base rate. numEducated gains its missing upper limit (numStudents).
Bounds-side validation also protects the public server from absurd
numStudents values. GET /api/config/default now returns
{config, bounds} so the frontend can drive its controls from the
same numbers; the frontend does not call it yet, so this commit
changes no UI behaviour.
This commit is contained in:
Justin Visser 2026-06-11 15:54:07 +02:00
parent 232faf892e
commit bd890384e5
7 changed files with 194 additions and 15 deletions

View file

@ -11,6 +11,14 @@ import (
"github.com/JustinZeus/spreadlab/internal/engine"
)
// DefaultConfigResponse is the body of GET /api/config/default: the
// default world plus the engine's field bounds, so the frontend drives
// its controls and clamping from the same numbers the engine enforces.
type DefaultConfigResponse struct {
Config engine.Config `json:"config"`
Bounds engine.Bounds `json:"bounds"`
}
// ComparisonResponse bundles what the dashboard needs to render one
// comparison: the config that was run, echoed back so frontend state stays
// honest, and one result per strategy.
@ -53,7 +61,10 @@ func NewServer() http.Handler {
}
func handleDefaultConfig(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, engine.DefaultConfig())
writeJSON(w, http.StatusOK, DefaultConfigResponse{
Config: engine.DefaultConfig(),
Bounds: engine.ConfigBounds(),
})
}
func handleComparison(w http.ResponseWriter, r *http.Request) {

View file

@ -51,12 +51,15 @@ func TestDefaultConfigEndpoint(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
}
var config engine.Config
if err := json.Unmarshal(recorder.Body.Bytes(), &config); err != nil {
var response DefaultConfigResponse
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
t.Fatal(err)
}
if config != engine.DefaultConfig() {
t.Errorf("served config %+v, want %+v", config, engine.DefaultConfig())
if response.Config != engine.DefaultConfig() {
t.Errorf("served config %+v, want %+v", response.Config, engine.DefaultConfig())
}
if response.Bounds != engine.ConfigBounds() {
t.Errorf("served bounds %+v, want %+v", response.Bounds, engine.ConfigBounds())
}
}