2026-06-13 16:32:33 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
|
|
|
import re
|
2026-05-13 21:17:57 +02:00
|
|
|
import time
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
import requests
|
2026-06-05 05:36:18 +02:00
|
|
|
from uuid import uuid4
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
|
|
|
from playwright.sync_api import expect
|
2026-08-09 11:39:53 +02:00
|
|
|
from tests.conftest import (
|
|
|
|
|
BASE_URL,
|
|
|
|
|
assert_no_horizontal_overflow,
|
|
|
|
|
assert_share_copies,
|
|
|
|
|
)
|
2026-06-05 05:36:18 +02:00
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.utils import make_combined_slug
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
DPBOT_SIZE = 112147
|
|
|
|
|
SOURCE_LENGTH_LIMIT = 400000
|
|
|
|
|
def _make_source(length):
|
|
|
|
|
unit = "def handler():\n return 0\n"
|
|
|
|
|
return (unit * (length // len(unit) + 1))[:length]
|
|
|
|
|
def _register_session(prefix):
|
|
|
|
|
name = f"{prefix}{int(time.time() * 1000)}"
|
|
|
|
|
session = requests.Session()
|
2026-06-09 18:48:08 +02:00
|
|
|
session.post(
|
|
|
|
|
f"{BASE_URL}/auth/signup",
|
|
|
|
|
data={
|
|
|
|
|
"username": name,
|
|
|
|
|
"email": f"{name}@t.dev",
|
|
|
|
|
"password": "secret123",
|
|
|
|
|
"confirm_password": "secret123",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"birth_date": "1990-01-01",
|
|
|
|
|
"accept_terms": "1",
|
2026-06-09 18:48:08 +02:00
|
|
|
},
|
|
|
|
|
allow_redirects=True,
|
|
|
|
|
)
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
return session, name
|
2026-06-05 05:36:18 +02:00
|
|
|
def _seed_gists(count):
|
|
|
|
|
owner = str(uuid4())
|
2026-06-09 18:48:08 +02:00
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": owner,
|
|
|
|
|
"username": f"pag_{owner[:8]}",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
2026-06-09 18:48:08 +02:00
|
|
|
"email": f"{owner[:8]}@test.devplace",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 05:36:18 +02:00
|
|
|
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
|
|
|
|
gists = get_table("gists")
|
|
|
|
|
for i in range(count):
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
title = f"Pag Gist {i}"
|
2026-06-09 18:48:08 +02:00
|
|
|
gists.insert(
|
|
|
|
|
{
|
2026-06-14 16:46:36 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": make_combined_slug(title, uid),
|
|
|
|
|
"title": title,
|
|
|
|
|
"language": "python",
|
|
|
|
|
"description": "paginated",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": (base - timedelta(seconds=i)).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 05:36:18 +02:00
|
|
|
return owner
|
2026-05-13 21:17:57 +02:00
|
|
|
def _set_cm_value(page, value):
|
2026-06-09 18:48:08 +02:00
|
|
|
page.evaluate(f"""() => {{
|
2026-05-13 21:17:57 +02:00
|
|
|
const cm = document.querySelector(".CodeMirror")?.CodeMirror;
|
|
|
|
|
if (cm) {{
|
|
|
|
|
cm.setValue({repr(value)});
|
|
|
|
|
cm.save();
|
|
|
|
|
}}
|
2026-06-09 18:48:08 +02:00
|
|
|
}}""")
|
|
|
|
|
def _create_gist(
|
|
|
|
|
page,
|
|
|
|
|
title="Test Gist",
|
|
|
|
|
description="Test description",
|
|
|
|
|
language="python",
|
|
|
|
|
source_code="print('hello')",
|
|
|
|
|
):
|
2026-05-13 21:17:57 +02:00
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("#create-gist-btn").wait_for(state="visible", timeout=10000)
|
|
|
|
|
page.locator("#create-gist-btn").click()
|
|
|
|
|
page.wait_for_timeout(800)
|
|
|
|
|
page.fill("#gist-title", title)
|
|
|
|
|
page.fill("#gist-description", description)
|
|
|
|
|
page.select_option("#gist-language", language)
|
|
|
|
|
page.wait_for_timeout(300)
|
|
|
|
|
_set_cm_value(page, source_code)
|
|
|
|
|
page.wait_for_timeout(300)
|
|
|
|
|
page.locator("button.btn-primary:has-text('Create Gist')").click()
|
|
|
|
|
page.wait_for_timeout(2000)
|
|
|
|
|
current = page.url
|
|
|
|
|
if "/gists/" in current and current != f"{BASE_URL}/gists":
|
|
|
|
|
return
|
|
|
|
|
page.wait_for_url("**/gists/*", timeout=15000, wait_until="domcontentloaded")
|
2026-06-13 16:32:33 +02:00
|
|
|
def _seed_searchable_gists(language="python"):
|
|
|
|
|
token = uuid4().hex[:10]
|
|
|
|
|
owner = str(uuid4())
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": owner,
|
|
|
|
|
"username": f"gsrch_{owner[:8]}",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
2026-06-13 16:32:33 +02:00
|
|
|
"email": f"{owner[:8]}@test.devplace",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
|
|
|
|
gists = get_table("gists")
|
|
|
|
|
match_title = f"Findable gist {token}"
|
|
|
|
|
other_title = f"Unrelated gist {uuid4().hex[:10]}"
|
|
|
|
|
for offset, title in ((0, match_title), (1, other_title)):
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
gists.insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": make_combined_slug(title, uid),
|
|
|
|
|
"title": title,
|
|
|
|
|
"language": language,
|
|
|
|
|
"description": f"Description for {title}",
|
|
|
|
|
"source_code": "x = 1",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": (base - timedelta(seconds=offset)).isoformat(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return token, match_title, other_title
|
2026-05-13 21:17:57 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_listing_loads(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible("h1:has-text('Gists')")
|
|
|
|
|
assert page.is_visible("text=Share and discover code snippets")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_listing_empty(page, app_server):
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
# filter to a user with no gists so the empty state is deterministic even when
|
|
|
|
|
# other tests (sharing the session DB) have created gists
|
2026-06-09 18:48:08 +02:00
|
|
|
page.goto(
|
|
|
|
|
f"{BASE_URL}/gists?user_uid=no-such-user-xyz", wait_until="domcontentloaded"
|
|
|
|
|
)
|
2026-05-13 21:17:57 +02:00
|
|
|
assert page.is_visible("text=No gists found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_listing_shows_sidebar(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible("text=Languages")
|
|
|
|
|
assert page.is_visible("text=All")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_guest_cannot_create(page, app_server):
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
assert not page.is_visible("#create-gist-btn")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_gist(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Create Test {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="print('create test')")
|
|
|
|
|
assert page.is_visible(f"text={title}")
|
|
|
|
|
assert page.is_visible("text=Python")
|
|
|
|
|
assert page.is_visible("text=print('create test')")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_detail_shows_all_sections(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Detail Test {int(time.time())}"
|
2026-06-09 18:48:08 +02:00
|
|
|
_create_gist(
|
|
|
|
|
page,
|
|
|
|
|
title=title,
|
|
|
|
|
description="See all sections",
|
|
|
|
|
source_code="def hello(): pass",
|
|
|
|
|
)
|
2026-05-13 21:17:57 +02:00
|
|
|
assert page.is_visible("text=See all sections")
|
|
|
|
|
assert page.is_visible("text=def hello(): pass")
|
|
|
|
|
assert page.is_visible("text=Copy")
|
|
|
|
|
assert page.is_visible("text=Back to Gists")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_detail_back_link(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Back Link Test {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="x = 1")
|
|
|
|
|
page.click("text=Back to Gists")
|
|
|
|
|
page.wait_for_url(f"{BASE_URL}/gists", timeout=10000, wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible("h1:has-text('Gists')")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_edit_gist(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Edit Test Original {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="x = 1")
|
|
|
|
|
page.locator("button:has-text('Edit')").wait_for(state="visible", timeout=5000)
|
|
|
|
|
page.click("button:has-text('Edit')")
|
|
|
|
|
page.wait_for_timeout(800)
|
|
|
|
|
new_title = f"Edit Test Updated {int(time.time())}"
|
|
|
|
|
page.fill("#edit-gist-title", new_title)
|
|
|
|
|
page.select_option("#edit-gist-language", "javascript")
|
|
|
|
|
_set_cm_value(page, "const x = 1;")
|
|
|
|
|
page.wait_for_timeout(300)
|
|
|
|
|
page.locator("button.btn-primary:has-text('Save Changes')").click()
|
|
|
|
|
page.wait_for_url("**/gists/*", timeout=15000, wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible(f"text={new_title}")
|
|
|
|
|
assert page.is_visible("text=JavaScript")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_non_owner_cannot_edit_or_delete(bob, alice, app_server):
|
|
|
|
|
bob_page, _ = bob
|
|
|
|
|
alice_page, alice_user = alice
|
|
|
|
|
title = f"Owner Check {int(time.time())}"
|
|
|
|
|
_create_gist(alice_page, title=title, source_code="owner_only = True")
|
|
|
|
|
slug = alice_page.url.split("/gists/")[-1]
|
|
|
|
|
bob_page.goto(f"{BASE_URL}/gists/{slug}", wait_until="domcontentloaded")
|
|
|
|
|
assert not bob_page.is_visible("button:has-text('Edit')")
|
|
|
|
|
assert not bob_page.is_visible("button:has-text('Delete')")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_gist(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Delete Test {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="delete_me = True")
|
|
|
|
|
page.locator("button:has-text('Delete')").wait_for(state="visible", timeout=5000)
|
|
|
|
|
page.click("button:has-text('Delete')")
|
feat: add project file system with CRUD, upload, inline editing, and video attachment support
- Add new `/projects/{slug}/files` endpoint group for per-project filesystem operations including directory and file CRUD, upload, and inline editing with public read and owner write access
- Extend attachment system to support video formats (webm, ogv, mov, m4v) with proper file icons and MIME types
- Implement configurable allowed file types via `allowed_file_types` site setting, replacing hardcoded `ALLOWED_UPLOAD_TYPES` with dynamic `allowed_extensions()` and `is_extension_allowed()` functions
- Add `delete_all_project_files()` call in `delete_content_item()` to clean up project files when a project is deleted
- Create database indexes on `project_files` table for `(project_uid, path)` and `(project_uid, parent_path)` to optimize file lookups
- Introduce `docs_prose.py` module with `render_prose()` function that renders Markdown content inside `data-render` divs using mistune, enabling dynamic prose rendering in documentation pages
- Enhance docs search with Markdown-aware text stripping (`_demarkdown()`) and improved HTML/script/style sanitization for better search indexing
- Update documentation API samples to reflect new attachment response fields (`is_image`, `is_video`, `mime_type`) and note video format support
- Update README to document the new project files endpoint and clarify AI gateway attribution for guest Devii sessions
2026-06-08 22:51:09 +02:00
|
|
|
page.locator(".dialog-overlay.visible .dialog-confirm").click()
|
2026-05-13 21:17:57 +02:00
|
|
|
page.wait_for_url(f"{BASE_URL}/gists", timeout=10000, wait_until="domcontentloaded")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_voting(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Vote Test {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="vote_me = True")
|
2026-05-23 03:21:55 +02:00
|
|
|
star_btn = page.locator("form[action*='/votes/gist/'] button").first
|
2026-05-13 21:17:57 +02:00
|
|
|
original_text = star_btn.text_content()
|
|
|
|
|
original_stars = int(original_text.strip("\u2606 "))
|
|
|
|
|
star_btn.click()
|
|
|
|
|
page.wait_for_timeout(500)
|
2026-05-23 03:21:55 +02:00
|
|
|
star_btn = page.locator("form[action*='/votes/gist/'] button").first
|
2026-05-13 21:17:57 +02:00
|
|
|
new_text = star_btn.text_content()
|
|
|
|
|
new_stars = int(new_text.strip("\u2606 "))
|
|
|
|
|
assert new_stars == original_stars + 1
|
|
|
|
|
|
|
|
|
|
|
2026-05-23 03:21:55 +02:00
|
|
|
def test_gist_detail_has_sourcecode_schema(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_create_gist(page, title=f"Schema Gist {int(time.time())}", source_code="x = 1")
|
|
|
|
|
scripts = page.locator('script[type="application/ld+json"]')
|
|
|
|
|
text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count()))
|
|
|
|
|
assert "SoftwareSourceCode" in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_detail_share_button(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_create_gist(page, title=f"Share Gist {int(time.time())}", source_code="x = 1")
|
|
|
|
|
assert_share_copies(page, "/gists/")
|
|
|
|
|
|
|
|
|
|
|
feat: replace raw form parsing with pydantic models for auth, admin, bugs, comments, and gists
Migrate all route handlers from manual `request.form()` parsing to typed `Annotated[Model, Form()]` dependencies using new pydantic models (SignupForm, LoginForm, ForgotPasswordForm, AdminRoleForm, AdminPasswordForm, BugForm, CommentForm, GistForm). Remove inline validation logic from signup, login, password reset, admin role/password change, bug creation, comment creation, and gist creation endpoints, delegating validation to model validators and field constraints. Add `RequestValidationError` exception handler with friendly error messages for auth form pages. Update model definitions to use `field_validator` and `model_validator` for cross-field checks like password matching.
2026-05-23 05:44:04 +02:00
|
|
|
def test_gist_code_copy_button(alice, app_server):
|
|
|
|
|
from playwright.sync_api import expect
|
2026-06-09 18:48:08 +02:00
|
|
|
|
feat: replace raw form parsing with pydantic models for auth, admin, bugs, comments, and gists
Migrate all route handlers from manual `request.form()` parsing to typed `Annotated[Model, Form()]` dependencies using new pydantic models (SignupForm, LoginForm, ForgotPasswordForm, AdminRoleForm, AdminPasswordForm, BugForm, CommentForm, GistForm). Remove inline validation logic from signup, login, password reset, admin role/password change, bug creation, comment creation, and gist creation endpoints, delegating validation to model validators and field constraints. Add `RequestValidationError` exception handler with friendly error messages for auth form pages. Update model definitions to use `field_validator` and `model_validator` for cross-field checks like password matching.
2026-05-23 05:44:04 +02:00
|
|
|
page, _ = alice
|
|
|
|
|
_create_gist(page, title=f"Copy Gist {int(time.time())}", source_code="copyme = 42")
|
|
|
|
|
btn = page.locator("button[data-copy='gist-code-content']")
|
|
|
|
|
btn.click()
|
|
|
|
|
expect(btn).to_have_text("Copied!", timeout=3000)
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 21:17:57 +02:00
|
|
|
def test_language_filter(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Lang Filter Test {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, language="go", source_code="package main")
|
|
|
|
|
page.goto(f"{BASE_URL}/gists?language=go", wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible(f"text={title}")
|
|
|
|
|
page.goto(f"{BASE_URL}/gists?language=python", wait_until="domcontentloaded")
|
|
|
|
|
assert not page.is_visible(f"text={title}")
|
|
|
|
|
|
|
|
|
|
|
docs: document shared search pattern for feed, gists, and project listings
Add a reusable `text_search_clause` helper in `database.py` that builds a SQLAlchemy `or_` of `ilike` clauses over specified columns, returning `None` when search is blank or the table lacks the columns. Wire it into `get_feed_posts`, `get_gists_list`, and the projects listing so all three public index pages support free-text search via a `search` query parameter. Introduce `_sidebar_search.html` as the single search-box partial, included at the top of each listing's left filter panel with `_action`, `_placeholder`, and `_hidden` locals to preserve active category/tab filters on submit. Expose the `search` field on `FeedOut`, `GistsOut`, and `ProjectsOut` API schemas with corresponding OpenAPI documentation. Update `CLAUDE.md` to note the rate-limiter exemption for `GET`/`HEAD` and the `/openai` gateway, and refresh `README.md` route tables to mention the new search capability on `/feed`, `/gists`, and `/projects`.
2026-06-13 15:24:48 +02:00
|
|
|
def test_gists_search_bar_visible(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
search = page.locator("input[name='search'][placeholder='Search gists...']")
|
|
|
|
|
assert search.is_visible()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gists_search_filters(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
token, match_title, other_title = _seed_searchable_gists()
|
|
|
|
|
page.goto(f"{BASE_URL}/gists?search={token}", wait_until="domcontentloaded")
|
|
|
|
|
assert page.locator(f".gist-card-title:has-text('{match_title}')").count() == 1
|
|
|
|
|
assert page.locator(f".gist-card-title:has-text('{other_title}')").count() == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gists_search_no_match(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_seed_searchable_gists()
|
|
|
|
|
page.goto(
|
|
|
|
|
f"{BASE_URL}/gists?search=zzz{uuid4().hex}", wait_until="domcontentloaded"
|
|
|
|
|
)
|
|
|
|
|
assert page.locator(".gist-card-title").count() == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gists_search_preserves_language(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
token, match_title, _ = _seed_searchable_gists(language="go")
|
|
|
|
|
page.goto(
|
|
|
|
|
f"{BASE_URL}/gists?language=go&search={token}", wait_until="domcontentloaded"
|
|
|
|
|
)
|
|
|
|
|
assert page.locator(f".gist-card-title:has-text('{match_title}')").count() == 1
|
|
|
|
|
preserved = page.locator(
|
|
|
|
|
".sidebar-card form input[type='hidden'][name='language'][value='go']"
|
|
|
|
|
)
|
|
|
|
|
assert preserved.count() == 1
|
|
|
|
|
|
|
|
|
|
|
2026-05-13 21:17:57 +02:00
|
|
|
def test_gist_comments(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Comment Test {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="has_comments = True")
|
|
|
|
|
comment_text = f"Nice gist! {int(time.time())}"
|
|
|
|
|
comment_input = page.locator("textarea[name='content']").first
|
|
|
|
|
comment_input.wait_for(state="visible", timeout=5000)
|
|
|
|
|
comment_input.fill(comment_text)
|
|
|
|
|
page.locator("button.comment-form-submit").first.click()
|
|
|
|
|
page.wait_for_url("**/gists/*", timeout=10000, wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible(f"text={comment_text}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_listing_shows_created_gist(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Listing Shows {int(time.time())}"
|
|
|
|
|
_create_gist(page, title=title, source_code="show_in_list = True")
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
assert page.is_visible(f"text={title}")
|
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
|
|
|
|
|
|
|
|
|
2026-06-05 05:36:18 +02:00
|
|
|
def test_gist_pagination_first_page(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
owner = _seed_gists(26)
|
|
|
|
|
page.goto(f"{BASE_URL}/gists?user_uid={owner}", wait_until="domcontentloaded")
|
|
|
|
|
assert page.locator(".gist-card").count() == 25
|
|
|
|
|
assert page.is_visible(".load-more-wrap")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_pagination_load_more(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
owner = _seed_gists(26)
|
|
|
|
|
page.goto(f"{BASE_URL}/gists?user_uid={owner}", wait_until="domcontentloaded")
|
|
|
|
|
page.click(".load-more-wrap a")
|
|
|
|
|
page.wait_for_url(lambda url: "before=" in url, wait_until="domcontentloaded")
|
|
|
|
|
assert f"user_uid={owner}" in page.url
|
|
|
|
|
assert page.locator(".gist-card").count() == 1
|
|
|
|
|
assert not page.is_visible(".load-more-wrap")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gist_no_pagination_below_page_size(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
owner = _seed_gists(10)
|
|
|
|
|
page.goto(f"{BASE_URL}/gists?user_uid={owner}", wait_until="domcontentloaded")
|
|
|
|
|
assert page.locator(".gist-card").count() == 10
|
|
|
|
|
assert not page.is_visible(".load-more-wrap")
|
|
|
|
|
|
|
|
|
|
|
feat: add leaderboard router, gamification backfill, and shared content creation helpers
Introduce a new `/leaderboard` endpoint ranking top 50 members by total stars, wire it into the app router and documentation. Implement `_backfill_gamification()` in `database.py` to compute XP/levels for existing accounts from prior posts, comments, votes, and follows. Extract `is_owner()`, `create_content_item()`, and `detail_context()` into `content.py` to centralize content creation, reward awarding, mention notifications, and attachment linking. Update comment creation/deletion in `comments.py` to use `award_rewards()` with `XP_COMMENT` and the new `is_owner()` helper. Add shared template partials documentation for vote bars, star buttons, post headers, and topic selectors to `AGENTS.md`.
2026-05-30 20:16:39 +02:00
|
|
|
def test_gist_voted_state_persists(alice):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
_create_gist(page, title="Voted State Gist")
|
|
|
|
|
star = "form[action*='/votes/gist/'] button"
|
|
|
|
|
page.locator(star).first.click()
|
|
|
|
|
page.wait_for_timeout(500)
|
|
|
|
|
page.reload(wait_until="domcontentloaded")
|
|
|
|
|
expect(page.locator(star).first).to_have_class(re.compile(r"\bvoted\b"))
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_oversized_gist_shows_client_error(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"Client Oversize {int(time.time())}"
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
page.locator("#create-gist-btn").wait_for(state="visible", timeout=10000)
|
|
|
|
|
page.locator("#create-gist-btn").click()
|
|
|
|
|
page.wait_for_timeout(800)
|
|
|
|
|
page.fill("#gist-title", title)
|
|
|
|
|
page.wait_for_timeout(300)
|
|
|
|
|
_set_cm_value(page, _make_source(SOURCE_LENGTH_LIMIT + 1))
|
|
|
|
|
page.wait_for_timeout(300)
|
|
|
|
|
page.locator("button.btn-primary:has-text('Create Gist')").click()
|
|
|
|
|
error = page.locator(".gist-length-error")
|
|
|
|
|
error.wait_for(state="visible", timeout=5000)
|
|
|
|
|
assert "maximum" in error.text_content()
|
|
|
|
|
assert page.url.rstrip("/") == f"{BASE_URL}/gists"
|
|
|
|
|
assert get_table("gists").find_one(title=title) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_create_large_gist_via_ui_saves(alice, app_server):
|
|
|
|
|
page, _ = alice
|
|
|
|
|
title = f"UI Large {int(time.time())}"
|
|
|
|
|
source = _make_source(DPBOT_SIZE)
|
|
|
|
|
_create_gist(page, title=title, description="ui large", source_code=source)
|
|
|
|
|
assert "/gists/" in page.url
|
|
|
|
|
saved = get_table("gists").find_one(title=title)
|
|
|
|
|
assert saved is not None
|
|
|
|
|
assert len(saved["source_code"]) > DPBOT_SIZE // 2
|
feat: add seed helpers for gist, news, and project comment hierarchies in e2e tests
Introduce `_seed_gist_with_comments`, `_seed_news_with_comments`, and `_seed_project_with_comments` helper functions that create a user, a parent entity, and four comments (excluded, flat, parent, reply) with a shared marker prefix and timestamps, enabling consistent comment tree seeding across test modules.
2026-06-16 06:06:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_gist_with_comments():
|
|
|
|
|
owner = str(uuid4())
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": owner,
|
|
|
|
|
"username": f"gcseed_{owner[:8]}",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
feat: add seed helpers for gist, news, and project comment hierarchies in e2e tests
Introduce `_seed_gist_with_comments`, `_seed_news_with_comments`, and `_seed_project_with_comments` helper functions that create a user, a parent entity, and four comments (excluded, flat, parent, reply) with a shared marker prefix and timestamps, enabling consistent comment tree seeding across test modules.
2026-06-16 06:06:16 +02:00
|
|
|
"email": f"{owner[:8]}@gc.seed",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
marker = f"gistcom-{uid[:8]}"
|
|
|
|
|
get_table("gists").insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": make_combined_slug(marker, uid),
|
|
|
|
|
"title": marker,
|
|
|
|
|
"description": "Gist with comment hierarchy.",
|
|
|
|
|
"source_code": "print('x')",
|
|
|
|
|
"language": "python",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
parent = str(uuid4())
|
|
|
|
|
base = datetime.now(timezone.utc)
|
|
|
|
|
rows = [
|
|
|
|
|
(f"{marker}-excluded", str(uuid4()), None, -2),
|
|
|
|
|
(f"{marker}-flat", str(uuid4()), None, -1),
|
|
|
|
|
(f"{marker}-parent", parent, None, 0),
|
|
|
|
|
(f"{marker}-reply", str(uuid4()), parent, 1),
|
|
|
|
|
]
|
|
|
|
|
comments = get_table("comments")
|
|
|
|
|
for content, cuid, par, off in rows:
|
|
|
|
|
comments.insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": cuid,
|
|
|
|
|
"target_type": "gist",
|
|
|
|
|
"target_uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"content": content,
|
|
|
|
|
"parent_uid": par,
|
|
|
|
|
"created_at": (base + timedelta(seconds=off)).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return marker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gists_list_preserves_comment_hierarchy(page, app_server):
|
|
|
|
|
marker = _seed_gist_with_comments()
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
card = page.locator(".gist-card").filter(has_text=marker).first
|
|
|
|
|
card.wait_for(state="visible")
|
|
|
|
|
previews = card.locator(".post-card-comments .comment-text")
|
|
|
|
|
expect(previews).to_have_count(3)
|
|
|
|
|
expect(card.locator(".post-card-comments")).not_to_contain_text(f"{marker}-excluded")
|
|
|
|
|
nested = card.locator(".post-card-comments .comment-replies .comment-text")
|
|
|
|
|
expect(nested).to_contain_text(f"{marker}-reply")
|
2026-08-09 11:39:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _seed_gist_with_long_code_line():
|
|
|
|
|
owner = str(uuid4())
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": owner,
|
|
|
|
|
"username": f"gcode_{owner[:8]}",
|
|
|
|
|
"email": f"{owner[:8]}@gcode.seed",
|
|
|
|
|
"password_hash": "x",
|
|
|
|
|
"role": "Member",
|
|
|
|
|
"is_active": True,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
uid = str(uuid4())
|
|
|
|
|
marker = f"gistcode-{uid[:8]}"
|
|
|
|
|
line = "unbreakable_identifier_" * 8
|
|
|
|
|
get_table("gists").insert(
|
|
|
|
|
{
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": make_combined_slug(marker, uid),
|
|
|
|
|
"title": marker,
|
|
|
|
|
"description": f"```python\n{line}\n```",
|
|
|
|
|
"source_code": "print('x')",
|
|
|
|
|
"language": "python",
|
|
|
|
|
"stars": 0,
|
|
|
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return marker
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_gists_long_code_line_does_not_widen_layout(page, app_server):
|
|
|
|
|
marker = _seed_gist_with_long_code_line()
|
|
|
|
|
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
|
|
|
|
|
card = page.locator(".gist-card").filter(has_text=marker).first
|
|
|
|
|
card.wait_for(state="visible")
|
|
|
|
|
assert card.locator("pre").count() == 1
|
|
|
|
|
assert_no_horizontal_overflow(page, ".gists-layout")
|