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.
This commit is contained in:
2026-08-09 00:18:20 +02:00
parent 68c2bbe387
commit 8e9d3fad98
348 changed files with 10633 additions and 238 deletions
+2
View File
@@ -12,6 +12,7 @@ def _make_user_cli(role="Member"):
{
"uid": generate_uid(),
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"role": role,
"created_at": datetime.now(timezone.utc).isoformat(),
@@ -138,6 +139,7 @@ def test_apikey_backfill_assigns_missing(local_db, capsys):
{
"uid": uid,
"username": f"cli_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@t.dev",
"role": "Member",
"created_at": datetime.now(timezone.utc).isoformat(),
+5
View File
@@ -38,6 +38,8 @@ def _signup_project_visibility():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -108,6 +110,7 @@ def _make_user(role="Member"):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"role": role,
"created_at": datetime.now(timezone.utc).isoformat(),
@@ -156,6 +159,7 @@ def _make_user_at(role, created_at):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"role": role,
"created_at": created_at.isoformat(),
@@ -385,6 +389,7 @@ def test_enrich_items_attaches_author_and_extras(local_db):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"created_at": datetime.now(timezone.utc).isoformat(),
}
+4
View File
@@ -21,6 +21,7 @@ def _user_db_helpers():
{
"uid": uid,
"username": f"dbh_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@t.dev",
"created_at": _now_db_helpers(),
}
@@ -532,6 +533,7 @@ def test_get_uids_by_username_match_returns_matching_uids(local_db):
{
"uid": target,
"username": "searchablehero",
"terms_version": "1",
"created_at": _now_db_helpers(),
}
)
@@ -548,6 +550,7 @@ def test_text_search_clause_matches_author_username(local_db):
{
"uid": author,
"username": "authorquery",
"terms_version": "1",
"created_at": _now_db_helpers(),
}
)
@@ -570,6 +573,7 @@ def test_text_search_clause_without_author_field_ignores_username(local_db):
{
"uid": author,
"username": "lonelyauthor",
"terms_version": "1",
"created_at": _now_db_helpers(),
}
)
+1
View File
@@ -28,6 +28,7 @@ def _user(prefix="award"):
{
"uid": uid,
"username": f"{prefix}_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@t.dev",
"created_at": datetime.now(timezone.utc).isoformat(),
"award_count": 0,
+131
View File
@@ -0,0 +1,131 @@
# retoor <retoor@molodetz.nl>
from datetime import date
import pytest
from devplacepy.database import (
AGE_BANDS,
MATURITY_TARGETS,
REPORTABLE_TARGETS,
REPORT_REASONS,
SOFT_DELETE_TABLES,
UNREPORTABLE_TABLES,
age_band_for,
years_between,
)
from devplacepy.database.moderation import MODERATION_TABLES
GUIDELINE_REASONS = (
"hate",
"violence",
"weapons",
"sexual",
"religious",
"misinformation",
"exploitative",
)
def test_every_visible_soft_delete_table_is_reportable():
reportable_tables = set(REPORTABLE_TARGETS.values())
excluded = set(UNREPORTABLE_TABLES) | set(MODERATION_TABLES)
unclassified = [
table
for table in SOFT_DELETE_TABLES
if table not in reportable_tables and table not in excluded
]
assert unclassified == []
def test_exclusion_list_only_names_real_soft_delete_tables():
unknown = [table for table in UNREPORTABLE_TABLES if table not in SOFT_DELETE_TABLES]
assert unknown == []
def test_registry_tables_are_unique_and_maturity_is_a_subset():
assert len(set(REPORTABLE_TARGETS.values())) == len(REPORTABLE_TARGETS)
assert MATURITY_TARGETS <= set(REPORTABLE_TARGETS)
def test_report_reasons_cover_every_guideline_category():
for reason in GUIDELINE_REASONS:
assert reason in REPORT_REASONS
assert REPORT_REASONS["intellectual_property"]
assert REPORT_REASONS["other"]
def test_every_registry_target_resolves_to_a_url(local_db):
from devplacepy.database import resolve_object_url
for target_type in REPORTABLE_TARGETS:
url = resolve_object_url(target_type, "missing-uid")
assert url.startswith("/")
def test_resolve_object_url_points_at_the_owning_surface(local_db):
from devplacepy.database import get_table, resolve_object_url
get_table("users").insert(
{"uid": "mod-user-1", "username": "mod_user_1", "deleted_at": None}
)
get_table("messages").insert(
{
"uid": "mod-msg-1",
"sender_uid": "mod-user-1",
"receiver_uid": "mod-user-2",
"content": "hello",
}
)
try:
assert resolve_object_url("user", "mod-user-1") == "/profile/mod_user_1"
assert (
resolve_object_url("message", "mod-msg-1")
== "/messages?with_uid=mod-user-1"
)
assert resolve_object_url("devii_output", "anything") == "/devii"
finally:
get_table("users").delete(uid="mod-user-1")
get_table("messages").delete(uid="mod-msg-1")
@pytest.mark.parametrize(
"age,expected",
[
(0, "under_min"),
(12, "under_min"),
(13, "13_15"),
(15, "13_15"),
(16, "16_17"),
(17, "16_17"),
(18, "adult"),
(120, "adult"),
],
)
def test_age_band_boundaries(age, expected):
assert age_band_for(age) == expected
def test_age_band_is_monotonic_across_the_whole_domain():
order = {band: index for index, band in enumerate(AGE_BANDS)}
previous = -1
for age in range(0, 150):
current = order[age_band_for(age)]
assert current >= previous
previous = current
def test_years_between_handles_leap_days_and_exact_boundaries():
assert years_between(date(2000, 2, 29), date(2018, 2, 28)) == 17
assert years_between(date(2000, 2, 29), date(2018, 3, 1)) == 18
assert years_between(date(2008, 1, 1), date(2026, 1, 1)) == 18
assert years_between(date(2008, 1, 2), date(2026, 1, 1)) == 17
def test_years_between_never_exceeds_the_calendar_difference():
born = date(2005, 6, 15)
for offset in range(0, 4000, 7):
today = date.fromordinal(born.toordinal() + offset)
years = years_between(born, today)
assert 0 <= years <= today.year - born.year
+4
View File
@@ -8,6 +8,7 @@ def test_primary_admin_ignores_accounts_without_a_signup_time(local_db):
{
"uid": "pa-real",
"username": "pa-real",
"terms_version": "1",
"role": "Admin",
"created_at": "1990-01-01T00:00:00",
"deleted_at": None,
@@ -17,6 +18,7 @@ def test_primary_admin_ignores_accounts_without_a_signup_time(local_db):
{
"uid": "pa-null",
"username": "pa-null",
"terms_version": "1",
"role": "Admin",
"created_at": None,
"deleted_at": None,
@@ -26,6 +28,7 @@ def test_primary_admin_ignores_accounts_without_a_signup_time(local_db):
{
"uid": "pa-blank",
"username": "pa-blank",
"terms_version": "1",
"role": "Admin",
"created_at": "",
"deleted_at": None,
@@ -54,6 +57,7 @@ def test_primary_admin_skips_deactivated_and_deleted_founders(local_db):
{
"uid": uid,
"username": uid,
"terms_version": "1",
"role": "Admin",
"created_at": created_at,
"is_active": active,
+4
View File
@@ -28,6 +28,8 @@ def _signup_project_file_lines():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -97,6 +99,8 @@ def _signup_project_files():
"email": f"{name}@t.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+2
View File
@@ -13,6 +13,8 @@ def _session_push():
"email": f"{name}@test.dev",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
+13 -13
View File
@@ -1,6 +1,6 @@
# retoor <retoor@molodetz.nl>
from devplacepy.routers.admin.users import _is_senior_admin, _seniority_key
from devplacepy.routers.admin._shared import is_senior_admin, seniority_key
SENIOR_ADMIN = {
"uid": "senior",
@@ -23,39 +23,39 @@ OLD_MEMBER = {
def test_junior_admin_cannot_manage_senior_admin():
assert _is_senior_admin(JUNIOR_ADMIN, SENIOR_ADMIN) is True
assert is_senior_admin(JUNIOR_ADMIN, SENIOR_ADMIN) is True
def test_senior_admin_can_manage_junior_admin():
assert _is_senior_admin(SENIOR_ADMIN, JUNIOR_ADMIN) is False
assert is_senior_admin(SENIOR_ADMIN, JUNIOR_ADMIN) is False
def test_members_are_never_protected():
assert _is_senior_admin(JUNIOR_ADMIN, OLD_MEMBER) is False
assert _is_senior_admin(SENIOR_ADMIN, OLD_MEMBER) is False
assert is_senior_admin(JUNIOR_ADMIN, OLD_MEMBER) is False
assert is_senior_admin(SENIOR_ADMIN, OLD_MEMBER) is False
def test_acting_on_self_is_not_protected():
assert _is_senior_admin(SENIOR_ADMIN, SENIOR_ADMIN) is False
assert _is_senior_admin(JUNIOR_ADMIN, JUNIOR_ADMIN) is False
assert is_senior_admin(SENIOR_ADMIN, SENIOR_ADMIN) is False
assert is_senior_admin(JUNIOR_ADMIN, JUNIOR_ADMIN) is False
def test_none_target_is_not_protected():
assert _is_senior_admin(SENIOR_ADMIN, None) is False
assert is_senior_admin(SENIOR_ADMIN, None) is False
def test_equal_created_at_tiebreaks_on_id():
earlier = {"uid": "e1", "role": "Admin", "created_at": "2024-01-01T00:00:00", "id": 3}
later = {"uid": "e2", "role": "Admin", "created_at": "2024-01-01T00:00:00", "id": 5}
assert _is_senior_admin(later, earlier) is True
assert _is_senior_admin(earlier, later) is False
assert is_senior_admin(later, earlier) is True
assert is_senior_admin(earlier, later) is False
def test_missing_created_at_sorts_oldest_failsafe():
no_date = {"uid": "nc", "role": "Admin", "created_at": None, "id": 2}
assert _is_senior_admin(SENIOR_ADMIN, no_date) is True
assert is_senior_admin(SENIOR_ADMIN, no_date) is True
def test_seniority_key_orders_by_created_at_then_id():
assert _seniority_key(SENIOR_ADMIN) < _seniority_key(JUNIOR_ADMIN)
assert _seniority_key({"created_at": None, "id": 0}) == ("", 0)
assert seniority_key(SENIOR_ADMIN) < seniority_key(JUNIOR_ADMIN)
assert seniority_key({"created_at": None, "id": 0}) == ("", 0)
+108
View File
@@ -0,0 +1,108 @@
# retoor <retoor@molodetz.nl>
import pytest
from devplacepy.database import get_table
from devplacepy.routers.workspaces import index_entries
OWNER = "wsidx-owner"
STRANGER = "wsidx-stranger"
PUBLIC_PROJECT = "wsidx-public"
PRIVATE_PROJECT = "wsidx-private"
ROWS = [
{
"uid": "wsidx-instance-public",
"project_uid": PUBLIC_PROJECT,
"owner_uid": OWNER,
"ingress_slug": "wsidx-pub",
"name": "Public workspace",
},
{
"uid": "wsidx-instance-private",
"project_uid": PRIVATE_PROJECT,
"owner_uid": OWNER,
"ingress_slug": "wsidx-priv",
"name": "Private workspace",
},
]
@pytest.fixture
def seeded_workspaces(local_db):
users = get_table("users")
projects = get_table("projects")
users.insert({"uid": OWNER, "username": "wsidx_owner", "role": "Member"})
users.insert({"uid": STRANGER, "username": "wsidx_stranger", "role": "Member"})
projects.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": PUBLIC_PROJECT,
"user_uid": OWNER,
"slug": "wsidx-public-project",
"title": "Public workspace project",
"description": "The public project description.",
"is_private": 0,
}
)
projects.insert(
{
"deleted_at": None,
"deleted_by": None,
"uid": PRIVATE_PROJECT,
"user_uid": OWNER,
"slug": "wsidx-private-project",
"title": "Private workspace project",
"description": "The private project description.",
"is_private": 1,
}
)
yield
projects.delete(uid=PUBLIC_PROJECT)
projects.delete(uid=PRIVATE_PROJECT)
users.delete(uid=OWNER)
users.delete(uid=STRANGER)
def _by_slug(entries):
return {entry["slug"]: entry for entry in entries}
def test_every_row_carries_its_uid_and_owner_uid(seeded_workspaces):
entries = _by_slug(index_entries(ROWS, None))
assert entries["wsidx-pub"]["uid"] == "wsidx-instance-public"
assert entries["wsidx-priv"]["uid"] == "wsidx-instance-private"
for entry in entries.values():
assert entry["owner_uid"] == OWNER
def test_a_guest_never_sees_a_private_projects_description_or_url(seeded_workspaces):
entries = _by_slug(index_entries(ROWS, None))
assert entries["wsidx-pub"]["description"] == "The public project description."
assert entries["wsidx-pub"]["project_url"] == "/projects/wsidx-public-project"
assert entries["wsidx-priv"]["description"] == ""
assert entries["wsidx-priv"]["project_url"] == ""
def test_a_stranger_is_treated_exactly_like_a_guest(seeded_workspaces):
viewer = {"uid": STRANGER, "username": "wsidx_stranger", "role": "Member"}
entries = _by_slug(index_entries(ROWS, viewer))
assert entries["wsidx-priv"]["description"] == ""
assert entries["wsidx-priv"]["project_url"] == ""
def test_the_project_owner_still_sees_their_private_project_fields(seeded_workspaces):
viewer = {"uid": OWNER, "username": "wsidx_owner", "role": "Member"}
entries = _by_slug(index_entries(ROWS, viewer))
assert entries["wsidx-priv"]["description"] == "The private project description."
assert entries["wsidx-priv"]["project_url"] == "/projects/wsidx-private-project"
def test_a_published_workspace_is_always_listed_with_its_link(seeded_workspaces):
entries = _by_slug(index_entries(ROWS, None))
assert len(entries) == 2
assert entries["wsidx-priv"]["name"] == "Private workspace"
assert entries["wsidx-priv"]["url"].endswith("/p/wsidx-priv")
assert entries["wsidx-priv"]["owner"] == "wsidx_owner"
assert entries["wsidx-priv"]["maturity"] == "general"
+43
View File
@@ -21,6 +21,8 @@ def _signup_media(prefix="media"):
"email": f"{name}@test.devplace",
"password": "secret123",
"confirm_password": "secret123",
"birth_date": "1990-01-01",
"accept_terms": "1",
},
allow_redirects=True,
)
@@ -135,6 +137,47 @@ def test_devii_delete_media_is_confirm_gated():
assert "confirm" in blob
def test_admin_report_subject_is_the_redacted_admin_user_model():
from devplacepy.schemas import AdminReportOut, AdminUserOut
annotation = AdminReportOut.model_fields["subject"].annotation
assert AdminUserOut in getattr(annotation, "__args__", (annotation,))
assert "password_hash" not in AdminUserOut.model_fields
assert "api_key" not in AdminUserOut.model_fields
def test_accept_terms_page_reports_versions_not_consents():
from devplacepy.schemas import AcceptTermsOut
assert set(AcceptTermsOut.model_fields) == {"terms_version", "accepted_version"}
assert "consents" not in AcceptTermsOut.model_fields
def test_the_workspace_index_row_carries_its_uid_and_owner():
from devplacepy.schemas import WorkspaceIndexItemOut
for field in ("uid", "owner_uid", "description", "project_url"):
assert field in WorkspaceIndexItemOut.model_fields
def test_devii_reads_the_account_deletion_page_before_deleting():
from devplacepy.services.devii.actions.catalog import ACTIONS
from devplacepy.services.devii.actions.dispatcher import (
CONFIRM_REQUIRED,
confirmation_error,
)
by_name = {action.name: action for action in ACTIONS}
reader = by_name["view_account_deletion"]
assert reader.method == "GET"
assert reader.path == "/profile/{username}/delete"
assert "view_account_deletion" not in CONFIRM_REQUIRED
assert "delete_my_account" in CONFIRM_REQUIRED
assert confirmation_error("delete_my_account", {}) is not None
assert confirmation_error("delete_my_account", {"confirm": "true"}) is None
assert any(param.name == "confirm" for param in by_name["delete_my_account"].params)
def test_game_legacy_out_defaults():
from devplacepy.schemas import GameLegacyOut
+1
View File
@@ -21,6 +21,7 @@ def _cli_user(role="Member"):
{
"uid": generate_uid(),
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": generate_uid(),
"role": role,
+69
View File
@@ -36,6 +36,7 @@ def env(tmp_path, monkeypatch):
{
"uid": user["uid"],
"username": user["username"],
"terms_version": "1",
"email": "ctestadmin@example.com",
"api_key": generate_uid(),
"password_hash": "",
@@ -296,6 +297,74 @@ def test_run_as_uid_rejects_unknown_user(env):
)
def _other_member(uid="ctest-u2", username="ctestmember"):
users = get_table("users")
if not users.find_one(uid=uid):
users.insert(
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@example.com",
"api_key": generate_uid(),
"password_hash": "",
"role": "Member",
"is_active": True,
"created_at": "2020-01-01T00:00:00+00:00",
}
)
refresh_snapshot()
return uid
def test_run_as_another_member_needs_their_credential_consent(env):
from devplacepy.database import set_consent
other = _other_member()
set_consent("user", other, "container_credentials", False)
with pytest.raises(api.ContainerError) as refused:
run_async(
api.create_instance(
env["project"],
name="noconsent",
run_as_uid=other,
autostart=False,
actor=("user", env["user"]["uid"]),
)
)
assert "consent" in str(refused.value).lower()
def test_run_as_another_member_is_allowed_once_they_consent(env):
from devplacepy.database import set_consent
other = _other_member()
set_consent("user", other, "container_credentials", True)
instance = run_async(
api.create_instance(
env["project"],
name="withconsent",
run_as_uid=other,
autostart=False,
actor=("user", env["user"]["uid"]),
)
)
assert instance["run_as_uid"] == other
def test_run_as_yourself_never_needs_a_consent(env):
instance = run_async(
api.create_instance(
env["project"],
name="selfrunas",
run_as_uid=env["user"]["uid"],
autostart=False,
actor=("user", env["user"]["uid"]),
)
)
assert instance["run_as_uid"] == env["user"]["uid"]
def test_start_on_boot_pass_forces_running(env):
inst = _ready_instance(env, name="boot", start_on_boot=True, autostart=False)
assert inst["desired_state"] == store.DESIRED_STOPPED
+1
View File
@@ -17,6 +17,7 @@ def _seed_dbapi_schema(local_db):
{
"uid": generate_uid(),
"username": "dbapi_seed",
"terms_version": "1",
"email": "seed@dbapi.test",
"role": "Member",
"xp": 0,
+1
View File
@@ -52,6 +52,7 @@ def _make_user_devii_quota(role="Member"):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": generate_uid(),
"role": role,
@@ -20,6 +20,7 @@ def _make_user_at(role, created_at):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"role": role,
"created_at": created_at.isoformat(),
+2
View File
@@ -87,6 +87,7 @@ def test_scheduled_tasks_only_ever_resolve_a_main_channel_session(local_db):
{
"uid": "sess-docs-sched",
"username": "sched-owner",
"terms_version": "1",
"role": "Admin",
"api_key": "k",
"deleted_at": None,
@@ -144,6 +145,7 @@ def _task_session(local_db, uid, role):
{
"uid": uid,
"username": uid,
"terms_version": "1",
"role": role,
"api_key": "k",
"deleted_at": None,
@@ -17,6 +17,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"ctx-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
@@ -33,6 +33,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"guard-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
@@ -16,6 +16,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"lim-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
@@ -28,6 +28,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"sched-{uid[-10:]}",
"terms_version": "1",
"role": role,
"api_key": "k",
"deleted_at": None,
+1
View File
@@ -26,6 +26,7 @@ def _account(local_db, role):
{
"uid": uid,
"username": f"store-{uid[-10:]}",
"terms_version": "1",
"role": role,
"deleted_at": None,
"created_at": SIGNUP_AFTER_PRIMARY_ADMIN,
+1
View File
@@ -17,6 +17,7 @@ def _user(username):
{
"uid": f"uid-{username}",
"username": username,
"terms_version": "1",
"role": "Member",
"created_at": datetime.now(timezone.utc).isoformat(),
}
+1
View File
@@ -98,6 +98,7 @@ def _make_user_issues_gitea():
{
"uid": uid,
"username": username,
"terms_version": "1",
"role": "member",
"xp": 0,
"level": 1,
@@ -96,6 +96,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@t.dev",
"api_key": generate_uid(),
"created_at": datetime.now(timezone.utc).isoformat(),
+1
View File
@@ -13,6 +13,7 @@ def _user_uid():
{
"uid": uid,
"username": f"tkt_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@tkt.dev",
"password_hash": "x",
"role": "Member",
+131
View File
@@ -0,0 +1,131 @@
# retoor <retoor@molodetz.nl>
import pytest
from devplacepy.services.moderation import filter as content_filter
from devplacepy.services.moderation.rules import (
ALWAYS_BLOCK_CATEGORIES,
FILTER_MODES,
RULES,
matches,
score_for,
)
MODE_ORDER = content_filter.MODE_ORDER
VERDICT_ORDER = content_filter.VERDICT_ORDER
DEVELOPER_PROSE = [
"Here is a proof of concept for CVE-2024-1234, the exploit overwrites the stack.",
"The kernel panic dump shows a segmentation fault in the parser.",
"Reverse engineering this malware sample took two evenings of disassembly.",
"I hate this framework and everyone who defends it should read the source.",
"Traceback (most recent call last): RuntimeError: process was killed",
"Our red team ran a penetration test against the staging cluster.",
"This weapon of a regex kills my CPU: (a+)+$",
"The build failed with 'fatal error: cannot execute'.",
]
PROHIBITED_PROSE = [
("i will kill you when i find you", "violence"),
("go kill yourself, nobody wants you here", "harassment"),
("selling stolen credit cards and bank logs here", "illegal"),
("how to build a pipe bomb at home", "weapons"),
]
def test_every_rule_has_a_positive_weight_and_a_known_category():
from devplacepy.database.moderation import REPORT_REASONS
for rule in RULES:
assert rule.weight > 0
assert rule.category in REPORT_REASONS
@pytest.mark.parametrize("text", DEVELOPER_PROSE)
def test_developer_prose_is_never_flagged(text, local_db):
result = content_filter.classify(text, "review")
assert result.verdict == "allow", f"{text!r} -> {result}"
@pytest.mark.parametrize("text,category", PROHIBITED_PROSE)
def test_prohibited_prose_is_flagged_with_its_category(text, category, local_db):
result = content_filter.classify(text, "review")
assert result.flagged
assert category in result.categories
def test_the_always_block_categories_block_in_every_active_mode(local_db):
text = "selling child porn here"
for mode in ("review", "block"):
assert content_filter.classify(text, mode).verdict == "block"
assert content_filter.classify(text, "off").verdict == "allow"
def test_score_is_monotonic_in_the_number_of_matched_rules():
previous = 0
accumulated = ""
for text, _ in PROHIBITED_PROSE:
accumulated = f"{accumulated}\n{text}"
current = score_for(accumulated, matches(accumulated))
assert current >= previous
previous = current
def test_technical_context_only_ever_lowers_the_score():
for text, _ in PROHIBITED_PROSE:
plain = score_for(text, matches(text))
technical = f"{text}\nThis is a malware analysis write-up."
discounted = score_for(technical, matches(technical))
assert discounted <= plain
def test_a_verdict_never_weakens_as_the_mode_strengthens():
samples = DEVELOPER_PROSE + [text for text, _ in PROHIBITED_PROSE]
for text in samples:
previous = -1
for mode in sorted(FILTER_MODES, key=lambda name: MODE_ORDER[name]):
current = VERDICT_ORDER[content_filter.classify(text, mode).verdict]
assert current >= previous, f"{text!r} weakened at mode {mode}"
previous = current
def test_resolve_verdict_is_total_over_every_mode_and_verdict():
for mode in FILTER_MODES:
for verdict in VERDICT_ORDER:
resolved = content_filter.resolve_verdict(verdict, mode)
assert resolved in VERDICT_ORDER
def test_classification_failure_falls_back_to_review(monkeypatch, local_db):
def explode(text):
raise RuntimeError("rule engine unavailable")
monkeypatch.setattr(content_filter, "matches", explode)
result = content_filter.classify("anything at all", "review")
assert result.verdict == "review"
assert result.failed is True
assert result.detail
def test_empty_and_blank_text_is_allowed(local_db):
for text in ("", " ", "\n\t"):
assert content_filter.classify(text, "block").verdict == "allow"
def test_screen_reports_the_worst_field_and_names_every_flagged_one(local_db):
screening = content_filter.screen(
{
"title": "a normal title",
"content": "i will kill you when i find you",
"extra": "buy now 100% free money",
},
"review",
)
assert screening.flagged
assert "content" in screening.fields
assert "extra" in screening.fields
assert "title" not in screening.fields
def test_always_block_categories_are_a_subset_of_the_rule_categories():
assert ALWAYS_BLOCK_CATEGORIES <= {rule.category for rule in RULES}
@@ -26,6 +26,7 @@ def _make_user_ai_usage_profile(role="Member"):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": api_key,
"role": role,
@@ -83,6 +83,7 @@ def _make_admin_openai_gateway(role="Admin"):
{
"uid": generate_uid(),
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"api_key": api_key,
"role": role,
+32 -1
View File
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta, timezone
import uuid_utils
from devplacepy.config import PRESENCE_TIMEOUT_SECONDS, PRESENCE_WRITE_SECONDS
from devplacepy.database import get_table
from devplacepy.database import get_table, set_consent
from devplacepy.services import presence
@@ -57,10 +57,12 @@ def test_touch_writes_once_then_throttles(local_db):
{
"uid": uid,
"username": f"presence_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@presence.test",
"last_seen": None,
}
)
set_consent("user", uid, "activity_recording", True)
presence._last_write.pop(uid, None)
presence.touch(uid)
@@ -182,3 +184,32 @@ def test_online_candidates_tracks_beyond_the_display_limit(local_db):
made.append(uid)
tracked = {u["uid"] for u in presence.online_candidates()}
assert set(made) <= tracked
def test_touch_is_silent_without_activity_recording_consent(local_db):
uid = str(uuid_utils.uuid7())
users = get_table("users")
users.insert(
{
"uid": uid,
"username": f"presence_{uid[:8]}",
"terms_version": "1",
"email": f"{uid[:8]}@presence.test",
"last_seen": None,
}
)
presence._last_write.pop(uid, None)
presence.touch(uid)
assert users.find_one(uid=uid)["last_seen"] is None
set_consent("user", uid, "activity_recording", True)
presence._last_write.pop(uid, None)
presence.touch(uid)
assert users.find_one(uid=uid)["last_seen"]
set_consent("user", uid, "activity_recording", False)
presence._last_write.pop(uid, None)
written = users.find_one(uid=uid)["last_seen"]
presence.touch(uid)
assert users.find_one(uid=uid)["last_seen"] == written
+1
View File
@@ -44,6 +44,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"terms_version": "1",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),
+1
View File
@@ -16,6 +16,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"terms_version": "1",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),
+1
View File
@@ -21,6 +21,7 @@ def _user(prefix):
{
"uid": uid,
"username": f"{prefix}{_counter[0]}",
"terms_version": "1",
"email": f"{prefix}{_counter[0]}@t.dev",
"role": "Member",
"api_key": generate_uid(),
+1
View File
@@ -50,6 +50,7 @@ def _seed_user(username, email, is_active=True, **extra):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": email,
"is_active": is_active,
"created_at": datetime.now(timezone.utc).isoformat(),
+1
View File
@@ -40,6 +40,7 @@ def _seed_user(username, email, is_active=True, **extra):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": email,
"is_active": is_active,
"created_at": datetime.now(timezone.utc).isoformat(),
+2
View File
@@ -30,6 +30,7 @@ def _seed_user(role="Member", xp=0, level=1):
{
"uid": uid,
"username": username,
"terms_version": "1",
"email": f"{username}@t.dev",
"role": role,
"xp": xp,
@@ -167,6 +168,7 @@ def _seed_user_at(role, created_at):
{
"uid": uid,
"username": f"pa_{uid[:8]}",
"terms_version": "1",
"email": f"pa_{uid[:8]}@t.dev",
"role": role,
"created_at": created_at.isoformat(),