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>
This commit is contained in:
1 parent
dd92b6be4c
commit
8623ba2d7b
16 files changed
+395
-120
No files matched your search
@@ -14,12 +14,12 @@ func textSettings(keys ...string) map[string]any {
|
||||
return map[string]any{"properties": props}
|
||||
}
|
||||
|
||||
func TestSplitSettingsCatchesTheValheimDraft(t *testing.T) {
|
||||
// What the builder drafted for Valheim: quoted in the assignment, split by the unquoted $ARGS
|
||||
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:-Valheim Server} -port 2456 -world ${WORLD_NAME:-Dedicated}"; ` +
|
||||
"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 ./valheim_server.x86_64 $ARGS`,
|
||||
`HOME=/server ./example_server $ARGS`,
|
||||
"configSchema": textSettings("ServerName", "WorldName", "Password"),
|
||||
}
|
||||
problems := splitSettings(game)
|
||||
@@ -36,8 +36,8 @@ func TestSplitSettingsCatchesTheValheimDraft(t *testing.T) {
|
||||
|
||||
func TestSplitSettingsAcceptsQuotedAndArrays(t *testing.T) {
|
||||
for _, startup := range []string{
|
||||
`./valheim_server.x86_64 -name "${SERVER_NAME}" -world "$WORLD_NAME" -port 2456`,
|
||||
`args=(-name "$SERVER_NAME" -world "${WORLD_NAME:-Dedicated}"); [ -n "$PASSWORD" ] && args+=(-password "$PASSWORD"); ./valheim_server.x86_64 "${args[@]}"`,
|
||||
`./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")}
|
||||
@@ -72,12 +72,12 @@ func TestSplitSettingsDirectAndArrayMisuse(t *testing.T) {
|
||||
|
||||
func TestPortSettings(t *testing.T) {
|
||||
game := map[string]any{
|
||||
"defaultPorts": []any{"2456/udp", "2457/udp"},
|
||||
"defaultPorts": []any{"30000/udp", "30001/udp"},
|
||||
"defaultConfig": map[string]any{
|
||||
"GamePort": json.Number("2456"),
|
||||
"GamePort": json.Number("30000"),
|
||||
"QueryPort": json.Number("27015"),
|
||||
"RCONPort": json.Number("25575"),
|
||||
"PublicPort": "{PUBLIC_PORT_2456}",
|
||||
"PublicPort": "{PUBLIC_PORT_30000}",
|
||||
"Transport": "udp",
|
||||
"Export": json.Number("9"),
|
||||
},
|
||||
@@ -86,7 +86,7 @@ func TestPortSettings(t *testing.T) {
|
||||
}},
|
||||
}
|
||||
got := strings.Join(portSettings(game), "\n")
|
||||
if !strings.Contains(got, "setting GamePort lets owners change the port") || !strings.Contains(got, "write 2456 directly") {
|
||||
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") {
|
||||
@@ -105,3 +105,47 @@ func TestPortSettings(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user