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