310 lines
18 KiB
Python
310 lines
18 KiB
Python
"""Real SQLite source-linked tools, isolated from every user's memory store."""
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from hermes_constants import get_hermes_home
|
|
from hermes_state import SessionDB
|
|
from plugins.memory.holographic import HolographicMemoryProvider
|
|
|
|
|
|
@pytest.fixture
|
|
def project_env(tmp_path):
|
|
home = get_hermes_home()
|
|
db = SessionDB(home / "state.db")
|
|
roots = {name: tmp_path / name for name in ("alpha", "beta")}
|
|
for root in roots.values():
|
|
root.mkdir()
|
|
for sid, project in (("current", "alpha"), ("source", "alpha"), ("other", "beta")):
|
|
db.create_session(sid, source="desktop", cwd=str(roots[project]))
|
|
mid = db.append_message("source", role="user", content='Use "Atlas" for deployment')
|
|
other = db.append_message("other", role="user", content="FOREIGN_PRIVATE deployment")
|
|
provider = HolographicMemoryProvider({"project_scoped": True, "hrr_dim": 64,
|
|
"db_path": str(tmp_path / "forbidden-global.db")})
|
|
provider.initialize("current")
|
|
yield provider, db, mid, other, roots
|
|
provider.shutdown()
|
|
db.close()
|
|
|
|
|
|
def call(provider, action, **args):
|
|
return json.loads(provider.handle_tool_call("fact_store", {"action": action, **args}))
|
|
|
|
|
|
def source(mid, sid="source", **extra):
|
|
return {"session_id": sid, "message_id": mid, **extra}
|
|
|
|
|
|
def test_source_linked_tool_roundtrip_and_revocation(project_env):
|
|
provider, db, mid, other, roots = project_env
|
|
rejected = call(provider, "add", content="unlinked candidate")
|
|
assert rejected["success"] is False
|
|
assert rejected["status"] == "sources_required"
|
|
added = call(provider, "add", content='"Atlas" deployment candidate', sources=[source(mid)])
|
|
assert added["success"] and added["status"] == "candidate"
|
|
assert added["identity"]["current_session_id"] == "current"
|
|
fact_id = added["fact_id"]
|
|
for query in ({"action": "list"}, {"action": "search", "query": "Atlas"},
|
|
{"action": "probe", "entity": "Atlas"}, {"action": "related", "entity": "Atlas"},
|
|
{"action": "reason", "entities": ["Atlas"]}):
|
|
result = call(provider, **query)
|
|
rows = result.get("facts", result.get("results"))
|
|
assert result["success"] and any(r["fact_id"] == fact_id for r in rows)
|
|
assert all(r["assertion"] == "candidate_not_confirmed" and r["sources"] for r in rows)
|
|
denied = call(provider, "add", content="FOREIGN_PRIVATE", sources=[source(other, "other")])
|
|
assert not denied["success"]
|
|
assert "FOREIGN_PRIVATE" not in json.dumps(denied)
|
|
assert not (roots["alpha"].parent / "forbidden-global.db").exists()
|
|
assert not (get_hermes_home() / "memory_store.db").exists()
|
|
db._conn.execute("UPDATE messages SET content=? WHERE id=?", ("new deployment source", mid))
|
|
db._conn.commit()
|
|
for query in ({"action": "list"}, {"action": "search", "query": "Atlas"},
|
|
{"action": "probe", "entity": "Atlas"}, {"action": "related", "entity": "Atlas"},
|
|
{"action": "reason", "entities": ["Atlas"]}, {"action": "contradict"}):
|
|
result = call(provider, **query)
|
|
assert result["success"]
|
|
assert "Atlas" not in json.dumps(result)
|
|
fresh = call(provider, "add", content="new candidate", sources=[source(mid)])
|
|
assert fresh["success"]
|
|
db._conn.execute("DELETE FROM messages WHERE id=?", (mid,))
|
|
db._conn.commit()
|
|
assert call(provider, "list")["facts"] == []
|
|
|
|
|
|
def test_project_switch_feedback_and_unlinked_hooks(project_env):
|
|
provider, db, mid, other, roots = project_env
|
|
first = call(provider, "add", content="alpha candidate", sources=[source(mid)])
|
|
alpha_path = provider._store.db_path
|
|
feedback = json.loads(provider.handle_tool_call("fact_feedback", {"action": "helpful", "fact_ref": first["fact_ref"]}))
|
|
assert feedback["success"] and feedback["new_trust"] > feedback["old_trust"]
|
|
assert feedback["assertion"] == "candidate_not_confirmed"
|
|
# Updating current-session ownership must invalidate a live provider's cached store.
|
|
db._conn.execute("UPDATE sessions SET cwd=? WHERE id='current'", (str(roots["beta"]),))
|
|
db._conn.commit()
|
|
assert call(provider, "list")["facts"] == []
|
|
assert provider._store.db_path != alpha_path
|
|
denied = json.loads(provider.handle_tool_call("fact_feedback", {"action": "helpful", "fact_ref": first["fact_ref"]}))
|
|
assert not denied["success"]
|
|
second = call(provider, "add", content="beta candidate", sources=[source(other, "other")])
|
|
assert second["success"]
|
|
provider._config["auto_extract"] = True
|
|
provider.on_memory_write("add", "user", "HOOK_PRIVATE")
|
|
provider.on_session_end([{"role": "user", "content": "I prefer HOOK_PRIVATE"}])
|
|
assert provider.prefetch("beta", session_id="other") == ""
|
|
assert "HOOK_PRIVATE" not in json.dumps(call(provider, "list"))
|
|
db._conn.execute("UPDATE sessions SET cwd=NULL WHERE id='current'")
|
|
db._conn.commit()
|
|
assert not call(provider, "list")["success"]
|
|
assert not call(provider, "add", content="no project", sources=[source(other, "other")])["success"]
|
|
assert not (get_hermes_home() / "memory_store.db").exists()
|
|
|
|
|
|
def test_source_safety_atomicity_and_legacy_contract(project_env, tmp_path, monkeypatch):
|
|
provider, db, mid, other, roots = project_env
|
|
for kwargs in ({"content": "password=hunter2"}, {"role": "tool", "content": "tool text"},
|
|
{"content": "hidden source", "display_kind": "hidden"},
|
|
{"content": [{"type": "image_url", "image_url": {"url": "data:image/png;base64,PRIVATE"}}]}):
|
|
role = kwargs.pop("role", "user")
|
|
unsafe = db.append_message("source", role=role, **kwargs)
|
|
assert not call(provider, "add", content="unsafe candidate", sources=[source(unsafe)])["success"]
|
|
for ref in (source(mid, content_hash="0" * 64), source(str(mid)), source(mid, content_hash="BAD")):
|
|
assert not call(provider, "add", content="invalid link", sources=[ref])["success"]
|
|
added = call(provider, "add", content="safe candidate", sources=[source(mid)])
|
|
assert added["success"]
|
|
assert not call(provider, "update", fact_ref=added["fact_ref"], content="edited without source")["success"]
|
|
updated = call(provider, "update", fact_ref=added["fact_ref"], content="updated candidate", sources=[source(mid)])
|
|
assert updated["success"] and updated["updated"]
|
|
store = provider._store
|
|
store._conn.execute("""CREATE TRIGGER reject_source BEFORE INSERT ON fact_sources
|
|
BEGIN SELECT RAISE(ABORT, 'source mapping failed'); END""")
|
|
assert not call(provider, "add", content="atomic rollback candidate", sources=[source(mid)])["success"]
|
|
assert not store._conn.execute("SELECT 1 FROM facts WHERE content='atomic rollback candidate'").fetchone()
|
|
store._conn.execute("DROP TRIGGER reject_source")
|
|
unlinked = store.add_fact("unlinked internal candidate")
|
|
assert "unlinked internal" not in json.dumps(call(provider, "list"))
|
|
assert not json.loads(provider.handle_tool_call("fact_feedback", {"action": "helpful", "fact_id": unlinked}))["success"]
|
|
from tools.session_search_project import ProjectRecall
|
|
original = ProjectRecall.response
|
|
def revoke_before_response(recall, **payload):
|
|
db._conn.execute("UPDATE messages SET active=0,compacted=0 WHERE id=?", (mid,))
|
|
db._conn.commit()
|
|
return original(recall, **payload)
|
|
with monkeypatch.context() as patcher:
|
|
patcher.setattr(ProjectRecall, "response", revoke_before_response)
|
|
race = call(provider, "add", content="raced candidate", sources=[source(mid)])
|
|
assert not race["success"] and race["status"] == "source_revoked"
|
|
assert not store._conn.execute("SELECT 1 FROM facts WHERE content='raced candidate'").fetchone()
|
|
scoped_schema = provider.get_tool_schemas()[0]
|
|
assert "candidate" in scoped_schema["description"].lower()
|
|
assert "sources" in scoped_schema["parameters"]["properties"]
|
|
legacy = HolographicMemoryProvider({"project_scoped": "false", "db_path": str(tmp_path / "legacy.db"), "hrr_dim": 64})
|
|
try:
|
|
legacy.initialize("legacy")
|
|
result = call(legacy, "add", content="legacy source free")
|
|
assert result["status"] == "added"
|
|
assert call(legacy, "list")["facts"][0]["content"] == "legacy source free"
|
|
legacy.on_memory_write("add", "user", "legacy mirror")
|
|
assert "legacy mirror" in json.dumps(call(legacy, "list"))
|
|
assert "legacy source free" in legacy.prefetch("legacy")
|
|
assert legacy.get_tool_schemas()[0]["description"] != scoped_schema["description"]
|
|
finally:
|
|
legacy.shutdown()
|
|
|
|
|
|
def test_invalid_feedback_scope_race_and_remove(project_env, monkeypatch):
|
|
provider, db, mid, other, roots = project_env
|
|
added = call(provider, "add", content="stable candidate", sources=[source(mid)])
|
|
before = call(provider, "list")["facts"][0]["trust_score"]
|
|
denied = json.loads(provider.handle_tool_call("fact_feedback", {"action": "bogus", "fact_ref": added["fact_ref"]}))
|
|
assert not denied["success"] and denied["status"] == "invalid_request"
|
|
assert call(provider, "list")["facts"][0]["trust_score"] == before
|
|
from tools.session_search_project import ProjectRecall
|
|
original = ProjectRecall.response
|
|
def move_before_response(recall, **payload):
|
|
db._conn.execute("UPDATE sessions SET cwd=? WHERE id='current'", (str(roots["beta"]),))
|
|
db._conn.commit()
|
|
return original(recall, **payload)
|
|
store = provider._store
|
|
with monkeypatch.context() as patcher:
|
|
patcher.setattr(ProjectRecall, "response", move_before_response)
|
|
race = call(provider, "add", content="scope raced candidate", sources=[source(mid)])
|
|
assert not race["success"] and race["status"] == "scope_changed"
|
|
assert not store._conn.execute("SELECT 1 FROM facts WHERE content='scope raced candidate'").fetchone()
|
|
db._conn.execute("UPDATE sessions SET cwd=? WHERE id='current'", (str(roots["alpha"]),))
|
|
db._conn.commit()
|
|
removed = call(provider, "remove", fact_ref=added["fact_ref"])
|
|
assert removed["success"] and removed["removed"]
|
|
assert call(provider, "list")["facts"] == []
|
|
assert store.sources(added["fact_id"]) == []
|
|
foreign_home = roots["alpha"].parent / "another-profile"
|
|
foreign_home.mkdir()
|
|
with monkeypatch.context() as patcher:
|
|
patcher.setenv("HERMES_HOME", str(foreign_home))
|
|
assert call(provider, "list")["status"] == "profile_changed"
|
|
assert not (foreign_home / "state.db").exists()
|
|
|
|
|
|
def test_session_switch_releases_old_store_and_rebinds(project_env):
|
|
provider, db, mid, other, roots = project_env
|
|
assert call(provider, "add", content="alpha private", sources=[source(mid)])["success"]
|
|
old_store = provider._store
|
|
config = dict(provider._config)
|
|
provider.on_session_switch("other", parent_session_id="current", reset=True)
|
|
result = call(provider, "list")
|
|
assert result["success"] and result["facts"] == []
|
|
assert result["identity"]["current_session_id"] == "other"
|
|
assert old_store._entry is None
|
|
assert provider._config == config
|
|
provider.on_session_switch("current", rewound=True)
|
|
assert call(provider, "list")["facts"][0]["content"] == "alpha private"
|
|
|
|
|
|
@pytest.mark.parametrize("owner,other_owner", [("A", "B"), (None, ""), (None, "None")])
|
|
def test_same_project_same_content_is_principal_isolated(project_env, owner, other_owner):
|
|
provider, db, mid, other, roots = project_env
|
|
db._conn.execute("UPDATE sessions SET user_id=? WHERE id IN ('current','source')", (owner,))
|
|
db._conn.commit()
|
|
db.create_session("user-b", source="desktop", cwd=str(roots["alpha"]), user_id=other_owner)
|
|
b_mid = db.append_message("user-b", role="user", content="B source")
|
|
a = call(provider, "add", content="same candidate", tags="A_PRIVATE", sources=[source(mid)])
|
|
assert a["success"]
|
|
a_path = provider._store.db_path
|
|
b_provider = HolographicMemoryProvider({"project_scoped": True, "hrr_dim": 64})
|
|
try:
|
|
b_provider.initialize("user-b")
|
|
b = call(b_provider, "add", content="same candidate", tags="B_TAG", sources=[source(b_mid, "user-b")])
|
|
assert b["success"]
|
|
b_rows = call(b_provider, "list")
|
|
assert "A_PRIVATE" not in json.dumps(b_rows)
|
|
assert b_rows["facts"][0]["tags"] == "B_TAG"
|
|
assert b_provider._store.db_path != a_path
|
|
a_rows = call(provider, "list")["facts"]
|
|
assert len(a_rows) == 1 and a_rows[0]["tags"] == "A_PRIVATE"
|
|
assert a_rows[0]["sources"][0]["session_id"] == "source"
|
|
assert not call(b_provider, "add", content="foreign source", sources=[source(mid)])["success"]
|
|
assert not call(b_provider, "remove", fact_ref=a["fact_ref"], fact_id=b["fact_id"])["success"]
|
|
assert call(b_provider, "list")["facts"][0]["fact_ref"] == b["fact_ref"]
|
|
finally:
|
|
b_provider.shutdown()
|
|
|
|
|
|
def test_old_project_ref_cannot_mutate_colliding_fact_id(project_env):
|
|
provider, db, mid, other, roots = project_env
|
|
a = call(provider, "add", content="A candidate", sources=[source(mid)])
|
|
db._conn.execute("UPDATE sessions SET cwd=? WHERE id='current'", (str(roots["beta"]),))
|
|
db._conn.commit()
|
|
b = call(provider, "add", content="B candidate", sources=[source(other, "other")])
|
|
assert a["fact_id"] == b["fact_id"] == 1
|
|
before = call(provider, "list")["facts"]
|
|
old_ref = a.get("fact_ref", str(a["fact_id"]))
|
|
for tool, action in (("fact_feedback", "helpful"), ("fact_store", "remove"), ("fact_store", "update")):
|
|
result = json.loads(provider.handle_tool_call(tool, {
|
|
"action": action, "fact_ref": old_ref, "fact_id": b["fact_id"], "tags": "WRONG"}))
|
|
assert not result["success"]
|
|
assert result["status"] == "invalid_fact_ref"
|
|
assert call(provider, "list")["facts"] == before
|
|
|
|
|
|
def test_fact_ref_version_sources_feedback_and_recreated_store(project_env):
|
|
provider, db, mid, other, roots = project_env
|
|
a = call(provider, "add", content="version one", sources=[source(mid)])
|
|
ref = a["fact_ref"]
|
|
assert call(provider, "list")["facts"][0]["fact_ref"] == ref
|
|
assert call(provider, "list")["coverage"]["complete"] is False
|
|
for tool, action in (("fact_feedback", "helpful"), ("fact_store", "update"), ("fact_store", "remove")):
|
|
denied = json.loads(provider.handle_tool_call(tool, {"action": action, "fact_id": a["fact_id"]}))
|
|
assert not denied["success"] and denied["status"] == "fact_ref_required"
|
|
feedback = json.loads(provider.handle_tool_call("fact_feedback", {"action": "helpful", "fact_ref": ref}))
|
|
assert feedback["success"] and feedback["fact_ref"] == ref
|
|
updated = call(provider, "update", fact_ref=ref, content="version two", sources=[source(mid)])
|
|
assert updated["success"] and updated["fact_ref"] != ref
|
|
assert call(provider, "remove", fact_ref=ref)["status"] == "invalid_fact_ref"
|
|
mid2 = db.append_message("source", role="user", content="replacement source")
|
|
replaced = call(provider, "update", fact_ref=updated["fact_ref"], sources=[source(mid2)])
|
|
assert replaced["success"] and replaced["fact_ref"] != updated["fact_ref"]
|
|
assert call(provider, "remove", fact_ref=updated["fact_ref"])["status"] == "invalid_fact_ref"
|
|
path = provider._store.db_path
|
|
provider.shutdown()
|
|
provider.initialize("current")
|
|
assert call(provider, "list")["facts"][0]["fact_ref"] == replaced["fact_ref"]
|
|
provider.shutdown()
|
|
path.unlink()
|
|
provider.initialize("current")
|
|
recreated = call(provider, "add", content="version two", sources=[source(mid2)])
|
|
assert recreated["fact_id"] == a["fact_id"]
|
|
assert recreated["fact_ref"] != replaced["fact_ref"]
|
|
assert call(provider, "remove", fact_ref=replaced["fact_ref"])["status"] == "invalid_fact_ref"
|
|
schemas = {s["name"]: s["parameters"] for s in provider.get_tool_schemas()}
|
|
assert "fact_ref" in schemas["fact_store"]["properties"]
|
|
assert "fact_id" not in schemas["fact_store"]["properties"]
|
|
assert "fact_ref" in schemas["fact_feedback"]["required"]
|
|
|
|
|
|
def test_trusted_caller_context_fails_closed_during_queued_switch(project_env):
|
|
provider, db, mid, other, roots = project_env
|
|
assert call(provider, "add", content="queued old candidate", sources=[source(mid)])["success"]
|
|
result = json.loads(provider.handle_tool_call("fact_store", {"action": "list", "session_id": "current"}, session_id="other"))
|
|
assert not result["success"] and result["status"] == "session_changed"
|
|
assert "queued old candidate" not in json.dumps(result)
|
|
# Model arguments cannot authorize a rebind, nor override trusted metadata.
|
|
result = json.loads(provider.handle_tool_call("fact_store", {"action": "list", "session_id": "other", "user_id": "B"}, session_id="current"))
|
|
assert result["success"] and result["facts"][0]["content"] == "queued old candidate"
|
|
provider.on_session_switch("other")
|
|
result = json.loads(provider.handle_tool_call("fact_store", {"action": "list"}, session_id="other"))
|
|
assert result["success"] and result["facts"] == []
|
|
|
|
|
|
def test_principal_changed_before_commit_rolls_back(project_env, monkeypatch):
|
|
provider, db, mid, other, roots = project_env
|
|
from tools.session_search_project import ProjectRecall
|
|
original = ProjectRecall.response
|
|
def change_after_response(recall, **payload):
|
|
response = original(recall, **payload)
|
|
db._conn.execute("UPDATE sessions SET user_id='B' WHERE id='current'")
|
|
db._conn.commit()
|
|
return response
|
|
with monkeypatch.context() as patcher:
|
|
patcher.setattr(ProjectRecall, "response", change_after_response)
|
|
result = call(provider, "add", content="principal raced candidate", sources=[source(mid)])
|
|
assert not result["success"] and result["status"] == "scope_changed"
|
|
assert not provider._store._conn.execute("SELECT 1 FROM facts WHERE content='principal raced candidate'").fetchone() |