chore: reorganize test files into domain-specific subdirectories under tests/

Split the monolithic test directory into three tiers (unit, api, e2e) with a path-mirroring directory structure. Added corresponding Makefile targets (test-unit, test-api, test-e2e) and updated all documentation references (CLAUDE.md, README.md, testing-cicd.html, testing-framework.html, testing-make.html) to reflect the new layout and naming conventions.
This commit is contained in:
2026-06-13 14:32:33 +00:00
parent 014c4c6bb3
commit 43f4011005
268 changed files with 20634 additions and 9206 deletions
View File
+376
View File
@@ -0,0 +1,376 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timedelta, timezone
import pytest
from devplacepy.database import get_table
from devplacepy.services.audit import store, query
from devplacepy.services.audit import record as audit
from devplacepy.services.audit.categories import category_for
from devplacepy.services.audit.record import _sanitize_summary, _actor_from, _coerce_scalar
def _now_audit():
return datetime.now(timezone.utc).isoformat()
@pytest.fixture(autouse=True)
def _tables():
store.ensure_tables()
yield
def _cli_user(role="Member"):
from devplacepy.utils import generate_uid
username = f"auditcli_{generate_uid()[:8]}"
get_table("users").insert(
{
"uid": generate_uid(),
"username": username,
"email": f"{username}@t.dev",
"api_key": generate_uid(),
"role": role,
"is_active": True,
"created_at": _now_audit(),
}
)
return username
def _latest(event_key, **match):
rows = sorted(
get_table(store.AUDIT_TABLE).find(event_key=event_key),
key=lambda r: r.get("created_at", ""),
reverse=True,
)
for row in rows:
if all(row.get(k) == v for k, v in match.items()):
return row
return None
_counter_audit = [0]
def audit_uid_marker():
_counter_audit[0] += 1
return f"{int(datetime.now(timezone.utc).timestamp() * 1000)}_{_counter_audit[0]}"
def test_category_for_known_prefixes():
assert category_for("auth.login.success") == "auth"
assert category_for("vote.post.up") == "engagement"
assert category_for("reaction.add") == "engagement"
assert category_for("file.write.create") == "project_files"
assert category_for("dir.create") == "project_files"
assert category_for("job.zip.complete") == "project_files"
assert category_for("admin.setting.update") == "admin"
assert category_for("container.instance.start") == "container"
assert category_for("devii.turn") == "devii"
assert category_for("cli.role.set") == "cli"
assert category_for("reward.xp.grant") == "reward"
assert category_for("security.authz.denied") == "security"
assert category_for("proxy.access") == "ingress"
assert category_for("ai.gateway.call") == "ai"
def test_category_for_unknown_falls_back_to_first_segment():
assert category_for("totallyunknown.event") == "totallyunknown"
assert category_for("") == "other"
def test_sanitize_summary_strips_html_and_truncates():
assert _sanitize_summary("<b>hi</b> <script>x()</script> there") == "hi x() there"
assert _sanitize_summary(None) is None
assert _sanitize_summary("") is None
long = "a" * 500
out = _sanitize_summary(long)
assert len(out) <= 140
assert out.endswith("...")
def test_actor_from_resolves_role():
assert _actor_from(None)["actor_kind"] == "guest"
assert _actor_from(None)["actor_role"] == "guest"
member = _actor_from({"uid": "u1", "username": "m", "role": "Member"})
assert member["actor_kind"] == "user" and member["actor_role"] == "member"
admin = _actor_from({"uid": "u2", "username": "a", "role": "Admin"})
assert admin["actor_role"] == "admin"
def test_coerce_scalar():
assert _coerce_scalar(None) is None
assert _coerce_scalar(True) == "1"
assert _coerce_scalar(False) == "0"
assert _coerce_scalar(5) == "5"
assert _coerce_scalar("x") == "x"
def test_record_system_inserts_row_with_fields():
key = f"test.unit.basic.{audit_uid_marker()}"
uid = audit.record_system(
key,
actor_kind="user",
actor_uid="actor-1",
actor_username="alice",
actor_role="admin",
target_type="post",
target_uid="post-1",
target_label="Hello",
old_value=0,
new_value=1,
summary="<i>did</i> a thing",
metadata={"k": "v", "n": 3},
result="success",
)
assert uid
row = store.get_event(uid)
assert row["event_key"] == key
assert row["category"] == "test"
assert row["actor_kind"] == "user"
assert row["actor_uid"] == "actor-1"
assert row["actor_role"] == "admin"
assert row["target_type"] == "post"
assert row["old_value"] == "0" and row["new_value"] == "1"
assert row["summary"] == "did a thing"
assert row["result"] == "success"
import json
assert json.loads(row["metadata"]) == {"k": "v", "n": 3}
def test_record_system_auto_appends_actor_link():
uid = audit.record_system(
f"test.unit.actorlink.{audit_uid_marker()}",
actor_kind="user",
actor_uid="actor-2",
actor_username="bob",
)
links = store.get_links(uid)
actor_links = [link for link in links if link["relation"] == "actor"]
assert len(actor_links) == 1
assert actor_links[0]["object_uid"] == "actor-2"
def test_record_system_explicit_links_preserved():
uid = audit.record_system(
f"test.unit.links.{audit_uid_marker()}",
actor_kind="user",
actor_uid="actor-3",
links=[audit.target("project", "proj-1", "P"), audit.author("author-1")],
)
links = store.get_links(uid)
relations = sorted(link["relation"] for link in links)
assert relations == ["actor", "author", "target"]
def test_record_never_raises(monkeypatch):
def boom(_row):
raise RuntimeError("db exploded")
monkeypatch.setattr(store, "insert_event", boom)
# must swallow the error and return None rather than propagate
assert audit.record_system("test.unit.raise", actor_kind="system") is None
assert audit.record(None, "test.unit.raise2", user=None) is None
def test_record_with_none_request_uses_explicit_user():
uid = audit.record(
None,
f"test.unit.norequest.{audit_uid_marker()}",
user={"uid": "u9", "username": "carol", "role": "Member"},
summary="no request object",
)
row = store.get_event(uid)
assert row["actor_uid"] == "u9"
assert row["actor_role"] == "member"
assert row["origin"] == "web"
assert row["request_method"] is None
def test_query_list_events_filters():
marker = f"qfilter{audit_uid_marker()}"
key = f"test.qf.{marker}"
for i in range(3):
audit.record_system(
key,
actor_kind="user",
actor_uid=f"u{i}",
actor_role="member",
result="success" if i < 2 else "failure",
summary=f"event {marker} number {i}",
)
rows, pagination = query.list_events({"event_key": key}, 1)
assert pagination["total"] == 3
rows_fail, pag_fail = query.list_events({"event_key": key, "result": "failure"}, 1)
assert pag_fail["total"] == 1
rows_q, pag_q = query.list_events({"q": f"{marker} number 1"}, 1)
assert pag_q["total"] >= 1
assert all(marker in (r["summary"] or "") for r in rows_q)
def test_query_list_events_pagination():
key = f"test.qpage.{audit_uid_marker()}"
for i in range(30):
audit.record_system(key, actor_kind="system", summary=f"page row {i}")
rows1, pag1 = query.list_events({"event_key": key}, 1)
assert pag1["total"] == 30
assert pag1["total_pages"] == 2
assert len(rows1) == 25
rows2, pag2 = query.list_events({"event_key": key}, 2)
assert len(rows2) == 5
# newest first ordering: page 1 first row is newer than page 2 last row
assert rows1[0]["created_at"] >= rows2[-1]["created_at"]
def test_query_date_range():
key = f"test.qdate.{audit_uid_marker()}"
audit.record_system(key, actor_kind="system", summary="dated")
rows, pag = query.list_events(
{"event_key": key, "date_from": "1999-01-01", "date_to": "2099-12-31"}, 1
)
assert pag["total"] == 1
rows_none, pag_none = query.list_events(
{"event_key": key, "date_to": "2000-01-01"}, 1
)
assert pag_none["total"] == 0
def test_filter_options_returns_distinct():
key = f"test.opts.{audit_uid_marker()}"
audit.record_system(key, actor_kind="cli", actor_role="system", origin="cli")
query._options_cache.clear()
options = query.filter_options()
assert key in options["event_key"]
assert "cli" in options["origin"]
assert "category" in options and "result" in options
def test_get_event_with_links():
uid = audit.record_system(
f"test.unit.detail.{audit_uid_marker()}",
actor_kind="user",
actor_uid="u-detail",
links=[audit.target("gist", "g1", "G")],
)
data = query.get_event_with_links(uid)
assert data is not None
assert data["event"]["uid"] == uid
relations = {link["relation"] for link in data["links"]}
assert "target" in relations and "actor" in relations
assert query.get_event_with_links("nonexistent-uid") is None
def test_sweep_removes_old_rows_and_links():
ancient = "1990-01-01T00:00:00+00:00"
recent_key = f"test.sweep.recent.{audit_uid_marker()}"
old_uids = []
for i in range(3):
uid = store.insert_event(
{
"event_key": f"test.sweep.old.{i}",
"category": "test",
"actor_kind": "system",
"created_at": ancient,
"result": "success",
}
)
store.insert_links(uid, [audit.target("post", f"p{i}")])
old_uids.append(uid)
recent_uid = audit.record_system(recent_key, actor_kind="system")
removed_links, removed_events = store.sweep("2000-01-01T00:00:00+00:00")
assert removed_events >= 3
assert removed_links >= 3
for uid in old_uids:
assert store.get_event(uid) is None
assert store.get_links(uid) == []
# the recent row survives
assert store.get_event(recent_uid) is not None
def test_sweep_zero_cutoff_is_safe():
# a cutoff far in the past removes nothing recent
recent_uid = audit.record_system(
f"test.sweep.keep.{audit_uid_marker()}", actor_kind="system"
)
store.sweep("1970-01-01T00:00:00+00:00")
assert store.get_event(recent_uid) is not None
def test_retention_service_prunes_old_rows():
from tests.conftest import run_async
from devplacepy.database import set_setting
from devplacepy.services.audit import AuditService
ancient = "1991-01-01T00:00:00+00:00"
old_uid = store.insert_event(
{
"event_key": f"test.retention.old.{audit_uid_marker()}",
"category": "test",
"actor_kind": "system",
"created_at": ancient,
"result": "success",
}
)
recent_uid = audit.record_system(
f"test.retention.recent.{audit_uid_marker()}", actor_kind="system"
)
set_setting("audit_log_retention_days", "30")
run_async(AuditService().run_once())
assert store.get_event(old_uid) is None
assert store.get_event(recent_uid) is not None
def test_retention_service_disabled_when_zero():
from tests.conftest import run_async
from devplacepy.database import set_setting
from devplacepy.services.audit import AuditService
ancient_uid = store.insert_event(
{
"event_key": f"test.retention.keep.{audit_uid_marker()}",
"category": "test",
"actor_kind": "system",
"created_at": "1992-01-01T00:00:00+00:00",
"result": "success",
}
)
set_setting("audit_log_retention_days", "0")
try:
run_async(AuditService().run_once())
assert store.get_event(ancient_uid) is not None
finally:
set_setting("audit_log_retention_days", "90")
def test_cli_role_set_recorded(local_db):
import argparse
from devplacepy import cli
username = _cli_user(role="Member")
uid = get_table("users").find_one(username=username)["uid"]
cli.cmd_role_set(argparse.Namespace(username=username, role="admin"))
event = _latest("cli.role.set", target_uid=uid)
assert event is not None
assert event["actor_kind"] == "cli"
assert event["origin"] == "cli"
import json
meta = json.loads(event["metadata"])
assert meta["old"] == "Member" and meta["new"] == "Admin"
links = store.get_links(event["uid"])
assert any(link["object_uid"] == uid for link in links)
def test_cli_apikey_reset_recorded(local_db):
import argparse
from devplacepy import cli
username = _cli_user()
uid = get_table("users").find_one(username=username)["uid"]
cli.cmd_apikey_reset(argparse.Namespace(username=username))
event = _latest("cli.apikey.reset", target_uid=uid)
assert event is not None
# the new key value is never written into the audit row
new_key = get_table("users").find_one(uid=uid)["api_key"]
assert new_key not in (event.get("summary") or "")
assert new_key not in (event.get("metadata") or "")
def test_cli_apikey_backfill_recorded(local_db):
import argparse
from devplacepy import cli
cli.cmd_apikey_backfill(argparse.Namespace())
event = _latest("cli.apikey.backfill")
assert event is not None
assert event["actor_kind"] == "cli"
assert (event.get("metadata") or "").find("count") != -1
+190
View File
@@ -0,0 +1,190 @@
# retoor <retoor@molodetz.nl>
from datetime import timedelta
import pytest
import requests
from tests.conftest import BASE_URL, run_async
from devplacepy.database import db, get_table, init_db, refresh_snapshot
from devplacepy import config, project_files
from devplacepy.services.containers import api, store, runtime
from devplacepy.services.containers.backend.base import Mount, PortMapping, RunSpec
from devplacepy.services.containers.backend.docker_cli import build_run_argv, parse_size
from devplacepy.services.containers.backend.fake import FakeBackend
from devplacepy.services.containers.service import ContainerService
from devplacepy.services.containers.api import INSTANCE_LABEL
from devplacepy.services.devii.tasks.schedule import Schedule, now_utc
_CONTAINER_TABLES = (
"instances",
"instance_events",
"instance_metrics",
"instance_schedules",
)
@pytest.fixture(autouse=True)
def _init_db_containers():
init_db()
yield
@pytest.fixture
def env(tmp_path, monkeypatch):
fake = FakeBackend()
runtime.set_backend(fake)
monkeypatch.setattr("devplacepy.config.CONTAINER_WORKSPACES_DIR", tmp_path / "ws")
pid = "ctest-p1"
project = {"uid": pid, "slug": "ctest", "title": "C", "user_uid": "ctest-u1"}
user = {"uid": "ctest-u1", "username": "ctestadmin"}
project_files.write_text_file(pid, user, "app.py", "print(1)\n")
yield {"fake": fake, "project": project, "user": user}
runtime.set_backend(None)
for table in _CONTAINER_TABLES:
if table in db.tables:
for row in [r for r in get_table(table).find()]:
if str(row.get("project_uid", "")).startswith("ctest"):
get_table(table).delete(uid=row["uid"])
for row in list(get_table("project_files").find()):
if str(row.get("project_uid", "")).startswith("ctest"):
get_table("project_files").delete(uid=row["uid"])
def _ready_instance(env, **kwargs):
return run_async(
api.create_instance(env["project"], name=kwargs.pop("name", "inst"), **kwargs)
)
def _promote_admin(username: str) -> None:
users = get_table("users")
user = users.find_one(username=username)
if user:
users.update({"uid": user["uid"], "role": "Admin"}, ["uid"])
def _api_key(username: str) -> str:
refresh_snapshot()
return get_table("users").find_one(username=username)["api_key"]
def test_build_run_argv_exact():
spec = RunSpec(
image="ppy:latest",
name="inst",
labels={INSTANCE_LABEL: "u1"},
env={"A": "B"},
cpu_limit="1.5",
mem_limit="512m",
ports=[PortMapping(8080, 80)],
mounts=[Mount("/ws", "/app")],
restart_policy="on-failure",
command=["python", "app.py"],
)
argv = build_run_argv(spec)
assert argv[:5] == ["docker", "run", "-d", "--name", "inst"]
assert "--label" in argv and f"{INSTANCE_LABEL}=u1" in argv
assert "--cpus" in argv and "1.5" in argv
assert "-p" in argv and "8080:80/tcp" in argv
assert "-v" in argv and "/ws:/app:rw" in argv
assert "--restart" in argv and "on-failure" in argv
assert argv[-3:] == ["ppy:latest", "python", "app.py"]
def test_never_policy_not_passed_to_docker():
spec = RunSpec(image="ppy:latest", name="n", restart_policy="never")
assert "--restart" not in build_run_argv(spec)
def test_parse_size():
assert parse_size("1.0GiB") == 1024**3
assert parse_size("512MB") == 512 * 1024**2
def test_create_instance_uses_shared_image(env):
inst = run_async(
api.create_instance(
env["project"], name="inst", actor=("user", env["user"]["uid"])
)
)
assert inst["name"] == "inst"
assert inst["owner_uid"] == "ctest-u1"
spec = api.run_spec_for(inst, config.CONTAINER_IMAGE)
assert spec.image == config.CONTAINER_IMAGE
assert any(m.container == "/app" for m in spec.mounts)
def test_create_instance_requires_built_image(env):
async def no_image(ref):
return False
env["fake"].image_exists = no_image
with pytest.raises(api.ContainerError):
run_async(api.create_instance(env["project"], name="inst"))
def test_reconcile_launches_and_stops(env):
inst = _ready_instance(env, restart_policy="never", autostart=True)
assert inst["desired_state"] == store.DESIRED_RUNNING
service = ContainerService()
run_async(service.run_once())
refresh_snapshot()
inst = store.get_instance(inst["uid"])
assert inst["status"] == store.ST_RUNNING and inst["container_id"]
assert [r.name for r in run_async(env["fake"].ps())] == [inst["slug"]]
api.set_desired_state(inst, store.DESIRED_STOPPED)
run_async(service.run_once())
refresh_snapshot()
assert store.get_instance(inst["uid"])["status"] == store.ST_STOPPED
def test_reconcile_reaps_orphan(env):
fake = env["fake"]
run_async(
fake.run(
RunSpec(
image="ppy:latest", name="ghost", labels={INSTANCE_LABEL: "missing-uid"}
)
)
)
service = ContainerService()
run_async(service.run_once())
assert not run_async(fake.ps())
assert fake.removed
def test_reconcile_removes_marked_instance(env):
inst = _ready_instance(env, autostart=True)
service = ContainerService()
run_async(service.run_once())
refresh_snapshot()
inst = store.get_instance(inst["uid"])
api.mark_for_removal(inst)
run_async(service.run_once())
refresh_snapshot()
assert store.get_instance(inst["uid"]) is None
assert not run_async(env["fake"].ps())
def test_schedule_fires(env):
inst = _ready_instance(env, autostart=False)
assert inst["desired_state"] == store.DESIRED_STOPPED
past = Schedule(kind="once", run_at=now_utc() - timedelta(hours=1))
api.add_schedule(inst, "start", past)
service = ContainerService()
run_async(service._fire_schedules())
refresh_snapshot()
assert store.get_instance(inst["uid"])["desired_state"] == store.DESIRED_RUNNING
def test_ingress_validation(env):
from devplacepy.services.containers.backend.base import PortMapping
assert api.validate_ingress("zwoeks", 8899, [PortMapping(8899, 8899)]) == (
"zwoeks",
8899,
)
assert api.validate_ingress("", None, []) == ("", 0)
with pytest.raises(api.ContainerError):
api.validate_ingress("BAD SLUG", None, [PortMapping(80, 80)])
with pytest.raises(api.ContainerError):
api.validate_ingress("x", 9999, [PortMapping(80, 80)])
store.create_instance(
{
"uid": "z",
"project_uid": "ctest-p1",
"name": "z",
"ports_json": "[]",
"ingress_slug": "taken",
}
)
with pytest.raises(api.ContainerError):
api.validate_ingress("taken", None, [PortMapping(80, 80)])
+56
View File
@@ -0,0 +1,56 @@
# retoor <retoor@molodetz.nl>
import json
from devplacepy.services.devii.agentic.loop import _run_tool_call
from tests.conftest import run_async
class _FakeDispatcher:
def __init__(self):
self.calls = []
async def dispatch(self, name, arguments):
self.calls.append((name, arguments))
return json.dumps({"status": "ok"})
def _run(call, dispatcher=None):
return json.loads(run_async(_run_tool_call(dispatcher, call)))
def test_truncated_arguments_reported_not_dispatched():
dispatcher = _FakeDispatcher()
call = {
"function": {
"name": "project_write_file",
"arguments": '{"path":"a.md","content":"# hi',
}
}
out = _run(call, dispatcher)
assert out["error"] == "tool_input_truncated"
assert "one write tool call per turn" in out["message"]
assert dispatcher.calls == []
def test_non_object_arguments_rejected():
dispatcher = _FakeDispatcher()
out = _run({"function": {"name": "x", "arguments": '"a string"'}}, dispatcher)
assert out["error"] == "tool_input_error"
assert dispatcher.calls == []
def test_valid_string_arguments_dispatched():
dispatcher = _FakeDispatcher()
out = _run({"function": {"name": "vote", "arguments": '{"value":1}'}}, dispatcher)
assert out["status"] == "ok"
assert dispatcher.calls == [("vote", {"value": 1})]
def test_valid_dict_arguments_dispatched():
dispatcher = _FakeDispatcher()
out = _run({"function": {"name": "vote", "arguments": {"value": -1}}}, dispatcher)
assert out["status"] == "ok"
assert dispatcher.calls == [("vote", {"value": -1})]
def test_missing_arguments_defaults_to_empty_object():
dispatcher = _FakeDispatcher()
out = _run({"function": {"name": "auth_status"}}, dispatcher)
assert out["status"] == "ok"
assert dispatcher.calls == [("auth_status", {})]
+177
View File
@@ -0,0 +1,177 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone
from types import SimpleNamespace
from tests.conftest import BASE_URL
from devplacepy.database import get_table, set_setting
from devplacepy.utils import generate_uid
from devplacepy.services.devii.config import (
effective_daily_limit,
FIELD_USER_DAILY_USD,
FIELD_GUEST_DAILY_USD,
FIELD_ADMIN_DAILY_USD,
)
from devplacepy.services.devii.store import UsageLedger
LEDGER = "devii_usage_ledger"
def _now_iso_devii_quota():
return datetime.now(timezone.utc).isoformat()
def _seed_ledger(owner_kind, owner_id, cost, n=1):
table = get_table(LEDGER)
for _ in range(n):
table.insert(
{
"owner_kind": owner_kind,
"owner_id": owner_id,
"created_at": _now_iso_devii_quota(),
"cost_usd": cost,
}
)
def _ensure_devii():
from devplacepy.services.manager import service_manager
from devplacepy.services.devii import DeviiService
if service_manager.get_service("devii") is None:
service_manager.register(DeviiService())
return service_manager.get_service("devii")
def _make_user_devii_quota(role="Member"):
uid = generate_uid()
username = f"quota_{uid[-12:]}"
get_table("users").insert(
{
"uid": uid,
"username": username,
"email": f"{username}@t.dev",
"api_key": generate_uid(),
"role": role,
"is_active": True,
}
)
return uid, username
def test_effective_limit_resolver():
cfg = {
FIELD_USER_DAILY_USD: 1.0,
FIELD_GUEST_DAILY_USD: 0.05,
FIELD_ADMIN_DAILY_USD: 0.0,
}
assert effective_daily_limit(cfg, "user", False) == 1.0
assert effective_daily_limit(cfg, "guest", False) == 0.05
assert effective_daily_limit(cfg, "user", True) == 0.0
cfg[FIELD_ADMIN_DAILY_USD] = 5.0
assert effective_daily_limit(cfg, "user", True) == 5.0
def test_admin_exempt_by_default(local_db):
from devplacepy.routers.profile import _ai_quota
_ensure_devii()
set_setting("devii_user_daily_usd", "1.0")
set_setting("devii_admin_daily_usd", "0.0")
try:
uid, _ = _make_user_devii_quota()
_seed_ledger("user", uid, 5.0)
member_view = _ai_quota(uid, is_admin=False)
admin_view = _ai_quota(uid, is_admin=True)
assert member_view["used_pct"] == 100.0
assert admin_view["used_pct"] == 0.0
finally:
set_setting("devii_user_daily_usd", "1.0")
set_setting("devii_admin_daily_usd", "0.0")
def test_admin_limit_is_zero_by_default(local_db):
svc = _ensure_devii()
set_setting("devii_admin_daily_usd", "0.0")
try:
assert svc.daily_limit_for("user", True) == 0.0
assert svc.daily_limit_for("user", False) == 1.0
finally:
set_setting("devii_admin_daily_usd", "0.0")
def test_admin_cap_configurable(local_db):
from devplacepy.routers.profile import _ai_quota
_ensure_devii()
set_setting("devii_admin_daily_usd", "2.0")
try:
uid, _ = _make_user_devii_quota()
_seed_ledger("user", uid, 1.0)
quota = _ai_quota(uid, is_admin=True)
assert quota["used_pct"] == 50.0
finally:
set_setting("devii_admin_daily_usd", "0.0")
def test_guest_cap_configurable(local_db):
svc = _ensure_devii()
set_setting("devii_guest_daily_usd", "0.10")
try:
assert svc.daily_limit_for("guest") == 0.10
finally:
set_setting("devii_guest_daily_usd", "0.05")
def test_reset_single_owner(local_db):
ledger = UsageLedger()
uid, _ = _make_user_devii_quota()
_seed_ledger("user", uid, 0.5, n=3)
assert ledger.spent_24h("user", uid) == 1.5
removed = ledger.reset("user", uid)
assert removed == 3
assert ledger.spent_24h("user", uid) == 0.0
def test_reset_guests_only_leaves_users(local_db):
ledger = UsageLedger()
guest_id = f"guest-{generate_uid()[:8]}"
user_id, _ = _make_user_devii_quota()
_seed_ledger("guest", guest_id, 0.02, n=2)
_seed_ledger("user", user_id, 0.5, n=1)
ledger.reset_owner_kind("guest")
assert ledger.spent_24h("guest", guest_id) == 0.0
assert ledger.spent_24h("user", user_id) == 0.5
def test_reset_all(local_db):
ledger = UsageLedger()
guest_id = f"guest-{generate_uid()[:8]}"
user_id, _ = _make_user_devii_quota()
_seed_ledger("guest", guest_id, 0.02, n=2)
_seed_ledger("user", user_id, 0.5, n=2)
ledger.reset_all()
assert ledger.spent_24h("guest", guest_id) == 0.0
assert ledger.spent_24h("user", user_id) == 0.0
def test_cli_reset_quota_for_user(local_db):
from devplacepy.cli import cmd_devii_reset_quota
uid, username = _make_user_devii_quota()
_seed_ledger("user", uid, 0.5, n=2)
cmd_devii_reset_quota(SimpleNamespace(username=username, guests=False, all=False))
assert get_table(LEDGER).count(owner_kind="user", owner_id=uid) == 0
def test_cli_reset_quota_guests(local_db):
from devplacepy.cli import cmd_devii_reset_quota
guest_id = f"guest-{generate_uid()[:8]}"
user_id, _ = _make_user_devii_quota()
_seed_ledger("guest", guest_id, 0.02, n=2)
_seed_ledger("user", user_id, 0.5, n=1)
cmd_devii_reset_quota(SimpleNamespace(username=None, guests=True, all=False))
assert get_table(LEDGER).count(owner_kind="guest", owner_id=guest_id) == 0
assert get_table(LEDGER).count(owner_kind="user", owner_id=user_id) == 1
def test_cli_reset_quota_all(local_db):
from devplacepy.cli import cmd_devii_reset_quota
guest_id = f"guest-{generate_uid()[:8]}"
user_id, _ = _make_user_devii_quota()
_seed_ledger("guest", guest_id, 0.02, n=1)
_seed_ledger("user", user_id, 0.5, n=1)
cmd_devii_reset_quota(SimpleNamespace(username=None, guests=False, all=True))
assert get_table(LEDGER).count() == 0
+230
View File
@@ -0,0 +1,230 @@
# retoor <retoor@molodetz.nl>
import asyncio
import pytest
from devplacepy.database import (
get_table,
init_db,
refresh_snapshot,
set_setting,
)
from devplacepy.services.gitea import runtime, store
from devplacepy.services.gitea.config import gitea_config
from devplacepy.services.gitea.enhance import enhance_ticket
from devplacepy.services.gitea.fake import FakeGiteaClient
from devplacepy.services.gitea.service import BugTrackerService
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.bug_create_service import BugCreateService
from tests.conftest import run_async
_counter_bugs_gitea = [0]
@pytest.fixture(autouse=True)
def _init_db_bugs_gitea():
init_db()
yield
@pytest.fixture
def gitea_env():
fake = FakeGiteaClient()
runtime.set_client(fake)
set_setting("gitea_base_url", "https://gitea.test")
set_setting("gitea_owner", "retoor")
set_setting("gitea_repo", "pydevplace")
set_setting("gitea_token", "test-token")
set_setting("bug_ai_enhance", "0")
yield fake
runtime.set_client(None)
for table in ("bug_tickets", "bug_comment_authors"):
for row in list(get_table(table).find()):
get_table(table).delete(uid=row["uid"])
for row in list(get_table("jobs").find(kind="bug_create")):
get_table("jobs").delete(uid=row["uid"])
def _make_user_bugs_gitea():
_counter_bugs_gitea[0] += 1
uid = f"bugtest-user-{_counter_bugs_gitea[0]}"
username = f"bugtester{_counter_bugs_gitea[0]}"
get_table("users").insert(
{
"uid": uid,
"username": username,
"role": "member",
"xp": 0,
"level": 1,
"is_active": True,
}
)
return uid, username
def _unread(user_uid):
return [
n
for n in get_table("notifications").find(user_uid=user_uid)
if n.get("type") == "bug"
]
def _drive_jobs():
async def run():
svc = BugCreateService()
for _ in range(200):
await svc.run_once()
refresh_snapshot()
pending = [
r
for r in get_table("jobs").find(kind="bug_create")
if r["status"] in ("pending", "running")
]
if not pending and not svc._inflight:
return
await asyncio.sleep(0.02)
run_async(run())
def _role_user(role: str):
uid, username = _make_user_bugs_gitea()
get_table("users").update({"uid": uid, "role": role}, ["uid"])
return uid, username
def _open_ticket(fake, role: str):
import devplacepy.main as m
from starlette.testclient import TestClient
from devplacepy.utils import create_session
uid, _ = _role_user(role)
issue = run_async(fake.create_issue("Submit does nothing", "details"))
number = int(issue["number"])
store.record_ticket(
number=number,
author_uid=uid,
original_title="Submit does nothing",
original_description="details",
enhanced_title="Submit does nothing",
html_url=issue.get("html_url", ""),
status="open",
)
client = TestClient(m.app)
client.cookies.set("session", create_session(uid))
return number, client
def test_fake_client_create_and_list(gitea_env):
async def go():
issue = await gitea_env.create_issue("Crash on save", "details")
assert issue["number"] == 1
assert issue["state"] == "open"
issues, total = await gitea_env.list_issues(state="open")
assert total == 1
assert issues[0]["number"] == 1
run_async(go())
def test_fake_client_state_and_comments(gitea_env):
async def go():
issue = await gitea_env.create_issue("t", "b")
number = issue["number"]
await gitea_env.create_comment(number, "first")
fetched = await gitea_env.get_issue(number)
assert fetched["comments"] == 1
closed = await gitea_env.set_state(number, "closed")
assert closed["state"] == "closed"
open_issues, _ = await gitea_env.list_issues(state="open")
assert open_issues == []
all_issues, total = await gitea_env.list_issues(state="all")
assert total == 1
run_async(go())
def test_store_records_and_maps(gitea_env):
uid, _ = _make_user_bugs_gitea()
store.record_ticket(7, uid, "orig", "desc", "Better", "https://x/7", "open")
assert store.author_uid_for_issue(7) == uid
assert store.author_map([7, 8]) == {7: uid}
cid = store.record_comment_author(99, 7, uid)
assert cid
assert store.comment_author_map([99, 100]) == {99: uid}
assert store.local_comment_ids(7) == {99}
def test_enhance_fallback_when_disabled(gitea_env):
result = run_async(enhance_ticket("Title", "Body text", gitea_config()))
assert result.enhanced is False
assert "## Summary" in result.body
assert "## Steps to Reproduce" in result.body
assert result.title == "Title"
def test_create_job_files_issue(gitea_env):
uid, username = _make_user_bugs_gitea()
job_uid = queue.enqueue(
"bug_create",
{"author_uid": uid, "title": "Login broken", "description": "cannot log in"},
"user",
uid,
"Login broken",
)
_drive_jobs()
job = queue.get_job(job_uid)
assert job["status"] == "done"
number = job["result"]["number"]
assert number == 1
ticket = store.get_ticket(number)
assert ticket["author_uid"] == uid
assert _unread(uid)
issue = run_async(gitea_env.get_issue(number))
assert username in issue["body"]
def test_create_job_fails_without_config(gitea_env):
set_setting("gitea_token", "")
uid, _ = _make_user_bugs_gitea()
job_uid = queue.enqueue(
"bug_create",
{"author_uid": uid, "title": "x", "description": "y"},
"user",
uid,
"x",
)
_drive_jobs()
job = queue.get_job(job_uid)
assert job["status"] == "failed"
assert "not configured" in job["error"].lower()
def test_poller_notifies_on_developer_reply(gitea_env):
uid, _ = _make_user_bugs_gitea()
issue = run_async(gitea_env.create_issue("bug", "body"))
number = issue["number"]
store.record_ticket(number, uid, "bug", "body", "bug", issue["html_url"], "open")
store.update_ticket_cache(number, "open", 0)
gitea_env.add_external_comment(number, "developer", "looking into it")
before = len(_unread(uid))
run_async(BugTrackerService().run_once())
refresh_snapshot()
assert len(_unread(uid)) == before + 1
assert int(store.get_ticket(number)["last_comment_count"]) == 1
def test_poller_ignores_local_comment(gitea_env):
uid, _ = _make_user_bugs_gitea()
issue = run_async(gitea_env.create_issue("bug", "body"))
number = issue["number"]
store.record_ticket(number, uid, "bug", "body", "bug", issue["html_url"], "open")
store.update_ticket_cache(number, "open", 0)
comment = run_async(gitea_env.create_comment(number, "me again"))
store.record_comment_author(comment["id"], number, uid)
before = len(_unread(uid))
run_async(BugTrackerService().run_once())
refresh_snapshot()
assert len(_unread(uid)) == before
def test_poller_notifies_on_status_change(gitea_env):
uid, _ = _make_user_bugs_gitea()
issue = run_async(gitea_env.create_issue("bug", "body"))
number = issue["number"]
store.record_ticket(number, uid, "bug", "body", "bug", issue["html_url"], "open")
store.update_ticket_cache(number, "open", 0)
run_async(gitea_env.set_state(number, "closed"))
before = len(_unread(uid))
run_async(BugTrackerService().run_once())
refresh_snapshot()
assert len(_unread(uid)) == before + 1
assert store.get_ticket(number)["last_status"] == "closed"
+212
View File
@@ -0,0 +1,212 @@
# retoor <retoor@molodetz.nl>
import asyncio
from datetime import datetime, timezone
from pathlib import Path
import pytest
from devplacepy.database import init_db, get_table, refresh_snapshot
from devplacepy import project_files
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.fork_service import ForkService
from tests.conftest import run_async
@pytest.fixture(autouse=True)
def _init_db_fork_jobs():
init_db()
yield
@pytest.fixture
def fork_env(tmp_path, monkeypatch):
monkeypatch.setattr(
"devplacepy.services.jobs.fork_service.STAGING_DIR", tmp_path / "staging"
)
monkeypatch.setattr("devplacepy.project_files.PROJECT_FILES_DIR", tmp_path / "pf")
yield tmp_path
jobs = get_table("jobs")
for row in list(jobs.find(kind="fork")):
jobs.delete(uid=row["uid"])
projects = get_table("projects")
files = get_table("project_files")
for project in list(projects.find()):
if str(project.get("user_uid", "")).startswith("forktest-owner"):
for node in list(files.find(project_uid=project["uid"])):
files.delete(uid=node["uid"])
projects.delete(uid=project["uid"])
for node in list(files.find()):
if str(node.get("project_uid", "")).startswith("forktest"):
files.delete(uid=node["uid"])
forks = get_table("project_forks")
for relation in list(forks.find()):
if str(relation.get("forked_by_uid", "")).startswith("forktest-owner"):
forks.delete(uid=relation["uid"])
users = get_table("users")
for user in list(users.find()):
if str(user.get("uid", "")).startswith("forktest-owner"):
users.delete(uid=user["uid"])
_counter_fork_jobs = [0]
def _make_source_project(*, is_private=False, binary=False):
_counter_fork_jobs[0] += 1
pid = f"forktest-{_counter_fork_jobs[0]}"
owner_uid = f"forktest-owner-{_counter_fork_jobs[0]}"
user = {"uid": owner_uid, "username": f"forktester{_counter_fork_jobs[0]}"}
get_table("users").insert(
{"uid": owner_uid, "username": user["username"], "xp": 0, "level": 1}
)
get_table("projects").insert(
{
"uid": pid,
"user_uid": owner_uid,
"slug": f"{pid}-source",
"title": "Source Project",
"description": "the original",
"project_type": "software",
"platforms": "linux",
"status": "Released",
"is_private": 1 if is_private else 0,
"read_only": 0,
"stars": 0,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
project_files.write_text_file(pid, user, "README.md", "# hello\nworld")
project_files.write_text_file(pid, user, "src/app.py", "print(1)\n")
if binary:
project_files.store_upload(pid, user, "assets", "logo.bin", bytes(range(256)))
return pid, owner_uid
def _tree(directory):
root = Path(directory)
out = {}
for path in sorted(root.rglob("*")):
if path.is_file():
out[path.relative_to(root).as_posix()] = path.read_bytes()
return out
def _process_fork_jobs():
async def drive():
svc = ForkService()
for _ in range(400):
await svc.run_once()
refresh_snapshot()
pending = [
r
for r in get_table("jobs").find(kind="fork")
if r["status"] in ("pending", "running")
]
if not pending and not svc._inflight:
return
await asyncio.sleep(0.05)
run_async(drive())
def _enqueue(source_uid, owner_uid, title="My Fork"):
return queue.enqueue(
"fork",
{"source_project_uid": source_uid, "title": title, "forked_by_uid": owner_uid},
"user",
owner_uid,
title,
)
def test_enqueue_creates_pending_job(fork_env):
uid = _enqueue("p", "forktest-owner-x")
job = queue.get_job(uid)
assert job["status"] == "pending"
assert job["kind"] == "fork"
assert job["payload"]["source_project_uid"] == "p"
def test_process_creates_fork_with_files(fork_env, tmp_path):
pid, owner_uid = _make_source_project(binary=True)
uid = _enqueue(pid, owner_uid, title="Forked Copy")
_process_fork_jobs()
job = queue.get_job(uid)
assert job["status"] == "done"
result = job["result"]
new_uid = result["project_uid"]
forked = get_table("projects").find_one(uid=new_uid)
assert forked is not None
assert forked["user_uid"] == owner_uid
assert forked["title"] == "Forked Copy"
assert result["project_url"] == f"/projects/{forked['slug']}"
source_dir = tmp_path / "exp_source"
fork_dir = tmp_path / "exp_fork"
project_files.export_to_dir(pid, "", source_dir)
project_files.export_to_dir(new_uid, "", fork_dir)
assert _tree(fork_dir) == _tree(source_dir)
def test_fork_relation_direction(fork_env):
pid, owner_uid = _make_source_project()
uid = _enqueue(pid, owner_uid)
_process_fork_jobs()
new_uid = queue.get_job(uid)["result"]["project_uid"]
relation = get_table("project_forks").find_one(forked_project_uid=new_uid)
assert relation is not None
assert relation["source_project_uid"] == pid
assert relation["forked_project_uid"] == new_uid
assert relation["forked_by_uid"] == owner_uid
def test_fork_copies_binary_file(fork_env, tmp_path):
pid, owner_uid = _make_source_project(binary=True)
uid = _enqueue(pid, owner_uid)
_process_fork_jobs()
new_uid = queue.get_job(uid)["result"]["project_uid"]
fork_dir = tmp_path / "binfork"
project_files.export_to_dir(new_uid, "", fork_dir)
assert (fork_dir / "assets" / "logo.bin").read_bytes() == bytes(range(256))
def test_fork_preserves_private_flag(fork_env):
pid, owner_uid = _make_source_project(is_private=True)
uid = _enqueue(pid, owner_uid)
_process_fork_jobs()
new_uid = queue.get_job(uid)["result"]["project_uid"]
assert get_table("projects").find_one(uid=new_uid)["is_private"] == 1
def test_missing_source_fails_job_without_orphan(fork_env):
before = {p["uid"] for p in get_table("projects").find()}
uid = _enqueue("does-not-exist", "forktest-owner-missing")
_process_fork_jobs()
job = queue.get_job(uid)
assert job["status"] == "failed"
assert "source project not found" in job["error"].lower()
after = {p["uid"] for p in get_table("projects").find()}
assert before == after
def test_cleanup_keeps_project(fork_env):
pid, owner_uid = _make_source_project()
uid = _enqueue(pid, owner_uid)
_process_fork_jobs()
job = queue.get_job(uid)
new_uid = job["result"]["project_uid"]
ForkService().cleanup(job)
assert get_table("projects").find_one(uid=new_uid) is not None
def test_retention_sweep_keeps_project(fork_env):
pid, owner_uid = _make_source_project()
uid = _enqueue(pid, owner_uid)
_process_fork_jobs()
new_uid = queue.get_job(uid)["result"]["project_uid"]
get_table("jobs").update(
{"uid": uid, "expires_at": "2000-01-01T00:00:00+00:00"}, ["uid"]
)
svc = ForkService()
run_async(svc.run_once())
refresh_snapshot()
assert queue.get_job(uid) is None
assert get_table("projects").find_one(uid=new_uid) is not None
def test_orphan_running_recovered_on_enable(fork_env):
uid = _enqueue("p", "forktest-owner-o")
get_table("jobs").update(
{"uid": uid, "status": "running", "started_at": "2020-01-01T00:00:00+00:00"},
["uid"],
)
svc = ForkService()
run_async(svc.on_enable())
job = queue.get_job(uid)
assert job["status"] == "pending"
assert job["retry_count"] == 1
+401
View File
@@ -0,0 +1,401 @@
# retoor <retoor@molodetz.nl>
import asyncio
import zipfile
import zlib
from pathlib import Path
import pytest
from devplacepy.database import init_db, get_table, refresh_snapshot
from devplacepy import project_files
from devplacepy.project_files import ProjectFileError
from devplacepy.services.jobs import queue
from devplacepy.services.jobs.zip_service import ZipService
from devplacepy.services.jobs import zip_worker
from tests.conftest import run_async
@pytest.fixture(autouse=True)
def _init_db_zip_jobs():
init_db()
yield
@pytest.fixture
def zip_env(tmp_path, monkeypatch):
monkeypatch.setattr(
"devplacepy.services.jobs.zip_service.ZIPS_DIR", tmp_path / "zips"
)
monkeypatch.setattr(
"devplacepy.services.jobs.zip_service.STAGING_DIR", tmp_path / "staging"
)
monkeypatch.setattr("devplacepy.project_files.PROJECT_FILES_DIR", tmp_path / "pf")
yield tmp_path
jobs = get_table("jobs")
for row in list(jobs.find(kind="zip")):
jobs.delete(uid=row["uid"])
files = get_table("project_files")
for row in list(files.find()):
if str(row.get("project_uid", "")).startswith("ziptest"):
files.delete(uid=row["uid"])
_pid_counter = [0]
def _make_project(text=None, binary=False):
_pid_counter[0] += 1
pid = f"ziptest-{_pid_counter[0]}"
user = {"uid": f"ziptest-owner-{_pid_counter[0]}"}
project_files.write_text_file(pid, user, "README.md", "# hello\nworld")
project_files.write_text_file(pid, user, "src/app.py", "print(1)\n")
if binary:
project_files.store_upload(pid, user, "assets", "logo.bin", bytes(range(256)))
return pid, user
def _process_zip_jobs():
async def drive():
svc = ZipService()
for _ in range(400):
await svc.run_once()
refresh_snapshot()
pending = [
r
for r in get_table("jobs").find(kind="zip")
if r["status"] in ("pending", "running")
]
if not pending and not svc._inflight:
return
await asyncio.sleep(0.05)
run_async(drive())
def test_enqueue_creates_pending_job(zip_env):
uid = queue.enqueue("zip", {"a": 1}, "user", "u1", "Name")
job = queue.get_job(uid)
assert job["status"] == "pending"
assert job["kind"] == "zip"
assert job["payload"] == {"a": 1}
assert job["result"] == {}
assert job["owner_kind"] == "user" and job["owner_id"] == "u1"
assert job["created_at"] and job["expires_at"] == ""
def test_get_job_missing_returns_none(zip_env):
assert queue.get_job("does-not-exist") is None
def test_touch_job_extends_expiry(zip_env):
uid = queue.enqueue("zip", {}, "guest", "g1", "x")
queue.touch_job(uid, 3600)
job = queue.get_job(uid)
assert job["last_accessed_at"] and job["expires_at"]
def test_list_jobs_filters(zip_env):
a = queue.enqueue("zip", {}, "user", "owner-A", "a")
queue.enqueue("zip", {}, "user", "owner-B", "b")
only_a = queue.list_jobs(kind="zip", owner=("user", "owner-A"))
assert [j["uid"] for j in only_a] == [a]
def test_zip_worker_stats_and_crc(tmp_path):
src = tmp_path / "src"
(src / "sub").mkdir(parents=True)
(src / "a.txt").write_text("hello")
(src / "sub" / "b.txt").write_text("world")
out = tmp_path / "out.zip"
stats = zip_worker._build(str(src), str(out))
assert stats["file_count"] == 2
assert stats["dir_count"] == 1
assert stats["bytes_in"] == 10
assert stats["bytes_out"] == out.stat().st_size
expected_crc = 0
with open(out, "rb") as handle:
for chunk in iter(lambda: handle.read(65536), b""):
expected_crc = zlib.crc32(chunk, expected_crc)
assert stats["crc32"] == (expected_crc & 0xFFFFFFFF)
assert sorted(zipfile.ZipFile(out).namelist()) == ["a.txt", "sub/", "sub/b.txt"]
def test_process_whole_project(zip_env):
pid, _ = _make_project(binary=True)
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": ""}},
"user",
"u",
"Proj",
)
_process_zip_jobs()
job = queue.get_job(uid)
assert job["status"] == "done"
assert job["duration_ms"] >= 0
assert job["expires_at"]
result = job["result"]
names = sorted(zipfile.ZipFile(result["local_path"]).namelist())
assert names == ["README.md", "assets/", "assets/logo.bin", "src/", "src/app.py"]
assert result["file_count"] == 3 and result["dir_count"] == 2
assert job["bytes_out"] == Path(result["local_path"]).stat().st_size
def test_process_subtree_folder(zip_env):
pid, _ = _make_project()
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": "src"}},
"user",
"u",
"src",
)
_process_zip_jobs()
result = queue.get_job(uid)["result"]
assert sorted(zipfile.ZipFile(result["local_path"]).namelist()) == [
"src/",
"src/app.py",
]
def test_process_single_file(zip_env):
pid, _ = _make_project()
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": "src/app.py"}},
"user",
"u",
"app.py",
)
_process_zip_jobs()
result = queue.get_job(uid)["result"]
assert zipfile.ZipFile(result["local_path"]).namelist() == ["app.py"]
def test_final_name_format_and_strip_zip(zip_env):
pid, _ = _make_project()
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": ""}},
"user",
"u",
"My Project.ZIP",
)
_process_zip_jobs()
result = queue.get_job(uid)["result"]
name = result["final_name"]
assert name.endswith(".my-project.zip")
crc_part = name.split(".", 1)[0]
assert len(crc_part) == 8 and int(crc_part, 16) >= 0
assert crc_part == result["crc32"]
def test_blank_preferred_name_defaults(zip_env):
pid, _ = _make_project()
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": ""}},
"user",
"u",
"",
)
_process_zip_jobs()
assert queue.get_job(uid)["result"]["final_name"].endswith(".download.zip")
def test_identical_content_replaces_same_name(zip_env):
pid, _ = _make_project()
payload = {"source": {"type": "project_tree", "project_uid": pid, "path": ""}}
uid1 = queue.enqueue("zip", payload, "user", "u", "thing")
_process_zip_jobs()
name1 = queue.get_job(uid1)["result"]["final_name"]
uid2 = queue.enqueue("zip", payload, "user", "u", "thing")
_process_zip_jobs()
name2 = queue.get_job(uid2)["result"]["final_name"]
assert name1 == name2
def test_cleanup_removes_artifact(zip_env):
pid, _ = _make_project()
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": ""}},
"user",
"u",
"thing",
)
_process_zip_jobs()
job = queue.get_job(uid)
path = Path(job["result"]["local_path"])
assert path.is_file()
ZipService().cleanup(job)
assert not path.exists()
def test_unsupported_source_fails_job(zip_env):
uid = queue.enqueue("zip", {"source": {"type": "evil"}}, "user", "u", "x")
_process_zip_jobs()
job = queue.get_job(uid)
assert job["status"] == "failed"
assert "unsupported" in job["error"].lower()
def test_normalize_path_rejects_parent(zip_env):
with pytest.raises(ProjectFileError):
project_files.normalize_path("a/../../b")
with pytest.raises(ProjectFileError):
project_files.normalize_path("../etc/passwd")
def test_export_blocks_malicious_db_path(zip_env, tmp_path):
pid = "ziptest-evil"
get_table("project_files").insert(
{
"uid": "evil-node",
"project_uid": pid,
"user_uid": "u",
"path": "../escape.txt",
"name": "escape.txt",
"parent_path": "",
"type": "file",
"content": "pwned",
"is_binary": 0,
"stored_name": None,
"directory": None,
"mime_type": "text/plain",
"size": 5,
"created_at": "x",
"updated_at": "x",
}
)
dest = tmp_path / "dest"
with pytest.raises(ProjectFileError):
project_files.export_to_dir(pid, "", dest)
assert not (tmp_path / "escape.txt").exists()
def test_traversal_payload_marks_job_failed(zip_env):
pid = "ziptest-evil2"
get_table("project_files").insert(
{
"uid": "evil-node2",
"project_uid": pid,
"user_uid": "u",
"path": "../../escape2.txt",
"name": "escape2.txt",
"parent_path": "",
"type": "file",
"content": "pwned",
"is_binary": 0,
"stored_name": None,
"directory": None,
"mime_type": "text/plain",
"size": 6,
"created_at": "x",
"updated_at": "x",
}
)
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": pid, "path": ""}},
"user",
"u",
"evil",
)
_process_zip_jobs()
assert queue.get_job(uid)["status"] == "failed"
def test_orphan_running_recovered_on_enable(zip_env):
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": "p", "path": ""}},
"user",
"u",
"n",
)
get_table("jobs").update(
{"uid": uid, "status": "running", "started_at": "2020-01-01T00:00:00+00:00"},
["uid"],
)
svc = ZipService()
run_async(svc.on_enable())
job = queue.get_job(uid)
assert job["status"] == "pending"
assert job["retry_count"] == 1
def test_orphan_exceeds_retry_limit_fails(zip_env):
uid = queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": "p", "path": ""}},
"user",
"u",
"n",
)
get_table("jobs").update(
{
"uid": uid,
"status": "running",
"retry_count": 3,
"started_at": "2020-01-01T00:00:00+00:00",
},
["uid"],
)
svc = ZipService()
run_async(svc.on_enable())
assert queue.get_job(uid)["status"] == "failed"
def test_retention_sweep_deletes_expired(zip_env, tmp_path):
artifact = tmp_path / "old.zip"
artifact.write_bytes(b"PK\x05\x06" + b"\x00" * 18)
uid = queue.enqueue("zip", {}, "user", "u", "old")
get_table("jobs").update(
{
"uid": uid,
"status": "done",
"result": '{"local_path": "%s"}' % artifact.as_posix(),
"expires_at": "2000-01-01T00:00:00+00:00",
},
["uid"],
)
svc = ZipService()
run_async(svc.run_once())
refresh_snapshot()
assert queue.get_job(uid) is None
assert not artifact.exists()
def test_retention_keeps_unexpired(zip_env, tmp_path):
uid = queue.enqueue("zip", {}, "user", "u", "fresh")
get_table("jobs").update(
{
"uid": uid,
"status": "done",
"result": "{}",
"expires_at": "2999-01-01T00:00:00+00:00",
},
["uid"],
)
svc = ZipService()
run_async(svc.run_once())
refresh_snapshot()
assert queue.get_job(uid) is not None
def test_max_concurrent_caps_inflight(zip_env, monkeypatch):
for _ in range(4):
queue.enqueue(
"zip",
{"source": {"type": "project_tree", "project_uid": "p", "path": ""}},
"user",
"u",
"n",
)
async def fake_process(self, job):
await asyncio.sleep(0.2)
return {}
monkeypatch.setattr(ZipService, "process", fake_process)
monkeypatch.setattr(ZipService, "max_concurrent", lambda self: 2)
async def check():
svc = ZipService()
svc._refill()
count = len(svc._inflight)
for entry in svc._inflight.values():
entry["task"].cancel()
return count
assert run_async(check()) == 2
+267
View File
@@ -0,0 +1,267 @@
# retoor <retoor@molodetz.nl>
import httpx
from devplacepy.services import news as news_mod
from devplacepy.services import base as base_mod
from devplacepy.services.news import (
NewsService,
_extract_grade,
_get_ai_key,
_get_article_images,
)
from devplacepy.database import get_table
from devplacepy.utils import generate_uid
from tests.conftest import run_async
API_URL = "http://news.test/api"
AI_URL = "http://ai.test/v1/chat"
LINK_HIGH = "http://news.test/high"
LINK_LOW = "http://news.test/low"
class FakeResp_news_service:
def __init__(self, json_data=None, text="", status=200):
self._json = json_data
self.text = text
self.status_code = status
def raise_for_status(self):
if self.status_code >= 400:
raise httpx.HTTPError(f"status {self.status_code}")
def json(self):
return self._json
class FakeClient_news_service:
def __init__(self, articles):
self.articles = articles
async def __aenter__(self):
return self
async def __aexit__(self, *exc):
return False
async def get(self, url, timeout=None):
if url == API_URL:
return FakeResp_news_service(json_data={"articles": self.articles})
return FakeResp_news_service(text='<img src="http://img.test/a.png">')
async def post(self, url, json=None, headers=None, timeout=None):
prompt = json["messages"][0]["content"]
if "FailArticle" in prompt:
return FakeResp_news_service(status=500, text="err")
if "EmptyArticle" in prompt:
return FakeResp_news_service(json_data={"choices": [{"message": {"content": ""}}]})
if "BadArticle" in prompt:
return FakeResp_news_service(
json_data={"choices": [{"message": {"content": "no number"}}]}
)
grade = "9" if "HighArticle" in prompt else "3"
return FakeResp_news_service(json_data={"choices": [{"message": {"content": grade}}]})
class FailingApiClient(FakeClient_news_service):
async def get(self, url, timeout=None):
if url == API_URL:
raise httpx.HTTPError("api down")
return FakeResp_news_service(text="")
def _settings_stub(threshold="7"):
def fake_get_setting(key, default=None):
return {
"news_api_url": API_URL,
"news_ai_url": AI_URL,
"news_ai_model": "test-model",
"news_grade_threshold": threshold,
"news_ai_key": "",
}.get(key, default)
return fake_get_setting
def test_extract_grade_parsing():
assert _extract_grade("8") == 8
assert _extract_grade("Grade: 9") == 9
assert _extract_grade("Score is 7/10") == 7
assert _extract_grade("0") is None
assert _extract_grade("11") is None
assert _extract_grade("not a number") is None
def test_get_ai_key_env_precedence(monkeypatch):
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
monkeypatch.setenv("NEWS_AI_KEY", "primary")
assert _get_ai_key() == "primary"
def test_get_ai_key_setting_fallback(monkeypatch):
monkeypatch.delenv("NEWS_AI_KEY", raising=False)
monkeypatch.setattr(
news_mod,
"get_setting",
lambda key, default=None: "from-setting" if key == "news_ai_key" else default,
)
assert _get_ai_key() == "from-setting"
def test_get_ai_key_internal_gateway_fallback(local_db, monkeypatch):
from devplacepy.database import internal_gateway_key
monkeypatch.delenv("NEWS_AI_KEY", raising=False)
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
assert _get_ai_key() == internal_gateway_key()
def test_grade_article_empty_content_returns_none(local_db, monkeypatch):
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
grade = run_async(
NewsService()._grade_article(
{"title": "EmptyArticle", "description": "d", "content": "c"},
AI_URL,
"m",
FakeClient_news_service([]),
)
)
assert grade is None
def test_grade_article_unparseable_returns_none(local_db, monkeypatch):
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
grade = run_async(
NewsService()._grade_article(
{"title": "BadArticle", "description": "d", "content": "c"},
AI_URL,
"m",
FakeClient_news_service([]),
)
)
assert grade is None
def test_run_once_handles_api_failure(local_db, monkeypatch):
monkeypatch.setattr(news_mod, "get_setting", _settings_stub())
monkeypatch.setattr(base_mod, "get_setting", _settings_stub())
monkeypatch.setattr(
news_mod.httpx, "AsyncClient", lambda *a, **k: FailingApiClient([])
)
run_async(NewsService().run_once())
def test_run_once_updates_existing_news_row(local_db, monkeypatch):
external_id = f"news-{generate_uid()}"
existing_uid = generate_uid()
get_table("news").insert(
{
"uid": existing_uid,
"external_id": external_id,
"slug": "",
"title": "Old Title",
"status": "draft",
"grade": 0,
"synced_at": "2020-01-01",
}
)
articles = [
{
"guid": external_id,
"title": "HighArticle",
"description": "d",
"content": "c",
"link": "",
"feed_name": "Feed",
"author": "A",
"published": "2026-01-01",
}
]
monkeypatch.setattr(news_mod, "get_setting", _settings_stub(threshold="7"))
monkeypatch.setattr(base_mod, "get_setting", _settings_stub(threshold="7"))
monkeypatch.setattr(
news_mod.httpx, "AsyncClient", lambda *a, **k: FakeClient_news_service(articles)
)
run_async(NewsService().run_once())
row = get_table("news").find_one(uid=existing_uid)
assert row["title"] == "HighArticle"
assert row["status"] == "published"
assert get_table("news").count(external_id=external_id) == 1
def test_get_article_images_filters_and_dedupes():
html = (
'<img src="http://img.test/a.png">'
"<img src='http://img.test/a.png'>"
'<img src="http://img.test/b.svg">'
'<img src="/relative.png">'
)
client = FakeClient_news_service([])
async def fake_get(url, timeout=None):
return FakeResp_news_service(text=html)
client.get = fake_get
images = run_async(_get_article_images("http://news.test/page", client))
assert [img["url"] for img in images] == ["http://img.test/a.png"]
def test_run_once_publishes_grades_and_is_idempotent(local_db, monkeypatch):
g_high, g_low, g_fail = (f"news-{generate_uid()}" for _ in range(3))
articles = [
{
"guid": g_high,
"title": "HighArticle",
"description": "d",
"content": "c",
"link": LINK_HIGH,
"feed_name": "Feed",
"author": "A",
"published": "2026-01-01",
},
{
"guid": g_low,
"title": "LowArticle",
"description": "d",
"content": "c",
"link": LINK_LOW,
"feed_name": "Feed",
"author": "A",
"published": "2026-01-01",
},
{
"guid": g_fail,
"title": "FailArticle",
"description": "d",
"content": "c",
"link": "",
"feed_name": "Feed",
"author": "A",
"published": "2026-01-01",
},
{
"guid": "",
"title": "NoGuid",
"description": "d",
"content": "c",
"link": "",
"feed_name": "Feed",
"author": "A",
"published": "2026-01-01",
},
]
monkeypatch.setattr(news_mod, "get_setting", _settings_stub(threshold="7"))
monkeypatch.setattr(base_mod, "get_setting", _settings_stub(threshold="7"))
monkeypatch.setattr(
news_mod.httpx, "AsyncClient", lambda *a, **k: FakeClient_news_service(articles)
)
run_async(NewsService().run_once())
news = get_table("news")
sync = get_table("news_sync")
assert news.find_one(external_id=g_high)["status"] == "published"
assert news.find_one(external_id=g_high)["grade"] == 9
assert news.find_one(external_id=g_low)["status"] == "draft"
assert news.find_one(external_id=g_low)["grade"] == 3
fail_row = news.find_one(external_id=g_fail)
assert fail_row["status"] == "draft"
assert fail_row["grade"] == 0
assert sync.find_one(external_id=g_fail)["status"] == "grading_failed"
assert sync.find_one(external_id=g_high)["status"] == "graded"
run_async(NewsService().run_once())
assert news.count(external_id=g_high) == 1
assert news.count(external_id=g_fail) == 1
@@ -0,0 +1,177 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone
from starlette.requests import Request
from tests.conftest import BASE_URL
from devplacepy.database import get_table, set_setting
from devplacepy.utils import generate_uid
from devplacepy.services.openai_gateway import GatewayService
from devplacepy.services.openai_gateway.analytics import build_user_usage
from devplacepy.services.devii.config import (
build_settings,
FIELD_AI_URL,
FIELD_AI_MODEL,
FIELD_AI_KEY,
FIELD_MAX_ITERATIONS,
FIELD_PLAN_REQUIRED,
FIELD_VERIFY_REQUIRED,
)
def _now_iso_ai_usage_profile():
return datetime.now(timezone.utc).isoformat()
def _make_user_ai_usage_profile(role="Member"):
username = f"aiu_{generate_uid()[:8]}"
api_key = generate_uid()
uid = generate_uid()
get_table("users").insert(
{
"uid": uid,
"username": username,
"email": f"{username}@t.dev",
"api_key": api_key,
"role": role,
"is_active": True,
}
)
return uid, username, api_key
def _make_request_ai_usage_profile(headers):
raw = [(k.lower().encode(), v.encode()) for k, v in headers.items()]
return Request(
{
"type": "http",
"method": "POST",
"path": "/openai/v1/chat/completions",
"query_string": b"",
"headers": raw,
"state": {},
}
)
def _seed_gateway(uid, n=3, cost=0.01, kind="user"):
table = get_table("gateway_usage_ledger")
for _ in range(n):
table.insert(
{
"created_at": _now_iso_ai_usage_profile(),
"owner_kind": kind,
"owner_id": uid,
"backend": "chat",
"model": "molodetz",
"success": 1,
"status_code": 200,
"prompt_tokens": 1000,
"completion_tokens": 300,
"total_tokens": 1300,
"upstream_latency_ms": 1500.0,
"tokens_per_second": 40.0,
"cost_usd": cost,
}
)
def _devii_cfg():
return {
FIELD_AI_URL: "",
FIELD_AI_MODEL: "",
FIELD_AI_KEY: "INTERNAL-KEY",
FIELD_MAX_ITERATIONS: "40",
FIELD_PLAN_REQUIRED: "1",
FIELD_VERIFY_REQUIRED: "1",
}
def _goto_profile(page, username):
page.goto(f"{BASE_URL}/profile/{username}", wait_until="domcontentloaded")
def test_devii_user_session_uses_own_api_key(local_db):
cfg = _devii_cfg()
user_settings = build_settings(cfg, "http://x", "USER-KEY-123", "user")
guest_settings = build_settings(cfg, "http://x", "", "guest")
assert user_settings.ai_key == "USER-KEY-123"
assert user_settings.platform_api_key == "USER-KEY-123"
assert guest_settings.ai_key == "INTERNAL-KEY"
def test_gateway_allows_users_by_default(local_db):
field = next(
f for f in GatewayService().config_fields if f.key == "gateway_allow_users"
)
assert field.default is True
def test_gateway_attributes_calls_to_the_user(local_db):
member_uid, _, member_key = _make_user_ai_usage_profile(role="Member")
admin_uid, _, admin_key = _make_user_ai_usage_profile(role="Admin")
svc = GatewayService()
assert svc.resolve_owner(
_make_request_ai_usage_profile({"Authorization": f"Bearer {member_key}"})
) == ("user", member_uid)
assert svc.resolve_owner(_make_request_ai_usage_profile({"X-API-KEY": admin_key})) == (
"admin",
admin_uid,
)
def test_build_user_usage_aggregates(local_db):
uid, _, _ = _make_user_ai_usage_profile()
_seed_gateway(uid, n=4, cost=0.02)
out = build_user_usage(uid, hours=24)
assert out["owner_id"] == uid
assert out["requests"] == 4
assert out["success"] == 4
assert out["success_pct"] == 100.0
assert round(out["cost"]["window_usd"], 4) == 0.08
assert out["cost"]["projected_30d_usd"] == round(0.08 * 30, 2)
assert out["tokens"]["total"] == 4 * 1300
assert any(m["key"] == "molodetz" for m in out["by_model"])
assert out["hourly"]
def test_build_user_usage_isolates_users(local_db):
mine, _, _ = _make_user_ai_usage_profile()
other, _, _ = _make_user_ai_usage_profile()
_seed_gateway(mine, n=2, cost=0.05)
_seed_gateway(other, n=9, cost=0.05)
out = build_user_usage(mine, hours=24)
assert out["requests"] == 2
def test_build_user_usage_empty_for_unknown(local_db):
out = build_user_usage("no-such-uid-xyz", hours=24)
assert out["requests"] == 0
assert out["cost"]["projected_30d_usd"] == 0.0
assert "projection" in out["notes"]
def test_ai_quota_percentage_and_clamp(local_db):
from devplacepy.services.manager import service_manager
from devplacepy.services.devii import DeviiService
from devplacepy.routers.profile import _ai_quota
if service_manager.get_service("devii") is None:
service_manager.register(DeviiService())
set_setting("devii_user_daily_usd", "2.0")
try:
half, _, _ = _make_user_ai_usage_profile()
get_table("devii_usage_ledger").insert(
{
"owner_kind": "user",
"owner_id": half,
"created_at": _now_iso_ai_usage_profile(),
"cost_usd": 1.0,
}
)
quota = _ai_quota(half)
assert quota is not None
assert quota["used_pct"] == 50.0
assert quota["turns"] == 1
over, _, _ = _make_user_ai_usage_profile()
for _ in range(3):
get_table("devii_usage_ledger").insert(
{
"owner_kind": "user",
"owner_id": over,
"created_at": _now_iso_ai_usage_profile(),
"cost_usd": 1.0,
}
)
capped = _ai_quota(over)
assert capped["used_pct"] == 100.0
finally:
set_setting("devii_user_daily_usd", "1.0")
@@ -0,0 +1,204 @@
# retoor <retoor@molodetz.nl>
import json
import requests
from starlette.requests import Request
from tests.conftest import BASE_URL, run_async
from devplacepy.database import get_table, set_setting
from devplacepy.utils import generate_uid
import devplacepy.services.openai_gateway.gateway as gwmod
from devplacepy.services.openai_gateway import GatewayService
class FakeResp_openai_gateway:
def __init__(self, status=200, payload=None, ctype="application/json", content=b""):
self.status_code = status
self._payload = payload
self.text = json.dumps(payload) if payload is not None else ""
self.headers = {"content-type": ctype}
self.content = content
def json(self):
if self._payload is None:
raise ValueError("no json")
return self._payload
class FakeRequest:
def __init__(self, method, url, json_body):
self.method = method
self.url = url
self.json_body = json_body
self.extensions = {}
class FakeClient_openai_gateway:
def __init__(self, *a, **k):
self.calls = []
def build_request(self, method, url, headers=None, json=None, content=None):
return FakeRequest(method, url, json)
async def send(self, request):
self.calls.append((request.url, request.json_body))
body = request.json_body or {}
return FakeResp_openai_gateway(
payload={
"id": "x",
"model": body.get("model"),
"choices": [{"message": {"content": "hi there"}}],
}
)
async def post(self, url, headers=None, json=None, timeout=None):
self.calls.append((url, json))
return FakeResp_openai_gateway(
payload={
"id": "x",
"model": json.get("model"),
"choices": [{"message": {"content": "hi there"}}],
}
)
async def request(self, method, url, headers=None, content=None):
return FakeResp_openai_gateway(payload={"ok": True})
async def aclose(self):
pass
def _make_request_openai_gateway(headers=None, cookies=None):
headers = headers or {}
raw = [(k.lower().encode(), v.encode()) for k, v in headers.items()]
if cookies:
raw.append(
(b"cookie", "; ".join(f"{k}={v}" for k, v in cookies.items()).encode())
)
return Request(
{
"type": "http",
"method": "POST",
"path": "/openai/v1/chat/completions",
"query_string": b"",
"headers": raw,
"state": {},
}
)
def _make_admin_openai_gateway(role="Admin"):
username = f"gw_{generate_uid()[:8]}"
api_key = generate_uid()
get_table("users").insert(
{
"uid": generate_uid(),
"username": username,
"email": f"{username}@t.dev",
"api_key": api_key,
"role": role,
"is_active": True,
}
)
return username, api_key
def _config(page, **fields):
page.request.post(f"{BASE_URL}/admin/services/openai/config", form=fields)
def test_model_is_forced(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_force_model"] = True
cfg["gateway_model"] = "deepseek-chat"
rt = svc.runtime()
run_async(
rt.handle_chat(
{"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]},
cfg,
("guest", "test"),
"test",
)
)
assert rt._client.calls[-1][1]["model"] == "deepseek-chat"
def test_vision_rewrites_image_to_text(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_vision_enabled"] = True
cfg["gateway_vision_key"] = "" # no key -> placeholder text, still rewrites to str
rt = svc.runtime()
msgs = [
{
"role": "user",
"content": [
{"type": "text", "text": "what is this"},
{"type": "image_url", "image_url": {"url": "http://x/y.png"}},
],
}
]
run_async(rt.handle_chat({"messages": msgs}, cfg, ("guest", "test"), "test"))
sent = rt._client.calls[-1][1]["messages"][0]["content"]
assert isinstance(sent, str) and "vision" in sent.lower()
def test_streaming_emits_sse(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
rt = svc.runtime()
resp = run_async(
rt.handle_chat(
{"messages": [{"role": "user", "content": "hi"}], "stream": True},
cfg,
("guest", "test"),
"test",
)
)
async def drain():
out = []
async for chunk in resp.body_iterator:
out.append(chunk if isinstance(chunk, str) else chunk.decode())
return "".join(out)
body = run_async(drain())
assert "chat.completion.chunk" in body
assert "[DONE]" in body
def test_authorize_static_access_key(local_db):
set_setting("gateway_require_auth", "1")
set_setting("gateway_access_key", "topsecret")
try:
svc = GatewayService()
assert svc.authorize(_make_request_openai_gateway(headers={"X-API-KEY": "topsecret"})) is True
assert svc.authorize(_make_request_openai_gateway(headers={"X-API-KEY": "wrong"})) is False
finally:
set_setting("gateway_access_key", "")
set_setting("gateway_require_auth", "1")
def test_authorize_admin_and_user_toggles(local_db):
set_setting("gateway_require_auth", "1")
set_setting("gateway_access_key", "")
set_setting("gateway_allow_admins", "1")
set_setting("gateway_allow_users", "0")
try:
_, admin_key = _make_admin_openai_gateway(role="Admin")
_, member_key = _make_admin_openai_gateway(role="Member")
svc = GatewayService()
assert (
svc.authorize(_make_request_openai_gateway(headers={"Authorization": f"Bearer {admin_key}"}))
is True
)
assert svc.authorize(_make_request_openai_gateway(headers={"X-API-KEY": member_key})) is False
set_setting("gateway_allow_users", "1")
assert svc.authorize(_make_request_openai_gateway(headers={"X-API-KEY": member_key})) is True
finally:
set_setting("gateway_allow_admins", "1")
set_setting("gateway_allow_users", "1")
set_setting("gateway_require_auth", "1")
set_setting("gateway_access_key", "")
def test_authorize_require_auth_off_is_open(local_db):
set_setting("gateway_require_auth", "0")
try:
svc = GatewayService()
assert svc.authorize(_make_request_openai_gateway()) is True
finally:
set_setting("gateway_require_auth", "1")