247 lines
8.6 KiB
Python
Raw Normal View History

2026-07-07 15:28:28 +02:00
# retoor <retoor@molodetz.nl>
import time
import requests
from tests.conftest import BASE_URL, login_user
from devplacepy.database import (
get_table,
get_primary_admin_uid,
invalidate_admins_cache,
refresh_snapshot,
)
from devplacepy.utils import clear_user_cache
_counter = [0]
def _make_admin():
_counter[0] += 1
name = f"ecm{int(time.time() * 1000)}{_counter[0]}"
password = "secret123"
requests.post(
f"{BASE_URL}/auth/signup",
data={
"username": name,
"email": f"{name}@t.dev",
"password": password,
"confirm_password": password,
},
allow_redirects=True,
)
refresh_snapshot()
row = get_table("users").find_one(username=name)
get_table("users").update({"uid": row["uid"], "role": "Admin"}, ["uid"])
clear_user_cache(row["uid"])
invalidate_admins_cache()
return {
"email": f"{name}@t.dev",
"password": password,
"uid": row["uid"],
"api_key": row["api_key"],
}
def _demote_existing_admins():
existing = [r["uid"] for r in get_table("users").find(role="Admin")]
for uid in existing:
get_table("users").update({"uid": uid, "role": "Member"}, ["uid"])
invalidate_admins_cache()
return existing
def _restore_admins(uids):
for uid in uids:
get_table("users").update({"uid": uid, "role": "Admin"}, ["uid"])
invalidate_admins_cache()
def _create_project(api_key, title, is_private=False):
data = {
"title": title,
"description": "e2e container manage isolation",
"project_type": "software",
"status": "In Development",
}
if is_private:
data["is_private"] = "on"
r = requests.post(
f"{BASE_URL}/projects/create",
headers={"Accept": "application/json", "X-API-KEY": api_key},
data=data,
)
assert r.status_code == 200, r.text
return r.json()["data"]
def _insert_instance(project_uid, created_by):
uid = f"ecm-{int(time.time() * 1000000)}"
get_table("instances").insert(
{
"uid": uid,
"slug": uid,
"name": "e2e-manage-test",
"project_uid": project_uid,
"created_by": created_by,
"owner_uid": "",
"status": "stopped",
"container_id": "",
"workspace_dir": "",
"restart_policy": "never",
"deleted_at": None,
"deleted_by": None,
}
)
refresh_snapshot()
return uid
def _cleanup(inst_uid):
get_table("instances").delete(uid=inst_uid)
refresh_snapshot()
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
ADMIN_CACHE_PROPAGATION_SECONDS = 5.0
ADMIN_CACHE_POLL_SECONDS = 0.2
def _await_instance_visible(api_key, inst_uid):
deadline = time.time() + ADMIN_CACHE_PROPAGATION_SECONDS
while time.time() < deadline:
response = requests.get(
f"{BASE_URL}/admin/containers/data",
headers={"Accept": "application/json", "X-API-KEY": api_key},
)
if response.status_code == 200 and any(
inst["uid"] == inst_uid for inst in response.json()["instances"]
):
return
time.sleep(ADMIN_CACHE_POLL_SECONDS)
raise AssertionError(f"instance {inst_uid} never became visible to the viewer")
2026-07-07 15:28:28 +02:00
def test_admin_list_shows_view_only_for_non_owned_public_instance(page, app_server):
owner = _make_admin()
other = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage List Public", is_private=False)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, other)
page.goto(f"{BASE_URL}/admin/containers", wait_until="domcontentloaded")
row = page.locator(f"tr.cm-row[data-uid='{inst_uid}']")
row.wait_for(state="visible", timeout=15000)
assert "view only" in row.inner_text().lower()
assert row.locator("[data-cm-action='delete']").count() == 0
finally:
_cleanup(inst_uid)
def test_admin_list_shows_actions_for_owner(page, app_server):
owner = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage List Owner", is_private=False)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, owner)
page.goto(f"{BASE_URL}/admin/containers", wait_until="domcontentloaded")
row = page.locator(f"tr.cm-row[data-uid='{inst_uid}']")
row.wait_for(state="visible", timeout=15000)
assert row.locator("[data-cm-action='delete']").count() == 1
finally:
_cleanup(inst_uid)
def test_admin_list_excludes_others_private_project_instance(page, app_server):
owner = _make_admin()
other = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage List Private", is_private=True)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, other)
page.goto(f"{BASE_URL}/admin/containers", wait_until="domcontentloaded")
assert page.locator(f"tr.cm-row[data-uid='{inst_uid}']").count() == 0
finally:
_cleanup(inst_uid)
def test_admin_detail_page_view_only_hides_manage_controls(page, app_server):
owner = _make_admin()
other = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage Detail Public", is_private=False)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, other)
page.goto(f"{BASE_URL}/admin/containers/{inst_uid}", wait_until="domcontentloaded")
actions = page.locator("#ci-actions")
actions.wait_for(state="visible")
assert "view only" in actions.inner_text().lower()
assert page.locator("#ci-term-toggle").count() == 0
assert page.locator("[data-modal='ci-schedule-modal']").count() == 0
assert page.locator("a:has-text('Edit configuration')").count() == 0
finally:
_cleanup(inst_uid)
def test_admin_detail_page_owner_sees_manage_controls(page, app_server):
owner = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage Detail Owner", is_private=False)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, owner)
page.goto(f"{BASE_URL}/admin/containers/{inst_uid}", wait_until="domcontentloaded")
page.locator("[data-modal='ci-schedule-modal']").wait_for(state="visible")
assert page.locator("a:has-text('Edit configuration')").count() == 1
assert page.locator("#ci-term-toggle").count() == 1
finally:
_cleanup(inst_uid)
def test_admin_detail_page_404_for_non_viewer_on_private_project(page, app_server):
owner = _make_admin()
other = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage Detail Private", is_private=True)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, other)
page.goto(f"{BASE_URL}/admin/containers/{inst_uid}", wait_until="domcontentloaded")
assert page.is_visible("text=Not Found") or page.is_visible("text=404")
finally:
_cleanup(inst_uid)
def test_admin_edit_page_redirects_non_owner_to_detail(page, app_server):
owner = _make_admin()
other = _make_admin()
project = _create_project(owner["api_key"], "E2E Manage Edit Redirect", is_private=False)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
login_user(page, other)
page.goto(f"{BASE_URL}/admin/containers/{inst_uid}/edit", wait_until="domcontentloaded")
page.wait_for_url(f"**/admin/containers/{inst_uid}", wait_until="domcontentloaded")
finally:
_cleanup(inst_uid)
def test_primary_admin_sees_and_manages_others_private_instance(page, app_server):
restore = _demote_existing_admins()
try:
primary = _make_admin()
owner = _make_admin()
assert get_primary_admin_uid() == primary["uid"]
project = _create_project(
owner["api_key"], "E2E Manage Primary Private", is_private=True
)
inst_uid = _insert_instance(project["uid"], owner["uid"])
try:
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
_await_instance_visible(primary["api_key"], inst_uid)
2026-07-07 15:28:28 +02:00
login_user(page, primary)
page.goto(f"{BASE_URL}/admin/containers", wait_until="domcontentloaded")
row = page.locator(f"tr.cm-row[data-uid='{inst_uid}']")
row.wait_for(state="visible", timeout=15000)
assert row.locator("[data-cm-action='delete']").count() == 1
page.goto(f"{BASE_URL}/admin/containers/{inst_uid}", wait_until="domcontentloaded")
page.locator("a:has-text('Edit configuration')").wait_for(state="visible")
finally:
_cleanup(inst_uid)
finally:
_restore_admins(restore)