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
@@ -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"