Code Farm: add front-view server rack and API-backed terminal

This commit is contained in:
2026-10-19 21:32:45 +02:00
parent 7274cc1840
commit 8155f394bd
18 changed files with 1375 additions and 98 deletions
+120
View File
@@ -0,0 +1,120 @@
"""The terminal must cover the real router contract, including form parameters."""
import ast
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[4]
COMMANDS = ROOT / "devplacepy/static/js/GameCommands.js"
def command_specs():
source = COMMANDS.read_text(encoding="utf-8")
body = source.split("static API = Object.freeze(", 1)[1].split(");", 1)[0]
return json.loads(body)
def game_routes():
routes = {}
for filename in ("index.py", "farm.py"):
tree = ast.parse((ROOT / "devplacepy/routers/game" / filename).read_text(encoding="utf-8"))
for node in tree.body:
if not isinstance(node, (ast.AsyncFunctionDef, ast.FunctionDef)):
continue
for decorator in node.decorator_list:
if not isinstance(decorator, ast.Call) or not isinstance(decorator.func, ast.Attribute):
continue
if decorator.func.attr in ("get", "post"):
routes[(decorator.func.attr.upper(), "/game" + ast.literal_eval(decorator.args[0]))] = node
return routes
def test_terminal_covers_every_current_game_route():
exposed = {(spec["method"], spec["path"]) for spec in command_specs()}
assert exposed == set(game_routes())
def test_terminal_parameters_match_game_form_models():
models = ast.parse((ROOT / "devplacepy/models/game.py").read_text(encoding="utf-8"))
fields = {
node.name: {field.target.id for field in node.body if isinstance(field, ast.AnnAssign)}
for node in models.body if isinstance(node, ast.ClassDef)
}
for spec in command_specs():
route = game_routes()[(spec["method"], spec["path"])]
for argument in route.args.args:
if argument.arg != "data":
continue
model = argument.annotation.slice.elts[0].id
assert set(spec.get("params", {})) | set(spec.get("fixed", {})) == fields[model], spec
for variable in re.findall(r"\{(\w+)\}", spec["path"]):
assert variable in spec["args"]
def test_terminal_parser_slots_permissions_and_completion(tmp_path):
node = shutil.which("node")
if not node:
pytest.skip("Node is unavailable; command interactions are also covered in browser tests.")
# Browsers treat this file as an ES module; Node 18 needs an explicit suffix.
module = tmp_path / "GameCommands.mjs"
module.write_bytes(COMMANDS.read_bytes())
script = r'''
import assert from "node:assert/strict";
const { GameCommands: C } = await import(MODULE_URL);
const farm = {
is_owner: true, max_plots: 12,
plots: Array.from({length:12},(_,slot)=>({slot,state:slot%2 ? "ready":"empty"})),
crops:[{key:"python"},{key:"shell"}], perks:[{key:"growth"}],
legacy:[{key:"speed"}], mastery:[{key:"analytics"}],
infrastructure:[{key:"registry"}], cosmetics:[{key:"skin_neon"}],
quests:[{kind:"plant",scope:"daily"}]
};
const cases = [
["plant 1 python","/game/plant",{slot:0,crop:"python"}],
["spinup 12 shell","/game/plant",{slot:11,crop:"shell"}],
["harvest 12","/game/harvest",{slot:11}],
["stop 2","/game/plot/stop",{slot:1,stop:"true"}],
["resume 2","/game/plot/stop",{slot:1,stop:"false"}],
["buy server","/game/buy-plot",{}], ["ci upgrade","/game/upgrade",{}],
["boost 3","/game/fertilize",{slot:2}], ["daily","/game/daily",{}],
["grant","/game/grant",{}], ["perk growth","/game/perk",{perk:"growth"}],
["refactor","/game/prestige",{}], ["legacy speed","/game/legacy",{key:"speed"}],
["mastery analytics","/game/mastery",{key:"analytics"}],
["claim plant","/game/quests/claim",{quest:"plant",scope:"daily"}],
["claim plant weekly","/game/quests/claim",{quest:"plant",scope:"weekly"}],
["defense upgrade","/game/defense/upgrade",{}],
["defense downgrade","/game/defense/downgrade",{}],
["infra registry","/game/infrastructure/buy",{key:"registry"}],
["cosmetic buy skin_neon","/game/cosmetics/buy",{key:"skin_neon"}],
["cosmetic equip skin_neon","/game/cosmetics/equip",{key:"skin_neon"}],
["water alice 1","/game/farm/alice/water",{slot:0}],
["steal alice 12","/game/farm/alice/steal",{slot:11}]
];
for (const [line,url,params] of cases) {
const parsed=C.parse(line,farm);
assert.equal(parsed.url,url,line);
assert.deepEqual(parsed.params,params,line);
}
assert.equal(C.parse("leaderboard",farm).url,"/game/leaderboard?board=score");
assert.equal(C.parse("leaderboard raids",farm).url,"/game/leaderboard?board=raids");
assert.equal(C.parse("view alice",farm).url,"/game/farm/alice");
assert.equal(C.parse("state",farm).url,"/game/state");
assert.equal(C.parse("home",farm).url,"/game");
assert.equal(C.parse("harvest all",farm).bulk,true);
assert.equal(C.parse("server 12",farm).slot,11);
assert.equal(C.parse("legacy",farm).local,"legacy");
assert.deepEqual(C.completions("plant 1 py",farm),["python"]);
assert.deepEqual(C.completions("claim plant w",farm),["weekly"]);
for (const line of ["plant 0 python","harvest 13","harvest 1.5","water ../alice 1","water alice 0","claim plant monthly","leaderboard invalid","perk nonexistent","constructor","__proto__","plant 1 python; alert(1)","harvest 1 extra"]) {
assert.throws(()=>C.parse(line,farm),undefined,line);
}
assert.throws(()=>C.parse("plant 1 python",{...farm,is_owner:false}),/own rack/);
assert.equal(C.parse("water alice 1",{...farm,is_owner:false}).url,"/game/farm/alice/water");
console.log("Parser, API bindings, 1-based slots, permissions, invalid commands and completion passed.");
'''.replace("MODULE_URL", json.dumps(module.as_uri()))
result = subprocess.run([node, "--input-type=module"], input=script, text=True, capture_output=True, timeout=15)
assert result.returncode == 0, result.stdout + result.stderr