This commit is contained in:
2026-07-19 18:57:43 +02:00
parent 48bb6c2ec2
commit c53e2a3319
179 changed files with 9307 additions and 897 deletions
-334
View File
@@ -1,334 +0,0 @@
# 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.utils import generate_uid
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"}
get_table("users").insert(
{
"uid": user["uid"],
"username": user["username"],
"email": "ctestadmin@example.com",
"api_key": generate_uid(),
"password_hash": "",
"role": "Member",
"is_active": True,
"level": 1,
"xp": 0,
"stars": 0,
"deleted_at": None,
"deleted_by": None,
}
)
refresh_snapshot()
project_files.write_text_file(pid, user, "app.py", "print(1)\n")
yield {"fake": fake, "project": project, "user": user}
runtime.set_backend(None)
get_table("users").delete(uid=user["uid"])
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_manual_start_relaunches_after_never_policy_exit(env):
fake = env["fake"]
inst = _ready_instance(env, restart_policy="never", autostart=True)
service = ContainerService()
run_async(service.run_once())
refresh_snapshot()
inst = store.get_instance(inst["uid"])
cid = inst["container_id"]
assert inst["status"] == store.ST_RUNNING and cid
fake._set_state(cid, "exited", 0)
run_async(service.run_once())
refresh_snapshot()
inst = store.get_instance(inst["uid"])
assert inst["status"] == store.ST_STOPPED
assert inst["desired_state"] == store.DESIRED_STOPPED
api.set_desired_state(inst, store.DESIRED_RUNNING)
refresh_snapshot()
run_async(service.run_once())
refresh_snapshot()
inst = store.get_instance(inst["uid"])
assert inst["status"] == store.ST_RUNNING
assert inst["desired_state"] == store.DESIRED_RUNNING
assert cid in fake.removed
assert inst["container_id"] and inst["container_id"] != cid
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)])
def test_validate_boot_languages():
assert api.validate_boot("none", "ignored") == ("none", "")
assert api.validate_boot("python", "print(1)") == ("python", "print(1)")
with pytest.raises(api.ContainerError):
api.validate_boot("ruby", "puts 1")
with pytest.raises(api.ContainerError):
api.validate_boot("bash", " ")
def test_boot_script_precedence_in_run_spec(env):
inst = _ready_instance(
env,
boot_language="python",
boot_script="print('boot')\n",
boot_command="python other.py",
autostart=False,
)
spec = api.run_spec_for(inst, config.CONTAINER_IMAGE)
assert spec.command == ["python", "/app/.devplace_boot.py"]
inst2 = _ready_instance(
env, name="i2", boot_command="python serve.py", autostart=False
)
spec2 = api.run_spec_for(inst2, config.CONTAINER_IMAGE)
assert spec2.command == ["/bin/sh", "-c", "python serve.py"]
def test_materialize_boot_script_writes_file(env, tmp_path):
workspace = tmp_path / "boot-ws"
inst = _ready_instance(
env, name="boot", boot_language="bash", boot_script="echo hi\n", autostart=False
)
store.update_instance(inst["uid"], {"workspace_dir": str(workspace)})
inst = store.get_instance(inst["uid"])
api.materialize_boot_script(inst)
written = workspace / ".devplace_boot.sh"
assert written.is_file()
assert written.read_text() == "echo hi\n"
def test_run_as_uid_drives_pravda_env(env):
_promote_admin(env["user"]["username"])
key = _api_key(env["user"]["username"])
inst = _ready_instance(
env, name="runas", run_as_uid=env["user"]["uid"], autostart=False
)
pravda = api.pravda_env(inst)
assert pravda["PRAVDA_API_KEY"] == key
assert pravda["PRAVDA_USER_UID"] == env["user"]["uid"]
def test_run_as_uid_rejects_unknown_user(env):
with pytest.raises(api.ContainerError):
run_async(
api.create_instance(
env["project"], name="bad", run_as_uid="nope-uid", autostart=False
)
)
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
service = ContainerService()
service._boot_pass(store.all_instances())
refresh_snapshot()
assert store.get_instance(inst["uid"])["desired_state"] == store.DESIRED_RUNNING
def test_status_change_records_event(env):
inst = _ready_instance(env, name="hist", restart_policy="never", autostart=True)
service = ContainerService()
run_async(service.run_once())
refresh_snapshot()
inst = store.get_instance(inst["uid"])
api.set_desired_state(inst, store.DESIRED_STOPPED)
run_async(service.run_once())
refresh_snapshot()
events = [e["event"] for e in store.list_events(inst["uid"])]
assert "status_change" in events
def test_bidirectional_sync_newer_wins(env, tmp_path):
workspace = tmp_path / "sync-ws"
workspace.mkdir()
pid = env["project"]["uid"]
user = env["user"]
project_files.write_text_file(pid, user, "shared.txt", "from project\n")
fs_only = workspace / "fromfs.txt"
fs_only.write_text("from fs\n")
counts = project_files.sync_dir_bidirectional(pid, str(workspace), user)
assert counts["exported"] >= 1
assert counts["imported"] >= 1
assert (workspace / "shared.txt").read_text() == "from project\n"
imported = project_files.read_file(pid, "fromfs.txt")
assert imported["content"] == "from fs\n"
@@ -48,6 +48,20 @@ def test_seo_report_is_public_read_only_http_action():
assert "uid" in names
def test_ui_prompt_is_local_interaction_action():
from devplacepy.services.devii.registry import CATALOG
by_name = CATALOG.by_name()
action = by_name["ui_prompt"]
assert action.handler == "interaction"
assert action.requires_auth is False
names = {p.name for p in action.params}
assert "title" in names
assert "widgets" in names
assert "ui_cancel" in by_name
assert "ui_notify" in by_name
def test_block_mute_tools_exist_as_http_actions():
expected = {
"block_user": "/block/{username}",
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
@@ -0,0 +1,48 @@
# retoor <retoor@molodetz.nl>
from tests.conftest import run_async
from devplacepy.services.devii.interaction.broker import InteractionBroker
def test_answer_text_completes_open_prompt():
captured = {}
async def wait_site(interaction_id, frame, timeout):
captured["id"] = interaction_id
outcome = broker.answer_text("13/01/1990")
assert outcome["handled"] is True
assert outcome["status"] == "submitted"
return outcome["result"]
broker = InteractionBroker("main", wait_site=wait_site)
result = run_async(
broker.prompt(
{
"title": "When?",
"widgets": [{"type": "date", "name": "day", "label": "Day"}],
}
)
)
assert result.status == "submitted"
assert result.values == {"day": "13/01/1990"}
assert result.meta.get("via") == "text"
def test_answer_text_rejects_free_chat_as_date():
async def wait_site(interaction_id, frame, timeout):
bad = broker.answer_text("Show me number input then.")
assert bad["status"] == "error"
good = broker.answer_text("13/01/1990")
return good["result"]
broker = InteractionBroker("main", wait_site=wait_site)
result = run_async(
broker.prompt(
{
"title": "When?",
"widgets": [{"type": "date", "name": "day", "label": "Day"}],
}
)
)
assert result.status == "submitted"
assert result.values == {"day": "13/01/1990"}
@@ -0,0 +1,93 @@
# retoor <retoor@molodetz.nl>
import asyncio
from tests.conftest import run_async
from devplacepy.services.devii.interaction.broker import InteractionBroker
def test_broker_site_prompt_returns_submitted_values():
async def wait_site(interaction_id, frame, timeout):
assert frame["title"] == "Deploy?"
assert frame["widgets"][0]["type"] == "confirm"
return {
"status": "submitted",
"interaction_id": interaction_id,
"values": {"delete": True},
}
broker = InteractionBroker("main", wait_site=wait_site)
async def run():
return await broker.prompt(
{
"title": "Deploy?",
"widgets": [
{
"type": "confirm",
"name": "delete",
"label": "Delete",
"default": False,
}
],
}
)
result = run_async(run())
assert result.status == "submitted"
assert result.values == {"delete": True}
assert result.meta["adapter"] == "site-chat"
assert result.meta["degraded"] is False
assert broker.open_id() is None
def test_broker_plain_adapter_parses_reply():
async def present_plain(menu: str) -> str:
assert "yes" in menu.lower() or "Delete" in menu
return "yes"
broker = InteractionBroker("cli", present_plain=present_plain)
result = run_async(
broker.prompt(
{
"title": "Delete?",
"widgets": [
{"type": "confirm", "name": "ok", "label": "OK", "default": False}
],
}
)
)
assert result.status == "submitted"
assert result.values == {"ok": True}
assert result.meta["degraded"] is True
def test_broker_invalid_args_raise():
from devplacepy.services.devii.errors import ToolInputError
broker = InteractionBroker("main")
try:
run_async(broker.prompt({"title": "x", "widgets": []}))
assert False, "expected ToolInputError"
except ToolInputError:
pass
def test_broker_timeout_status():
async def wait_site(interaction_id, frame, timeout):
raise asyncio.TimeoutError()
broker = InteractionBroker("main", wait_site=wait_site)
result = run_async(
broker.prompt(
{
"title": "T?",
"timeout_sec": 1,
"widgets": [
{"type": "confirm", "name": "ok", "label": "OK"}
],
}
)
)
assert result.status == "timeout"
assert result.values == {}
@@ -0,0 +1,61 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.interaction.capabilities import (
CHANNEL_CLI,
CHANNEL_SITE_CHAT,
CHANNEL_TELEGRAM,
CHANNEL_UNKNOWN,
channel_id_for_session,
context_for,
fragment_for,
interactions_enabled,
)
def test_session_channel_mapping():
assert channel_id_for_session("main") == CHANNEL_SITE_CHAT
assert channel_id_for_session("docs") == CHANNEL_SITE_CHAT
assert channel_id_for_session("telegram") == CHANNEL_TELEGRAM
assert channel_id_for_session("cli") == CHANNEL_CLI
assert channel_id_for_session("nope") == CHANNEL_UNKNOWN
def test_site_context_exposes_ui_prompt(local_db):
ctx = context_for(CHANNEL_SITE_CHAT, owner_kind="user", owner_id="u1")
assert ctx.capabilities["rich_widgets"] is True
assert "ui_prompt" in ctx.tools
assert "ui_cancel" in ctx.tools
assert "ui_notify" in ctx.tools
assert "interactions_get" in ctx.tools
assert "interactions_set" in ctx.tools
def test_open_interaction_gates_to_cancel_only(local_db):
ctx = context_for(
CHANNEL_SITE_CHAT,
open_interaction_id="ix-abc",
owner_kind="user",
owner_id="u1",
)
assert "ui_cancel" in ctx.tools
assert "ui_prompt" not in ctx.tools
assert "interactions_get" in ctx.tools
def test_unknown_channel_has_no_ui_prompt(local_db):
ctx = context_for(CHANNEL_UNKNOWN, owner_kind="user", owner_id="u1")
assert "ui_prompt" not in ctx.tools
assert ctx.capabilities["confirm"] is False
assert "interactions_get" in ctx.tools
def test_fragment_is_compact(local_db):
text = fragment_for(context_for(CHANNEL_TELEGRAM, owner_kind="guest"))
assert "CHANNEL: telegram" in text
assert "TOOLS: ui_prompt" in text
assert "INTERACTION: none" in text
assert len(text) < 600
def test_interactions_enabled_default(local_db):
assert interactions_enabled("guest", "") is True
@@ -0,0 +1,55 @@
# retoor <retoor@molodetz.nl>
import json
from tests.conftest import run_async
from devplacepy.services.devii.interaction.broker import InteractionBroker
from devplacepy.services.devii.interaction.controller import InteractionController
def test_ui_prompt_dispatch():
async def wait_site(interaction_id, frame, timeout):
return {
"status": "submitted",
"interaction_id": interaction_id,
"values": {"env": "staging"},
}
broker = InteractionBroker("main", wait_site=wait_site)
controller = InteractionController(broker)
raw = run_async(
controller.dispatch(
"ui_prompt",
{
"title": "Env?",
"widgets": [
{
"type": "choice",
"name": "env",
"label": "Env",
"options": [
{"value": "staging", "label": "Staging"},
{"value": "prod", "label": "Prod"},
],
}
],
},
)
)
data = json.loads(raw)
assert data["status"] == "submitted"
assert data["values"]["env"] == "staging"
def test_ui_notify_on_site():
events = []
async def emit(payload):
events.append(payload)
broker = InteractionBroker("main", emit=emit)
controller = InteractionController(broker)
raw = run_async(controller.dispatch("ui_notify", {"text": "Working..."}))
data = json.loads(raw)
assert data["status"] == "ok"
assert events and events[0]["type"] == "status"
@@ -0,0 +1,57 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.interaction.markdown import (
project_markdown,
project_plain_menu,
)
from devplacepy.services.devii.interaction.schema import validate_prompt_args
def _request():
return validate_prompt_args(
{
"title": "Deploy to staging?",
"description": "Ship the current build.",
"widgets": [
{
"type": "choice",
"name": "env",
"label": "Target environment",
"default": "staging",
"options": [
{"value": "production", "label": "Production"},
{"value": "staging", "label": "Staging"},
{"value": "dev", "label": "Development"},
],
},
{
"type": "text",
"name": "notes",
"label": "Ship notes",
"required": False,
"max_length": 200,
},
],
}
)
def test_markdown_projection_has_title_and_id():
md = project_markdown(_request(), "ix-a1b2")
assert "### Deploy to staging?" in md
assert "`staging`" in md
assert "<!-- ai-interaction:ix-a1b2 -->" in md
assert "[Confirm]" in md
def test_plain_menu_for_confirm():
req = validate_prompt_args(
{
"title": "Delete?",
"widgets": [
{"type": "confirm", "name": "delete", "label": "Delete", "default": False}
],
}
)
text = project_plain_menu(req)
assert "yes | no" in text.lower() or "Reply: yes" in text
@@ -0,0 +1,117 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.interaction.parse import parse_plain_reply
from devplacepy.services.devii.interaction.schema import validate_prompt_args
def test_parse_confirm_yes():
req = validate_prompt_args(
{
"title": "Delete?",
"widgets": [{"type": "confirm", "name": "delete", "label": "Delete"}],
}
)
status, values, err = parse_plain_reply(req, "yes")
assert status == "submitted"
assert values == {"delete": True}
assert err is None
def test_parse_choice_by_index():
req = validate_prompt_args(
{
"title": "Env?",
"widgets": [
{
"type": "choice",
"name": "env",
"label": "Env",
"options": [
{"value": "production", "label": "Production"},
{"value": "staging", "label": "Staging"},
],
}
],
}
)
status, values, err = parse_plain_reply(req, "2")
assert status == "submitted"
assert values == {"env": "staging"}
def test_parse_cancel():
req = validate_prompt_args(
{
"title": "Env?",
"widgets": [
{
"type": "choice",
"name": "env",
"label": "Env",
"options": [{"value": "a", "label": "A"}],
}
],
}
)
status, values, err = parse_plain_reply(req, "cancel")
assert status == "cancelled"
assert values == {}
def test_parse_date_european():
req = validate_prompt_args(
{
"title": "When?",
"widgets": [{"type": "date", "name": "day", "label": "Day"}],
}
)
status, values, err = parse_plain_reply(req, "13/01/1990")
assert status == "submitted"
assert values == {"day": "13/01/1990"}
assert err is None
def test_parse_date_iso_normalized_to_european():
req = validate_prompt_args(
{
"title": "When?",
"widgets": [{"type": "date", "name": "day", "label": "Day"}],
}
)
status, values, err = parse_plain_reply(req, "1990-01-13")
assert status == "submitted"
assert values == {"day": "13/01/1990"}
def test_parse_confirm_dutch_ja():
req = validate_prompt_args(
{
"title": "Ok?",
"widgets": [{"type": "confirm", "name": "ok", "label": "Ok"}],
}
)
status, values, err = parse_plain_reply(req, "ja")
assert status == "submitted"
assert values == {"ok": True}
def test_parse_choice_by_label():
req = validate_prompt_args(
{
"title": "Lang?",
"widgets": [
{
"type": "choice",
"name": "lang",
"label": "Language",
"options": [
{"value": "python", "label": "Python"},
{"value": "rust", "label": "Rust"},
],
}
],
}
)
status, values, err = parse_plain_reply(req, "Rust")
assert status == "submitted"
assert values == {"lang": "rust"}
@@ -0,0 +1,61 @@
# retoor <retoor@molodetz.nl>
from devplacepy.database import get_table, set_setting
from devplacepy.services.devii.interaction import prefs
from devplacepy.utils.accounts import register_account
def _user(username="ixpref1"):
try:
register_account(username, f"{username}@t.dev", "secret123")
except Exception:
pass
return get_table("users").find_one(username=username)
def test_admin_default_applies_when_user_inherits(local_db):
set_setting("devii_interactions_default", "1")
user = _user("ixpref_inherit")
get_table("users").update(
{"uid": user["uid"], "interactions_enabled": -1}, ["uid"]
)
assert prefs.effective_for("user", user["uid"]) is True
snap = prefs.snapshot("user", user["uid"])
assert snap["source"] == "default"
assert snap["override"] is None
def test_user_override_off(local_db):
set_setting("devii_interactions_default", "1")
user = _user("ixpref_off")
prefs.set_user_pref(user["uid"], False)
assert prefs.effective_for("user", user["uid"]) is False
snap = prefs.snapshot("user", user["uid"])
assert snap["source"] == "user"
assert snap["override"] is False
assert snap["default"] is True
def test_user_override_on_when_default_off(local_db):
set_setting("devii_interactions_default", "0")
user = _user("ixpref_on")
prefs.set_user_pref(user["uid"], True)
assert prefs.effective_for("user", user["uid"]) is True
assert prefs.admin_default() is False
def test_reset_returns_to_default(local_db):
set_setting("devii_interactions_default", "0")
user = _user("ixpref_reset")
prefs.set_user_pref(user["uid"], True)
snap = prefs.set_user_pref(user["uid"], None)
assert snap["source"] == "default"
assert snap["enabled"] is False
assert get_table("users").find_one(uid=user["uid"])["interactions_enabled"] == -1
def test_guest_uses_admin_default(local_db):
set_setting("devii_interactions_default", "0")
assert prefs.effective_for("guest", "cookie") is False
set_setting("devii_interactions_default", "1")
assert prefs.effective_for("guest", "cookie") is True
@@ -0,0 +1,96 @@
# retoor <retoor@molodetz.nl>
import pytest
from devplacepy.services.devii.errors import ToolInputError
from devplacepy.services.devii.interaction.schema import (
validate_prompt_args,
)
def test_valid_confirm_prompt():
req = validate_prompt_args(
{
"title": "Delete archive row?",
"description": "Remote files are untouched.",
"widgets": [
{
"type": "confirm",
"name": "delete",
"label": "Delete archive row",
"default": False,
}
],
"submit_label": "Delete",
"cancel_label": "Keep",
}
)
assert req.title.startswith("Delete")
assert req.widgets[0].type == "confirm"
assert req.widgets[0].name == "delete"
def test_choice_requires_options():
with pytest.raises(ToolInputError):
validate_prompt_args(
{
"title": "Env?",
"widgets": [{"type": "choice", "name": "env", "label": "Env"}],
}
)
def test_rejects_empty_widgets():
with pytest.raises(ToolInputError):
validate_prompt_args({"title": "x", "widgets": []})
def test_rejects_bad_option_value():
with pytest.raises(ToolInputError):
validate_prompt_args(
{
"title": "Pick",
"widgets": [
{
"type": "choice",
"name": "x",
"label": "X",
"options": [{"value": "BAD VALUE!", "label": "Bad"}],
}
],
}
)
def test_group_nesting_limit():
leaf = {
"type": "text",
"name": "a",
"label": "A",
"required": False,
}
deep = {
"type": "group",
"label": "G1",
"widgets": [
{
"type": "group",
"label": "G2",
"widgets": [
{
"type": "group",
"label": "G3",
"widgets": [
{
"type": "group",
"label": "G4",
"widgets": [leaf],
}
],
}
],
}
],
}
with pytest.raises(ToolInputError):
validate_prompt_args({"title": "Deep", "widgets": [deep]})
+27 -1
View File
@@ -41,8 +41,34 @@ def test_main_channel_keeps_full_catalog_and_behavior(local_db):
main = _session("main", "sess-main")
names = _tool_names(main)
assert "search_docs" in names
assert "ui_prompt" in names
assert "ui_cancel" in names
assert len(names) > 1
assert "TRUTH RULES AND BEHAVIOR" in main._compose_system_prompt()
prompt = main._compose_system_prompt()
assert "TRUTH RULES AND BEHAVIOR" in prompt
assert "CHANNEL: site-chat" in prompt
assert "Interactive asks (CA-IWP)" in prompt
def test_docs_channel_excludes_ui_prompt(local_db):
docs = _session("docs", "sess-docs-ui")
assert "ui_prompt" not in _tool_names(docs)
def test_telegram_channel_includes_ui_prompt(local_db):
tg = _session("telegram", "sess-tg-ui")
names = _tool_names(tg)
assert "ui_prompt" in names
prompt = tg._compose_system_prompt()
assert "CHANNEL: telegram" in prompt
def test_guest_main_has_ui_prompt_without_pref_tools(local_db):
main = _session("main", "sess-guest-ui")
names = _tool_names(main)
assert "ui_prompt" in names
assert "interactions_get" not in names
assert "interactions_set" not in names
def test_docs_channel_does_not_start_scheduler(local_db):
@@ -0,0 +1,23 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.devii.text import normalize_newlines
def test_normalize_literal_backslash_n():
raw = "[compacted earlier turns]\n\nHello\\n\\n## Title\\n\\n| A | B |"
out = normalize_newlines(raw)
assert "\\n" not in out or out.count("\n") > raw.count("\n")
assert "\n\n## Title\n\n" in out
assert out.startswith("[compacted earlier turns]\n\nHello\n")
def test_normalize_leaves_real_newlines():
raw = "line one\nline two\nline three"
assert normalize_newlines(raw) == raw
def test_normalize_double_escaped():
raw = "a\\\\n\\\\nb"
out = normalize_newlines(raw)
assert "\n" in out
assert out == "a\n\nb"
+174
View File
@@ -0,0 +1,174 @@
# retoor <retoor@molodetz.nl>
import base64
import io
from datetime import datetime, timezone
import pytest
from PIL import Image
from devplacepy.db_client import get_award_usage, get_table, init_db
from devplacepy.db_client import recompute_user_award_stats
from devplacepy.services.jobs.award_service import AwardService
from devplacepy.utils import generate_uid, make_combined_slug
from tests.conftest import run_async
@pytest.fixture(autouse=True)
def _db(local_db, tmp_path, monkeypatch):
init_db()
monkeypatch.setattr("devplacepy.attachments.ATTACHMENTS_DIR", tmp_path / "attachments")
yield
def _png_bytes():
buf = io.BytesIO()
Image.new("RGBA", (64, 64), (40, 80, 120, 255)).save(buf, format="PNG")
return buf.getvalue()
class _FakeResponse:
def __init__(self, payload, headers=None):
self._payload = payload
self.headers = headers or {}
def raise_for_status(self):
return None
def json(self):
return self._payload
class _FakeClient:
def __init__(self, *args, **kwargs):
self._png = _png_bytes()
def __enter__(self):
return self
def __exit__(self, *args):
return False
def post(self, url, json=None, headers=None):
encoded = base64.b64encode(self._png).decode("ascii")
return _FakeResponse(
{"data": [{"b64_json": encoded}]},
headers={
"X-Gateway-Cost-USD": "0.001",
"X-Gateway-Prompt-Tokens": "10",
"X-Gateway-Completion-Tokens": "0",
"X-Gateway-Total-Tokens": "10",
"X-Gateway-Upstream-Latency-Ms": "100",
"X-Gateway-Total-Latency-Ms": "120",
},
)
def _seed_pending(giver_uid, receiver_uid, description="Nice job"):
uid = generate_uid()
slug = make_combined_slug(description, uid)
get_table("awards").insert(
{
"uid": uid,
"slug": slug,
"description": description,
"giver_uid": giver_uid,
"receiver_uid": receiver_uid,
"attachment_uid_512": "",
"attachment_uid_256": "",
"attachment_uid_64": "",
"generated_at": None,
"created_at": datetime.now(timezone.utc).isoformat(),
"job_uid": "",
"deleted_at": None,
"deleted_by": None,
}
)
return uid, slug
def _user(prefix):
uid = generate_uid()
get_table("users").insert(
{
"uid": uid,
"username": f"{prefix}_{uid[:8]}",
"email": f"{uid[:8]}@t.dev",
"api_key": generate_uid(),
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
return uid
def _job(award_uid, giver_uid, receiver_uid, api_key):
return {
"uid": generate_uid(),
"payload": {
"award_uid": award_uid,
"giver_uid": giver_uid,
"receiver_uid": receiver_uid,
"description": "Nice job",
"api_key": api_key,
},
}
def test_process_finalizes_award(monkeypatch):
monkeypatch.setattr("devplacepy.services.jobs.award_service.stealth.stealth_sync_client", _FakeClient)
giver = _user("giver")
receiver = _user("recv")
api_key = get_table("users").find_one(uid=giver)["api_key"]
award_uid, _ = _seed_pending(giver, receiver)
run_async(AwardService().process(_job(award_uid, giver, receiver, api_key)))
row = get_table("awards").find_one(uid=award_uid)
assert row.get("generated_at")
assert row.get("attachment_uid_512")
assert row.get("attachment_uid_256")
assert row.get("attachment_uid_64")
recompute_user_award_stats(receiver)
user = get_table("users").find_one(uid=receiver)
assert user.get("award_count") == 1
def test_process_is_idempotent(monkeypatch):
monkeypatch.setattr("devplacepy.services.jobs.award_service.stealth.stealth_sync_client", _FakeClient)
giver = _user("giver2")
receiver = _user("recv2")
api_key = get_table("users").find_one(uid=giver)["api_key"]
award_uid, _ = _seed_pending(giver, receiver)
job = _job(award_uid, giver, receiver, api_key)
svc = AwardService()
run_async(svc.process(job))
first = get_table("awards").find_one(uid=award_uid)
run_async(svc.process(job))
second = get_table("awards").find_one(uid=award_uid)
assert first["generated_at"] == second["generated_at"]
assert first["attachment_uid_512"] == second["attachment_uid_512"]
def test_process_skips_soft_deleted_award(monkeypatch):
monkeypatch.setattr("devplacepy.services.jobs.award_service.stealth.stealth_sync_client", _FakeClient)
giver = _user("giver3")
receiver = _user("recv3")
api_key = get_table("users").find_one(uid=giver)["api_key"]
award_uid, _ = _seed_pending(giver, receiver)
get_table("awards").update(
{"uid": award_uid, "deleted_at": datetime.now(timezone.utc).isoformat(), "deleted_by": giver},
["uid"],
)
result = run_async(AwardService().process(_job(award_uid, giver, receiver, api_key)))
assert result.get("skipped") is True
assert not get_table("awards").find_one(uid=award_uid).get("generated_at")
def test_award_usage_accumulates_from_gateway_headers(monkeypatch):
monkeypatch.setattr("devplacepy.services.jobs.award_service.stealth.stealth_sync_client", _FakeClient)
get_table("award_usage").delete()
giver = _user("giver4")
receiver = _user("recv4")
api_key = get_table("users").find_one(uid=giver)["api_key"]
award_uid, _ = _seed_pending(giver, receiver)
run_async(AwardService().process(_job(award_uid, giver, receiver, api_key)))
usage = get_award_usage()
assert usage["calls"] >= 1
assert usage["cost_usd"] > 0
+279 -3
View File
@@ -107,6 +107,7 @@ def test_model_is_forced(local_db, monkeypatch):
cfg,
("guest", "test"),
"test",
"default",
)
)
assert rt._client.calls[-1][1]["model"] == "deepseek-chat"
@@ -143,6 +144,7 @@ def test_model_route_overrides_upstream(local_db, monkeypatch):
cfg,
("guest", "test"),
"test",
"default",
)
)
url, body = rt._client.calls[-1]
@@ -169,7 +171,7 @@ def test_vision_rewrites_image_to_text(local_db, monkeypatch):
],
}
]
run_async(rt.handle_chat({"messages": msgs}, cfg, ("guest", "test"), "test"))
run_async(rt.handle_chat({"messages": msgs}, cfg, ("guest", "test"), "test", "default"))
sent = rt._client.calls[-1][1]["messages"][0]["content"]
assert isinstance(sent, str) and "vision" in sent.lower()
@@ -185,6 +187,7 @@ def test_streaming_emits_sse(local_db, monkeypatch):
cfg,
("guest", "test"),
"test",
"default",
)
)
@@ -282,6 +285,7 @@ def test_embeddings_remaps_alias_to_configured_model(local_db, monkeypatch):
cfg,
("guest", "test"),
"test",
"default",
)
)
assert rt._client.calls[-1][1]["model"] == "qwen/qwen3-embedding-8b"
@@ -302,6 +306,7 @@ def test_embeddings_success_records_ledger(local_db, monkeypatch):
cfg,
("guest", "ledger_probe"),
"test",
"default",
)
)
assert resp.status_code == 200
@@ -325,7 +330,7 @@ def test_embeddings_disabled_returns_503(local_db, monkeypatch):
rt = svc.runtime()
resp = run_async(
rt.handle_embeddings(
{"input": "hello"}, cfg, ("guest", "test"), "test"
{"input": "hello"}, cfg, ("guest", "test"), "test", "default"
)
)
assert resp.status_code == 503
@@ -372,6 +377,7 @@ def test_images_remaps_alias_to_configured_model(local_db, monkeypatch):
cfg,
("guest", "test"),
"test",
"default",
)
)
assert rt._client.calls[-1][1]["model"] == "black-forest-labs/flux.2-pro"
@@ -411,6 +417,7 @@ def test_image_route_overrides_upstream(local_db, monkeypatch):
cfg,
("guest", "test"),
"test",
"default",
)
)
url, body = rt._client.calls[-1]
@@ -435,6 +442,7 @@ def test_images_success_records_ledger(local_db, monkeypatch):
cfg,
("guest", "img_ledger"),
"test",
"default",
)
)
assert resp.status_code == 200
@@ -459,7 +467,7 @@ def test_images_disabled_returns_503(local_db, monkeypatch):
rt = svc.runtime()
resp = run_async(
rt.handle_images(
{"prompt": "badge"}, cfg, ("guest", "test"), "test"
{"prompt": "badge"}, cfg, ("guest", "test"), "test", "default"
)
)
assert resp.status_code == 503
@@ -489,3 +497,271 @@ def test_compute_cost_embed_branch():
assert output_cost == 0.0
assert abs(total - 0.01) < 1e-9
assert abs(input_cost - 0.01) < 1e-9
def test_app_reference_stored_in_ledger_for_chat(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(
{"messages": [{"role": "user", "content": "ref_test"}]},
cfg,
("guest", "ref_probe"),
"test-ua",
"my-custom-app",
)
)
row = get_table("gateway_usage_ledger").find_one(owner_id="ref_probe")
assert row is not None
assert row["app_reference"] == "my-custom-app"
assert row["backend"] == "chat"
def test_app_reference_defaults_when_not_passed(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(
{"messages": [{"role": "user", "content": "default_test"}]},
cfg,
("guest", "default_probe"),
"test-ua",
"default",
)
)
row = get_table("gateway_usage_ledger").find_one(owner_id="default_probe")
assert row is not None
assert row["app_reference"] == "default"
def test_app_reference_stored_in_ledger_for_embeddings(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_embed_enabled"] = True
cfg["gateway_embed_model"] = "qwen/qwen3-embedding-8b"
rt = svc.runtime()
run_async(
rt.handle_embeddings(
{"input": "hello"},
cfg,
("guest", "embed_ref"),
"test-ua",
"devplace-embed-test-v-1-0-0",
)
)
row = get_table("gateway_usage_ledger").find_one(owner_id="embed_ref")
assert row is not None
assert row["app_reference"] == "devplace-embed-test-v-1-0-0"
assert row["backend"] == "embed"
def test_app_reference_stored_in_ledger_for_images(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeImageClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_image_enabled"] = True
cfg["gateway_image_model"] = "black-forest-labs/flux.2-pro"
rt = svc.runtime()
run_async(
rt.handle_images(
{"prompt": "ref badge"},
cfg,
("guest", "img_ref"),
"test-ua",
"devplace-image-test-v-1-0-0",
)
)
row = get_table("gateway_usage_ledger").find_one(owner_id="img_ref")
assert row is not None
assert row["app_reference"] == "devplace-image-test-v-1-0-0"
assert row["backend"] == "image"
def test_app_reference_column_exists(local_db):
db = get_table("gateway_usage_ledger").db
if "gateway_usage_ledger" not in db.tables:
return
rows = list(db.query("PRAGMA table_info('gateway_usage_ledger')"))
column_names = [r["name"] for r in rows]
assert "app_reference" in column_names
def test_chat_unknown_model_falls_back_to_default(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_force_model"] = False
cfg["gateway_model"] = "deepseek-v4-flash"
rt = svc.runtime()
run_async(
rt.handle_chat(
{"model": "nonexistent-model-v99", "messages": [{"role": "user", "content": "hi"}]},
cfg,
("guest", "fallback_test_chat"),
"test",
"default",
)
)
assert rt._client.calls[-1][1]["model"] == "deepseek-v4-flash"
def test_embeddings_unknown_model_falls_back_to_default(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeEmbedClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_force_model"] = False
cfg["gateway_embed_enabled"] = True
cfg["gateway_embed_model"] = "qwen/qwen3-embedding-8b"
rt = svc.runtime()
run_async(
rt.handle_embeddings(
{"model": "nonexistent-embed-model", "input": "hello"},
cfg,
("guest", "fallback_test_embed"),
"test",
"default",
)
)
assert rt._client.calls[-1][1]["model"] == "qwen/qwen3-embedding-8b"
def test_images_unknown_model_falls_back_to_default(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeImageClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
cfg["gateway_force_model"] = False
cfg["gateway_image_enabled"] = True
cfg["gateway_image_model"] = "black-forest-labs/flux.2-pro"
rt = svc.runtime()
run_async(
rt.handle_images(
{"model": "nonexistent-image-model", "prompt": "test badge"},
cfg,
("guest", "fallback_test_img"),
"test",
"default",
)
)
assert rt._client.calls[-1][1]["model"] == "black-forest-labs/flux.2-pro"
class FakeClientWithUsage_openai_gateway(FakeClient_openai_gateway):
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"}}],
"usage": {"prompt_tokens": 7, "completion_tokens": 3, "total_tokens": 10},
}
)
def test_stream_options_stripped_from_upstream(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClient_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
rt = svc.runtime()
run_async(
rt.handle_chat(
{
"messages": [{"role": "user", "content": "hi"}],
"stream": True,
"stream_options": {"include_usage": True},
},
cfg,
("guest", "test"),
"test",
"default",
)
)
upstream_payload = rt._client.calls[-1][1]
assert "stream_options" not in upstream_payload
assert upstream_payload["stream"] is False
def test_include_usage_emits_usage_chunk(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClientWithUsage_openai_gateway)
svc = GatewayService()
cfg = svc.effective_config()
rt = svc.runtime()
resp = run_async(
rt.handle_chat(
{
"messages": [{"role": "user", "content": "hi"}],
"stream": True,
"stream_options": {"include_usage": True},
},
cfg,
("guest", "test"),
"test",
"default",
)
)
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 '"usage"' in body
assert '"total_tokens": 10' in body
assert "[DONE]" in body
def test_no_usage_chunk_without_include_usage(local_db, monkeypatch):
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeClientWithUsage_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",
"default",
)
)
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 '"usage"' not in body
assert "[DONE]" in body
def test_models_endpoint_publishes_molodetz(local_db):
from devplacepy.services.openai_gateway import routing
routing.seed_default_deepseek_routes()
try:
svc = GatewayService()
resp = svc._models_response()
payload = json.loads(resp.body.decode())
ids = {m["id"] for m in payload["data"]}
assert payload["object"] == "list"
assert {"molodetz", "molodetz-pro"} <= ids
for model in payload["data"]:
assert model["object"] == "model"
assert model["owned_by"] == "molodetz"
finally:
routing.model_store.remove("molodetz")
routing.model_store.remove("molodetz-pro")
@@ -280,3 +280,19 @@ def test_seed_default_deepseek_routes_is_idempotent_and_preserves_customization(
context_window=1_048_576,
)
)
def test_seed_publishes_molodetz_aliases(local_db):
r.seed_default_deepseek_routes()
try:
molodetz = r.model_store.resolve("molodetz", "chat")
molodetz_pro = r.model_store.resolve("molodetz-pro", "chat")
assert molodetz is not None
assert molodetz.target_model == "deepseek-v4-flash"
assert molodetz_pro is not None
assert molodetz_pro.target_model == "deepseek-v4-pro"
sources = {row["source_model"] for row in r.model_store.list()}
assert {"molodetz", "molodetz-pro"} <= sources
finally:
r.model_store.remove("molodetz")
r.model_store.remove("molodetz-pro")
@@ -219,3 +219,32 @@ def test_compute_cost_native_upstream_cost_unaffected_by_tiering():
total, _, _, native = compute_cost({"cost": 0.05}, norm, tiered, "chat")
assert native is True
assert total == 0.05
def test_validate_app_reference_accepts_valid_slugs():
from devplacepy.services.openai_gateway.service import _validate_app_reference
assert _validate_app_reference("devplace-devii-v-1-0-0") == "devplace-devii-v-1-0-0"
assert _validate_app_reference("my-app") == "my-app"
assert _validate_app_reference("a") == "a"
assert _validate_app_reference("abcdefghijklmnopqrstuvwxyz0123") == "abcdefghijklmnopqrstuvwxyz0123"
assert _validate_app_reference("test_app.release-2") == "test_app.release-2"
def test_validate_app_reference_rejects_invalid():
from devplacepy.services.openai_gateway.service import _validate_app_reference
assert _validate_app_reference("") == "default"
assert _validate_app_reference(" ") == "default"
assert _validate_app_reference("hello world") == "default"
assert _validate_app_reference("name@domain") == "default"
assert _validate_app_reference("name/domain") == "default"
assert _validate_app_reference(None) == "default"
assert _validate_app_reference("abcdefghijklmnopqrstuvwxyz01234") == "default"
def test_validate_app_reference_strips_whitespace():
from devplacepy.services.openai_gateway.service import _validate_app_reference
assert _validate_app_reference(" my-app ") == "my-app"
assert _validate_app_reference("\tdevplace\t") == "devplace"
+28 -2
View File
@@ -38,6 +38,32 @@ def test_private_text_message_emitted():
assert messages[0]["chat_id"] == 42 and messages[0]["text"] == "hello"
def test_callback_query_emitted():
backend = FakeTelegramBackend()
worker, frames = _worker(backend)
_run(
worker.process_update(
{
"update_id": 2,
"callback_query": {
"id": "cb1",
"from": {"id": 7},
"data": "i=del1;k=env;v=stg",
"message": {
"message_id": 9,
"chat": {"id": 42, "type": "private"},
},
},
}
)
)
callbacks = [f for f in frames if f["type"] == "callback"]
assert len(callbacks) == 1
assert callbacks[0]["chat_id"] == 42
assert callbacks[0]["data"] == "i=del1;k=env;v=stg"
assert callbacks[0]["callback_query_id"] == "cb1"
def test_group_message_ignored():
backend = FakeTelegramBackend()
worker, frames = _worker(backend)
@@ -78,14 +104,14 @@ def test_photo_becomes_data_uri():
def test_html_send_falls_back_to_plain_on_entity_error():
class EntityBackend(FakeTelegramBackend):
async def send_message(self, chat_id, text, parse_mode):
async def send_message(self, chat_id, text, parse_mode, reply_markup=None):
if parse_mode:
return {
"ok": False,
"error_code": 400,
"description": "Bad Request: can't parse entities",
}
return await super().send_message(chat_id, text, None)
return await super().send_message(chat_id, text, None, reply_markup=reply_markup)
backend = EntityBackend()
worker, frames = _worker(backend)
+212
View File
@@ -0,0 +1,212 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timezone
import pytest
from devplacepy.db_client import get_table
from devplacepy.utils import generate_uid
def _now():
return datetime.now(timezone.utc).isoformat()
def _make_user(local_db, **overrides):
uid = generate_uid()
row = {
"uid": uid,
"username": f"cpd_{uid[:8]}",
"email": f"cpd_{uid[:8]}@t.dev",
"api_key": generate_uid(),
"role": "Member",
"is_active": True,
"created_at": _now(),
"bio": "",
}
row.update(overrides)
get_table("users").insert(row)
return row
def _make_post(local_db, user, **overrides):
from devplacepy.utils import make_combined_slug
uid = generate_uid()
row = {
"uid": uid,
"user_uid": user["uid"],
"title": "Compound test post",
"content": "Body of the compound test post.",
"topic": "devlog",
"slug": make_combined_slug("Compound test post", uid),
"stars": 0,
"created_at": _now(),
"deleted_at": None,
"deleted_by": None,
}
row.update(overrides)
get_table("posts").insert(row)
return row
def _make_project(local_db, user, **overrides):
from devplacepy.utils import make_combined_slug
uid = generate_uid()
row = {
"uid": uid,
"user_uid": user["uid"],
"title": "Compound test project",
"description": "",
"slug": make_combined_slug("Compound test project", uid),
"stars": 0,
"is_private": 0,
"created_at": _now(),
"deleted_at": None,
"deleted_by": None,
}
row.update(overrides)
get_table("projects").insert(row)
return row
def _make_gist(local_db, user, **overrides):
from devplacepy.utils import make_combined_slug
uid = generate_uid()
row = {
"uid": uid,
"user_uid": user["uid"],
"title": "Compound test gist",
"description": "",
"source_code": "print('hi')",
"language": "python",
"slug": make_combined_slug("Compound test gist", uid),
"stars": 0,
"created_at": _now(),
"deleted_at": None,
"deleted_by": None,
}
row.update(overrides)
get_table("gists").insert(row)
return row
# Section 5.3 page compounds actually wired to a caller (db_client.build_*)
# and consumed by a real web route. Each assertion checks the template
# context keys the corresponding route/schema depends on.
def test_build_feed_page_shape(local_db):
from devplacepy_services.database.compounds_page import build_feed_page
user = _make_user(local_db)
post = _make_post(local_db, user)
result = build_feed_page(user=user)
for key in ("posts", "next_cursor", "stats", "top_authors", "daily_topic", "current_tab"):
assert key in result
uids = [item["post"]["uid"] for item in result["posts"]]
assert post["uid"] in uids
item = next(i for i in result["posts"] if i["post"]["uid"] == post["uid"])
for key in ("attachments", "recent_comments", "reactions", "bookmarked", "poll"):
assert key in item
def test_build_post_detail_shape(local_db):
from devplacepy_services.database.compounds_page import build_post_detail
user = _make_user(local_db)
post = _make_post(local_db, user)
result = build_post_detail(post["uid"], user=user)
assert result["post"]["uid"] == post["uid"]
assert result["author"]["uid"] == user["uid"]
assert isinstance(result["comments"], list)
assert isinstance(result["attachments"], list)
def test_build_post_detail_missing_post(local_db):
from devplacepy_services.database.compounds_page import build_post_detail
result = build_post_detail("does-not-exist")
assert result["post"] is None
def test_build_profile_bundle_shape(local_db):
from devplacepy_services.database.compounds_page import build_profile_bundle
user = _make_user(local_db)
result = build_profile_bundle(user["uid"])
assert result["user"]["uid"] == user["uid"]
assert "followers" in result["follow_counts"]
assert "following" in result["follow_counts"]
def test_build_leaderboard_page_shape(local_db):
from devplacepy_services.database.compounds_page import build_leaderboard_page
user = _make_user(local_db)
result = build_leaderboard_page(viewer_uid=user["uid"])
assert "leaderboard" in result
assert isinstance(result["leaderboard"], list)
def test_build_project_detail_shape(local_db):
from devplacepy_services.database.compounds_page import build_project_detail
user = _make_user(local_db)
project = _make_project(local_db, user)
result = build_project_detail(project["uid"])
assert result["project"]["uid"] == project["uid"]
assert result["owner"]["uid"] == user["uid"]
def test_build_gist_detail_shape(local_db):
from devplacepy_services.database.compounds_page import build_gist_detail
user = _make_user(local_db)
gist = _make_gist(local_db, user)
result = build_gist_detail(gist["uid"], user=user)
assert result["gist"]["uid"] == gist["uid"]
assert result["author"]["uid"] == user["uid"]
assert isinstance(result["comments"], list)
# Section 5.3 rows registered as HTTP endpoints (devplacepy_services/database/routes.py)
# but not yet called by any web route (routers/messages.py, notifications.py,
# game/, admin/index.py all still do their own direct db_client calls) - these
# four builders are unfinished stubs, tracked here so a real implementation
# lands as a visible test change rather than silently.
@pytest.mark.parametrize(
"fn_name,expected_keys",
[
("build_messages_page", {"threads", "unread", "partners"}),
("build_notifications_page", {"notifications", "actors", "unread_count"}),
("build_game_state", {"store", "leaderboard"}),
("build_admin_dashboard", {"stats", "service_states"}),
],
)
def test_stub_compounds_documented(local_db, fn_name, expected_keys):
import devplacepy_services.database.compounds_page as compounds_page
fn = getattr(compounds_page, fn_name)
result = fn() if fn_name != "build_admin_dashboard" else fn()
assert set(result.keys()) == expected_keys, (
f"{fn_name} shape changed - if this is a real implementation now, "
"replace this stub-tracking test with a real assertion and update "
"splitup.md Section 17.2"
)
+56
View File
@@ -0,0 +1,56 @@
# retoor <retoor@molodetz.nl>
import os
import subprocess
import sys
import pytest
from devplacepy_services.base.config import PROFILES
SERVICE_MODULES = (
"devplacepy_services.database.main",
"devplacepy_services.pubsub.main",
"devplacepy_services.web.main",
"devplacepy_services.gateway.main",
"devplacepy_services.jobs.main",
"devplacepy_services.devii.main",
"devplacepy_services.bot.main",
"devplacepy_services.backup.main",
"devplacepy_services.containers.main",
"devplacepy_services.telegram.main",
"devplacepy_services.email.main",
"devplacepy_services.news.main",
"devplacepy_services.gitea.main",
"devplacepy_services.audit.main",
"devplacepy_services.xmlrpc.main",
)
@pytest.mark.parametrize("module_name", SERVICE_MODULES)
def test_service_imports(module_name):
env = os.environ.copy()
env["DEVPLACE_DISABLE_SERVICES"] = "1"
env["DEVPLACE_DATABASE_URL"] = "sqlite:///:memory:"
env.pop("DEVPLACE_REMOTE_DB", None)
env.pop("DEVPLACE_DB_SERVICE", None)
script = (
f"import importlib; m = importlib.import_module('{module_name}'); "
"assert hasattr(m, 'app')"
)
result = subprocess.run(
[sys.executable, "-c", script],
env=env,
capture_output=True,
text=True,
timeout=30,
check=False,
)
assert result.returncode == 0, result.stderr or result.stdout
def test_port_profiles_complete():
for profile in ("micro-dev", "micro-test", "micro-prod"):
ports = PROFILES[profile]
assert "web" in ports
assert ports["web"] in (10500, 20500)