54 lines
2.5 KiB
Python
54 lines
2.5 KiB
Python
"""Executable completeness contract for memory namespaces and access."""
|
|
from __future__ import annotations
|
|
import pytest
|
|
from aios_v2r1.memory_scope import MemoryScope, qualify_memory_key
|
|
from aios_v2r1.tenant_context import ScopeError, ValidationError
|
|
|
|
def scope(kind: str = "session", **changes: object) -> MemoryScope:
|
|
values: dict[str, object] = {
|
|
"tenant_id": "tenant-a", "cell_id": "cell-a", "profile_id": "profile-a",
|
|
"session_id": "session-a", "kind": kind}
|
|
values.update(changes)
|
|
return MemoryScope(**values) # type: ignore[arg-type]
|
|
|
|
@pytest.mark.parametrize(("kind", "session_id", "expected"), [
|
|
("session", "session-a", "tenant/tenant-a/cell/cell-a/profile/profile-a/session/session-a/memory/session"),
|
|
("profile", None, "tenant/tenant-a/cell/cell-a/profile/profile-a/memory/profile"),
|
|
("cell", None, "tenant/tenant-a/cell/cell-a/memory/cell"),
|
|
("tenant", None, "tenant/tenant-a/cell/cell-a/memory/tenant"),
|
|
("shared_artifact", None, "tenant/tenant-a/cell/cell-a/memory/shared_artifact")])
|
|
def test_every_namespace_is_explicit(kind: str, session_id: str | None, expected: str) -> None:
|
|
assert scope(kind, session_id=session_id).namespace == expected
|
|
|
|
@pytest.mark.parametrize("requested", [
|
|
scope(tenant_id="tenant-b"), scope(cell_id="cell-b"),
|
|
scope(profile_id="profile-b"), scope(session_id="session-b")])
|
|
def test_cross_scope_access_is_denied(requested: MemoryScope) -> None:
|
|
with pytest.raises(ScopeError):
|
|
scope().assert_access(requested)
|
|
|
|
def test_cross_kind_access_is_denied() -> None:
|
|
with pytest.raises(ScopeError, match="cross-kind"):
|
|
scope("session").assert_access(scope("profile", session_id=None))
|
|
|
|
def test_shared_artifacts_are_read_only() -> None:
|
|
owner = scope()
|
|
shared = scope("shared_artifact", session_id=None)
|
|
owner.assert_access(shared)
|
|
with pytest.raises(ScopeError, match="immutable"):
|
|
owner.assert_access(shared, write=True)
|
|
|
|
def test_qualified_key_contains_the_full_namespace() -> None:
|
|
assert qualify_memory_key(scope(), "facts.v1") == (
|
|
"tenant/tenant-a/cell/cell-a/profile/profile-a/session/session-a/"
|
|
"memory/session/facts.v1")
|
|
|
|
@pytest.mark.parametrize("key", ["../escape", "a/b", ".", "..", " space ", "x?query"])
|
|
def test_memory_key_traversal_and_malformed_keys_are_rejected(key: str) -> None:
|
|
with pytest.raises(ValidationError):
|
|
qualify_memory_key(scope(), key)
|
|
|
|
def test_memory_search_surface_is_disabled() -> None:
|
|
assert not hasattr(scope(), "search")
|
|
assert not hasattr(scope(), "search_all")
|