#!/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")))