Full test suite is now the mandatory final validation; fix everything it surfaced

- Policy: every change ends with make test (all tiers, all tests) green; docs and agent guardrails updated accordingly
- schema.py: ensure the full filtered/indexed column set of instances via get_table (ingress_slug, ports_json, container_gateway, slug, status, ...) so a partial first insert can never break the ingress proxy
- docs_api: award body param location body -> json; gateway endpoints documented public -> user to match enforced auth
- tests: missing get_table import (trash restore), audit read as admin (award), deterministic online-roster and leaderboard-cache handling, container visibility updated to the primary-admin-only rule, scoped AI usage heading selector past the hidden Tools nav links
This commit is contained in:
2026-07-22 23:55:46 +02:00
parent 77f043640e
commit 34fa56a836
13 changed files with 86 additions and 41 deletions
+4 -6
View File
@@ -1,14 +1,12 @@
# CLAUDE.md
This file documents detailed testing patterns, fixtures, and pitfalls for devplacepy/tests/ (Playwright + unit tests across api/e2e/unit tiers). Claude Code loads it automatically whenever a file under this directory is read or edited. Never run the test suite unless the user explicitly asks - validate with imports and manual checks instead.
This file documents detailed testing patterns, fixtures, and pitfalls for devplacepy/tests/ (Playwright + unit tests across api/e2e/unit tiers). Claude Code loads it automatically whenever a file under this directory is read or edited. Every change ends with the full suite: `make test` runs all tests across all three tiers, and it must pass.
## Never run tests unless explicitly asked (critical guardrail)
## Always run the full suite (critical guardrail)
**NEVER run tests unless specifically asked by user.** Not the full suite, not a single file - do not run any tests unless the user explicitly requests it. Validate each touched language manually instead: Python (compile + import), JS (parse / bracket matching), CSS (brace matching), HTML (tag matching). Zero tolerance. A clean `python -c "from devplacepy.main import app"` import is the baseline check.
**Every change is validated by running the full test suite - ALL tests, all three tiers, via `make test`.** No tier skipped, no subset substituted for the whole. Preliminary per-language checks (Python compile + import, JS parse, CSS brace matching, HTML tag matching) and a clean `python -c "from devplacepy.main import app"` import come first, but they never replace the suite. A failure is a real signal and blocks completion until fixed.
**Never run tests unless the user explicitly asks for it.** Not the full suite, not a single file. Validate code with a clean import (`python -c "from devplacepy.main import app"`) and per-language manual checks instead.
If you do need to run a single test on explicit request: `python -m pytest tests/e2e/feed.py::test_name -v --tb=line -x`.
During iteration a single test may be run for a fast feedback loop: `python -m pytest tests/e2e/feed.py::test_name -v --tb=line -x` - but the change is finished only when the complete suite is green.
## Fixture stack
+1
View File
@@ -107,6 +107,7 @@ def test_restore_revives_post_and_records_audit(seeded_db):
def test_restore_revoked_award_recomputes_stats(seeded_db):
from datetime import datetime, timezone
from devplacepy.database import get_table
from devplacepy.database.awards import revoke_award
from devplacepy.utils import generate_uid, make_combined_slug
+12 -2
View File
@@ -115,7 +115,6 @@ def _seed_news_audit_log():
)
refresh_snapshot()
return uid
from devplacepy.database import get_table
_counter_polls = [0]
AJAX_polls = {"X-Requested-With": "fetch"}
def _session_polls():
@@ -363,7 +362,18 @@ def test_feed_avatar_has_presence_dot(app_server):
def test_feed_shows_online_now_section(app_server):
s, name = _member()
name = _unique("0onl")
s = requests.Session()
s.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
},
allow_redirects=True,
)
html = s.get(f"{BASE_URL}/feed").text
assert "online-users" in html
assert "data-online-users-list" in html
+12 -4
View File
@@ -87,15 +87,23 @@ def test_leaderboard_entries_carry_score_and_prestige(app_server, seeded_db):
session, name = _signup()
_reset_farm(name)
_set_farm(name, prestige=50, total_harvests=20, ci_tier=3)
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
assert response.status_code == 200
entries = response.json()["entries"]
deadline = time.time() + 20
while True:
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
assert response.status_code == 200
entries = response.json()["entries"]
mine = next(
(entry for entry in entries if entry["username"] == name), None
)
if mine is not None or time.time() >= deadline:
break
time.sleep(0.5)
assert entries
scores = [entry["score"] for entry in entries]
assert scores == sorted(scores, reverse=True)
for entry in entries:
assert "score" in entry and "prestige" in entry
mine = next(entry for entry in entries if entry["username"] == name)
assert mine is not None
assert mine["prestige"] == 50
assert mine["score"] == economy.farm_score(store.get_farm(
get_table("users").find_one(username=name)["uid"]
+3 -4
View File
@@ -2,10 +2,8 @@
import time
import requests
from datetime import datetime, timezone
from tests.conftest import BASE_URL
from devplacepy.db_client import get_table
from devplacepy.utils import generate_uid, make_combined_slug
JSON = {"Accept": "application/json"}
_counter = [0]
@@ -177,7 +175,7 @@ def test_valid_post_creates_pending_row_and_job(app_server):
assert job and job.get("kind") == "award"
def test_award_give_audit_recorded(app_server):
def test_award_give_audit_recorded(app_server, seeded_db):
giver, _ = _signup()
_, receiver_name = _signup()
r = giver.post(
@@ -186,5 +184,6 @@ def test_award_give_audit_recorded(app_server):
json={"description": "Audit me"},
)
assert r.status_code == 200
entry = _audit_find(giver, "award.give", _uid(receiver_name))
admin = _login(seeded_db, "alice")
entry = _audit_find(admin, "award.give", _uid(receiver_name))
assert entry is not None
+6 -3
View File
@@ -3,7 +3,7 @@
import time
import requests
from tests.conftest import BASE_URL
from devplacepy.database import get_table
from devplacepy.database import get_primary_admin_uid, get_table, invalidate_admins_cache
from devplacepy.utils import clear_user_cache
_counter_cv = [0]
@@ -70,11 +70,14 @@ def test_admin_private_project_containers_hidden_from_other_admin(app_server):
assert _containers_data_status(owner_key, slug) == 200
def test_member_private_project_containers_visible_to_admin(app_server):
def test_member_private_project_containers_primary_admin_only(app_server):
_, _, owner_key = _signup()
_, _, admin_key = _make_admin()
slug = _create_project(owner_key, "Member Container Hidden", is_private=True)
assert _containers_data_status(admin_key, slug) == 200
assert _containers_data_status(admin_key, slug) == 404
invalidate_admins_cache()
primary_key = get_table("users").find_one(uid=get_primary_admin_uid())["api_key"]
assert _containers_data_status(primary_key, slug) == 200
def test_public_project_containers_visible_to_any_admin(app_server):
+1 -1
View File
@@ -68,4 +68,4 @@ def _seed_target_user():
def test_admin_ai_usage_page_loads(alice):
page, _ = alice
page.goto(f"{BASE_URL}/admin/ai-usage", wait_until="domcontentloaded")
assert page.is_visible("text=AI usage") or page.is_visible("text=AI Usage")
page.locator("h2:has-text('AI usage')").wait_for(state="visible")