Code Farm: add front-view server rack and API-backed terminal
DevPlace CI / test (pull_request) Failing after 46m19s

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
+157 -14
View File
@@ -111,8 +111,8 @@ def test_plant_crop(alice):
reset_farm("alice_test")
open_game(page)
form = page.locator("form[data-game-action='plant']").first
form.locator("select[name='crop']").select_option("shell")
form.locator("button").click()
form.locator("input[name='crop'][value='shell']").check()
form.locator("button[type='submit']").click()
expect(page.locator(".game-plot-growing")).to_have_count(1)
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99995))
@@ -122,8 +122,8 @@ def test_harvest_crop(alice):
reset_farm("alice_test")
open_game(page)
form = page.locator("form[data-game-action='plant']").first
form.locator("select[name='crop']").select_option("shell")
form.locator("button").click()
form.locator("input[name='crop'][value='shell']").check()
form.locator("button[type='submit']").click()
expect(page.locator(".game-plot-growing")).to_have_count(1)
warp_ready("alice_test")
open_game(page)
@@ -138,8 +138,8 @@ def test_fertilize_spends_coins(alice):
reset_farm("alice_test")
open_game(page)
form = page.locator("form[data-game-action='plant']").first
form.locator("select[name='crop']").select_option("python")
form.locator("button").click()
form.locator("input[name='crop'][value='python']").check()
form.locator("button[type='submit']").click()
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99985))
fert = page.locator("form[data-game-action='fertilize'] button").first
fert.wait_for(state="visible")
@@ -154,6 +154,8 @@ def test_buy_plot(alice):
open_game(page)
page.locator("form[data-game-action='buy-plot'] button").click()
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(5)
expect(page.locator("[data-select-slot='4']")).to_have_attribute("aria-pressed", "true")
expect(page.locator("[data-inspector-slot='4']")).to_be_visible()
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99900))
@@ -474,7 +476,7 @@ def test_crop_shows_saturated_label_when_overfarmed(alice):
}
)
open_game(page)
option = page.locator("form[data-game-action='plant'] select option[value='shell']").first
option = page.locator(".build-card:has(input[value='shell'])").first
option.wait_for(state="attached")
deadline = datetime.now(timezone.utc) + timedelta(seconds=10)
while "saturated" not in option.inner_text() and datetime.now(timezone.utc) < deadline:
@@ -482,7 +484,7 @@ def test_crop_shows_saturated_label_when_overfarmed(alice):
page.reload(wait_until="domcontentloaded")
page.locator("[data-game-grid]").first.wait_for(state="visible")
option = page.locator(
"form[data-game-action='plant'] select option[value='shell']"
".build-card:has(input[value='shell'])"
).first
option.wait_for(state="attached")
assert "saturated" in option.inner_text()
@@ -502,8 +504,8 @@ def test_plot_stop_toggle_hidden_without_continuous_delivery(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
page.select_option("[data-slot='0'] select[name='crop']", "shell")
page.click("[data-slot='0'] button:has-text('Plant')")
page.locator("input[data-build-choice][value='shell']").check()
page.locator("form[data-game-action='plant'] button[type='submit']").click()
page.locator("[data-slot='0'].game-plot-growing").wait_for(state="visible")
assert page.locator("form[data-game-action='plot-stop']").count() == 0
@@ -513,14 +515,155 @@ def test_plot_stop_toggle_flips_and_leaves_plot_empty_after_harvest(alice):
reset_farm("alice_test")
set_farm("alice_test", mastery_autoreplant=1)
open_game(page)
page.select_option("[data-slot='0'] select[name='crop']", "shell")
page.click("[data-slot='0'] button:has-text('Plant')")
toggle = page.locator("[data-slot='0'] form[data-game-action='plot-stop'] button")
page.locator("input[data-build-choice][value='shell']").check()
page.locator("form[data-game-action='plant'] button[type='submit']").click()
toggle = page.locator("[data-inspector-slot='0'] form[data-game-action='plot-stop'] button")
toggle.wait_for(state="visible")
expect(toggle).to_contain_text("Stop after harvest")
toggle.click()
expect(toggle).to_contain_text("Resume auto-replant")
warp_ready("alice_test")
open_game(page)
page.locator("[data-slot='0'] form[data-game-action='harvest'] button").click()
page.locator("[data-inspector-slot='0'] form[data-game-action='harvest'] button").click()
page.locator("[data-slot='0'].game-plot-empty").wait_for(state="visible")
def test_datacenter_select_plot_and_deploy_chosen_project(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
page.locator("[data-select-slot='2']").click()
expect(page.locator("[data-inspector-slot='2']")).to_be_visible()
expect(page.locator("[data-select-slot='2']")).to_have_attribute("aria-pressed", "true")
page.locator("input[data-build-choice][value='python']").check()
page.locator("form[data-game-action='plant'] button[type='submit']").click()
expect(page.locator("[data-slot='2']")).to_have_attribute("data-state", "growing")
expect(page.locator("[data-slot='0']")).to_have_attribute("data-state", "empty")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99985))
def test_datacenter_keyboard_and_refresh_preserve_selection(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
page.locator("[data-select-slot='0']").focus()
page.keyboard.press("ArrowRight")
expect(page.locator("[data-inspector-slot='1']")).to_be_visible()
page.locator("input[data-build-choice][value='python']").check()
page.evaluate("window.app.gameFarm.load()")
expect(page.locator("[data-select-slot='1']")).to_have_attribute("aria-pressed", "true")
expect(page.locator("input[data-build-choice][value='python']")).to_be_checked()
def test_build_catalog_explains_locks_and_filters_projects(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
kernel = page.locator(".build-card:has(input[value='kernel'])")
expect(kernel).to_contain_text("Unlocks at level 8")
expect(kernel.locator("input")).to_be_disabled()
page.locator("[data-build-filter='available']").click()
expect(page.locator("input[data-build-choice]")).to_have_count(2)
page.locator("[data-build-filter='all']").click()
expect(page.locator("input[data-build-choice][value='kernel']")).to_be_disabled()
def test_rack_hardware_reflects_existing_upgrades(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
expect(page.locator(".rack-cabinet")).to_have_attribute("data-cooling-level", "0")
page.locator("form[data-game-action='perk']:has(input[value='growth']) button").click()
expect(page.locator(".rack-cabinet")).to_have_attribute("data-cooling-level", "1")
page.locator("form[data-game-action='upgrade'] button").click()
expect(page.locator(".rack-cabinet")).to_have_attribute("data-network-tier", "2")
def test_full_rack_has_no_extra_purchase_bay(alice):
page, _ = alice
reset_farm("alice_test")
from devplacepy.services.game import store
from devplacepy.database import get_table
user = get_table("users").find_one(username="alice_test")
for _ in range(8):
store.buy_plot({"uid": user["uid"], "username": "alice_test"})
open_game(page)
expect(page.locator(".rack-server")).to_have_count(12)
expect(page.locator("form[data-game-action='buy-plot']")).to_have_count(0)
page.locator("[data-select-slot='11']").click()
expect(page.locator("#rack-server-title")).to_have_text("SERVER 12")
def test_terminal_commands_deploy_to_visible_server_number(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
command = page.locator("[data-terminal-input]")
command.fill("server 2")
command.press("Enter")
expect(page.locator("[data-select-slot='1']")).to_have_attribute("aria-pressed", "true")
expect(command).to_be_visible()
expect(command).to_be_focused()
command.fill("plant 2 py")
command.press("Tab")
expect(command).to_have_value("plant 2 python ")
command.press("Enter")
expect(page.locator("[data-slot='1']")).to_have_attribute("data-state", "growing")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99985))
expect(page.locator(".terminal-line").last).to_contain_text("Server 02 is running python")
def test_terminal_draft_history_and_output_survive_refresh(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
command = page.locator("[data-terminal-input]")
command.fill("status")
command.press("Enter")
expect(page.locator(".terminal-line").last).to_contain_text("alice_test's rack")
command.fill("unfinished draft")
page.evaluate("window.app.gameFarm.load()")
expect(command).to_have_value("unfinished draft")
expect(command).to_be_focused()
command.press("ArrowUp")
expect(command).to_have_value("status")
command.press("ArrowDown")
expect(command).to_have_value("unfinished draft")
page.locator("[data-select-slot='2']").click()
expect(command).to_have_value("unfinished draft")
expect(page.locator("[data-game-terminal]")).to_have_count(1)
expect(page.locator("[data-terminal-output]")).to_contain_text("alice_test's rack")
def test_terminal_neighbour_action_keeps_own_rack(bob):
page, _ = bob
from tests.e2e.game.farm import _plant_growing
reset_farm("alice_test")
reset_farm("bob_test", coins=0)
_plant_growing("alice_test")
open_game(page)
command = page.locator("[data-terminal-input]")
command.fill("water alice_test 1")
command.press("Enter")
expect(page.locator(".terminal-line").last).to_contain_text("Watered alice_test's Server 01")
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(6))
assert page.evaluate("window.app.gameFarm.farm.owner_username") == "bob_test"
expect(page.locator(".game-plot-growing")).to_have_count(0)
def test_mastery_and_boosts_appear_when_unlocked_without_reload(alice):
page, _ = alice
reset_farm("alice_test")
open_game(page)
expect(page.locator("[data-mastery-section]")).to_be_hidden()
expect(page.locator("[data-hud-mastery-stat]")).to_be_hidden()
expect(page.locator("[data-contract-banner]")).to_be_hidden()
set_farm("alice_test", mastery_points_earned_total=1, mastery_points=1,
contract_boost_until=(datetime.now(timezone.utc) + timedelta(hours=1)).isoformat())
page.evaluate("window.app.gameFarm.load()")
expect(page.locator("[data-mastery-section]")).to_be_visible()
expect(page.locator("[data-hud-mastery-stat]")).to_be_visible()
expect(page.locator("[data-hud-mastery]")).to_have_text("1")
expect(page.locator("[data-contract-banner]")).to_be_visible()
+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