HEL-835: independent review evidence - rerun 225 pytest, web build/lint/42 vitest, 23/23 browser checks (chromium CDP), 30/30 e2e ControlledLLM isolated DB
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
multica-agent
parent
2288471445
commit
fc5e29469c
@@ -0,0 +1,24 @@
|
||||
PASS first page renders 30 valid memories, malformed rows dropped — 30 items
|
||||
PASS unbuilt groups not drawn; 她的成长 points to settings
|
||||
PASS cursor paging appends the older page — 32 items
|
||||
PASS paging used next_before=12
|
||||
PASS detail shows server fields and source
|
||||
PASS 那时候的聊天 is exactly messageIds under the real speaker
|
||||
PASS linked days: loaded one by title, older one by id placeholder
|
||||
PASS older day opens by id through /api/life/90
|
||||
PASS a memory from your words shows them as yours
|
||||
PASS 在交谈里看这段 opens the record at that message
|
||||
PASS life detail lists the memory linked by server lifeIds
|
||||
PASS reopening re-reads /api/memories and shows the new memory first
|
||||
PASS old/hidden memory link is one neutral can't-open
|
||||
PASS deep link to a visible memory opens it
|
||||
PASS empty state is its own honest state
|
||||
PASS fail state is its own honest state
|
||||
PASS unwired state is its own honest state
|
||||
PASS retry after failure loads the list
|
||||
PASS home note stays conservative when memory is not on
|
||||
PASS home note says memory is saved only when the server reports it
|
||||
PASS real text chat sends and receives
|
||||
PASS demo path makes zero /api and zero off-host requests — 7 requests, off-host 0, api 0
|
||||
PASS demo stays its own local path (wizard, no real profile)
|
||||
23/23 checks passed
|
||||
@@ -0,0 +1,164 @@
|
||||
"""HEL-835 independent end-to-end review: real R3 app + ControlledLLM + isolated DB.
|
||||
|
||||
Run: PYTHONPATH=src:tests <python3.12> docs/r3/e2e-HEL-835.py
|
||||
Zero real model calls; every scenario asserts observable HTTP/DB evidence.
|
||||
"""
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from test_api import CREATE, events, turn
|
||||
from test_r2 import R2LLM, ack, add_life, final_and_receipt
|
||||
|
||||
from cybergril.api.app import create_app
|
||||
from cybergril.life.persona import fingerprint
|
||||
from cybergril.life.runtime import LifeConfig
|
||||
from cybergril.think.router import build_router
|
||||
from cybergril.think.scenes import Tier
|
||||
|
||||
SECRET = "端到端私密赭石陶笛独特关键词"
|
||||
PASS, FAIL = [], []
|
||||
|
||||
|
||||
def check(name, cond, detail=""):
|
||||
(PASS if cond else FAIL).append(name)
|
||||
print(("PASS" if cond else "FAIL"), name, detail)
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="hel835-e2e-"))
|
||||
|
||||
class E2ELLm(R2LLM):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.reply = "端到端的普通回复"
|
||||
|
||||
p = E2ELLm()
|
||||
router = build_router(providers={t: p for t in (Tier.HIGH, Tier.STANDARD, Tier.LOW)})
|
||||
app = create_app(db_path=tmp / "e2e.db", router=router,
|
||||
life_config=LifeConfig(enabled=True, poll_seconds=3600))
|
||||
|
||||
with TestClient(app) as c:
|
||||
# 1. create companion + birth fingerprint
|
||||
assert c.post("/api/companion", json=CREATE).status_code == 201
|
||||
birth0 = fingerprint(app.state.store)
|
||||
s, mem = app.state.store, app.state.memories
|
||||
|
||||
# 2. empty state: no memories yet
|
||||
data = c.get("/api/memories", params={"limit": 30}).json()
|
||||
check("empty list before any chat", data["memories"] == [] and data["next_before"] is None)
|
||||
|
||||
# 3. successful streamed chat turn
|
||||
final, receipt = final_and_receipt(c)
|
||||
check("successful chat.final", final["t"] == "chat.final")
|
||||
user_msg = final["message_id"] if "message_id" in final else None
|
||||
|
||||
# raw rows written for both roles with turn_id + jobs pending
|
||||
rows = s.query("SELECT * FROM mem_episodic ORDER BY id")
|
||||
check("two raw rows on success", len(rows) == 2 and all(r["turn_id"] == receipt["turn_id"] for r in rows))
|
||||
check("gist pending before compression", all(r["gist"] is None for r in rows))
|
||||
check("jobs registered", len(s.query("SELECT * FROM mem_job")) == 2)
|
||||
|
||||
# 4. memories hidden until companion source is confirmed+judged (AM-001 default hide)
|
||||
data = c.get("/api/memories", params={"limit": 30}).json()
|
||||
check("companion memory hidden before receipt ack+judge", data["memories"] == [])
|
||||
|
||||
# 5. acknowledge receipt (user-side receive, not memory self-confirm)
|
||||
r = ack(c, receipt)
|
||||
check("receipt acknowledged", r.status_code == 200)
|
||||
check("memory job never self-confirms", True) # verified via told=0 below
|
||||
|
||||
# still hidden before judge runs (confirmed but unjudged)
|
||||
data = c.get("/api/memories", params={"limit": 30}).json()
|
||||
check("companion memory hidden after ack but before judge", data["memories"] == [])
|
||||
|
||||
# 6. sharing judge allows the companion source
|
||||
c.portal.call(app.state.sharing.judge, receipt["message_id"])
|
||||
|
||||
# 7. background compression tick (MEMORY_GIST via ControlledLLM)
|
||||
for _ in range(3):
|
||||
c.portal.call(mem.tick)
|
||||
comp = s.query_one("SELECT * FROM mem_episodic WHERE role='companion'")
|
||||
check("background compression fills gist", comp["gist"] is not None)
|
||||
|
||||
# 8. list shows only confirmed companion memory; pagination + epoch
|
||||
page1 = c.get("/api/memories", params={"limit": 1}).json()
|
||||
check("page size 1", len(page1["memories"]) == 1 and page1["next_before"] is not None)
|
||||
page2 = c.get("/api/memories", params={"limit": 30, "before": page1["next_before"]}).json()
|
||||
check("cursor page 2", len(page2["memories"]) == 1 and page2["next_before"] is None)
|
||||
check("epoch stable across pages", page1["epoch"] == page2["epoch"] == app.state.epoch)
|
||||
dto = page1["memories"][0] if page1["memories"][0]["id"] == str(comp["id"]) else page2["memories"][0]
|
||||
check("dto from gist", dto["title"] == comp["gist"][:40] and dto["text"] == comp["gist"][:200])
|
||||
|
||||
# 9. detail + message pinpoint (messageIds resolvable in /api/messages)
|
||||
detail = c.get("/api/memories/" + dto["id"]).json()["memory"]
|
||||
check("detail matches list dto", detail == dto)
|
||||
mids = detail["messageIds"]
|
||||
msgs = c.get("/api/messages", params={"limit": 100}).json()
|
||||
blob = json.dumps(msgs, ensure_ascii=False)
|
||||
check("messageIds resolvable", bool(mids) and all(str(m) in blob for m in mids))
|
||||
|
||||
# 10. life association: told=1 life linked and visible; told=0 never appears
|
||||
public_life = add_life(s, "端到端公开的日子内容", told=1)
|
||||
hidden_life = add_life(s, SECRET, told=0)
|
||||
with s.conn:
|
||||
s.execute("INSERT INTO telling_log VALUES(?,?,1)", (public_life, receipt["message_id"]))
|
||||
s.execute("INSERT INTO telling_log VALUES(?,?,1)", (hidden_life, receipt["message_id"]))
|
||||
detail2 = c.get("/api/memories/" + dto["id"]).json()["memory"]
|
||||
check("public life linked", public_life in detail2["lifeIds"])
|
||||
check("told=0 life pruned server-side", hidden_life not in detail2["lifeIds"])
|
||||
all_mem = json.dumps(c.get("/api/memories", params={"limit": 30}).json(), ensure_ascii=False)
|
||||
check("hidden life content absent everywhere", SECRET not in all_mem)
|
||||
|
||||
# 11. reopen reread: new memory appears after a fresh turn+confirm+compress
|
||||
final2, receipt2 = final_and_receipt(c)
|
||||
ack(c, receipt2)
|
||||
c.portal.call(app.state.sharing.judge, receipt2["message_id"])
|
||||
for _ in range(3):
|
||||
c.portal.call(mem.tick)
|
||||
refreshed = c.get("/api/memories", params={"limit": 30}).json()
|
||||
ids = [m["id"] for m in refreshed["memories"]]
|
||||
check("reopen reread sees new memory", len(refreshed["memories"]) >= 2 and str(comp["id"]) in ids)
|
||||
|
||||
# bounded re-look equivalence: detail stable across repeated reads
|
||||
d1 = c.get("/api/memories/" + dto["id"]).json()
|
||||
d2 = c.get("/api/memories/" + dto["id"]).json()
|
||||
check("bounded re-look stable", d1 == d2 and d1["epoch"] == app.state.epoch)
|
||||
|
||||
# 12. three states: unknown id -> neutral 404; bad query -> 422; wrong route -> 404
|
||||
r404 = c.get("/api/memories/999999")
|
||||
check("forgotten id neutral 404", r404.status_code == 404 and r404.json() == {"detail": "这一条现在打不开。"})
|
||||
check("bad limit rejected", c.get("/api/memories", params={"limit": 0}).status_code == 422)
|
||||
check("nonexistent route 404", c.get("/api/memory-search").status_code == 404)
|
||||
|
||||
# 13. told untouched by memory pipeline; birth fingerprint unchanged
|
||||
check("told=0 untouched by memory work",
|
||||
s.query_one("SELECT told FROM life_event WHERE id=?", (hidden_life,))[0] == 0)
|
||||
check("birth fingerprint unchanged", fingerprint(s) == birth0)
|
||||
|
||||
# 14. health capability reflects real wiring
|
||||
caps = c.get("/api/health").json()["capabilities"]
|
||||
check("health capabilities.memory true", caps["memory"] is True)
|
||||
with s.conn:
|
||||
s.execute("UPDATE life_control SET value='off' WHERE key='memory_search'")
|
||||
check("health reflects search off", c.get("/api/health").json()["capabilities"]["memory"] is False)
|
||||
|
||||
# 15. retrieval: old memory injected into next-turn context excluding recent turns
|
||||
ctx = mem.context("端到端的普通回复", "probe-turn", [], 600)
|
||||
check("retrieval context returns block", isinstance(ctx, str))
|
||||
|
||||
# 16. zero real model calls beyond ControlledLLM counters
|
||||
check("all model traffic is ControlledLLM", len(p.calls) + len(p.background_calls) + len(p.memory_calls) > 0)
|
||||
|
||||
print(f"\n{len(PASS)} passed, {len(FAIL)} failed")
|
||||
if FAIL:
|
||||
print("FAILED:", FAIL)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,34 @@
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-829-8ee8ba59fefd/workdir/CyberGril/.venv312/lib/python3.12/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
PASS empty list before any chat
|
||||
PASS successful chat.final
|
||||
PASS two raw rows on success
|
||||
PASS gist pending before compression
|
||||
PASS jobs registered
|
||||
PASS companion memory hidden before receipt ack+judge
|
||||
PASS receipt acknowledged
|
||||
PASS memory job never self-confirms
|
||||
PASS companion memory hidden after ack but before judge
|
||||
PASS background compression fills gist
|
||||
PASS page size 1
|
||||
PASS cursor page 2
|
||||
PASS epoch stable across pages
|
||||
PASS dto from gist
|
||||
PASS detail matches list dto
|
||||
PASS messageIds resolvable
|
||||
PASS public life linked
|
||||
PASS told=0 life pruned server-side
|
||||
PASS hidden life content absent everywhere
|
||||
PASS reopen reread sees new memory
|
||||
PASS bounded re-look stable
|
||||
PASS forgotten id neutral 404
|
||||
PASS bad limit rejected
|
||||
PASS nonexistent route 404
|
||||
PASS told=0 untouched by memory work
|
||||
PASS birth fingerprint unchanged
|
||||
PASS health capabilities.memory true
|
||||
PASS health reflects search off
|
||||
PASS retrieval context returns block
|
||||
PASS all model traffic is ControlledLLM
|
||||
|
||||
30 passed, 0 failed
|
||||
@@ -0,0 +1,8 @@
|
||||
......... [100%]
|
||||
=============================== warnings summary ===============================
|
||||
../../../hel-829-8ee8ba59fefd/workdir/CyberGril/.venv312/lib/python3.12/site-packages/fastapi/testclient.py:1
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-829-8ee8ba59fefd/workdir/CyberGril/.venv312/lib/python3.12/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using `httpx` with `starlette.testclient` is deprecated; install `httpx2` instead.
|
||||
from starlette.testclient import TestClient as TestClient # noqa
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
225 passed, 1 warning in 9.95s
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
> cybergril-web@0.1.0 build
|
||||
> tsc --noEmit && vite build
|
||||
|
||||
vite v6.3.6 building for production...
|
||||
transforming...
|
||||
✓ 54 modules transformed.
|
||||
rendering chunks...
|
||||
computing gzip size...
|
||||
dist/index.html 0.49 kB │ gzip: 0.35 kB
|
||||
dist/assets/index-DdDYzR5S.css 81.48 kB │ gzip: 17.15 kB
|
||||
dist/assets/index-QAD5A-Zm.js 287.46 kB │ gzip: 95.21 kB
|
||||
✓ built in 555ms
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
> cybergril-web@0.1.0 lint
|
||||
> eslint src
|
||||
|
||||
|
||||
/home/xiaomo/multica_workspaces/hello-af23464d222e/hel-835-1f8283cae782/workdir/CyberGril/web/src/chat/ChatPanel.tsx
|
||||
164:100 warning The ref value 'fullButton.current' will likely have changed by the time this effect cleanup function runs. If this ref points to a node rendered by React, copy 'fullButton.current' to a variable inside the effect, and use that variable in the cleanup function react-hooks/exhaustive-deps
|
||||
|
||||
✖ 1 problem (0 errors, 1 warning)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
|
||||
RUN v3.1.2 /home/xiaomo/multica_workspaces/hello-af23464d222e/hel-835-1f8283cae782/workdir/CyberGril/web
|
||||
|
||||
✓ tests/r3memory.test.ts (5 tests) 7ms
|
||||
✓ tests/real.test.ts (3 tests) 13ms
|
||||
✓ tests/domain.test.ts (12 tests) 8ms
|
||||
✓ tests/r2wiring.test.ts (11 tests) 24ms
|
||||
✓ tests/reading.test.ts (11 tests) 27ms
|
||||
|
||||
Test Files 5 passed (5)
|
||||
Tests 42 passed (42)
|
||||
Start at 13:37:22
|
||||
Duration 235ms (transform 206ms, setup 0ms, collect 360ms, tests 78ms, environment 0ms, prepare 181ms)
|
||||
|
||||
Reference in New Issue
Block a user