Files
echo/eval/test_features.py
T

136 lines
6.1 KiB
Python
Raw Normal View History

2026-06-21 11:46:54 -05:00
#!/usr/bin/env python3
"""test_features.py — end-to-end test of the v0.9 features against the mock OLRAPI.
Exercises the real shipped scripts (echo.py capture/resolve/recall/link + sweep.py)
through subprocess, then reads ground truth back from the mock. No network, no creds,
no live vault.
Run: python test_features.py [--port 8801]
"""
import argparse, json, os, subprocess, sys, time, urllib.request, urllib.error
from pathlib import Path
HERE = Path(__file__).resolve().parent
SCRIPTS = HERE.parent / "echo-memory.plugin.src" / "skills" / "echo-memory" / "scripts"
ECHO = SCRIPTS / "echo.py"
SWEEP = SCRIPTS / "sweep.py"
KEY = "241265fbe6830934a9a4ad3e69335f64a42153b663aa5b0017cb1ea1217b2bab"
failures = []
def check(name, cond, detail=""):
print(f"{'ok ' if cond else 'FAIL'} {name}" + (f" -- {detail}" if not cond else ""))
if not cond:
failures.append(name)
class Harness:
def __init__(self, base):
self.base = base
def http(self, method, url, body=None, headers=None):
data = body.encode() if isinstance(body, str) else body
req = urllib.request.Request(url, data=data, method=method,
headers={"Authorization": f"Bearer {KEY}", **(headers or {})})
try:
with urllib.request.urlopen(req, timeout=10) as r:
return r.status, r.read().decode("utf-8", "replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode("utf-8", "replace")
except Exception as e:
return 0, str(e)
def reset(self):
self.http("POST", f"{self.base}/__debug__reset")
def seed(self, path, content):
self.http("PUT", f"{self.base}/vault/{path}", content)
def ground(self, path):
_, body = self.http("GET", f"{self.base}/__debug__?path={path}")
return None if body == "<<MISSING>>" else body
def echo(self, *args):
env = dict(os.environ, ECHO_BASE=self.base, ECHO_KEY=KEY, ECHO_VERIFY="1",
ECHO_TODAY="2026-06-21")
return subprocess.run([sys.executable, str(args[0]), *args[1:]],
capture_output=True, text=True, env=env)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=8801)
a = ap.parse_args()
base = f"http://127.0.0.1:{a.port}"
srv = subprocess.Popen([sys.executable, str(HERE / "mock_olrapi.py"), "--port", str(a.port)],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
try:
for _ in range(50):
try:
urllib.request.urlopen(f"{base}/__debug__reset", data=b"", timeout=1); break
except Exception:
time.sleep(0.1)
h = Harness(base)
h.reset()
h.seed("_agent/echo-vault.md", "---\nschema_version: 2\n---\n# marker\n")
# 1. capture a person (with an alias)
r = h.echo(ECHO, "capture", "Bob Smith", "--kind", "person", "--aliases", "bob,rs")
note = h.ground("resources/people/bob-smith.md")
check("capture person creates note", note is not None and "type: person" in note, r.stderr)
check("capture stamps agent_written", note and "agent_written: true" in note)
idx = h.ground("_agent/index/entities.json")
check("index records entity", idx and "bob-smith" in idx and '"bob"' in idx)
# 2. resolve by alias
r = h.echo(ECHO, "resolve", "bob")
out = json.loads(r.stdout) if r.stdout.strip().startswith("{") else {}
check("resolve alias -> path", out.get("match") and out.get("path") == "resources/people/bob-smith.md", r.stdout)
# 3. capture a company whose body mentions Bob Smith -> auto bidirectional link
r = subprocess.run([sys.executable, str(ECHO), "capture", "MPM", "--kind", "company", "-"],
input="Bob Smith is the principal here.\n", capture_output=True, text=True,
env=dict(os.environ, ECHO_BASE=base, ECHO_KEY=KEY, ECHO_TODAY="2026-06-21"))
mpm = h.ground("resources/companies/mpm.md")
bob = h.ground("resources/people/bob-smith.md")
check("auto-link forward (mpm -> bob)", mpm and "[[resources/people/bob-smith]]" in mpm, r.stdout + r.stderr)
check("auto-link reciprocal (bob -> mpm)", bob and "[[resources/companies/mpm]]" in bob)
# 4. recall an entity surfaces its linked neighborhood (search is mocked empty,
# so this exercises the index-resolve + 1-hop expansion path)
r = h.echo(ECHO, "recall", "MPM")
check("recall surfaces linked person", "resources/people/bob-smith.md" in r.stdout, r.stdout)
# 5. explicit link between two fresh notes
h.echo(ECHO, "capture", "Alpha", "--kind", "concept")
h.echo(ECHO, "capture", "Beta", "--kind", "concept")
h.echo(ECHO, "link", "resources/concepts/alpha.md", "resources/concepts/beta.md")
alpha = h.ground("resources/concepts/alpha.md")
beta = h.ground("resources/concepts/beta.md")
check("link adds A->B", alpha and "[[resources/concepts/beta]]" in alpha)
check("link adds B->A", beta and "[[resources/concepts/alpha]]" in beta)
# 6. inbox capture (unknown home)
h.echo(ECHO, "capture", "stray thought", "--inbox")
inbox = h.ground("inbox/captures/inbox.md")
check("inbox capture lands", inbox and "stray thought" in inbox)
# 7. sweep --apply rebuilds the index, symmetrizes, stamps schema 3
r = h.echo(SWEEP, "--apply")
marker = h.ground("_agent/echo-vault.md")
check("sweep runs clean", r.returncode == 0, r.stdout + r.stderr)
check("sweep stamps schema 3", marker and "schema_version=3" in marker.replace(": ", "="), marker)
idx2 = h.ground("_agent/index/entities.json")
check("sweep rebuilt index has all entities",
idx2 and all(s in idx2 for s in ["bob-smith", "mpm", "alpha", "beta"]))
print(f"\n{len(failures)} failure(s)" if failures else "\nall feature tests passed")
return 1 if failures else 0
finally:
srv.terminate()
if __name__ == "__main__":
raise SystemExit(main())