1
0
forked from jason/echo
Files
chorus/eval/test_multimember.py
T

149 lines
7.7 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""test_multimember.py — two members share one vault (end-to-end vs the mock).
Covers the schema-5 group behavior: jason bootstraps; alice self-onboards
via load; both capture; the daily log interleaves member-tagged lines; scopes are
independent; triage sees member+group inboxes; lint is clean; schema is 5."""
import os
import subprocess
import sys
import time
import urllib.request
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SCRIPTS = REPO / "chorus-memory.plugin.src" / "skills" / "chorus-memory" / "scripts"
PORT = 8807
BASE = f"http://127.0.0.1:{PORT}"
fails = []
def check(name, cond, detail=""):
print(("ok " if cond else "FAIL") + f" {name}" + (f" — {detail}" if detail and not cond else ""))
if not cond:
fails.append(name)
def run(member, script, *args, stdin=None):
env = {k: v for k, v in os.environ.items() if not k.startswith("CHORUS_")}
env.update({"CHORUS_BASE": BASE, "CHORUS_KEY": "k", "CHORUS_GROUP": "MPM",
"CHORUS_MEMBER": member, "CHORUS_TODAY": "2026-07-21"})
return subprocess.run([sys.executable, str(SCRIPTS / script), *args],
capture_output=True, text=True, env=env, input=stdin)
def vault(path):
try:
return urllib.request.urlopen(f"{BASE}/vault/{path}", timeout=2).read().decode()
except Exception:
return None
mock = subprocess.Popen([sys.executable, str(REPO / "eval/mock_olrapi.py"), "--port", str(PORT)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
for _ in range(50):
try:
urllib.request.urlopen(f"{BASE}/__debug__reset", data=b"", timeout=1)
break
except Exception:
time.sleep(0.1)
try:
# --- jason stands up the vault ---
r = run("jason", "bootstrap.py")
check("jason bootstraps", r.returncode == 0, r.stderr[:200])
marker = vault("_agent/chorus-vault.md") or ""
check("marker is schema 5", "schema_version: 5" in marker, marker[:120])
check("group profile seeded + jason rostered",
"- jason (joined 2026-07-21)" in (vault("_agent/memory/semantic/group-profile.md") or ""))
check("jason anchors exist", vault("_agent/members/jason/preferences.md") is not None
and "author: jason" in vault("_agent/members/jason/preferences.md"))
check("group inbox seeded", vault("inbox/captures/group.md") is not None)
# --- alice self-onboards via load ---
r = run("alice", "chorus.py", "load")
check("alice load self-onboards", "self-onboarding: member 'alice'" in r.stdout, r.stdout[:600])
check("alice anchors created", vault("_agent/members/alice/preferences.md") is not None)
profile = vault("_agent/memory/semantic/group-profile.md") or ""
check("alice rostered in group profile", "- alice (joined 2026-07-21)" in profile, profile[-200:])
r = run("alice", "chorus.py", "load")
check("second alice load does NOT re-onboard", "self-onboarding" not in r.stdout)
check("load reads group profile + both inboxes",
"group-profile" in r.stdout and "my-inbox" in r.stdout and "group-inbox" in r.stdout)
# --- both capture; daily log interleaves member-tagged lines ---
run("jason", "chorus.py", "capture", "Acme Rockets", "--kind", "company")
run("alice", "chorus.py", "capture", "Widget Live", "--kind", "project")
daily = vault("journal/daily/2026-07-21.md") or ""
check("daily log tags jason", "[jason]: created company" in daily, daily[-300:])
check("daily log tags alice", "[alice]: created project" in daily, daily[-300:])
check("captures carry authors",
"author: jason" in (vault("resources/companies/acme-rockets.md") or "")
and "author: alice" in (vault("projects/active/widget-live.md")
or vault("projects/incubating/widget-live.md") or ""))
# --- alice updates jason's entity: dated bullet attributes alice ---
run("alice", "chorus.py", "capture", "Acme Rockets", "--kind", "company", "-",
stdin="Alice met their CTO.\n")
acme = vault("resources/companies/acme-rockets.md") or ""
check("cross-member update attributed", "- 2026-07-21 [alice]: Alice met their CTO." in acme,
acme[-300:])
# --- scopes are independent (raw files: the basic mock's heading-PATCH only
# appends, so real replace semantics live in test_patch_semantics; here we
# prove the writes land in EACH member's own file and nowhere else) ---
r1 = run("jason", "chorus.py", "scope", "set", "fiber buildout")
r2 = run("alice", "chorus.py", "scope", "set", "widget launch")
jctx = vault("_agent/members/jason/current-context.md") or ""
actx = vault("_agent/members/alice/current-context.md") or ""
check("scope set succeeds per member", r1.returncode == 0 and r2.returncode == 0,
r1.stderr[:150] + r2.stderr[:150])
check("scopes independent", "fiber buildout" in jctx and "widget launch" in actx
and "widget launch" not in jctx and "fiber buildout" not in actx)
# --- member inbox + group inbox; triage lists both ---
run("jason", "chorus.py", "capture", "look into starlink backhaul", "--inbox")
run("jason", "chorus.py", "append", "inbox/captures/group.md",
"- 2026-07-21 [jason]: someone should test the new proxy")
r = run("alice", "chorus.py", "triage", "--list")
check("alice's triage sees the group item", "test the new proxy" in r.stdout, r.stdout[:400])
check("alice's triage does NOT see jason's personal item",
"starlink" not in r.stdout, r.stdout[:400])
# --- session logs namespaced; lint clean ---
run("jason", "chorus.py", "put", "_agent/sessions/jason/2026-07-21-0900-fiber.md", "-",
stdin="---\ntype: session-log\nstatus: done\ncreated: 2026-07-21\nupdated: 2026-07-21\n"
"tags: [session]\nagent_written: true\nauthor: jason\nsource_notes: []\n---\n# Fiber session\n")
# --- Phase 4: gate names the existing entity's author ---
r = run("alice", "chorus.py", "capture", "Acme Rockets Inc", "--kind", "company")
check("gate blocks near-duplicate", r.returncode == 76, f"rc={r.returncode}")
check("gate names the author", "created by jason" in r.stdout, r.stdout[:300])
# --- Phase 4: recall --author filters to one member's notes ---
r = run("jason", "chorus.py", "recall", "Acme", "--author", "jason")
check("recall --author keeps jason's note", "acme-rockets" in r.stdout, r.stdout[:300])
r = run("jason", "chorus.py", "recall", "Acme", "--author", "alice")
check("recall --author filters out others", "acme-rockets" not in r.stdout.split("Linked context")[0].split("Primary")[-1], r.stdout[:400])
r = run("jason", "chorus.py", "recall", "Acme")
check("recall surfaces author stamp", "author: jason" in r.stdout, r.stdout[:400])
# --- Phase 4: load --all-members group view ---
run("jason", "chorus.py", "put", "_agent/members/jason/heartbeat.md", "-",
stdin="_agent/sessions/jason/2026-07-21-0900-fiber.md @ 2026-07-21T14:00:00Z\n")
r = run("alice", "chorus.py", "load", "--all-members")
check("load --all-members lists both", "group: all members (2)" in r.stdout
and "alice (me)" in r.stdout and "jason" in r.stdout, r.stdout[-600:])
check("all-members shows jason's heartbeat", "2026-07-21-0900-fiber" in r.stdout,
r.stdout[-600:])
run("jason", "sweep.py", "--apply") # index the seeded anchors (documented bootstrap→sweep flow)
run("jason", "chorus.py", "link", "_agent/memory/semantic/group-profile.md",
"resources/companies/acme-rockets.md")
r = run("jason", "vault_lint.py")
check("lint clean on two-member vault", r.returncode == 0, r.stdout[-500:])
finally:
mock.terminate()
print()
sys.exit(1 if fails else print("all multimember tests passed") or 0)