clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/tinyagent
tinyrelay / labs / hermes / smoke.py
smoke.py 6.5K
1"""Exercise unchanged Hermes over the native Tiny connection."""23import base644import hashlib5import json6import time7import uuid8from urllib.parse import urlsplit910from rpc import Client, answer, public, tag, wait_event111213def check(condition, message):14 if not condition:15 raise AssertionError(message)161718def send(client, text, extra=None):19 return client.publish(9, text, [["h", "lab"], ["p", public("agent")], *(extra or [])])202122def done(client, marker, since):23 event = wait_event(client, {"kinds": [9, 40003], "authors": [public("agent")],24 "#h": ["lab"], "since": since},25 lambda row: f"LAB_DONE {marker}" in row["content"])26 check(not any(row[0] in ("p", "mention") for row in event["tags"]),27 "Ordinary agent output unexpectedly mentions someone")28 return event293031def interaction(client, scenario, run):32 marker = f"LAB_{scenario}_{run}"33 prior = {row["id"] for row in client.query({"kinds": [9], "#h": ["lab"], "#request": ["question"]})}34 sent = send(client, marker)35 request = wait_event(client, {"kinds": [9], "authors": [public("agent")], "#h": ["lab"],36 "#request": ["question"], "since": sent["created_at"]},37 lambda row: row["id"] not in prior)38 check(tag(request, "tinyagent") == "1", "Request lost its native interaction marker")39 check(tag(request, "p") == public("owner"), "Request does not name its human assignee")40 check(tag(request, "mention") is None, "Assignee was turned into an unsolicited mention")41 check(int(tag(request, "expiration") or 0) > time.time(), "Request has no future expiry")42 options = {row[1]: row[2] for row in request["tags"] if len(row) >= 3 and row[0] == "option"}43 check(bool(options) or scenario == "OPEN", "Options were flattened into text")44 if scenario == "OPEN":45 check(tag(request, "selection") == "text", "Open question lost its text input")46 choice = "Custom lab answer"47 elif scenario == "APPROVAL":48 check(tag(request, "interaction") == "approval", "Approval lost its type")49 choice = next(key for key in options if key == "once")50 else:51 check(tag(request, "interaction") == "question", "Question lost its type")52 choice = next(key for key, label in options.items() if label.startswith("Continue"))53 if scenario == "MULTI":54 check(tag(request, "selection") == "multiple", "Multiple choice became single choice")55 choice = json.dumps([key for key, label in options.items() if label.startswith(("Continue", "Stop"))])56 if scenario == "CUSTOM":57 check(tag(request, "freeform") == "true", "Choice question lost its custom input")58 choice = json.dumps({"text": "Custom lab answer"})59 # A signed response from another room member must not resolve the operator's request.60 with Client("observer") as observer:61 answer(observer, request, choice)62 answer(client, request, "" if scenario == "OPEN" else "option-that-was-not-offered")63 time.sleep(0.5)64 state = client.browse("browseapproval", {"id": request["id"]})65 check(state["item"]["state"] == "open", "An unauthorized or invalid answer settled the card")66 answer(client, request, choice)67 final = done(client, marker, sent["created_at"])68 expected = "tinyagent-lab" if scenario == "APPROVAL" else "Custom lab answer" if scenario in ("OPEN", "CUSTOM") else "Continue"69 check(expected in final["content"], "Hermes did not receive the selected response")70 if scenario == "MULTI":71 check("Stop" in final["content"], "Hermes lost a selected choice")72 state = client.browse("browseapproval", {"id": request["id"]})73 check(state["item"]["state"] == "answered", "The valid answer did not settle the card")74 print(f"PASS {scenario.lower()}: native card, silent assignment, authorized answer, Hermes resumed")757677def media(client, run):78 marker = f"LAB_MEDIA_{run}"79 content = f"tinyagent-attachment-{run}\n".encode()80 descriptor = client.call("upload", {"room": "lab", "filename": "lab-note.txt", "type": "text/plain",81 "data": base64.b64encode(content).decode()})82 digest = hashlib.sha256(content).hexdigest()83 check(descriptor["sha256"] == digest, "Uploaded attachment hash differs")84 path = urlsplit(descriptor["url"]).path85 downloaded = client.call("download", {"path": path})86 check(base64.b64decode(downloaded["data"]) == content, "Authenticated download differs")87 sent = send(client, marker, [["imeta", f"url {descriptor['url']}", "m text/plain", f"x {digest}",88 f"size {len(content)}", "filename lab-note.txt"]])89 final = done(client, marker, sent["created_at"])90 check(f"tinyagent-attachment-{run}" in final["content"], "Attachment text did not reach Hermes")91 returned = wait_event(client, {"kinds": [9], "authors": [public("agent")], "#h": ["lab"],92 "since": sent["created_at"]},93 lambda row: any(t[0] == "imeta" and f"x {digest}" in t for t in row["tags"]))94 attachment = next(t for t in returned["tags"] if t[0] == "imeta")95 url = next(t[4:] for t in attachment if t.startswith("url "))96 check(base64.b64decode(client.call("download", {"path": urlsplit(url).path})["data"]) == content,97 "Hermes outbound attachment differs")98 print("PASS media: authenticated upload, Hermes read_file, native outbound attachment, matching bytes")99100101def main():102 run = uuid.uuid4().hex[:12]103 with Client() as client:104 marker = f"LAB_HELLO_{run}"105 sent = send(client, marker)106 done(client, marker, sent["created_at"])107 print("PASS connection: real Hermes replied through Tiny without a mention")108 for scenario in ("QUESTION", "MULTI", "CUSTOM", "OPEN", "APPROVAL"):109 interaction(client, scenario, run)110 media(client, run)111 marker = f"LAB_MENTION_{run}"112 sent = send(client, f"{marker} @{public('owner')}")113 mentioned = wait_event(client, {"kinds": [9, 40003], "authors": [public("agent")],114 "#h": ["lab"], "#p": [public("owner")], "since": sent["created_at"]},115 lambda row: f"LAB_DONE {marker}" in row["content"])116 check(tag(mentioned, "mention") == public("owner"), "Explicit agent mention was not preserved")117 print("PASS mention: explicit model mention becomes a native recipient")118 print(json.dumps({"ok": True, "run": run, "connection": "tiny", "connector": "tinyagent"}))119120121if __name__ == "__main__":122 main()