import json
import subprocess
import sys
import tempfile
import os
if len(sys.argv) < 2:
sys.exit("usage: dogfood.py <path-to-verified-pmat>\n"
" BIN=$(scripts/verify-artifact.sh) && dogfood.py \"$BIN\"")
BIN = sys.argv[1]
ENV = {"MCP_VERSION": "1", "PATH": "/usr/bin:/bin", "HOME": os.path.expanduser("~")}
results = []
def record(name, ok, detail=""):
results.append((name, ok, detail))
print(f"{'PASS' if ok else 'FAIL'} {name}" + (f" — {detail}" if detail else ""))
def run(args, **kw):
return subprocess.run([BIN, *args], capture_output=True, text=True, timeout=300, **kw)
v = run(["--version"]).stdout.strip()
record("binary reports build provenance", "commit:" in v, v.replace(chr(10), " | "))
reqs = [
{"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
"clientInfo": {"name": "dogfood", "version": "1"}}},
{"jsonrpc": "2.0", "method": "notifications/initialized"},
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}},
]
payload = "".join(json.dumps(r) + "\n" for r in reqs)
answered = hangs = 0
TRIALS = 30
for _ in range(TRIALS):
try:
p = subprocess.run([BIN], input=payload, capture_output=True, text=True,
env=ENV, timeout=45)
except subprocess.TimeoutExpired:
hangs += 1
continue
ids = set()
for line in p.stdout.splitlines():
line = line.strip()
if line.startswith("{"):
try:
m = json.loads(line)
except json.JSONDecodeError:
continue
if "id" in m:
ids.add(m["id"])
if 2 in ids:
answered += 1
record("MCP one-shot answers every request", answered == TRIALS and hangs == 0,
f"{answered}/{TRIALS} answered, {hangs} hangs")
p = subprocess.Popen([BIN], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True, bufsize=1, env=ENV)
tools = -1
try:
for r in reqs:
p.stdin.write(json.dumps(r) + "\n")
p.stdin.flush()
for _ in range(20):
line = p.stdout.readline()
if not line:
break
line = line.strip()
if line.startswith("{"):
m = json.loads(line)
if m.get("id") == 2:
tools = len(m["result"]["tools"])
break
finally:
p.stdin.close()
p.wait(timeout=20)
record("MCP serves 20 tools", tools == 20, f"{tools} tools")
call = [reqs[0], reqs[1],
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
"params": {"name": "analyze_complexity",
"arguments": {"paths": ["/definitely-not-real-9f3a"]}}}]
p = subprocess.run([BIN], input="".join(json.dumps(r) + "\n" for r in call),
capture_output=True, text=True, env=ENV, timeout=120)
rejected = False
for line in p.stdout.splitlines():
line = line.strip()
if line.startswith("{"):
m = json.loads(line)
if m.get("id") == 3 and "error" in m:
rejected = "not found" in json.dumps(m["error"])
record("MCP rejects nonexistent path", rejected)
r = run(["analyze", "satd", "--path", "/definitely-not-real-9f3a"])
record("CLI satd rejects nonexistent path", r.returncode != 0)
r = run(["five-whys", "the thing is broken", "--depth", "2", "--format", "markdown"])
record("five-whys withholds unlocatable cause", "Not determined" in r.stdout)
record("five-whys reports TDG honestly", "not measured" in r.stdout,
"must not print 0.0/100 for an unmeasured metric")
with tempfile.TemporaryDirectory() as d:
os.makedirs(os.path.join(d, "src"), exist_ok=True)
with open(os.path.join(d, "src", "widget.rs"), "w") as f:
f.write("fn widget_throttle(retries: u32) -> u32 {\n"
" if retries > 3 { retries * 2 } else { retries }\n}\n")
r = run(["five-whys", "widget_throttle retries miscounted",
"--depth", "3", "--format", "markdown", "--path", d])
record("five-whys cites located code", "widget.rs" in r.stdout)
record("five-whys confidence is not pinned to 100%",
"**Confidence**: 100%" not in r.stdout)
r = run(["serve"])
hint = r.stdout + r.stderr
record("serve hint names only the working route",
"MCP_VERSION=1" in hint and "agent mcp-server" not in hint and "PMAT_PMCP_MCP" not in hint)
r = subprocess.run([BIN, "agent", "mcp-server"], capture_output=True, text=True,
stdin=subprocess.DEVNULL, timeout=60)
record("fatal errors are never silent",
r.returncode == 0 or r.stderr.strip() != "")
help_out = run(["--help"])
subcommands = []
in_commands = False
for line in help_out.stdout.splitlines():
if line.startswith("Commands:"):
in_commands = True
continue
if in_commands:
if not line.strip() or not line.startswith(" "):
break
name = line.split()[0]
if name != "help": subcommands.append(name)
subcommands = sorted(set(subcommands))
broken = []
for sc in subcommands:
try:
r = run([sc, "--help"])
if r.returncode != 0:
broken.append(f"{sc}({r.returncode})")
except subprocess.TimeoutExpired:
broken.append(f"{sc}(timeout)")
record(f"CLI: all {len(subcommands)} subcommands render help", not broken,
", ".join(broken) if broken else f"{len(subcommands)} ok")
with tempfile.TemporaryDirectory() as d:
src = os.path.join(d, "src")
os.makedirs(src, exist_ok=True)
with open(os.path.join(src, "lib.rs"), "w") as f:
f.write(
"pub fn tangled(a: u32, b: u32, c: u32) -> u32 {\n"
" if a > 1 { if b > 2 { if c > 3 { return a + b + c; } } }\n"
" // TODO: this is self-admitted technical debt\n"
" let v: Option<u32> = None;\n"
" v.unwrap()\n"
"}\n")
with open(os.path.join(d, "Cargo.toml"), "w") as f:
f.write('[package]\nname = "fixture"\nversion = "0.1.0"\nedition = "2021"\n')
r = run(["analyze", "complexity", "--path", d, "--format", "json"])
ok = r.returncode == 0 and r.stdout.strip().startswith(("{", "["))
record("CLI: analyze complexity emits JSON", ok, f"rc={r.returncode}")
r = run(["analyze", "satd", "--path", d, "--format", "json"])
record("CLI: analyze satd emits JSON",
r.returncode == 0 and r.stdout.strip().startswith(("{", "[")),
f"rc={r.returncode}")
r = run(["context", "--project-path", d, "--format", "json"])
record("CLI: context emits JSON",
r.returncode == 0 and r.stdout.strip().startswith(("{", "[")),
f"rc={r.returncode}")
r = run(["tdg", d, "--format", "json"])
record("CLI: tdg emits JSON",
r.returncode == 0 and r.stdout.strip().startswith(("{", "[")),
f"rc={r.returncode}")
import socket
import time
import urllib.request
def free_port():
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close()
return port
port = free_port()
proc = subprocess.Popen([BIN, "serve", "--port", str(port)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
text=True, stdin=subprocess.DEVNULL)
served = None
deadline = time.time() + 30
while time.time() < deadline:
if proc.poll() is not None:
break
try:
with urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=2) as resp:
served = resp.status
break
except Exception:
try:
with urllib.request.urlopen(f"http://127.0.0.1:{port}/", timeout=2) as resp:
served = resp.status
break
except Exception:
time.sleep(0.5)
out = ""
if proc.poll() is None:
proc.terminate()
try:
out = proc.communicate(timeout=10)[0] or ""
except subprocess.TimeoutExpired:
proc.kill()
else:
out = proc.communicate(timeout=5)[0] or ""
if served is not None:
record("HTTP: serve binds and answers", served < 500, f"status {served}")
else:
unimplemented = "not yet implemented" in out
record("HTTP: serve fails honestly when unimplemented",
unimplemented and "MCP_VERSION=1" in out,
"unimplemented + working hint" if unimplemented
else f"did not bind and did not explain why: {out.strip()[:200]}")
if unimplemented:
manifest = os.path.join(os.path.dirname(os.path.dirname(
os.path.abspath(__file__))), "Cargo.toml")
desc = ""
try:
for line in open(manifest):
if line.startswith("description"):
desc = line
break
except OSError:
pass
claims_http = "HTTP" in desc
record("docs: package description does not claim unimplemented HTTP",
not claims_http,
desc.strip()[:160] if claims_http else "description matches reality")
short = run(["-V"]).stdout.strip()
record("CLI: -V stays a single line", short.count("\n") == 0 and "commit:" not in short,
short)
failed = [n for n, ok, _ in results if not ok]
print()
print(f"{len(results) - len(failed)}/{len(results)} checks passed")
if failed:
print("FAILED:", ", ".join(failed))
sys.exit(1)