diff --git a/docs/r3/browser-HEL-835.txt b/docs/r3/browser-HEL-835.txt new file mode 100644 index 0000000..4b28c7c --- /dev/null +++ b/docs/r3/browser-HEL-835.txt @@ -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 diff --git a/docs/r3/e2e-HEL-835.py b/docs/r3/e2e-HEL-835.py new file mode 100644 index 0000000..4404bb7 --- /dev/null +++ b/docs/r3/e2e-HEL-835.py @@ -0,0 +1,164 @@ +"""HEL-835 independent end-to-end review: real R3 app + ControlledLLM + isolated DB. + +Run: PYTHONPATH=src:tests 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() diff --git a/docs/r3/e2e-HEL-835.txt b/docs/r3/e2e-HEL-835.txt new file mode 100644 index 0000000..d040d9c --- /dev/null +++ b/docs/r3/e2e-HEL-835.txt @@ -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 diff --git a/docs/r3/pytest-rerun-HEL-835.txt b/docs/r3/pytest-rerun-HEL-835.txt new file mode 100644 index 0000000..0d801fb --- /dev/null +++ b/docs/r3/pytest-rerun-HEL-835.txt @@ -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 diff --git a/docs/r3/web-build-HEL-835.txt b/docs/r3/web-build-HEL-835.txt new file mode 100644 index 0000000..aca75de --- /dev/null +++ b/docs/r3/web-build-HEL-835.txt @@ -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 diff --git a/docs/r3/web-lint-HEL-835.txt b/docs/r3/web-lint-HEL-835.txt new file mode 100644 index 0000000..68102c9 --- /dev/null +++ b/docs/r3/web-lint-HEL-835.txt @@ -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) + diff --git a/docs/r3/web-test-HEL-835.txt b/docs/r3/web-test-HEL-835.txt new file mode 100644 index 0000000..fe1b935 --- /dev/null +++ b/docs/r3/web-test-HEL-835.txt @@ -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) +