test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
from uuid import uuid4
|
|
|
|
|
from datetime import datetime, timezone, timedelta
|
|
|
|
|
from playwright.sync_api import expect
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_project():
|
|
|
|
|
"""Create a project seeded directly into DB and return (slug, uid, user_uid)."""
|
|
|
|
|
owner = str(uuid4())
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": owner,
|
|
|
|
|
"username": f"dlowner_{owner[:8]}",
|
|
|
|
|
"email": f"{owner[:8]}@dl.test",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
slug = make_combined_slug("E2E Devlog Project", uid)
|
|
|
|
|
get_table("projects").insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": slug,
|
|
|
|
|
"title": "E2E Devlog Project",
|
|
|
|
|
"description": "Project for devlog browser tests.",
|
|
|
|
|
"project_type": "software",
|
|
|
|
|
"platforms": "",
|
|
|
|
|
"status": "In Development",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return slug, uid, owner
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_project_post(project_uid, user_uid, order, title=None):
|
|
|
|
|
"""Insert a post linked to a project with precise ordering."""
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
marker = title or f"dlpost-{uid[:8]}"
|
|
|
|
|
get_table("posts").insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": user_uid,
|
|
|
|
|
"slug": make_combined_slug(marker, uid),
|
|
|
|
|
"title": marker,
|
|
|
|
|
"content": f"Devlog post content {order}",
|
|
|
|
|
"topic": "devlog",
|
|
|
|
|
"project_uid": project_uid,
|
|
|
|
|
"image": None,
|
|
|
|
|
"stars": 0,
|
Fix circular import, primary-admin NULL trap, and add gateway quota reset
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
2026-07-27 11:17:48 +02:00
|
|
|
"created_at": (datetime(2026, 1, 1, tzinfo=timezone.utc) + timedelta(seconds=order)).isoformat(),
|
test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return marker, uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _create_project_ui(page, title):
|
|
|
|
|
"""Create a project via the UI."""
|
|
|
|
|
page.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("#create-project-btn").click()
|
|
|
|
|
page.fill("#title", title)
|
|
|
|
|
page.fill("#description", "Project for devlog UI test")
|
|
|
|
|
page.click("button:has-text('Create Project')")
|
|
|
|
|
page.wait_for_url(f"{BASE_URL}/projects/*", wait_until="domcontentloaded")
|
|
|
|
|
return page.url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_empty_state_on_project_page(alice):
|
|
|
|
|
"""Project with no linked posts displays 'No devlog posts yet'."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_create_project_ui(page, "Empty Devlog Project")
|
|
|
|
|
|
|
|
|
|
devlog_section = page.locator(".project-devlog")
|
|
|
|
|
expect(devlog_section).to_be_visible()
|
|
|
|
|
expect(devlog_section.locator("h3:has-text('Devlog')")).to_be_visible()
|
|
|
|
|
expect(page.locator(".empty-state:has-text('No devlog posts yet.')")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_shows_linked_post_title(alice):
|
|
|
|
|
"""Linked post title renders in the devlog section."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
marker = f"visible-dl-{uuid4().hex[:8]}"
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
devlog = page.locator(".project-devlog")
|
|
|
|
|
expect(devlog.locator(f"h3:has-text('{marker}')")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_shows_author_info(alice):
|
|
|
|
|
"""Author avatar, name and level appear on devlog posts."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
marker = f"auth-dl-{uuid4().hex[:8]}"
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
post_card = page.locator(".project-devlog .post-card").first
|
|
|
|
|
expect(post_card.locator(".post-header")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-author-link")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-time")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_shows_vote_and_comment_buttons(alice):
|
|
|
|
|
"""Devlog post renders vote buttons and comment count."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
marker = f"action-dl-{uuid4().hex[:8]}"
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, 0, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
post_card = page.locator(".project-devlog .post-card").first
|
Fix circular import, primary-admin NULL trap, and add gateway quota reset
Restores a working import graph and closes two data-correctness bugs, plus
adds a reset for the AI gateway's rolling 24h spend.
Circular import: database/__init__ -> engagement -> content -> utils ->
database made the package unimportable. get_project_devlog moves out of
database/engagement.py into content.py, where enrich_items already lives.
Primary administrator: _can_hold_primary_admin read is_active with
bool(row.get("is_active")), so an admin row whose is_active column is SQL
NULL (any row predating the column) was treated as deactivated and skipped.
Every other site defaults an unknown is_active to active; this one now does
too.
Profile JSON: xp_next_level and xp_progress_pct were computed but only put on
the top-level context, never on profile_user, so they serialised as null even
though UserOut declares them and the API docs document them as embedded there.
Gateway quota reset: a cap previously lifted only with the passage of time.
quota.reset upserts a watermark row into gateway_quota_resets, scoped by the
same three nullable dimensions as a quota rule, and spent_24h sums from
max(24h cutoff, watermark). No ledger row is deleted, so the cost analytics on
/admin/ai-usage stay intact. Reaches every surface: POST
/admin/gateway/quota-resets, a per-rule Reset spend button, the Devii tool
gateway_quota_reset (confirm-gated), devplace gateway quota reset, and the API
docs. Admin's Reset all quotas now stamps a global gateway watermark too,
which is what a caller stuck on "AI gateway daily quota exceeded" needed.
Startup: _backfill_gamification swept every xp=0 user on every boot in every
worker and could never converge, since a user with no content earns no XP.
It now intersects pending users with _milestone_candidates(). db.tables is a
live reflection, so it is hoisted out of the loops that probed it per row.
Docker: the dependency layer now depends on pyproject.toml only, so a source
edit no longer reinstalls every dependency and re-downloads Chromium.
Adds start_interval so the healthcheck probes during the start period, and a
docker-reload target, since docker-up does not restart an unchanged container.
Adds events.md, the audit event catalogue that README, CLAUDE.md, the quiz
docs and the tooling all referenced but which never existed: 288 keys across
28 categories, including the families built from a variable at the call site.
Test fixes: both devlog helpers dated post 0 as the newest while the tests
assumed post 2 was; a profile login posted username= to a form that takes
email=; a devlog assertion matched six buttons under strict mode; and the
primary-admin tests seeded founders newer than the back-dated fixture admin,
so they only passed without the api tier.
Full suite: 2989 passed, 1 skipped.
2026-07-27 11:17:48 +02:00
|
|
|
expect(post_card.locator(".post-action-btn.vote-up")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-action-btn.vote-down")).to_be_visible()
|
|
|
|
|
expect(post_card.locator(".post-vote-count")).to_be_visible()
|
test(sveta): Write tests for devlog timeline
Outcome: done
Changed: tests/api/projects/devlog.py, tests/e2e/projects/devlog.py
Verified by: python3 -m py_compile and pyflakes on both files - passed. Full suite not runnable (Python 3.11 env, project requires >=3.12 - pre-existing).
Findings: tests/api/projects/devlog.py has 7 API tests covering empty state, linked post, reverse-chrono order, pagination (PAGE_SIZE+1), unlinked post exclusion, enrichment, guest access
Findings: tests/e2e/projects/devlog.py has 7 E2E tests covering empty state UI, post title, author info, action buttons, load-more link, guest HTML, newest-first order
Findings: Both files follow existing patterns (alice fixture, expect assertions, uuid4 seeds, wait_until=domcontentloaded)
Open: None
Confidence: high - all acceptance criteria addressed across both test tiers, static analysis clean
Typosaurus-Run: 45c0aee6df2649ffaa248729bdfd5841
Typosaurus-Node: a6579fc028b845c598ed059a20702a91
Typosaurus-Agent: @sveta
Refs: #135
2026-07-26 23:59:25 +02:00
|
|
|
expect(post_card.locator("form[action*='/posts/delete/']")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_load_more_appears_with_many_posts(alice):
|
|
|
|
|
"""More than PAGE_SIZE posts produces a Load More link."""
|
|
|
|
|
from devplacepy.database.pagination import PAGE_SIZE
|
|
|
|
|
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
count = PAGE_SIZE + 1
|
|
|
|
|
for i in range(count):
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, i, title=f"loadmore-{i}")
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
devlog = page.locator(".project-devlog")
|
|
|
|
|
expect(devlog.locator(".post-card")).to_have_count(PAGE_SIZE)
|
|
|
|
|
expect(devlog.locator(".load-more-wrap")).to_be_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_guest_sees_devlog_section(app_server):
|
|
|
|
|
"""Unauthenticated visitors can see the devlog section."""
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
slug, _, _ = _seed_project()
|
|
|
|
|
|
|
|
|
|
r = requests.get(f"{BASE_URL}/projects/{slug}")
|
|
|
|
|
assert r.status_code == 200, r.text[:200]
|
|
|
|
|
|
|
|
|
|
assert "No devlog posts yet." in r.text
|
|
|
|
|
assert 'class="project-devlog"' in r.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_devlog_multiple_posts_order(alice):
|
|
|
|
|
"""Posts appear newest-first in the devlog."""
|
|
|
|
|
page, _ = alice
|
|
|
|
|
slug, project_uid, owner_uid = _seed_project()
|
|
|
|
|
|
|
|
|
|
markers = []
|
|
|
|
|
for i in range(3):
|
|
|
|
|
marker = f"order-{i}-{uuid4().hex[:8]}"
|
|
|
|
|
markers.append(marker)
|
|
|
|
|
_seed_project_post(project_uid, owner_uid, i, title=marker)
|
|
|
|
|
|
|
|
|
|
page.goto(f"{BASE_URL}/projects/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
titles = page.locator(".project-devlog .post-card .post-title")
|
|
|
|
|
expect(titles).to_have_count(3)
|
|
|
|
|
first_text = titles.nth(0).inner_text()
|
|
|
|
|
assert markers[-1] == first_text, (
|
|
|
|
|
f"Expected newest post first: {markers[-1]}, got: {first_text}"
|
|
|
|
|
)
|
|
|
|
|
|