Jaarvis / e2e /test_knowledge.py
abc1181's picture
Real accounts, native globe skin, and a way to notice things
6fa11a3 verified
Raw History Blame Contribute Delete
13.5 kB
"""E2E — Phase 6: knowledge, transcripts and the recall that goes in the prompt.
The claim under test is narrow and important: **JARVIS stops asking things it
was already told.** So the checks are not "does the vector store work" — they
are:
* a paraphrase with NO shared keywords still finds the document (that is
embeddings, not grep);
* a past conversation turn is findable by what was said, not by document id;
* the prompt block is empty when there is nothing to say, so the model is
never handed an empty "here is what you remember" header;
* deleting a document really removes it;
* the lexical fallback works with no API key, because a vector store that
returns nothing and looks broken is worse than one that degrades honestly.
"""
from __future__ import annotations
import json
import os
import shutil
import socket
import subprocess
import sys
import time
import urllib.error
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
PORT = int(os.environ.get("JARVIS_E2E_PORT") or "3117")
BASE = f"http://127.0.0.1:{PORT}"
DATA = ROOT / "e2e" / "data_knowledge"
LOG = Path("/tmp/jarvis_e2e_knowledge.log")
_failures: list[str] = []
_passes: list[str] = []
def check(name: str, ok: bool, detail: str = "") -> None:
if ok:
_passes.append(name)
show = detail if detail and any(c.isdigit() for c in str(detail)) else ""
print(f" PASS {name}" + (f" — {show}" if show else ""), flush=True)
else:
_failures.append(f"{name}: {detail}")
print(f" FAIL {name} — {str(detail)[:150]}", flush=True)
# ── unit ─────────────────────────────────────────────────────────────────────
def unit() -> None:
print("== unit: store, chunking, lexical fallback ==", flush=True)
# keep the unit phase out of the repo root
tmp = Path("/tmp/jarvis_e2e_knowledge_unit")
shutil.rmtree(tmp, ignore_errors=True)
tmp.mkdir(parents=True, exist_ok=True)
os.environ["JARVIS_DATA"] = str(tmp)
sys.path.insert(0, str(ROOT))
from core import knowledge as K
K.reset()
check("empty_prompt_block", K.prompt_block() == "",
repr(K.prompt_block()[:40]))
check("empty_search", K.search("") == [] and K.search(" ") == [])
try:
K.ingest("nothing", "y")
check("rejects_empty_text", False, "accepted 1 char")
except ValueError:
check("rejects_empty_text", True)
# chunking: paragraphs joined, oversized paragraphs hard-wrapped
long_para = "word " * 600
parts = K._chunk_text(long_para)
check("chunks.hard_wraps_long_text", len(parts) > 1, f"{len(parts)} parts")
check("chunks.respect_the_limit", all(len(p) <= K.CHUNK_CHARS + 60 for p in parts),
max(len(p) for p in parts))
joined = K._chunk_text("one para\n\ntwo para\n\nthree para")
check("chunks.keep_paragraphs", len(joined) == 1 and "two para" in joined[0],
joined)
# turns
K.record_turn("user", "my hourly rate is 85 euros, I do Next.js and React")
K.record_turn("jarvis", "Noted — 85 EUR per hour")
check("turns.recorded", K.turn_count() == 2, K.turn_count())
check("turns.ignore_empty", K.record_turn("user", " ") is False)
hits = K.search("what do I charge")
kinds = {h["kind"] for h in hits}
check("turns.findable_by_meaning", "turn" in kinds, [h["kind"] for h in hits[:3]])
check("turns.filter_by_role",
all(r["role"] == "jarvis" for r in K.recent_turns(5, role="jarvis")))
# lexical fallback: no key, and it must still find things
real = K._api_key
K._api_key = lambda: ""
try:
K.ingest("Client Acme", "Acme wants a Next.js dashboard. Budget 4000 EUR. "
"Contact ops@acme.example. Weekly sprints, Slack.", source="note")
hits = K.search("acme budget euros")
check("fallback.finds_documents", any(h["kind"] == "doc" for h in hits),
[h["kind"] for h in hits[:3]])
check("fallback.still_finds_turns", any(h["kind"] == "turn" for h in hits))
check("fallback.mode_says_lexical", K.stats()["mode"] == "lexical", K.stats())
finally:
K._api_key = real
# a nonsense query returns nothing rather than everything
check("nonsense_query_is_empty", K.search("zzqqxx unrelated gibberish") == [])
# prompt block
block = K.prompt_block()
check("prompt.has_section", "[WHAT YOU KNOW]" in block, block[:60])
check("prompt.mentions_documents", "stored document" in block, block[:120])
check("prompt.uses_real_newlines", "\\n" not in block and "\n" in block,
repr(block[:60]))
K.note_query("what is the acme budget")
check("note_query_stashes_recall", "[WHAT YOU KNOW]" in K.prompt_block())
K.note_query("hi") # too short to be worth a recall
# forget
docs = K.docs()
check("docs.listed", len(docs) >= 1, len(docs))
target = docs[0]["id"]
check("forget.removes", K.forget(target) is True)
check("forget.gone", all(d["id"] != target for d in K.docs()))
check("forget.missing_is_false", K.forget(99999) is False)
check("chunks_went_with_it",
all(h.get("doc_id") != target for h in K.search("acme budget")))
def embeddings() -> None:
"""Only meaningful with a real key; skipped honestly otherwise."""
print("== unit: semantic recall (needs the API key) ==", flush=True)
sys.path.insert(0, str(ROOT))
from core import knowledge as K
K.reset()
if not K._api_key():
check("semantic.skipped_no_key", True, "no key on this machine")
return
d = K.ingest("Client Acme", "Acme wants a Next.js dashboard. Budget 4000 EUR. "
"Contact ops@acme.example. Weekly sprints, Slack.", source="note")
if not d.get("embedded"):
check("semantic.embeddings_work", False, "no vectors stored")
return
check("semantic.embeddings_work", True, f"{d['chunks']} embedded")
# the real test: no shared keywords with the source text
hits = K.search("what does the customer have money for")
check("semantic.paraphrase_finds_it",
any(h["kind"] == "doc" for h in hits),
[(h["kind"], h["score"]) for h in hits[:2]])
hits2 = K.search("how do they prefer to work together")
check("semantic.second_paraphrase",
any("weekly" in (h.get("text") or "").lower() or h["kind"] == "doc"
for h in hits2), [(h["kind"], h["score"]) for h in hits2[:2]])
check("semantic.stats_mode", K.stats()["mode"] == "semantic", K.stats()["mode"])
# ── server ───────────────────────────────────────────────────────────────────
def _port_free(port: int) -> bool:
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
s.bind(("127.0.0.1", port))
return True
except OSError:
return False
finally:
s.close()
def start() -> subprocess.Popen:
if not _port_free(PORT):
raise RuntimeError(f"port {PORT} in use")
if LOG.exists():
LOG.unlink()
shutil.rmtree(DATA, ignore_errors=True)
DATA.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env.update({"JARVIS_MODE": "server", "JARVIS_PORT": str(PORT),
"JARVIS_DATA": str(DATA), "PYTHONUNBUFFERED": "1"})
p = subprocess.Popen([sys.executable, "-u", "main.py"], cwd=str(ROOT), env=env,
stdout=LOG.open("w"), stderr=subprocess.STDOUT, start_new_session=True)
end = time.time() + 60
while time.time() < end:
try:
with urllib.request.urlopen(f"{BASE}/login", timeout=2) as r:
if r.status < 500:
return p
except Exception:
pass
time.sleep(0.25)
p.kill()
raise RuntimeError("server did not start:\n"
+ (LOG.read_text(errors="replace")[-1200:] if LOG.exists() else ""))
_tok = ""
def token() -> str:
global _tok
if _tok:
return _tok
import re
r = urllib.request.Request(BASE + "/api/bootstrap-key", data=b"{}", method="POST")
with urllib.request.urlopen(r, timeout=30) as x:
key = json.loads(x.read())["key"]
with urllib.request.urlopen(f"{BASE}/auto-login?key={key}", timeout=30) as x:
html = x.read().decode(errors="replace")
_tok = re.search(r"sessionStorage\.setItem\('jarvis_token','([^']+)'", html).group(1)
return _tok
def api(path: str, body: dict | None = None, tok: str | None = None):
data = json.dumps(body).encode() if body is not None else None
r = urllib.request.Request(BASE + path, data=data,
method="POST" if data else "GET")
if data:
r.add_header("Content-Type", "application/json")
if tok:
r.add_header("Authorization", f"Bearer {tok}")
try:
with urllib.request.urlopen(r, timeout=90) as x:
b = x.read()
return x.status, (json.loads(b) if b else {})
except urllib.error.HTTPError as e:
b = e.read()
try:
return e.code, (json.loads(b) if b else {})
except Exception:
return e.code, {}
def served() -> None:
print("== server: /api/knowledge ==", flush=True)
tok = token()
check("auth.401_search", api("/api/knowledge?q=x")[0] == 401)
check("auth.401_post", api("/api/knowledge", {"title": "x", "text": "y"})[0] == 401)
check("auth.401_forget", api("/api/knowledge/forget", {"id": 1})[0] == 401)
st, d = api("/api/knowledge?q=anything", tok=tok)
check("get.search_shape",
st == 200 and "hits" in d and "stats" in d, sorted(d)[:4])
st, d = api("/api/knowledge?view=stats", tok=tok)
check("get.stats", st == 200 and "docs" in d.get("stats", {}), d.get("stats"))
st, d = api("/api/knowledge?view=docs", tok=tok)
check("get.docs", st == 200 and isinstance(d.get("docs"), list), st)
st, d = api("/api/knowledge?view=recent", tok=tok)
check("get.recent", st == 200 and isinstance(d.get("turns"), list), st)
st, d = api("/api/knowledge",
{"title": "Client Globex", "text": "Globex pays 3000 EUR for a "
"landing page. Primary contact sales@globex.example.",
"source": "client"}, tok=tok)
check("post.stores", st == 201 and d.get("chunks", 0) >= 1, d)
doc_id = d.get("id")
st, d = api("/api/knowledge?q=globex%20payment", tok=tok)
found = [h for h in d.get("hits", []) if h.get("kind") == "doc"]
check("post.findable", any("Globex" in (h.get("title") or "") for h in found),
[h.get("title") for h in d.get("hits", [])[:3]])
st, d = api("/api/knowledge", {"title": "x", "text": "y"}, tok=tok)
check("post.rejects_too_short", st == 400, st)
st, d = api("/api/knowledge/forget", {"id": doc_id}, tok=tok)
check("post.forget", st == 200 and d.get("ok") is True, d)
st, d = api("/api/knowledge?q=globex", tok=tok)
check("post.forget_removed_it",
not any("Globex" in (h.get("title") or "") for h in d.get("hits", [])),
[h.get("title") for h in d.get("hits", [])[:3]])
# forgetting is a delete, so it goes through the policy's audit trail
st, d = api("/api/audit?action=knowledge.forget&limit=5", tok=tok)
rows = d.get("rows", [])
check("forget_is_audited", bool(rows) and rows[0]["tier"] == "delete",
rows[0] if rows else d)
app = (ROOT / "dashboard" / "static" / "app.html").read_text(encoding="utf-8")
for probe, label in [('id="kn-btn"', "header button"),
("function openKnowledge(", "panel opener"),
("_knRender", "renderer"),
("_knForget", "forget action"),
("/api/knowledge?view=recent", "recent tab")]:
check(f"client.{label.replace(' ', '_')}", probe in app, probe)
mp = (ROOT / "main.py").read_text(encoding="utf-8")
check("tool.declared", '"name": "knowledge"' in mp)
check("tool.dispatched", 'elif name == "knowledge":' in mp)
check("tool.turns_persisted", 'record_turn("user"' in mp)
check("tool.recall_in_prompt", "_kn.prompt_block()" in mp)
def main() -> int:
unit()
embeddings()
proc = start()
try:
served()
finally:
proc.terminate()
try:
proc.wait(timeout=8)
except Exception:
proc.kill()
log = LOG.read_text(errors="replace") if LOG.exists() else ""
check("boot.no_traceback", "Traceback (most recent call last)" not in log,
log[-400:] if "Traceback" in log else "")
print()
print(f"PASS {len(_passes)} FAIL {len(_failures)}")
for f in _failures:
print(f" x {f}")
return 1 if _failures else 0
def _hard_exit(code: int) -> None:
"""Leave immediately. A suite that has printed its result can still hang in
interpreter teardown (a Playwright browser, a thread that never joins), which
is the difference between a 2-minute suite and a 15-minute one."""
try:
sys.stdout.flush()
sys.stderr.flush()
except Exception:
pass
os._exit(code)
if __name__ == "__main__":
_hard_exit(main())