Files
gbr-games/cmd/gbrgames/build_test.go
T
gregandClaude Opus 5.5 17f6477b71
Validate game templates / validate (push) Successful in 20s
Validate game templates / boot-test (push) Successful in 1m11s
Validate game templates / publish (push) Skipped
feat(builder): repair existing templates, a weekly check that queues repairs, and eval
The builder could only create games; nothing noticed when a live template
stopped working, and there was no way to tell whether a builder change made
it better or worse.

- repair -id ID: boots an existing template as nodes run it; if it fails,
  the same investigation as for a draft runs and the model changes only what
  the diagnosis says. The worker handles "repair" jobs (gameId from the
  panel) and opens "fix(games): repair ..." on draft/<id>; a template that
  boots as it is reports "nothing to repair" and opens no PR. The boot and
  fix loop is now shared by builds and repairs.
- validate.yml runs weekly and boot-tests every game; failed games are sent
  to the panel (X-GBR-Event: games_boot_failed) to queue repairs.
- eval -ids all: builds games that already have a template from scratch,
  blind, and scores each draft against it (boots, same image and app, ports,
  ready line, settings found) into eval.md and eval.json.
- research skips our own sites, so neither research nor eval reads our
  templates back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:27:58 -04:00

111 lines
4.3 KiB
Go

package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestBootFailureKeepsVerdictAndServerOutput(t *testing.T) {
out := "[boot-test] deploying boottest-example (cm2network/steamcmd)\n" +
"[boot-test] container started; waiting for ready pattern /Server ready/\n" +
"[boot-test] FAILED: container exited before it was ready\n" +
"---- last 2 log lines ----\n" +
"Unknown argument --save-dir\n" +
"Usage: example_server ...\n"
got := bootFailure(out)
if !strings.Contains(got, "failed: FAILED: container exited before it was ready") {
t.Errorf("verdict missing: %s", got)
}
if !strings.Contains(got, "Unknown argument --save-dir") || strings.Contains(got, "deploying boottest") {
t.Errorf("want only the server's last lines, got: %s", got)
}
if v := bootVerdict("[boot-test] PASSED: READY: Server ready"); v != "PASSED: READY: Server ready" {
t.Errorf("verdict = %q", v)
}
}
func TestOutsideDirsSkipsSteamNoise(t *testing.T) {
r := &bootReport{ContainerChanges: []string{
"A /home/steam/.config/example-game/worlds/w.db",
"A /home/steam/Steam/logs/connection_log.txt",
"C /home/steam/Steam/config/config.vdf",
"C /home/steam/.steam/registry.vdf",
"A /usr/sbin/docker-init",
}}
got := outsideDirs(r)
if len(got) != 1 || got[0] != "/home/steam/.config/example-game/worlds" {
t.Errorf("got %v", got)
}
if outsideDirs(nil) != nil {
t.Error("nil report should have no dirs")
}
}
func TestVariantNotes(t *testing.T) {
v := variant{Title: "Crossplay", Description: "PC + Xbox", StartupHint: "./example_server -crossplay", Ports: []string{"30000/udp"}}
got := variantNotes(v, "needs Xbox")
for _, want := range []string{"variant: Crossplay (PC + Xbox)", "documented startup: ./example_server -crossplay", "30000/udp", "the admin asked for: needs Xbox"} {
if !strings.Contains(got, want) {
t.Errorf("missing %q in %q", want, got)
}
}
}
func TestBlankRequiredLookingSettings(t *testing.T) {
dir := t.TempDir()
game := filepath.Join(dir, "example")
os.MkdirAll(game, 0o755)
os.WriteFile(filepath.Join(game, "game.yaml"), []byte(`id: example
defaultConfig:
JoinPassword: ""
AdminPassword: ""
ServerPassword: set-by-default
ServerName: x
configSchema:
properties:
JoinPassword: {type: string}
AdminPassword: {type: string, x-generate: {type: password}}
ServerPassword: {type: string}
ServerName: {type: string}
`), 0o644)
got := blankRequiredLooking(dir, "example")
if len(got) != 1 || got[0] != "JoinPassword" {
t.Errorf("got %v, want only JoinPassword (AdminPassword is generated, ServerPassword has a default)", got)
}
msg := emptySettingsProblem(got, "FAILED: server crashed")
if !strings.Contains(msg, "stops when JoinPassword is empty") || !strings.Contains(msg, `"x-generate": {"type": "password"}`) {
t.Errorf("message: %s", msg)
}
}
func TestScoreDraftAgainstTheRealTemplate(t *testing.T) {
real := map[string]any{"defaultDockerImage": "cm2network/steamcmd", "installMethod": "STEAMCMD",
"installConfig": map[string]any{"appId": "1000000"}, "defaultPorts": []any{"30000/udp", "30001/udp"},
"configSchema": map[string]any{"properties": map[string]any{"ServerName": map[string]any{}, "MaxPlayers": map[string]any{}}}}
draft := map[string]any{"defaultDockerImage": "cm2network/steamcmd", "installMethod": "STEAMCMD",
"installConfig": map[string]any{"appId": "1000000"}, "defaultPorts": []any{"30000/udp", "30002/tcp"},
"readyPattern": `Server is ready`,
"configSchema": map[string]any{"properties": map[string]any{"SERVER_NAME": map[string]any{}, "WorldName": map[string]any{}}}}
var s evalScore
scoreDraft(&s, real, map[string]any{"readyExample": "12:00:01 Server is ready for players"}, draft)
if !s.SameRoute || s.PortsSame || !s.ReadyMatch || s.SettingsHit != 1 || s.RealSetting != 2 || s.Settings != 2 {
t.Errorf("score %+v", s)
}
if strings.Join(s.PortsDiff, ",") != "extra 30002/tcp,missing 30001/udp" {
t.Errorf("ports diff %v", s.PortsDiff)
}
}
func TestPRTextForBuildsAndRepairs(t *testing.T) {
_, title := prText("Example", "job1", &buildResult{Passed: true})
if title != "feat(games): add Example" {
t.Errorf("build title %q", title)
}
commit, title := prText("Example", "job1", &buildResult{Repair: true})
if title != "WIP: fix(games): repair Example (did not boot yet)" || !strings.HasPrefix(commit, "fix(games): repair Example") {
t.Errorf("repair %q / %q", title, commit)
}
}