Files
tai/taicpp/tests/glue_flags.py
T
retoorandClaude Sonnet 5.5 9f790e3177 glue: wire every port, flag dispatch and REPL host so build/detz runs the real agent
Adapters in src/glue for all ports of agent/ports.h (store, runtime, shell, ui, mcp, skills,
plexer, tui, ops, scheduler db), llm log sink and box, repl::ReplHost over agent::Agent, cap-speak,
serve-mcp, telegram and tui-local seams. main.cpp dispatches the flags in tai.py main() order.
Tests: tests/glue_persist.py (real model, restart persistence, one-shot shell call), tests/glue_flags.py,
tests/glue_ports.cpp. Spec: docs/glue.md, gaps: docs/glue-issues.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 07:20:01 +01:00

233 lines
11 KiB
Python

#!/usr/bin/env python3
"""tests/glue_flags.py: the flag dispatch of build/detz that needs no model (docs/glue.md).
Same harness as glue_persist.py (long-lived bash pane, throwaway TAI_HOME, tai_test.TestCase). Everything here is
plumbing: slash commands that dispatch tools through the ports, the long-running modes starting and stopping
cleanly, and the served MCP endpoint answering. The model-driven proof is in glue_persist.py.
GlueSlashCommands /tools /sysinfo /install status /skills /models /agents /schedules /env /profiles /bots /ctx
GlueSchedulerFlag --scheduler comes online and stops on SIGINT with exit code 0
GlueTelegramFlag --telegram without a token refuses loudly (exit code != 0)
GlueServeMcp --serve-mcp KEY answers initialize and tools/list over HTTP, refuses a wrong key
GlueCapFlags --cap-manifest, --cap help, --demo-shot, argument errors
GlueCapSpeak --cap-speak with a real model: system, assistant and result frames, vault session
Usage: python3 tests/glue_flags.py [--filter NAME]
"""
import json
import os
import shlex
import sys
import urllib.error
import urllib.request
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
from glue_persist import DETZ, PaneCase, RC_CAPTURE, RC_RE, TAICPP # noqa: E402
from tai_test import TestAssertionFailed, main_cli # noqa: E402
class GlueSlashCommands(PaneCase):
name = "GlueSlashCommands"
timeout = 120.0
async def run(self):
pane = await self.spawn()
rc, output = await self.pane_run(pane, ["/tools", "/sysinfo", "/install status", "/skills", "/agents", "/schedules", "/env", "/profiles", "/bots", "/ctx", "/tags", "/records nothing", "/quit"])
return {"rc": rc, "output": output}
async def verify(self, result):
out = result["output"]
wanted = {
"/tools": "core, always loaded:",
"/sysinfo": "cpu:",
"/install status": "container:",
"/skills": "blueprints, built on first load:",
"/agents": "(no agents)",
"/env": "current environment: home",
"/profiles": "* default",
"/bots": "* main",
"/ctx": "ctx",
}
missing = [cmd for cmd, text in wanted.items() if text not in out]
problems = ["%s: expected %r in the output" % (cmd, wanted[cmd]) for cmd in missing]
if "unknown command" in out:
problems.append("a slash command was reported unknown")
if result["rc"] != 0:
problems.append("exit code %d" % result["rc"])
if problems:
raise TestAssertionFailed("; ".join(problems) + "\n" + out[-1500:])
class GlueSchedulerFlag(PaneCase):
name = "GlueSchedulerFlag"
timeout = 60.0
async def run(self):
pane = await self.spawn()
command = "%s --scheduler & P=$!; sleep 4; kill -INT $P; wait $P; echo __RC=$?" % shlex.quote(DETZ)
before = len(pane.transcript)
await pane.send_line(command)
output = await pane.wait_for_pattern(RC_RE, timeout=40)
text = "".join(item.text for item in pane.transcript[before:])
return {"rc": int(RC_CAPTURE.findall(text)[-1]), "output": output}
async def verify(self, result):
problems = []
if "scheduler online, press Ctrl-C to stop" not in result["output"]:
problems.append("no online line: %r" % result["output"][-300:])
if result["rc"] != 0:
problems.append("exit code %d after SIGINT" % result["rc"])
if problems:
raise TestAssertionFailed("; ".join(problems))
class GlueTelegramFlag(PaneCase):
name = "GlueTelegramFlag"
timeout = 60.0
async def run(self):
pane = await self.spawn()
command = "%s --telegram; echo __RC=$?" % shlex.quote(DETZ)
before = len(pane.transcript)
await pane.send_line(command)
output = await pane.wait_for_pattern(RC_RE, timeout=40)
text = "".join(item.text for item in pane.transcript[before:])
return {"rc": int(RC_CAPTURE.findall(text)[-1]), "output": output}
async def verify(self, result):
if result["rc"] == 0 or "TELEGRAM_BOT_TOKEN" not in result["output"]:
raise TestAssertionFailed("--telegram without a token must refuse naming TELEGRAM_BOT_TOKEN: rc=%s output=%r" % (result["rc"], result["output"][-300:]))
class GlueServeMcp(PaneCase):
name = "GlueServeMcp"
timeout = 90.0
port = 18943
def call(self, key, body, sid=""):
headers = {"Content-Type": "application/json", "Authorization": "Bearer " + key, "Accept": "application/json, text/event-stream"}
if sid:
headers["Mcp-Session-Id"] = sid
request = urllib.request.Request("http://127.0.0.1:%d/mcp" % self.port, data=json.dumps(body).encode(), method="POST", headers=headers)
try:
with urllib.request.urlopen(request, timeout=60) as response:
return response.status, response.read().decode(), response.headers.get("Mcp-Session-Id", "")
except urllib.error.HTTPError as exc:
return exc.code, exc.read().decode(), ""
async def run(self):
pane = await self.spawn()
await pane.send_line("%s --serve-mcp secretkey --mcp-port %d & echo __SERVER=$!" % (shlex.quote(DETZ), self.port))
import asyncio
results = {}
for _ in range(40):
await asyncio.sleep(0.5)
try:
results["init"] = self.call("secretkey", {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "t", "version": "1"}}})
break
except OSError:
continue
if "init" in results:
sid = results["init"][2]
results["wrong"] = self.call("nope", {"jsonrpc": "2.0", "id": 2, "method": "tools/list"}, sid)
results["tools"] = self.call("secretkey", {"jsonrpc": "2.0", "id": 3, "method": "tools/list"}, sid)
results["call"] = self.call("secretkey", {"jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "sysinfo", "arguments": {"checks": ["cpu"]}}}, sid)
await pane.send_line("kill %1; wait; echo __RC=$?")
await pane.wait_for_pattern(RC_RE, timeout=30)
return results
async def verify(self, result):
problems = []
if "init" not in result:
raise TestAssertionFailed("--serve-mcp never answered on the port")
if result["init"][0] != 200 or "serverInfo" not in result["init"][1]:
problems.append("initialize: %s" % (result["init"],))
if result["wrong"][0] not in (401, 403):
problems.append("a wrong key must be refused, got %s" % (result["wrong"],))
body = result["tools"][1]
names = [t["name"] for t in json.loads(body[body.index("{"):])["result"]["tools"]] if result["tools"][0] == 200 else []
if "shell" not in names or len(names) < 61:
problems.append("tools/list returned %d tools" % len(names))
if "cpu:" not in result["call"][1]:
problems.append("tools/call sysinfo through the served agent: %s" % (result["call"][1][:200],))
if problems:
raise TestAssertionFailed("; ".join(problems))
class GlueCapSpeak(PaneCase):
name = "GlueCapSpeak"
timeout = 300.0
async def run(self):
pane = await self.spawn()
frame = json.dumps({"type": "user", "message": {"role": "user", "content": [{"type": "text", "text": "what is 2+3? answer with just the number"}]}})
rc, output = await self.pane_run(pane, [frame], flags=("--cap-speak",))
return {"rc": rc, "output": output}
async def verify(self, result):
frames = []
for line in result["output"].splitlines():
if line.startswith("{"):
try:
frames.append(json.loads(line))
except ValueError:
pass
types = [f.get("type") for f in frames]
problems = []
for needed in ("system", "assistant", "result"):
if needed not in types:
problems.append("no %s frame, saw %s" % (needed, types))
done = [f for f in frames if f.get("type") == "result"]
if done and "5" not in str(done[-1].get("text", "")):
problems.append("result frame does not carry the answer: %r" % (done[-1],))
sessions = self.sessions()
if len(sessions) != 1 or not any(m["role"] == "user" and "2+3" in (m.get("content") or "") for m in self.load(sessions[0])):
problems.append("the vault session does not hold the user turn: %s" % sessions)
if result["rc"] != 0:
problems.append("exit code %d" % result["rc"])
if problems:
raise TestAssertionFailed("; ".join(problems))
class GlueCapFlags(PaneCase):
name = "GlueCapFlags"
timeout = 90.0
async def run(self):
pane = await self.spawn()
outputs = {}
for key, flags in (("manifest", ["--cap-manifest"]), ("cap", ["--cap"]), ("shot", ["--demo-shot"]), ("env", ["--env", "bogus", "hello"]),
("profile", ["--profile", "bad name!", "hello"]), ("serve", ["--serve-mcp", ""])):
command = "%s %s; echo __RC=$?" % (shlex.quote(DETZ), " ".join(shlex.quote(f) for f in flags))
before = len(pane.transcript)
await pane.send_line(command)
outputs[key] = await pane.wait_for_pattern(RC_RE, timeout=40)
text = "".join(item.text for item in pane.transcript[before:])
outputs[key + "_rc"] = int(RC_CAPTURE.findall(text)[-1])
return outputs
async def verify(self, result):
problems = []
if "[agent]" not in result["manifest"] and "name" not in result["manifest"]:
problems.append("manifest: %r" % result["manifest"][:100])
if result["cap_rc"] not in (0, 2):
problems.append("--cap exit %d" % result["cap_rc"])
if "tai TUI demo" not in result["shot"]:
problems.append("--demo-shot: %r" % result["shot"][:100])
if result["env_rc"] != 1 or "use --env home or --env sandbox" not in result["env"]:
problems.append("--env bogus: rc=%s %r" % (result["env_rc"], result["env"][-100:]))
if result["profile_rc"] != 1 or "invalid profile name" not in result["profile"]:
problems.append("bad profile: rc=%s %r" % (result["profile_rc"], result["profile"][-100:]))
if result["serve_rc"] != 2 or "--serve-mcp needs a KEY" not in result["serve"]:
problems.append("empty serve key: rc=%s %r" % (result["serve_rc"], result["serve"][-100:]))
if problems:
raise TestAssertionFailed("; ".join(problems))
ALL_CASES = [GlueSlashCommands(), GlueSchedulerFlag(), GlueTelegramFlag(), GlueServeMcp(), GlueCapFlags(), GlueCapSpeak()]
if __name__ == "__main__":
sys.exit(main_cli(ALL_CASES, default_log_dir=str(TAICPP / "build" / "test-logs")))