chore: add agents/reports to gitignore and update agent infrastructure with timestamp streaming, write budget, and codename generation
- Add `agents/reports/` to `.gitignore` to prevent generated agent report files from being tracked - Implement `_TimestampStream` class and `install_timestamps()` function in `agents/agent.py` for prefixing stdout/stderr with timestamps and elapsed time - Export `install_timestamps`, `set_write_budget`, `clear_write_budget`, and `set_shell_restricted` from `agents/base.py`; add `_RUN_LOCK`, `AGENT_ICONS` dictionary, and `WRITE_BUDGET = 20` constant - Add `report_codename()` function and `CODENAME_ADJECTIVES`/`CODENAME_ANIMALS` tuples to `agents/core/__init__.py` for generating random agent report codenames - Expand `WRITE_TOOLS` tuple in `agents/core/__init__.py` to include `replace_lines`, `insert_lines`, and `delete_lines`; remove `SWARM_TOOLS` from `payloads_for` exclusion list - Update `CLAUDE.md` and `AGENTS.md` documentation with dataset column initialization rules and new agent infrastructure details - Reorder route table in `README.md` to list `/uploads` before `/messages` and document Swagger/ReDoc/OpenAPI schema endpoints
This commit is contained in:
@@ -50,7 +50,9 @@ def test_generate_avatar_svg_different_for_different_seeds():
|
||||
|
||||
|
||||
def test_generate_avatar_svg_fallback_on_exception(monkeypatch):
|
||||
import multiavatar.multiavatar as ma
|
||||
import importlib
|
||||
|
||||
ma = importlib.import_module("multiavatar.multiavatar")
|
||||
|
||||
def _boom(*args, **kwargs):
|
||||
raise RuntimeError("avatar backend down")
|
||||
@@ -63,7 +65,9 @@ def test_generate_avatar_svg_fallback_on_exception(monkeypatch):
|
||||
|
||||
|
||||
def test_generate_avatar_svg_includes_rect_for_fallback(monkeypatch):
|
||||
import multiavatar.multiavatar as ma
|
||||
import importlib
|
||||
|
||||
ma = importlib.import_module("multiavatar.multiavatar")
|
||||
|
||||
monkeypatch.setattr(ma, "multiavatar", lambda *a, **k: None)
|
||||
svg = generate_avatar_svg("zeta")
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import time
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
|
||||
AJAX = {"X-Requested-With": "fetch"}
|
||||
|
||||
_counter = [0]
|
||||
|
||||
|
||||
def _session():
|
||||
_counter[0] += 1
|
||||
name = f"cmt{int(time.time() * 1000)}{_counter[0]}"
|
||||
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,
|
||||
)
|
||||
return s, name
|
||||
|
||||
|
||||
def _create_post(session, title):
|
||||
r = session.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={
|
||||
"content": "Post for comment test.",
|
||||
"title": title,
|
||||
"topic": "devlog",
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
slug = r.headers["location"].split("/posts/")[-1]
|
||||
return get_table("posts").find_one(slug=slug)["uid"]
|
||||
|
||||
|
||||
def test_create_comment_on_post(app_server):
|
||||
s, _ = _session()
|
||||
title = f"comment-test-{int(time.time() * 1000)}"
|
||||
post_uid = _create_post(s, title)
|
||||
r = s.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": "Nice post!",
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (302, 303)
|
||||
comment = get_table("comments").find_one(target_uid=post_uid)
|
||||
assert comment is not None
|
||||
assert comment["content"] == "Nice post!"
|
||||
|
||||
|
||||
def test_create_reply_to_comment(app_server):
|
||||
s, _ = _session()
|
||||
title = f"reply-test-{int(time.time() * 1000)}"
|
||||
post_uid = _create_post(s, title)
|
||||
s.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": "Parent comment",
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
parent = get_table("comments").find_one(target_uid=post_uid)
|
||||
r = s.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": "Reply to parent",
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
"parent_uid": parent["uid"],
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (302, 303)
|
||||
replies = list(get_table("comments").find(parent_uid=parent["uid"]))
|
||||
assert len(replies) == 1
|
||||
assert replies[0]["content"] == "Reply to parent"
|
||||
|
||||
|
||||
def test_delete_own_comment(app_server):
|
||||
s, _ = _session()
|
||||
title = f"del-cmt-{int(time.time() * 1000)}"
|
||||
post_uid = _create_post(s, title)
|
||||
s.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": "Delete me",
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
comment = get_table("comments").find_one(target_uid=post_uid)
|
||||
r = s.post(
|
||||
f"{BASE_URL}/comments/delete/{comment['uid']}",
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (302, 303)
|
||||
assert get_table("comments").find_one(uid=comment["uid"]) is None
|
||||
|
||||
|
||||
def test_delete_comment_requires_owner(app_server):
|
||||
s_alice, _ = _session()
|
||||
title = f"owner-test-{int(time.time() * 1000)}"
|
||||
post_uid = _create_post(s_alice, title)
|
||||
s_alice.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": "Alice comment",
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
comment = get_table("comments").find_one(target_uid=post_uid)
|
||||
s_bob, _ = _session()
|
||||
r = s_bob.post(
|
||||
f"{BASE_URL}/comments/delete/{comment['uid']}",
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (302, 303)
|
||||
assert get_table("comments").find_one(uid=comment["uid"]) is not None
|
||||
|
||||
|
||||
def test_create_comment_requires_login(app_server):
|
||||
title = f"anon-cmt-{int(time.time() * 1000)}"
|
||||
s, name = _session()
|
||||
post_uid = _create_post(s, title)
|
||||
anon = requests.Session()
|
||||
r = anon.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": "Anon comment",
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
assert get_table("comments").find_one(target_uid=post_uid) is None
|
||||
|
||||
|
||||
def test_comment_form_submission_ui(alice, app_server):
|
||||
page, user = alice
|
||||
title = f"ui-comment-{int(time.time() * 1000)}"
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={"email": user["email"], "password": user["password"]},
|
||||
)
|
||||
r = s.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={"content": "UI comment target.", "title": title, "topic": "devlog"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
slug = r.headers["location"].split("/posts/")[-1]
|
||||
page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded")
|
||||
textarea = page.locator("form.comment-form textarea[name='content']").first
|
||||
textarea.wait_for(state="visible", timeout=5000)
|
||||
textarea.fill("UI test comment")
|
||||
page.locator("button.comment-form-submit").first.click()
|
||||
page.wait_for_timeout(1000)
|
||||
assert page.is_visible("text=UI test comment")
|
||||
@@ -0,0 +1,49 @@
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
|
||||
|
||||
def test_containers_admin_page_requires_login(app_server):
|
||||
r = requests.get(f"{BASE_URL}/admin/containers", allow_redirects=False)
|
||||
assert r.status_code in (302, 303)
|
||||
|
||||
|
||||
def test_containers_admin_page_loads(alice, app_server):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/admin/containers", wait_until="domcontentloaded")
|
||||
assert page.is_visible(".admin-toolbar h2:has-text('Containers')")
|
||||
|
||||
|
||||
def test_containers_admin_data_returns_json(alice, seeded_db):
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={
|
||||
"email": seeded_db["alice"]["email"],
|
||||
"password": seeded_db["alice"]["password"],
|
||||
},
|
||||
)
|
||||
r = s.get(f"{BASE_URL}/admin/containers/data")
|
||||
assert r.status_code == 200
|
||||
data = r.json()
|
||||
assert "instances" in data
|
||||
assert isinstance(data["instances"], list)
|
||||
|
||||
|
||||
def test_containers_admin_data_denied_for_non_admin(app_server, seeded_db):
|
||||
s = requests.Session()
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={
|
||||
"email": seeded_db["bob"]["email"],
|
||||
"password": seeded_db["bob"]["password"],
|
||||
},
|
||||
)
|
||||
r = s.get(f"{BASE_URL}/admin/containers/data", allow_redirects=False)
|
||||
assert r.status_code in (302, 303, 403)
|
||||
|
||||
|
||||
def test_container_instance_page_404_for_missing(alice, app_server):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/admin/containers/nonexistent", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=Not Found") or page.is_visible("text=404")
|
||||
@@ -224,3 +224,11 @@ def test_orphan_running_recovered_on_enable(fork_env):
|
||||
job = queue.get_job(uid)
|
||||
assert job["status"] == "pending"
|
||||
assert job["retry_count"] == 1
|
||||
|
||||
|
||||
def test_fork_status_http_unknown_returns_404(app_server):
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
r = requests.get(f"{BASE_URL}/forks/nonexistent-uid")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import io
|
||||
import uuid
|
||||
import requests
|
||||
from PIL import Image
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table
|
||||
|
||||
AJAX = {"X-Requested-With": "fetch"}
|
||||
|
||||
|
||||
def _session():
|
||||
s = requests.Session()
|
||||
name = f"med_{uuid.uuid4().hex[:10]}"
|
||||
s.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@test.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return s, name
|
||||
|
||||
|
||||
def _png_bytes():
|
||||
buf = io.BytesIO()
|
||||
Image.new("RGB", (4, 4), (255, 0, 0)).save(buf, "PNG")
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def _upload(s):
|
||||
r = s.post(
|
||||
f"{BASE_URL}/uploads/upload",
|
||||
files={"file": ("a.png", _png_bytes(), "image/png")},
|
||||
)
|
||||
assert r.status_code == 201, r.text
|
||||
return r.json()["uid"]
|
||||
|
||||
|
||||
def test_delete_own_media(app_server):
|
||||
s, name = _session()
|
||||
uid = _upload(s)
|
||||
r = s.post(f"{BASE_URL}/media/{uid}/delete", allow_redirects=False)
|
||||
assert r.status_code in (302, 303)
|
||||
attachment = get_table("attachments").find_one(uid=uid)
|
||||
assert attachment is not None
|
||||
assert attachment.get("deleted_at") is not None
|
||||
|
||||
|
||||
def test_delete_others_media_denied(app_server):
|
||||
s_alice, _ = _session()
|
||||
uid = _upload(s_alice)
|
||||
s_bob, _ = _session()
|
||||
r = s_bob.post(f"{BASE_URL}/media/{uid}/delete", allow_redirects=False)
|
||||
assert r.status_code in (302, 303)
|
||||
attachment = get_table("attachments").find_one(uid=uid)
|
||||
assert attachment.get("deleted_at") is None
|
||||
|
||||
|
||||
def test_delete_nonexistent_media(app_server):
|
||||
s, _ = _session()
|
||||
r = s.post(f"{BASE_URL}/media/nonexistent-uid/delete", allow_redirects=False)
|
||||
assert r.status_code in (302, 303, 404)
|
||||
|
||||
|
||||
def test_admin_can_delete_any_media(app_server, seeded_db):
|
||||
s_user, _ = _session()
|
||||
uid = _upload(s_user)
|
||||
s_admin = requests.Session()
|
||||
s_admin.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={
|
||||
"email": seeded_db["alice"]["email"],
|
||||
"password": seeded_db["alice"]["password"],
|
||||
},
|
||||
)
|
||||
r = s_admin.post(f"{BASE_URL}/media/{uid}/delete", allow_redirects=False)
|
||||
assert r.status_code in (302, 303)
|
||||
attachment = get_table("attachments").find_one(uid=uid)
|
||||
assert attachment.get("deleted_at") is not None
|
||||
|
||||
|
||||
def test_restore_media_requires_admin(app_server, seeded_db):
|
||||
s_user, _ = _session()
|
||||
uid = _upload(s_user)
|
||||
s_admin = requests.Session()
|
||||
s_admin.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={
|
||||
"email": seeded_db["alice"]["email"],
|
||||
"password": seeded_db["alice"]["password"],
|
||||
},
|
||||
)
|
||||
s_admin.post(f"{BASE_URL}/media/{uid}/delete", allow_redirects=False)
|
||||
attachment = get_table("attachments").find_one(uid=uid)
|
||||
assert attachment.get("deleted_at") is not None
|
||||
r = s_admin.post(f"{BASE_URL}/media/{uid}/restore", allow_redirects=False)
|
||||
assert r.status_code in (302, 303)
|
||||
attachment = get_table("attachments").find_one(uid=uid)
|
||||
assert attachment.get("deleted_at") is None
|
||||
|
||||
|
||||
def test_restore_media_denied_for_non_admin(app_server):
|
||||
s, _ = _session()
|
||||
uid = _upload(s)
|
||||
r = s.post(f"{BASE_URL}/media/{uid}/restore", allow_redirects=False)
|
||||
assert r.status_code in (302, 303, 403)
|
||||
@@ -175,39 +175,47 @@ def test_streaming_emits_sse(local_db, monkeypatch):
|
||||
def test_authorize_static_access_key(local_db):
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "topsecret")
|
||||
svc = GatewayService()
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": "topsecret"})) is True
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": "wrong"})) is False
|
||||
set_setting("gateway_access_key", "")
|
||||
try:
|
||||
svc = GatewayService()
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": "topsecret"})) is True
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": "wrong"})) is False
|
||||
finally:
|
||||
set_setting("gateway_access_key", "")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
|
||||
|
||||
def test_authorize_admin_and_user_toggles(local_db):
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
_, admin_key = _make_admin(role="Admin")
|
||||
_, member_key = _make_admin(role="Member")
|
||||
svc = GatewayService()
|
||||
|
||||
set_setting("gateway_allow_admins", "1")
|
||||
set_setting("gateway_allow_users", "0")
|
||||
assert (
|
||||
svc.authorize(_make_request(headers={"Authorization": f"Bearer {admin_key}"}))
|
||||
is True
|
||||
)
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": member_key})) is False
|
||||
try:
|
||||
_, admin_key = _make_admin(role="Admin")
|
||||
_, member_key = _make_admin(role="Member")
|
||||
svc = GatewayService()
|
||||
|
||||
set_setting("gateway_allow_users", "1")
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": member_key})) is True
|
||||
assert (
|
||||
svc.authorize(_make_request(headers={"Authorization": f"Bearer {admin_key}"}))
|
||||
is True
|
||||
)
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": member_key})) is False
|
||||
|
||||
set_setting("gateway_allow_admins", "1")
|
||||
set_setting("gateway_allow_users", "0")
|
||||
set_setting("gateway_allow_users", "1")
|
||||
assert svc.authorize(_make_request(headers={"X-API-KEY": member_key})) is True
|
||||
finally:
|
||||
set_setting("gateway_allow_admins", "1")
|
||||
set_setting("gateway_allow_users", "1")
|
||||
set_setting("gateway_require_auth", "1")
|
||||
set_setting("gateway_access_key", "")
|
||||
|
||||
|
||||
def test_authorize_require_auth_off_is_open(local_db):
|
||||
set_setting("gateway_require_auth", "0")
|
||||
svc = GatewayService()
|
||||
assert svc.authorize(_make_request()) is True
|
||||
set_setting("gateway_require_auth", "1")
|
||||
try:
|
||||
svc = GatewayService()
|
||||
assert svc.authorize(_make_request()) is True
|
||||
finally:
|
||||
set_setting("gateway_require_auth", "1")
|
||||
|
||||
|
||||
# ---------- HTTP behavior (admin configures via the Services tab endpoints) ----------
|
||||
@@ -220,8 +228,13 @@ def _config(page, **fields):
|
||||
def test_gateway_disabled_returns_503(alice):
|
||||
page, _ = alice
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/stop")
|
||||
r = requests.post(f"{BASE_URL}/openai/v1/chat/completions", json={"messages": []})
|
||||
assert r.status_code == 503
|
||||
try:
|
||||
r = requests.post(
|
||||
f"{BASE_URL}/openai/v1/chat/completions", json={"messages": []}
|
||||
)
|
||||
assert r.status_code == 503
|
||||
finally:
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/start")
|
||||
|
||||
|
||||
def test_gateway_auth_and_routing(alice):
|
||||
@@ -256,4 +269,4 @@ def test_gateway_auth_and_routing(alice):
|
||||
assert with_admin.status_code == 502
|
||||
finally:
|
||||
_config(page, gateway_access_key="")
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/stop")
|
||||
page.request.post(f"{BASE_URL}/admin/services/openai/start")
|
||||
|
||||
@@ -381,8 +381,9 @@ def test_readonly_action_requires_confirmation():
|
||||
assert confirmation_error("project_set_readonly", {"confirm": "true"}) is None
|
||||
|
||||
|
||||
def test_private_action_needs_no_confirmation():
|
||||
assert confirmation_error("project_set_private", {"value": "true"}) is None
|
||||
def test_private_action_requires_confirmation():
|
||||
assert confirmation_error("project_set_private", {"value": "true"}) is not None
|
||||
assert confirmation_error("project_set_private", {"value": "true", "confirm": "true"}) is None
|
||||
assert confirmation_error("vote", {}) is None
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def test_proxy_unknown_slug_returns_404(app_server):
|
||||
r = requests.get(f"{BASE_URL}/p/nonexistent-slug", allow_redirects=False)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_proxy_unknown_slug_json(app_server):
|
||||
r = requests.get(
|
||||
f"{BASE_URL}/p/nonexistent-slug",
|
||||
headers={"X-Requested-With": "fetch"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_proxy_unknown_slug_with_path(app_server):
|
||||
r = requests.get(
|
||||
f"{BASE_URL}/p/nonexistent-slug/some/path", allow_redirects=False
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_proxy_post_unknown_slug(app_server):
|
||||
r = requests.post(f"{BASE_URL}/p/nonexistent-slug", allow_redirects=False)
|
||||
assert r.status_code == 404
|
||||
@@ -10,10 +10,15 @@ def _open_project_with_delete(page, title):
|
||||
return proj_url, slug
|
||||
|
||||
|
||||
def _click_delete(page):
|
||||
page.locator(".project-actions-more").click()
|
||||
page.locator(".context-menu-item:has-text('Delete')").click()
|
||||
|
||||
|
||||
def test_data_confirm_opens_custom_dialog(alice):
|
||||
page, _ = alice
|
||||
_open_project_with_delete(page, "Confirm Dialog Open")
|
||||
page.locator("button[data-confirm='Delete this project?']").click()
|
||||
_click_delete(page)
|
||||
expect(page.locator(".dialog-overlay.visible")).to_be_visible()
|
||||
expect(page.locator(".dialog-overlay.visible .dialog-message")).to_contain_text(
|
||||
"Delete"
|
||||
@@ -23,7 +28,7 @@ def test_data_confirm_opens_custom_dialog(alice):
|
||||
def test_data_confirm_accept_proceeds(alice):
|
||||
page, _ = alice
|
||||
proj_url, _ = _open_project_with_delete(page, "Confirm Accept")
|
||||
page.locator("button[data-confirm='Delete this project?']").click()
|
||||
_click_delete(page)
|
||||
page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
||||
page.wait_for_url(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
resp = page.goto(proj_url, wait_until="domcontentloaded")
|
||||
@@ -33,7 +38,7 @@ def test_data_confirm_accept_proceeds(alice):
|
||||
def test_data_confirm_cancel_aborts(alice):
|
||||
page, _ = alice
|
||||
proj_url, _ = _open_project_with_delete(page, "Confirm Cancel")
|
||||
page.locator("button[data-confirm='Delete this project?']").click()
|
||||
_click_delete(page)
|
||||
page.locator(".dialog-overlay.visible .dialog-cancel").click()
|
||||
expect(page.locator(".dialog-overlay")).not_to_be_visible()
|
||||
resp = page.goto(proj_url, wait_until="domcontentloaded")
|
||||
@@ -43,7 +48,7 @@ def test_data_confirm_cancel_aborts(alice):
|
||||
def test_dialog_escape_cancels(alice):
|
||||
page, _ = alice
|
||||
_open_project_with_delete(page, "Confirm Escape")
|
||||
page.locator("button[data-confirm='Delete this project?']").click()
|
||||
_click_delete(page)
|
||||
expect(page.locator(".dialog-overlay.visible")).to_be_visible()
|
||||
page.keyboard.press("Escape")
|
||||
expect(page.locator(".dialog-overlay")).not_to_be_visible()
|
||||
@@ -52,7 +57,7 @@ def test_dialog_escape_cancels(alice):
|
||||
def test_dialog_backdrop_click_cancels(alice):
|
||||
page, _ = alice
|
||||
_open_project_with_delete(page, "Confirm Backdrop")
|
||||
page.locator("button[data-confirm='Delete this project?']").click()
|
||||
_click_delete(page)
|
||||
overlay = page.locator(".dialog-overlay.visible")
|
||||
expect(overlay).to_be_visible()
|
||||
overlay.click(position={"x": 5, "y": 5})
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ from devplacepy.database import get_table
|
||||
from devplacepy.utils import (
|
||||
award_badge,
|
||||
award_xp,
|
||||
badge_info,
|
||||
get_badge,
|
||||
check_milestone_badges,
|
||||
create_mention_notifications,
|
||||
extract_mentions,
|
||||
@@ -111,13 +111,13 @@ def test_level_for_xp_negative():
|
||||
|
||||
|
||||
def test_badge_info_known():
|
||||
meta = badge_info("Star Author")
|
||||
meta = get_badge("Star Author")
|
||||
assert meta["icon"]
|
||||
assert meta["description"] == "Earned 100 stars"
|
||||
|
||||
|
||||
def test_badge_info_unknown_fallback():
|
||||
meta = badge_info("Nonexistent Badge")
|
||||
meta = get_badge("Nonexistent Badge")
|
||||
assert meta["icon"]
|
||||
assert meta["description"] == "Nonexistent Badge"
|
||||
|
||||
|
||||
@@ -303,7 +303,8 @@ def test_detail_download_button_downloads(app_server, page):
|
||||
_write(key, slug, "README.md", "# hi")
|
||||
_login(page, name)
|
||||
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
||||
btn = page.locator("button:has-text('Download zip')")
|
||||
page.locator(".project-actions-more").click()
|
||||
btn = page.locator(".context-menu-item:has-text('Download zip')")
|
||||
btn.wait_for(state="visible")
|
||||
with page.expect_download(timeout=30000) as dl_info:
|
||||
btn.click()
|
||||
|
||||
Reference in New Issue
Block a user