2026-06-22 18:41:53 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
|
|
|
|
|
|
from playwright.sync_api import expect
|
|
|
|
|
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reset_farm(username, coins=100000):
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
uid = user["uid"]
|
|
|
|
|
farm = store.get_farm(uid)
|
|
|
|
|
if farm:
|
|
|
|
|
get_table("game_plots").delete(farm_uid=farm["uid"])
|
|
|
|
|
get_table("game_quests").delete(farm_uid=farm["uid"])
|
|
|
|
|
get_table("game_farms").delete(uid=farm["uid"])
|
2026-07-21 03:36:29 +02:00
|
|
|
get_table("game_cosmetics").delete(user_uid=uid)
|
2026-07-23 02:14:49 +02:00
|
|
|
get_table("game_steals").delete(thief_uid=uid)
|
|
|
|
|
get_table("game_steals").delete(owner_uid=uid)
|
2026-07-21 03:36:29 +02:00
|
|
|
get_table("game_market_ticks").delete()
|
|
|
|
|
clear_game_caches()
|
2026-06-22 18:41:53 +02:00
|
|
|
farm = store.ensure_farm(uid)
|
|
|
|
|
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
|
|
|
|
return {"uid": uid, "username": username}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def warp_ready(username):
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
|
|
|
|
|
for plot in store.get_plots(farm["uid"]):
|
|
|
|
|
if plot.get("crop_key"):
|
|
|
|
|
get_table("game_plots").update(
|
|
|
|
|
{"uid": plot["uid"], "ready_at": past}, ["uid"]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_farm_xp(username, xp):
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
get_table("game_farms").update({"uid": farm["uid"], "xp": xp}, ["uid"])
|
|
|
|
|
|
|
|
|
|
|
2026-07-21 03:36:29 +02:00
|
|
|
def set_farm(username, **fields):
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
get_table("game_farms").update({"uid": farm["uid"], **fields}, ["uid"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def clear_game_caches():
|
|
|
|
|
from devplacepy.services.game.store import farm as farm_store
|
|
|
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
|
|
|
|
|
|
farm_store._leaderboard_cache.clear()
|
|
|
|
|
farm_store._board_cache.clear()
|
|
|
|
|
market_store._saturation_cache.clear()
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 18:41:53 +02:00
|
|
|
def open_game(page):
|
|
|
|
|
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("[data-game-grid]").first.wait_for(state="visible")
|
|
|
|
|
page.wait_for_timeout(800)
|
|
|
|
|
|
|
|
|
|
|
2026-07-26 14:57:18 +02:00
|
|
|
def hud_number(value):
|
|
|
|
|
from devplacepy.templating import format_number
|
|
|
|
|
|
|
|
|
|
return format_number(value)
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 18:41:53 +02:00
|
|
|
def coins(page):
|
2026-07-26 14:57:18 +02:00
|
|
|
node = page.locator("[data-hud-coins]")
|
|
|
|
|
exact = node.get_attribute("title")
|
|
|
|
|
if exact:
|
|
|
|
|
return int(exact.replace(",", ""))
|
|
|
|
|
return int(node.inner_text().replace(",", ""))
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_game_page_loads(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
assert page.locator(".game-header h1").is_visible()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(100000))
|
2026-06-22 18:41:53 +02:00
|
|
|
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(4)
|
2026-07-23 02:14:49 +02:00
|
|
|
expect(page.locator("[data-perk-host] .perk-card")).to_have_count(4)
|
2026-06-22 18:41:53 +02:00
|
|
|
expect(page.locator(".quest-item")).to_have_count(3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_guest_redirected_to_login(page):
|
|
|
|
|
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
|
|
|
|
page.wait_for_timeout(500)
|
|
|
|
|
assert "/auth/login" in page.url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_plant_crop(alice):
|
|
|
|
|
page, _ = 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()
|
|
|
|
|
expect(page.locator(".game-plot-growing")).to_have_count(1)
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99995))
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_harvest_crop(alice):
|
|
|
|
|
page, _ = 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()
|
|
|
|
|
expect(page.locator(".game-plot-growing")).to_have_count(1)
|
|
|
|
|
warp_ready("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
page.locator("form[data-game-action='harvest'] button").first.click()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).not_to_have_text(hud_number(99995))
|
2026-07-23 19:09:49 +02:00
|
|
|
assert coins(page) > 99995
|
2026-06-22 18:41:53 +02:00
|
|
|
expect(page.locator(".game-plot-growing")).to_have_count(0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_fertilize_spends_coins(alice):
|
|
|
|
|
page, _ = 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()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99985))
|
2026-06-22 18:41:53 +02:00
|
|
|
fert = page.locator("form[data-game-action='fertilize'] button").first
|
|
|
|
|
fert.wait_for(state="visible")
|
|
|
|
|
fert.click()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).not_to_have_text(hud_number(99985))
|
2026-06-22 18:41:53 +02:00
|
|
|
assert coins(page) < 99985
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_buy_plot(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
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)
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99900))
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_ci(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
page.locator("form[data-game-action='upgrade'] button").click()
|
|
|
|
|
expect(page.locator("[data-hud-ci]")).to_have_text("2")
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99850))
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_claim_daily(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
page.locator("form[data-game-action='daily'] button").click()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(100020))
|
2026-07-23 02:14:49 +02:00
|
|
|
expect(page.locator("[data-daily-host] .daily-claimed")).to_be_visible()
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_perk(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
growth = page.locator(
|
|
|
|
|
"form[data-game-action='perk']:has(input[value='growth']) button"
|
|
|
|
|
)
|
|
|
|
|
growth.click()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(99850))
|
2026-06-22 18:41:53 +02:00
|
|
|
expect(
|
|
|
|
|
page.locator(".perk-card:has-text('Build Cache') .perk-level")
|
|
|
|
|
).to_have_text("Lv 1/10")
|
|
|
|
|
|
|
|
|
|
|
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-28 00:31:34 +02:00
|
|
|
def _set_farm_stars(username, stars):
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
get_table("game_farms").update({"uid": farm["uid"], "stars": stars}, ["uid"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_legacy_spends_stars(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
_set_farm_stars("alice_test", 1000)
|
|
|
|
|
open_game(page)
|
|
|
|
|
buy = page.locator(
|
|
|
|
|
"form[data-game-action='legacy']:has(input[value='multiplier']) button"
|
|
|
|
|
)
|
|
|
|
|
buy.wait_for(state="visible")
|
|
|
|
|
buy.click()
|
|
|
|
|
expect(
|
|
|
|
|
page.locator(".legacy-card:has-text('Tech Debt Payoff') .perk-level")
|
|
|
|
|
).to_have_text("Lv 1/10")
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 18:41:53 +02:00
|
|
|
def test_claim_quest(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
farm_info = reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
farm = store.get_farm(farm_info["uid"])
|
|
|
|
|
quest = list(get_table("game_quests").find(farm_uid=farm["uid"]))[0]
|
|
|
|
|
get_table("game_quests").update(
|
|
|
|
|
{"uid": quest["uid"], "progress": quest["goal"]}, ["uid"]
|
|
|
|
|
)
|
|
|
|
|
open_game(page)
|
|
|
|
|
page.locator("form[data-game-action='claim-quest'] button").first.click()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).not_to_have_text(hud_number(100000))
|
2026-06-22 18:41:53 +02:00
|
|
|
assert coins(page) > 100000
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_prestige_hidden_below_level(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
assert page.locator("form[data-game-action='prestige']").count() == 0
|
|
|
|
|
assert page.is_visible("text=Reach level 10 to refactor")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_prestige_resets_farm(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
from devplacepy.services.game import economy
|
|
|
|
|
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
set_farm_xp("alice_test", economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
2026-07-23 01:14:10 +02:00
|
|
|
fee = economy.refactor_cost(0, 100000)
|
|
|
|
|
carried = economy.refactor_carryover(100000, fee, 0)
|
2026-06-22 18:41:53 +02:00
|
|
|
open_game(page)
|
|
|
|
|
prestige = page.locator("form[data-game-action='prestige'] button")
|
|
|
|
|
prestige.wait_for(state="visible")
|
|
|
|
|
prestige.click()
|
|
|
|
|
confirm_btn = page.locator(".dialog-confirm")
|
|
|
|
|
confirm_btn.wait_for(state="visible")
|
|
|
|
|
confirm_btn.click()
|
|
|
|
|
expect(page.locator("[data-hud-prestige]")).to_have_text("1")
|
2026-07-23 01:14:10 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(
|
2026-07-26 14:57:18 +02:00
|
|
|
hud_number(economy.STARTING_COINS + carried)
|
2026-07-23 01:14:10 +02:00
|
|
|
)
|
|
|
|
|
assert page.locator("form[data-game-action='prestige']").count() == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_prestige_unaffordable_shows_cost_without_button(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
from devplacepy.services.game import economy
|
|
|
|
|
|
|
|
|
|
reset_farm("alice_test", coins=100)
|
|
|
|
|
set_farm_xp("alice_test", economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
|
|
|
|
open_game(page)
|
2026-06-22 18:41:53 +02:00
|
|
|
assert page.locator("form[data-game-action='prestige']").count() == 0
|
2026-07-23 01:14:10 +02:00
|
|
|
assert page.is_visible("text=keep farming to afford it")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_grant_section_claim(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import economy, store
|
|
|
|
|
from devplacepy.services.game.store import _iso_week, _now
|
|
|
|
|
|
|
|
|
|
reset_farm("alice_test", coins=100)
|
|
|
|
|
store.ensure_treasury()
|
|
|
|
|
get_table("game_treasury").update(
|
|
|
|
|
{"uid": "treasury-main", "balance": 100000}, ["uid"]
|
|
|
|
|
)
|
|
|
|
|
set_farm(
|
|
|
|
|
"alice_test",
|
|
|
|
|
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
|
|
|
|
harvests_week_start=_iso_week(_now()),
|
|
|
|
|
)
|
|
|
|
|
open_game(page)
|
|
|
|
|
claim = page.locator("form[data-game-action='grant'] button")
|
|
|
|
|
claim.wait_for(state="visible")
|
|
|
|
|
claim.click()
|
|
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(
|
2026-07-26 14:57:18 +02:00
|
|
|
hud_number(100 + economy.GRANT_CAP)
|
2026-07-23 01:14:10 +02:00
|
|
|
)
|
|
|
|
|
assert page.locator("form[data-game-action='grant']").count() == 0
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaderboard_panel_populates(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
page.locator(".game-lb-row").first.wait_for(state="visible")
|
2026-07-23 19:09:49 +02:00
|
|
|
listed = False
|
|
|
|
|
for _ in range(8):
|
|
|
|
|
if page.is_visible("a.game-lb-name:has-text('alice_test')"):
|
|
|
|
|
listed = True
|
|
|
|
|
break
|
|
|
|
|
page.wait_for_timeout(3000)
|
|
|
|
|
page.reload(wait_until="domcontentloaded")
|
|
|
|
|
page.locator(".game-lb-row").first.wait_for(state="visible")
|
|
|
|
|
assert listed, "alice_test never appeared on the farm leaderboard within the 15s board cache TTL"
|
2026-06-22 18:41:53 +02:00
|
|
|
|
|
|
|
|
|
2026-07-21 03:36:29 +02:00
|
|
|
def test_infrastructure_hidden_below_prestige_requirement(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test", coins=100_000_000)
|
|
|
|
|
open_game(page)
|
|
|
|
|
assert page.is_visible("text=Requires prestige 3")
|
|
|
|
|
assert page.locator("form[data-game-action='infrastructure']").count() == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_infrastructure_buy_succeeds_when_eligible(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test", coins=100_000_000)
|
|
|
|
|
set_farm("alice_test", prestige=5)
|
|
|
|
|
open_game(page)
|
|
|
|
|
buy = page.locator("form[data-game-action='infrastructure'] button").first
|
|
|
|
|
buy.wait_for(state="visible")
|
|
|
|
|
buy.click()
|
|
|
|
|
confirm_btn = page.locator(".dialog-confirm")
|
|
|
|
|
confirm_btn.wait_for(state="visible")
|
|
|
|
|
confirm_btn.click()
|
|
|
|
|
expect(page.locator("form[data-game-action='infrastructure']")).to_have_count(0)
|
2026-07-26 14:57:18 +02:00
|
|
|
from devplacepy.services.game import economy
|
|
|
|
|
|
|
|
|
|
assert coins(page) == 100_000_000 - economy.infra_for("registry").cost
|
2026-07-21 03:36:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_defense_upgrade_spends_coins(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
upgrade = page.locator("form[data-game-action='defense'] button").first
|
|
|
|
|
upgrade.wait_for(state="visible")
|
|
|
|
|
upgrade.click()
|
2026-07-26 14:57:18 +02:00
|
|
|
expect(page.locator("[data-hud-coins]")).to_have_text(hud_number(95000))
|
2026-07-21 03:36:29 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cosmetics_buy_and_equip_flow(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test", coins=1_000_000)
|
|
|
|
|
open_game(page)
|
|
|
|
|
buy = page.locator("form[data-game-action='cosmetic-buy']").first
|
|
|
|
|
buy.locator("button").wait_for(state="visible")
|
|
|
|
|
buy.locator("button").click()
|
|
|
|
|
confirm_btn = page.locator(".dialog-confirm")
|
|
|
|
|
confirm_btn.wait_for(state="visible")
|
|
|
|
|
confirm_btn.click()
|
|
|
|
|
equip = page.locator("form[data-game-action='cosmetic-equip'] button").first
|
|
|
|
|
equip.wait_for(state="visible")
|
|
|
|
|
equip.click()
|
|
|
|
|
expect(page.locator(".game-cosmetics")).to_contain_text("Equipped")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mastery_panel_hidden_without_mastery_points(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
assert page.locator(".game-mastery").count() == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mastery_panel_visible_and_functional_once_unlocked(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
set_farm("alice_test", mastery_points=1000, mastery_points_earned_total=1)
|
|
|
|
|
open_game(page)
|
|
|
|
|
upgrade = page.locator(
|
|
|
|
|
"form[data-game-action='mastery']:has(input[value='autoreplant']) button"
|
|
|
|
|
)
|
|
|
|
|
upgrade.wait_for(state="visible")
|
|
|
|
|
upgrade.click()
|
|
|
|
|
expect(
|
|
|
|
|
page.locator(".perk-card:has-text('Continuous Delivery') .perk-level")
|
|
|
|
|
).to_have_text("Lv 1/1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_leaderboard_board_selector_switches_boards(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
set_farm("alice_test", prestige=7)
|
|
|
|
|
open_game(page)
|
|
|
|
|
page.locator(".game-lb-row").first.wait_for(state="visible")
|
|
|
|
|
page.select_option("[data-lb-board]", "prestige")
|
|
|
|
|
page.wait_for_timeout(400)
|
|
|
|
|
page.locator(".game-lb-row").first.wait_for(state="visible")
|
|
|
|
|
assert page.is_visible("a.game-lb-name:has-text('alice_test')")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_underdog_banner_shows_after_raiding_a_much_richer_farm(bob):
|
|
|
|
|
page, _ = bob
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
|
|
|
|
|
reset_farm("alice_test", coins=1_000_000)
|
|
|
|
|
reset_farm("bob_test", coins=0)
|
|
|
|
|
alice_user = get_table("users").find_one(username="alice_test")
|
|
|
|
|
bob_user = get_table("users").find_one(username="bob_test")
|
|
|
|
|
store.plant({"uid": alice_user["uid"], "username": "alice_test"}, 0, "shell")
|
|
|
|
|
past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
|
|
|
|
|
farm = store.get_farm(alice_user["uid"])
|
|
|
|
|
plot = store._plot_at(farm["uid"], 0)
|
|
|
|
|
get_table("game_plots").update(
|
|
|
|
|
{"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
|
|
|
|
|
)
|
|
|
|
|
store.steal({"uid": bob_user["uid"], "username": "bob_test"}, alice_user, 0)
|
|
|
|
|
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("[data-underdog-banner]").wait_for(state="visible")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_weekly_contract_appears_and_can_be_claimed(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import store
|
|
|
|
|
from devplacepy.services.game.store.common import _iso_week
|
|
|
|
|
from devplacepy.services.game.store.quests import ensure_weekly_contract
|
|
|
|
|
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
set_farm("alice_test", mastery_points=1000)
|
|
|
|
|
user = get_table("users").find_one(username="alice_test")
|
|
|
|
|
store.upgrade_mastery({"uid": user["uid"], "username": "alice_test"}, "contracts")
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
iso_week = _iso_week(datetime.now(timezone.utc))
|
|
|
|
|
ensure_weekly_contract(farm, iso_week)
|
|
|
|
|
row = get_table("game_quests").find_one(
|
|
|
|
|
farm_uid=farm["uid"], day=iso_week, scope="weekly"
|
|
|
|
|
)
|
|
|
|
|
get_table("game_quests").update(
|
|
|
|
|
{"uid": row["uid"], "progress": row["goal"]}, ["uid"]
|
|
|
|
|
)
|
|
|
|
|
open_game(page)
|
|
|
|
|
claim = page.locator(
|
|
|
|
|
"form[data-game-action='claim-quest']:has(input[value='weekly']) button"
|
|
|
|
|
)
|
|
|
|
|
claim.wait_for(state="visible")
|
|
|
|
|
claim.click()
|
|
|
|
|
page.wait_for_timeout(400)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
assert int(farm["stars"]) > 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_crop_shows_saturated_label_when_overfarmed(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
|
from devplacepy.utils import generate_uid
|
|
|
|
|
|
|
|
|
|
reset_farm("alice_test")
|
2026-07-23 19:09:49 +02:00
|
|
|
from devplacepy.services.game import economy
|
|
|
|
|
|
2026-07-21 03:36:29 +02:00
|
|
|
now = datetime.now(timezone.utc)
|
2026-07-23 19:09:49 +02:00
|
|
|
crop = economy.crop_for("shell")
|
|
|
|
|
worst_days = economy.MARKET_SATURATION_TIERS[-1][0]
|
Report every test failure in one pass and fix the whole suite
The suite ran with -x, so a run stopped at the first failure and finding N
failures cost N full runs. Move -rf into the pytest addopts so every run
lists each failure, and add the triage targets test-fast (unit + api, no
browser), test-failed (--last-failed), test-first-failure (the old -x),
test-slowest and test-cache-clean. A stale .pytest_cache holding node ids
from deleted files made --last-failed select everything; make clean and
test-cache-clean drop it.
Fix the fifteen failures this surfaced.
DeepSearch crawling raised AttributeError in its finally block on every run:
async_playwright().__aenter__() returns a Playwright, which has no __aexit__.
Use start()/stop() at both call sites.
Update the tests left behind by changed signatures: VectorStore is async now,
fetch_page takes a browser, _summary_payload takes dom_evidence, and the
messages/notifications page compounds take a user_uid.
Close four real flakes that fail-fast had been hiding, all of them late in
the run. Harvest assertions pinned crop.reward_coins while is_golden pays
five times on about five percent of harvests, so they now assert through
realizable_harvest_coins with the observed golden flag. Market saturation
fixtures assumed a single active farm and landed two tiers milder once the
api and e2e tiers had created farms, so they scale by active_farms(). The
primary-administrator container test raced the one second cross-worker cache
version window and now waits for the server to agree. The isslop tools test
matched the collapsed nav dropdown link instead of the tools grid card.
Stop burning ninety seconds waiting out server-side display caches:
DEVPLACE_RANKING_TTL and DEVPLACE_MARKET_SATURATION_TTL follow the existing
sitemap and home cache precedent and are zero for the suite, taking the
leaderboard test from 60.6s to 4.4s and the saturation test from 30.1s to
under a second.
2881 passed, 1 skipped in 15:13.
2026-07-26 16:02:36 +02:00
|
|
|
supply_per_harvest = crop.grow_seconds / 86400.0 / market_store.active_farms()
|
2026-07-21 03:36:29 +02:00
|
|
|
get_table("game_market_ticks").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": generate_uid(),
|
|
|
|
|
"crop_key": "shell",
|
|
|
|
|
"hour_bucket": market_store._hour_bucket(now),
|
Report every test failure in one pass and fix the whole suite
The suite ran with -x, so a run stopped at the first failure and finding N
failures cost N full runs. Move -rf into the pytest addopts so every run
lists each failure, and add the triage targets test-fast (unit + api, no
browser), test-failed (--last-failed), test-first-failure (the old -x),
test-slowest and test-cache-clean. A stale .pytest_cache holding node ids
from deleted files made --last-failed select everything; make clean and
test-cache-clean drop it.
Fix the fifteen failures this surfaced.
DeepSearch crawling raised AttributeError in its finally block on every run:
async_playwright().__aenter__() returns a Playwright, which has no __aexit__.
Use start()/stop() at both call sites.
Update the tests left behind by changed signatures: VectorStore is async now,
fetch_page takes a browser, _summary_payload takes dom_evidence, and the
messages/notifications page compounds take a user_uid.
Close four real flakes that fail-fast had been hiding, all of them late in
the run. Harvest assertions pinned crop.reward_coins while is_golden pays
five times on about five percent of harvests, so they now assert through
realizable_harvest_coins with the observed golden flag. Market saturation
fixtures assumed a single active farm and landed two tiers milder once the
api and e2e tiers had created farms, so they scale by active_farms(). The
primary-administrator container test raced the one second cross-worker cache
version window and now waits for the server to agree. The isslop tools test
matched the collapsed nav dropdown link instead of the tools grid card.
Stop burning ninety seconds waiting out server-side display caches:
DEVPLACE_RANKING_TTL and DEVPLACE_MARKET_SATURATION_TTL follow the existing
sitemap and home cache precedent and are zero for the suite, taking the
leaderboard test from 60.6s to 4.4s and the saturation test from 30.1s to
under a second.
2881 passed, 1 skipped in 15:13.
2026-07-26 16:02:36 +02:00
|
|
|
"harvests": int(worst_days / supply_per_harvest) + 1,
|
2026-07-21 03:36:29 +02:00
|
|
|
"updated_at": now.isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
open_game(page)
|
|
|
|
|
option = page.locator("form[data-game-action='plant'] select option[value='shell']").first
|
|
|
|
|
option.wait_for(state="attached")
|
Report every test failure in one pass and fix the whole suite
The suite ran with -x, so a run stopped at the first failure and finding N
failures cost N full runs. Move -rf into the pytest addopts so every run
lists each failure, and add the triage targets test-fast (unit + api, no
browser), test-failed (--last-failed), test-first-failure (the old -x),
test-slowest and test-cache-clean. A stale .pytest_cache holding node ids
from deleted files made --last-failed select everything; make clean and
test-cache-clean drop it.
Fix the fifteen failures this surfaced.
DeepSearch crawling raised AttributeError in its finally block on every run:
async_playwright().__aenter__() returns a Playwright, which has no __aexit__.
Use start()/stop() at both call sites.
Update the tests left behind by changed signatures: VectorStore is async now,
fetch_page takes a browser, _summary_payload takes dom_evidence, and the
messages/notifications page compounds take a user_uid.
Close four real flakes that fail-fast had been hiding, all of them late in
the run. Harvest assertions pinned crop.reward_coins while is_golden pays
five times on about five percent of harvests, so they now assert through
realizable_harvest_coins with the observed golden flag. Market saturation
fixtures assumed a single active farm and landed two tiers milder once the
api and e2e tiers had created farms, so they scale by active_farms(). The
primary-administrator container test raced the one second cross-worker cache
version window and now waits for the server to agree. The isslop tools test
matched the collapsed nav dropdown link instead of the tools grid card.
Stop burning ninety seconds waiting out server-side display caches:
DEVPLACE_RANKING_TTL and DEVPLACE_MARKET_SATURATION_TTL follow the existing
sitemap and home cache precedent and are zero for the suite, taking the
leaderboard test from 60.6s to 4.4s and the saturation test from 30.1s to
under a second.
2881 passed, 1 skipped in 15:13.
2026-07-26 16:02:36 +02:00
|
|
|
deadline = datetime.now(timezone.utc) + timedelta(seconds=10)
|
2026-07-23 02:14:49 +02:00
|
|
|
while "saturated" not in option.inner_text() and datetime.now(timezone.utc) < deadline:
|
Report every test failure in one pass and fix the whole suite
The suite ran with -x, so a run stopped at the first failure and finding N
failures cost N full runs. Move -rf into the pytest addopts so every run
lists each failure, and add the triage targets test-fast (unit + api, no
browser), test-failed (--last-failed), test-first-failure (the old -x),
test-slowest and test-cache-clean. A stale .pytest_cache holding node ids
from deleted files made --last-failed select everything; make clean and
test-cache-clean drop it.
Fix the fifteen failures this surfaced.
DeepSearch crawling raised AttributeError in its finally block on every run:
async_playwright().__aenter__() returns a Playwright, which has no __aexit__.
Use start()/stop() at both call sites.
Update the tests left behind by changed signatures: VectorStore is async now,
fetch_page takes a browser, _summary_payload takes dom_evidence, and the
messages/notifications page compounds take a user_uid.
Close four real flakes that fail-fast had been hiding, all of them late in
the run. Harvest assertions pinned crop.reward_coins while is_golden pays
five times on about five percent of harvests, so they now assert through
realizable_harvest_coins with the observed golden flag. Market saturation
fixtures assumed a single active farm and landed two tiers milder once the
api and e2e tiers had created farms, so they scale by active_farms(). The
primary-administrator container test raced the one second cross-worker cache
version window and now waits for the server to agree. The isslop tools test
matched the collapsed nav dropdown link instead of the tools grid card.
Stop burning ninety seconds waiting out server-side display caches:
DEVPLACE_RANKING_TTL and DEVPLACE_MARKET_SATURATION_TTL follow the existing
sitemap and home cache precedent and are zero for the suite, taking the
leaderboard test from 60.6s to 4.4s and the saturation test from 30.1s to
under a second.
2881 passed, 1 skipped in 15:13.
2026-07-26 16:02:36 +02:00
|
|
|
page.wait_for_timeout(500)
|
2026-07-23 02:14:49 +02:00
|
|
|
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']"
|
|
|
|
|
).first
|
|
|
|
|
option.wait_for(state="attached")
|
2026-07-21 03:36:29 +02:00
|
|
|
assert "saturated" in option.inner_text()
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 18:41:53 +02:00
|
|
|
def test_mobile_no_horizontal_overflow(mobile_page):
|
|
|
|
|
page, _ = mobile_page
|
|
|
|
|
reset_farm("alice_test")
|
|
|
|
|
open_game(page)
|
|
|
|
|
scroll_w = page.evaluate("document.documentElement.scrollWidth")
|
|
|
|
|
client_w = page.evaluate("document.documentElement.clientWidth")
|
|
|
|
|
assert scroll_w - client_w <= 1, f"horizontal overflow: {scroll_w} vs {client_w}"
|
|
|
|
|
assert page.locator("[data-game-grid]").first.is_visible()
|