forked from retoor/devplacepy
feat: enforce hard test-tier requirement across all DevPlace workflow agents and feature-builder docs
Update the feature-builder agent prompt, test-maintainer agent, and all four workflow JS files (devii-tool, endpoint, feature, job-service) to codify the DevPlace test standard as a non-optional project requirement: one test file per endpoint, directory tree mirroring the URL/source path, split into three tiers (unit, api, e2e). Add explicit Test phases to devii-tool, endpoint, feature, and job-service workflows, and embed tier-specific test instructions (path mapping, fixture choice, coverage scope) directly in each workflow's meta description and TESTS constant.
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
|
||||
JSON_edit = {"Accept": "application/json"}
|
||||
_counter_edit = [0]
|
||||
|
||||
|
||||
def _session_edit():
|
||||
_counter_edit[0] += 1
|
||||
name = f"cmtedit{int(time.time() * 1000)}{_counter_edit[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_edit(session, title):
|
||||
r = session.post(
|
||||
f"{BASE_URL}/posts/create",
|
||||
data={"content": "Post for comment edit.", "title": title, "topic": "devlog"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
slug = r.headers["location"].split("/posts/")[-1]
|
||||
return get_table("posts").find_one(slug=slug)["uid"]
|
||||
|
||||
|
||||
def _create_comment_edit(session, post_uid, content="Original body"):
|
||||
session.post(
|
||||
f"{BASE_URL}/comments/create",
|
||||
data={
|
||||
"content": content,
|
||||
"target_type": "post",
|
||||
"post_uid": post_uid,
|
||||
"target_uid": post_uid,
|
||||
},
|
||||
allow_redirects=False,
|
||||
)
|
||||
refresh_snapshot()
|
||||
return get_table("comments").find_one(target_uid=post_uid)
|
||||
|
||||
|
||||
def test_edit_own_comment_json(app_server):
|
||||
s, _ = _session_edit()
|
||||
post_uid = _create_post_edit(s, f"edit-json-{int(time.time() * 1000)}")
|
||||
comment = _create_comment_edit(s, post_uid)
|
||||
r = s.post(
|
||||
f"{BASE_URL}/comments/edit/{comment['uid']}",
|
||||
headers=JSON_edit,
|
||||
data={"content": "Edited body text"},
|
||||
)
|
||||
assert r.status_code == 200, r.text[:300]
|
||||
body = r.json()
|
||||
assert body["uid"] == comment["uid"]
|
||||
assert body["content"] == "Edited body text"
|
||||
assert f"#comment-{comment['uid']}" in body["url"]
|
||||
assert body["updated_at"]
|
||||
refresh_snapshot()
|
||||
stored = get_table("comments").find_one(uid=comment["uid"])
|
||||
assert stored["content"] == "Edited body text"
|
||||
|
||||
|
||||
def test_edit_comment_no_js_redirects(app_server):
|
||||
s, _ = _session_edit()
|
||||
post_uid = _create_post_edit(s, f"edit-nojs-{int(time.time() * 1000)}")
|
||||
comment = _create_comment_edit(s, post_uid)
|
||||
r = s.post(
|
||||
f"{BASE_URL}/comments/edit/{comment['uid']}",
|
||||
data={"content": "No JS edited body"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (302, 303)
|
||||
assert f"#comment-{comment['uid']}" in r.headers["location"]
|
||||
refresh_snapshot()
|
||||
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "No JS edited body"
|
||||
|
||||
|
||||
def test_edit_comment_requires_owner(app_server):
|
||||
owner, _ = _session_edit()
|
||||
post_uid = _create_post_edit(owner, f"edit-owner-{int(time.time() * 1000)}")
|
||||
comment = _create_comment_edit(owner, post_uid, "Owner body")
|
||||
other, _ = _session_edit()
|
||||
r = other.post(
|
||||
f"{BASE_URL}/comments/edit/{comment['uid']}",
|
||||
headers=JSON_edit,
|
||||
data={"content": "Hijacked body"},
|
||||
)
|
||||
assert r.status_code == 403
|
||||
refresh_snapshot()
|
||||
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Owner body"
|
||||
|
||||
|
||||
def test_edit_comment_requires_login(app_server):
|
||||
owner, _ = _session_edit()
|
||||
post_uid = _create_post_edit(owner, f"edit-anon-{int(time.time() * 1000)}")
|
||||
comment = _create_comment_edit(owner, post_uid, "Guarded body")
|
||||
anon = requests.Session()
|
||||
r = anon.post(
|
||||
f"{BASE_URL}/comments/edit/{comment['uid']}",
|
||||
data={"content": "Anon body"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 303
|
||||
refresh_snapshot()
|
||||
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Guarded body"
|
||||
|
||||
|
||||
def test_edit_comment_too_short_rejected(app_server):
|
||||
s, _ = _session_edit()
|
||||
post_uid = _create_post_edit(s, f"edit-short-{int(time.time() * 1000)}")
|
||||
comment = _create_comment_edit(s, post_uid, "Long enough body")
|
||||
r = s.post(
|
||||
f"{BASE_URL}/comments/edit/{comment['uid']}",
|
||||
headers=JSON_edit,
|
||||
data={"content": "ab"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code in (400, 422)
|
||||
refresh_snapshot()
|
||||
assert get_table("comments").find_one(uid=comment["uid"])["content"] == "Long enough body"
|
||||
@@ -0,0 +1,63 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from playwright.sync_api import expect
|
||||
from tests.e2e.post import create_post
|
||||
|
||||
|
||||
def _post_comment(page, body):
|
||||
textarea = page.locator(".comment-form textarea[name='content']")
|
||||
textarea.fill(body)
|
||||
page.locator(".comment-form button:has-text('Post')").click()
|
||||
comment = page.locator(f".comment-text:has-text('{body}')")
|
||||
expect(comment).to_be_visible()
|
||||
return comment
|
||||
|
||||
|
||||
def _delete_comment(page, body):
|
||||
wrapper = page.locator(f".comment:has(.comment-text:has-text('{body}'))").last
|
||||
wrapper.locator(".comment-action-btn:has-text('Delete')").click()
|
||||
page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
||||
|
||||
|
||||
def test_delete_comment_removes_it(alice):
|
||||
page, _ = alice
|
||||
create_post(page, "fun", "Post for delete scroll removal")
|
||||
comment = _post_comment(page, "Comment that will be deleted")
|
||||
_delete_comment(page, "Comment that will be deleted")
|
||||
expect(comment).to_have_count(0)
|
||||
|
||||
|
||||
def test_delete_comment_scrolls_to_previous_comment(alice):
|
||||
page, _ = alice
|
||||
create_post(page, "devlog", "Post for delete scroll anchor")
|
||||
for index in range(6):
|
||||
_post_comment(page, f"Scroll anchor comment {index}")
|
||||
|
||||
target = "Scroll anchor comment 5"
|
||||
previous = "Scroll anchor comment 4"
|
||||
previous_wrapper = page.locator(
|
||||
f".comment:has(.comment-text:has-text('{previous}'))"
|
||||
).last
|
||||
anchor_top = previous_wrapper.evaluate(
|
||||
"el => el.getBoundingClientRect().top + window.scrollY"
|
||||
)
|
||||
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
_delete_comment(page, target)
|
||||
expect(page.locator(f".comment-text:has-text('{target}')")).to_have_count(0)
|
||||
|
||||
page.wait_for_function(
|
||||
"expected => Math.abs(window.scrollY - expected) < 120",
|
||||
arg=anchor_top,
|
||||
)
|
||||
expect(previous_wrapper).to_be_visible()
|
||||
|
||||
|
||||
def test_delete_only_comment_scrolls_without_error(alice):
|
||||
page, _ = alice
|
||||
create_post(page, "random", "Post for delete only comment")
|
||||
_post_comment(page, "Sole comment to delete")
|
||||
page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
|
||||
_delete_comment(page, "Sole comment to delete")
|
||||
expect(page.locator(".comment-text:has-text('Sole comment to delete')")).to_have_count(0)
|
||||
assert page.evaluate("window.scrollY") >= 0
|
||||
@@ -0,0 +1,54 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from playwright.sync_api import expect
|
||||
from tests.conftest import BASE_URL
|
||||
from tests.e2e.post import create_post
|
||||
|
||||
|
||||
def _post_comment(page, body):
|
||||
textarea = page.locator(".comment-form textarea[name='content']")
|
||||
textarea.fill(body)
|
||||
page.locator(".comment-form button:has-text('Post')").click()
|
||||
comment = page.locator(f".comment-text:has-text('{body}')")
|
||||
expect(comment).to_be_visible()
|
||||
return comment
|
||||
|
||||
|
||||
def test_inline_edit_own_comment(alice):
|
||||
page, _ = alice
|
||||
create_post(page, "devlog", "Post for inline comment edit")
|
||||
comment = _post_comment(page, "Original comment body")
|
||||
edit_btn = page.locator(".comment-action-btn:has-text('Edit')").last
|
||||
expect(edit_btn).to_be_visible()
|
||||
edit_btn.click()
|
||||
edit_textarea = page.locator(".comment-edit-form textarea[name='content']")
|
||||
expect(edit_textarea).to_be_visible()
|
||||
expect(edit_textarea).to_have_value("Original comment body")
|
||||
edit_textarea.fill("Edited comment body")
|
||||
page.locator(".comment-edit-form button:has-text('Save')").click()
|
||||
expect(page.locator(".comment-text:has-text('Edited comment body')")).to_be_visible()
|
||||
expect(page.locator(".comment-text:has-text('Original comment body')")).to_have_count(0)
|
||||
|
||||
|
||||
def test_cancel_inline_edit(alice):
|
||||
page, _ = alice
|
||||
create_post(page, "fun", "Post for cancel edit")
|
||||
_post_comment(page, "Keep this body")
|
||||
page.locator(".comment-action-btn:has-text('Edit')").last.click()
|
||||
edit_textarea = page.locator(".comment-edit-form textarea[name='content']")
|
||||
expect(edit_textarea).to_be_visible()
|
||||
edit_textarea.fill("Discarded body")
|
||||
page.locator(".comment-edit-form button:has-text('Cancel')").click()
|
||||
expect(page.locator(".comment-edit-form")).to_have_count(0)
|
||||
expect(page.locator(".comment-text:has-text('Keep this body')")).to_be_visible()
|
||||
|
||||
|
||||
def test_non_owner_sees_no_edit_button(alice, bob):
|
||||
page, _ = alice
|
||||
create_post(page, "random", "Post owned by alice")
|
||||
_post_comment(page, "Alice only comment")
|
||||
post_url = page.url
|
||||
bob_page, _ = bob
|
||||
bob_page.goto(post_url, wait_until="domcontentloaded")
|
||||
expect(bob_page.locator(".comment-text:has-text('Alice only comment')")).to_be_visible()
|
||||
expect(bob_page.locator(".comment-action-btn:has-text('Edit')")).to_have_count(0)
|
||||
@@ -569,6 +569,50 @@ def test_feed_top_authors(alice):
|
||||
assert page.is_visible("text=Top Authors")
|
||||
|
||||
|
||||
def _resources_panel(page):
|
||||
return page.locator(".sidebar-card .sidebar-section").filter(
|
||||
has=page.locator(".sidebar-heading:has-text('Resources')")
|
||||
)
|
||||
|
||||
|
||||
def test_feed_resources_panel_visible(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
panel = _resources_panel(page)
|
||||
panel.wait_for(state="visible")
|
||||
expect(panel.locator(".sidebar-heading")).to_have_text("Resources")
|
||||
expect(panel.locator("a.sidebar-link:has-text('Docs')")).to_be_visible()
|
||||
expect(panel.locator("a.sidebar-link:has-text('Issues')")).to_be_visible()
|
||||
expect(panel.locator("a.sidebar-link[data-devii-open]")).to_be_visible()
|
||||
|
||||
|
||||
def test_feed_resources_docs_link(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
docs = _resources_panel(page).locator("a.sidebar-link:has-text('Docs')")
|
||||
assert docs.get_attribute("href") == "/docs"
|
||||
docs.click()
|
||||
page.wait_for_url(f"{BASE_URL}/docs", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_feed_resources_issues_link(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
issues = _resources_panel(page).locator("a.sidebar-link:has-text('Issues')")
|
||||
assert issues.get_attribute("href") == "/issues"
|
||||
issues.click()
|
||||
page.wait_for_url(f"{BASE_URL}/issues", wait_until="domcontentloaded")
|
||||
|
||||
|
||||
def test_feed_resources_devii_opens_assistant(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
devii = _resources_panel(page).locator("a.sidebar-link[data-devii-open]")
|
||||
expect(devii).to_be_visible()
|
||||
assert devii.get_attribute("href") == "/devii/"
|
||||
expect(devii).to_have_text(re.compile("Devii"))
|
||||
|
||||
|
||||
def test_feed_daily_topic(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
|
||||
@@ -249,3 +249,67 @@ def test_landing_page_has_unique_title(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
title = page.title()
|
||||
assert title and "DevPlace" in title
|
||||
|
||||
|
||||
def _help_section(page):
|
||||
return page.locator("section.landing-help")
|
||||
|
||||
|
||||
def test_landing_build_with_us_section(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
assert section.locator("h2:has-text('Build With Us')").is_visible()
|
||||
assert section.locator(".landing-help-intro").is_visible()
|
||||
|
||||
|
||||
def test_landing_help_docs_card(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
link = section.locator(".landing-help-card:has-text('Documentation') a.landing-help-link")
|
||||
assert link.get_attribute("href") == "/docs/index.html"
|
||||
|
||||
|
||||
def test_landing_help_api_reference_links(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
card = section.locator(".landing-help-card:has-text('API Reference')")
|
||||
assert card.locator("a:has-text('Swagger UI')").get_attribute("href") == "/swagger"
|
||||
assert card.locator("a:has-text('OpenAPI JSON')").get_attribute("href") == "/openapi.json"
|
||||
|
||||
|
||||
def test_landing_help_issue_tracker_link(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
link = section.locator(".landing-help-card:has-text('Contribute') a.landing-help-link")
|
||||
assert link.get_attribute("href") == "/issues"
|
||||
|
||||
|
||||
def test_landing_help_devii_card_explained(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
card = section.locator(".landing-help-card:has-text('Meet Devii')")
|
||||
assert card.locator("h3:has-text('Meet Devii')").is_visible()
|
||||
assert "AI developer" in card.locator("p").first.text_content()
|
||||
assert card.locator("a.landing-help-link").get_attribute("href") == "/devii/"
|
||||
|
||||
|
||||
def test_landing_help_launch_devii_button(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
button = section.locator("button.landing-help-cta[data-devii-open]")
|
||||
assert button.is_visible()
|
||||
assert "Launch Devii" in button.text_content()
|
||||
|
||||
|
||||
def test_landing_launch_devii_opens_terminal(page, app_server):
|
||||
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
section = _help_section(page)
|
||||
section.wait_for(state="visible", timeout=5000)
|
||||
section.locator("button.landing-help-cta[data-devii-open]").click()
|
||||
page.locator("devii-terminal").wait_for(state="attached", timeout=5000)
|
||||
|
||||
Reference in New Issue
Block a user