Files
gbr-games/cmd/gbrgames/draftchecks_test.go
T
gregandClaude Opus 5.5 8623ba2d7b
Validate game templates / validate (push) Successful in 20s
Validate game templates / boot-test (push) Skipped
Validate game templates / publish (push) Skipped
feat(builder): documented launch options only, and no game-specific code
Launch options must be documented. A draft passed an option its server
doesn't have; servers ignore unknown options, so it booted and passed every
boot test while its setting did nothing. build now reads the pages research
based the variant on, in full, and the drafter rejects any launch option in
the startup command that none of them, nor the image's own scripts, mention.
Shell tools' options (mkdir -p, [ -n ...]) are skipped; the check is off
without sources.

The builder is game-agnostic:
- AUTHORING.md (the model's rules), the research prompt and the schema
  describe the rules without naming a game.
- The draft prompt no longer includes real templates from the repo. Its
  format example is one invented server (no real command, options, ports or
  app id), checked by TestExampleTemplateFollowsTheRules.
- Comments, help texts, BUILDER.md and test fixtures use invented names and
  values.
- TestBuilderIsGameAgnostic reads every game's name and id from games/ and
  fails if any appears in the builder's code, tests, rules, schemas or docs.

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

152 lines
6.6 KiB
Go

package main
import (
"encoding/json"
"strings"
"testing"
)
func textSettings(keys ...string) map[string]any {
props := map[string]any{}
for _, k := range keys {
props[k] = map[string]any{"type": "string"}
}
return map[string]any{"properties": props}
}
func TestSplitSettingsCatchesAnArgsString(t *testing.T) {
// A real draft: quoted in the assignment, split by the unquoted $ARGS
game := map[string]any{
"defaultStartupCommand": `ARGS="-name ${SERVER_NAME:-Example Server} -port 30000 -world ${WORLD_NAME:-Dedicated}"; ` +
`[ -n "$PASSWORD" ] && ARGS="$ARGS -password $PASSWORD"; [ "$CROSSPLAY" = "true" ] && ARGS="$ARGS -crossplay"; ` +
`HOME=/server ./example_server $ARGS`,
"configSchema": textSettings("ServerName", "WorldName", "Password"),
}
problems := splitSettings(game)
got := strings.Join(problems, "\n")
if len(problems) != 3 {
t.Fatalf("want ServerName, WorldName and Password, got:\n%s", got)
}
for _, key := range []string{"setting Password", "setting ServerName", "setting WorldName", "inside $ARGS"} {
if !strings.Contains(got, key) {
t.Errorf("missing %q in:\n%s", key, got)
}
}
}
func TestSplitSettingsAcceptsQuotedAndArrays(t *testing.T) {
for _, startup := range []string{
`./example_server -name "${SERVER_NAME}" -world "$WORLD_NAME" -port 30000`,
`args=(-name "$SERVER_NAME" -world "${WORLD_NAME:-Dedicated}"); [ -n "$PASSWORD" ] && args+=(-password "$PASSWORD"); ./example_server "${args[@]}"`,
`bash -c 'exec ./server -name "$SERVER_NAME"'`,
} {
game := map[string]any{"defaultStartupCommand": startup, "configSchema": textSettings("ServerName", "WorldName", "Password")}
if p := splitSettings(game); len(p) != 0 {
t.Errorf("%s\n flagged: %v", startup, p)
}
}
}
func TestSplitSettingsDirectAndArrayMisuse(t *testing.T) {
cases := map[string]string{
`./server -name ${SERVER_NAME}`: "is not in double quotes",
`bash -c 'exec ./server -name $SERVER_NAME'`: "is not in double quotes",
`args=(-name "$SERVER_NAME"); ./server ${args[@]}`: "inside $args",
`args=(-name $SERVER_NAME); ./server "${args[@]}"`: "is not in double quotes",
`export OPTS="-name ${SERVER_NAME}" && ./run $OPTS`: "inside $OPTS",
}
for startup, want := range cases {
game := map[string]any{"defaultStartupCommand": startup, "configSchema": textSettings("ServerName")}
got := strings.Join(splitSettings(game), "\n")
if !strings.Contains(got, want) {
t.Errorf("%s\n want %q, got %q", startup, want, got)
}
}
// Numbers and booleans can't contain spaces
game := map[string]any{"defaultStartupCommand": `./server -port ${GAME_PORT}`, "configSchema": map[string]any{
"properties": map[string]any{"GamePort": map[string]any{"type": "integer"}}}}
if p := splitSettings(game); len(p) != 0 {
t.Errorf("integer setting flagged: %v", p)
}
}
func TestPortSettings(t *testing.T) {
game := map[string]any{
"defaultPorts": []any{"30000/udp", "30001/udp"},
"defaultConfig": map[string]any{
"GamePort": json.Number("30000"),
"QueryPort": json.Number("27015"),
"RCONPort": json.Number("25575"),
"PublicPort": "{PUBLIC_PORT_30000}",
"Transport": "udp",
"Export": json.Number("9"),
},
"configSchema": map[string]any{"properties": map[string]any{
"GamePort": map[string]any{"type": "integer"}, "QueryPort": map[string]any{"type": "integer"},
}},
}
got := strings.Join(portSettings(game), "\n")
if !strings.Contains(got, "setting GamePort lets owners change the port") || !strings.Contains(got, "write 30000 directly") {
t.Errorf("editable published port not caught:\n%s", got)
}
if !strings.Contains(got, "setting QueryPort defaults to port 27015, which defaultPorts does not publish") {
t.Errorf("unpublished port not caught:\n%s", got)
}
for _, key := range []string{"RCONPort", "PublicPort", "Transport", "Export"} {
if strings.Contains(got, "setting "+key+" ") {
t.Errorf("%s should be left alone:\n%s", key, got)
}
}
// A published port written by the template itself, not offered to owners, is fine
delete(game["configSchema"].(map[string]any)["properties"].(map[string]any), "GamePort")
delete(game["defaultConfig"].(map[string]any), "QueryPort")
if p := portSettings(game); len(p) != 0 {
t.Errorf("got %v", p)
}
}
func TestUndocumentedOptionsCatchesMaxPlayers(t *testing.T) {
// A real draft's startup: every option is documented except -maxplayers
game := map[string]any{"defaultStartupCommand": `args=(-name "${SERVER_NAME:-GBRHosting - Example}" -port 30000 -world "${WORLD_NAME:-Dedicated}" -datadir /server); ` +
`[ -n "${SERVER_PASSWORD}" ] && args+=(-password "${SERVER_PASSWORD}"); [ "${CROSSPLAY}" = "true" ] && args+=(-crossplay); ` +
`[ -n "${MAX_PLAYERS}" ] && args+=(-maxplayers "${MAX_PLAYERS}"); ./example_server "${args[@]}"`}
docs := "Launch with `-name`, `-port`, `-world`, `-password`, `-datadir <path>` and `-crossplay` to allow consoles."
got := undocumentedOptions(game, docs, nil)
if len(got) != 1 || !strings.Contains(got[0], "passes -maxplayers,") {
t.Fatalf("want only -maxplayers flagged, got %v", got)
}
// Once removed, nothing is left to flag
game["defaultStartupCommand"] = strings.Replace(game["defaultStartupCommand"].(string), `[ -n "${MAX_PLAYERS}" ] && args+=(-maxplayers "${MAX_PLAYERS}"); `, "", 1)
if got := undocumentedOptions(game, docs, nil); len(got) != 0 {
t.Errorf("got %v", got)
}
}
func TestUndocumentedOptionsIgnoresShellAndUsesImageScripts(t *testing.T) {
game := map[string]any{"defaultStartupCommand": `mkdir -p /server/.config; cp -f /a /b; [ -n "$X" ] && [ "$Y" -eq 1 ] && ` +
`exec ./server --listen-port=7777 -ServerPlatform=ALL -logs`}
facts := &imageFacts{Scripts: map[string]string{"/start.sh": `exec "$@" -logs`}}
got := strings.Join(undocumentedOptions(game, "Use --listen-port and -ServerPlatform=ALL for crossplay.", facts), "\n")
if got != "" {
t.Errorf("shell options, documented ones and ones the image's script uses must pass, got %q", got)
}
// Without sources the check is off (draft run by hand, or no reader configured)
game["defaultStartupCommand"] = "./server -madeup"
if p := undocumentedOptions(game, "", nil); p != nil {
t.Errorf("got %v", p)
}
if p := undocumentedOptions(game, "unrelated page", nil); len(p) != 1 {
t.Errorf("want -madeup flagged, got %v", p)
}
}
func TestUndocumentedOptionsWholeWords(t *testing.T) {
// "-port" in the docs doesn't document "-portal", and "-port" isn't documented by "-portal"
game := map[string]any{"defaultStartupCommand": "./server -port 1 -portal x"}
got := strings.Join(undocumentedOptions(game, "options: -port", nil), "")
if !strings.Contains(got, "passes -portal,") || strings.Contains(got, "-port,") {
t.Errorf("got %q", got)
}
}