@@ -6,7 +6,7 @@ from datetime import datetime, timezone, timedelta
|
||||
import requests
|
||||
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from devplacepy.services.game import store
|
||||
from devplacepy.services.game import economy, store
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
JSON = {"Accept": "application/json"}
|
||||
@@ -337,3 +337,91 @@ def test_steal_cooldown_blocks_second_raid(app_server, seeded_db):
|
||||
f"{BASE_URL}/game/farm/{owner}/steal", data={"slot": 0}, headers=JSON
|
||||
)
|
||||
assert second.status_code == 400
|
||||
|
||||
|
||||
def test_prestige_insufficient_coins_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100)
|
||||
_set_farm(name, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
r = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
|
||||
assert r.status_code == 400
|
||||
assert "costs" in r.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_prestige_charges_fee_and_carries_over(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100000)
|
||||
_set_farm(name, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
fee = economy.refactor_cost(0, 100000)
|
||||
carried = economy.refactor_carryover(100000, fee, 0)
|
||||
r = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
farm = r.json()["farm"]
|
||||
assert farm["prestige"] == 1
|
||||
assert farm["coins"] == economy.STARTING_COINS + carried
|
||||
refresh_snapshot()
|
||||
assert store.treasury_balance() >= fee
|
||||
|
||||
|
||||
def test_state_exposes_refactor_and_grant_fields(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
r = session.get(f"{BASE_URL}/game/state", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
farm = r.json()["farm"]
|
||||
for key in (
|
||||
"refactor_cost",
|
||||
"refactor_affordable",
|
||||
"refactor_carryover_pct",
|
||||
"refactor_carryover_preview",
|
||||
"grant_available",
|
||||
"grant_amount",
|
||||
"grant_reason",
|
||||
"treasury_balance",
|
||||
):
|
||||
assert key in farm
|
||||
assert farm["refactor_cost"] == economy.refactor_cost(0, farm["coins"])
|
||||
|
||||
|
||||
def test_grant_requires_auth(app_server, seeded_db):
|
||||
r = requests.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert r.status_code in (401, 303)
|
||||
|
||||
|
||||
def test_grant_claim_flow(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100)
|
||||
refresh_snapshot()
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": 100000}, ["uid"]
|
||||
)
|
||||
week = store._iso_week(store._now())
|
||||
_set_farm(
|
||||
name,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=week,
|
||||
)
|
||||
r = session.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["farm"]["coins"] == 100 + economy.GRANT_CAP
|
||||
second = session.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert second.status_code == 400
|
||||
|
||||
|
||||
def test_grant_ineligible_when_rich_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=economy.GRANT_WEALTH_CEILING + 1)
|
||||
refresh_snapshot()
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": 100000}, ["uid"]
|
||||
)
|
||||
week = store._iso_week(store._now())
|
||||
_set_farm(
|
||||
name,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=week,
|
||||
)
|
||||
r = session.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert r.status_code == 400
|
||||
|
||||
@@ -54,7 +54,7 @@ def test_admin_sees_revoke_button(alice, bob):
|
||||
admin_page, _ = alice
|
||||
member_page, _ = bob
|
||||
admin_page.goto(f"{BASE_URL}/profile/bob_test?tab=awards", wait_until="domcontentloaded")
|
||||
expect(admin_page.locator(AWARD_REVOKE)).to_be_visible()
|
||||
expect(admin_page.locator(AWARD_REVOKE).first).to_be_visible()
|
||||
member_page.goto(f"{BASE_URL}/profile/bob_test?tab=awards", wait_until="domcontentloaded")
|
||||
expect(member_page.locator(AWARD_REVOKE)).to_have_count(0)
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_member_sees_only_quota_percentage(bob):
|
||||
assert (
|
||||
page.locator(".ai-quota-only .ai-usage-title", has_text="AI quota").count() == 1
|
||||
)
|
||||
assert "%" in page.locator(".ai-quota-only .ai-quota-pct").inner_text()
|
||||
assert "%" in page.locator(".ai-quota-only .ai-quota-pct").first.inner_text()
|
||||
assert page.locator("[data-ai-usage]").count() == 0
|
||||
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ def test_landing_authenticated_shows_dashboard(page, app_server):
|
||||
assert page.url.rstrip("/") == BASE_URL.rstrip("/")
|
||||
assert page.is_visible("text=Welcome back")
|
||||
assert page.is_visible("text=home_user")
|
||||
feed_cta = page.locator("a.landing-cta:has-text('Go to your feed')")
|
||||
feed_cta = page.locator("a.dashboard-btn-primary:has-text('New Post')")
|
||||
assert feed_cta.is_visible()
|
||||
assert feed_cta.get_attribute("href") == "/feed"
|
||||
assert not page.is_visible("text=Join DevPlace Free")
|
||||
|
||||
+42
-1
@@ -232,6 +232,8 @@ def test_prestige_resets_farm(alice):
|
||||
|
||||
reset_farm("alice_test")
|
||||
set_farm_xp("alice_test", economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
fee = economy.refactor_cost(0, 100000)
|
||||
carried = economy.refactor_carryover(100000, fee, 0)
|
||||
open_game(page)
|
||||
prestige = page.locator("form[data-game-action='prestige'] button")
|
||||
prestige.wait_for(state="visible")
|
||||
@@ -240,10 +242,49 @@ def test_prestige_resets_farm(alice):
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
expect(page.locator("[data-hud-prestige]")).to_have_text("1")
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("50")
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text(
|
||||
str(economy.STARTING_COINS + carried)
|
||||
)
|
||||
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)
|
||||
assert page.locator("form[data-game-action='prestige']").count() == 0
|
||||
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(
|
||||
str(100 + economy.GRANT_CAP)
|
||||
)
|
||||
assert page.locator("form[data-game-action='grant']").count() == 0
|
||||
|
||||
|
||||
def test_leaderboard_panel_populates(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
|
||||
@@ -399,3 +399,67 @@ def test_weekly_contract_deterministic():
|
||||
assert first == second
|
||||
assert first["kind"] in economy.QUEST_DEFS
|
||||
assert first["reward_stars"] > 0
|
||||
|
||||
|
||||
# --- refactor fee, carry-over, and grants -------------------------------------------
|
||||
|
||||
|
||||
def test_refactor_cost_monotonic_in_prestige():
|
||||
previous = 0
|
||||
for prestige in range(0, 200):
|
||||
cost = economy.refactor_cost(prestige, 0)
|
||||
assert cost > previous
|
||||
previous = cost
|
||||
|
||||
|
||||
def test_refactor_cost_monotonic_in_wealth():
|
||||
previous = -1
|
||||
for coins in range(0, 2_000_000, 10_000):
|
||||
cost = economy.refactor_cost(0, coins)
|
||||
assert cost >= previous
|
||||
previous = cost
|
||||
|
||||
|
||||
def test_refactor_cost_floor_is_base():
|
||||
assert economy.refactor_cost(0, 0) == economy.REFACTOR_BASE_COST
|
||||
assert economy.refactor_cost(-5, -100) == economy.REFACTOR_BASE_COST
|
||||
|
||||
|
||||
def test_refactor_cost_includes_wealth_tax():
|
||||
coins = 100_000
|
||||
expected = round(
|
||||
economy.REFACTOR_BASE_COST + economy.REFACTOR_WEALTH_PCT * coins
|
||||
)
|
||||
assert economy.refactor_cost(0, coins) == expected
|
||||
|
||||
|
||||
def test_refactor_carryover_fraction_bounds():
|
||||
for level in range(0, 50):
|
||||
fraction = economy.refactor_carryover_fraction(level)
|
||||
assert economy.REFACTOR_CARRYOVER_BASE <= fraction <= economy.REFACTOR_CARRYOVER_MAX
|
||||
|
||||
|
||||
def test_refactor_carryover_never_exceeds_remainder():
|
||||
for coins in range(0, 500_000, 7_777):
|
||||
for level in (0, 3, 5):
|
||||
fee = economy.refactor_cost(0, coins)
|
||||
carried = economy.refactor_carryover(coins, fee, level)
|
||||
assert 0 <= carried <= max(0, coins - fee)
|
||||
|
||||
|
||||
def test_refactor_carryover_zero_when_unaffordable():
|
||||
assert economy.refactor_carryover(100, 20_000, 5) == 0
|
||||
|
||||
|
||||
def test_grant_amount_capped_and_bounded():
|
||||
assert economy.grant_amount(0) == 0
|
||||
assert economy.grant_amount(-10) == 0
|
||||
assert economy.grant_amount(100) == 100
|
||||
assert economy.grant_amount(10**9) == economy.GRANT_CAP
|
||||
|
||||
|
||||
def test_legacy_carryover_upgrade_registered():
|
||||
upgrade = economy.legacy_for("carryover")
|
||||
assert upgrade is not None
|
||||
assert upgrade.max_level == 5
|
||||
assert "carry-over" in economy.legacy_value_text(upgrade, 2)
|
||||
|
||||
@@ -45,6 +45,7 @@ def _reset(username, coins=100000):
|
||||
get_table("game_steals").delete(owner_uid=user["uid"])
|
||||
get_table("game_cosmetics").delete(user_uid=user["uid"])
|
||||
get_table("game_market_ticks").delete()
|
||||
get_table("game_treasury").delete()
|
||||
_clear_game_caches()
|
||||
farm = store.ensure_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
@@ -493,10 +494,15 @@ def test_prestige_resets_farm_and_increments(local_db):
|
||||
ci_tier=3,
|
||||
)
|
||||
store.buy_plot(user)
|
||||
coins_before = _coins(user)
|
||||
fee = economy.refactor_cost(0, coins_before)
|
||||
carried = economy.refactor_carryover(coins_before, fee, 0)
|
||||
result = store.prestige(user)
|
||||
assert result["prestige"] == 1
|
||||
assert result["fee"] == fee
|
||||
assert result["carried"] == carried
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert farm["coins"] == economy.STARTING_COINS
|
||||
assert farm["coins"] == economy.STARTING_COINS + carried
|
||||
assert farm["xp"] == 0
|
||||
assert farm["ci_tier"] == 1
|
||||
assert farm["perk_growth"] == 0
|
||||
@@ -504,6 +510,45 @@ def test_prestige_resets_farm_and_increments(local_db):
|
||||
assert len(store.get_plots(farm["uid"])) == economy.STARTING_PLOTS
|
||||
|
||||
|
||||
def test_prestige_insufficient_coins_blocked(local_db):
|
||||
user = _reset("unit_a", coins=100)
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
with pytest.raises(GameError):
|
||||
store.prestige(user)
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert int(farm["prestige"] or 0) == 0
|
||||
assert int(farm["coins"]) == 100
|
||||
|
||||
|
||||
def test_prestige_fee_funds_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
fee = economy.refactor_cost(0, _coins(user))
|
||||
store.prestige(user)
|
||||
assert store.treasury_balance() == fee
|
||||
|
||||
|
||||
def test_prestige_carryover_scales_with_legacy(local_db):
|
||||
user = _reset("unit_a")
|
||||
_set(
|
||||
user,
|
||||
xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL),
|
||||
legacy_carryover=5,
|
||||
)
|
||||
coins_before = _coins(user)
|
||||
fee = economy.refactor_cost(0, coins_before)
|
||||
carried = economy.refactor_carryover(coins_before, fee, 5)
|
||||
assert carried > economy.refactor_carryover(coins_before, fee, 0)
|
||||
store.prestige(user)
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert farm["coins"] == economy.STARTING_COINS + carried
|
||||
|
||||
|
||||
def test_prestige_fee_rises_with_prestige_and_wealth(local_db):
|
||||
assert economy.refactor_cost(1, 0) > economy.refactor_cost(0, 0)
|
||||
assert economy.refactor_cost(0, 1_000_000) > economy.refactor_cost(0, 0)
|
||||
|
||||
|
||||
# --- legacy upgrades ---------------------------------------------------------
|
||||
|
||||
|
||||
@@ -851,7 +896,8 @@ def test_farm_analytics_tracks_lifetime_totals_once_unlocked(local_db):
|
||||
|
||||
def test_prestige_awards_mastery_at_threshold(local_db):
|
||||
user = _reset("unit_a")
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=49)
|
||||
coins = economy.refactor_cost(49, 0) * 2
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=49, coins=coins)
|
||||
result = store.prestige(user)
|
||||
assert result["mastery_awarded"] == 1
|
||||
farm = store.get_farm(user["uid"])
|
||||
@@ -946,3 +992,87 @@ def test_weekly_contract_claim_pays_stars_and_boost(local_db):
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert int(farm["stars"]) == result["reward_stars"]
|
||||
assert farm.get("contract_boost_until")
|
||||
|
||||
|
||||
# --- treasury and community grant --------------------------------------------
|
||||
|
||||
|
||||
def _seed_treasury(amount):
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": amount}, ["uid"]
|
||||
)
|
||||
|
||||
|
||||
def _make_grant_eligible(user):
|
||||
_set(
|
||||
user,
|
||||
coins=100,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=store._iso_week(store._now()),
|
||||
)
|
||||
|
||||
|
||||
def test_claim_grant_pays_from_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_make_grant_eligible(user)
|
||||
result = store.claim_grant(user)
|
||||
assert result["amount"] == economy.GRANT_CAP
|
||||
assert _coins(user) == 100 + economy.GRANT_CAP
|
||||
assert store.treasury_balance() == 100_000 - economy.GRANT_CAP
|
||||
|
||||
|
||||
def test_claim_grant_once_per_week(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_make_grant_eligible(user)
|
||||
store.claim_grant(user)
|
||||
_set(user, coins=100)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_requires_activity(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_set(user, coins=100, harvests_week=economy.GRANT_MIN_WEEK_HARVESTS - 1)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_wealth_ceiling(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_set(
|
||||
user,
|
||||
coins=economy.GRANT_WEALTH_CEILING,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_prestige_cap(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_make_grant_eligible(user)
|
||||
_set(user, prestige=economy.GRANT_MAX_PRESTIGE + 1)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_empty_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_make_grant_eligible(user)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_partial_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(300)
|
||||
_make_grant_eligible(user)
|
||||
result = store.claim_grant(user)
|
||||
assert result["amount"] == 300
|
||||
assert store.treasury_balance() == 0
|
||||
|
||||
Reference in New Issue
Block a user