test: comprehensive test suite

Unit tests:
- test_security_boundaries.py: vault block, vault lock sentinel
- test_security_injection.py: all 4 injection categories, case-insensitive
- test_vault_rw.py: roundtrip, file permissions (chmod 400), no key in config
- test_config.py: schema roundtrip, no api_key field, chmod 600 on config.yaml
- test_memory_reader.py: list, read, sandboxing, context loading
- test_memory_writer.py: write, append, index update, traversal blocked, chmod 600
- test_providers.py: required fields, unique IDs, litellm prefix format
- test_renderer.py: key redaction for sk-ant-, sk-, AIza patterns

Security tests:
- test_vault_ai_isolation.py: 7 traversal patterns blocked via memory read/write
- test_path_traversal.py: 20+ traversal patterns — all rejected for read and write
- test_prompt_injection.py: 21-item corpus + 5 clean texts (no false positives)

Integration tests:
- test_lmstudio.py: live call to localhost:1234, streaming, full stack session,
  injection scan on real output (skips if LM Studio not running)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-17 12:55:06 +02:00
parent e792c5e0c9
commit 251e509ee0
13 changed files with 746 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
import json
import os
from pathlib import Path
import pytest
@pytest.fixture
def tmp_pyra_home(tmp_path, monkeypatch):
"""Redirect pyra_home() to a temporary directory for isolation."""
fake_home = tmp_path / ".pyra"
# Patch Path.home() so pyra_home() returns our tmp dir
monkeypatch.setattr(
"pyra.utils.paths.Path",
type("FakePath", (Path,), {"home": staticmethod(lambda: tmp_path)}),
)
# Also patch the module-level constants already computed
import pyra.security.boundaries as b
import pyra.memory.index as mi
import pyra.memory.reader as mr
import pyra.memory.writer as mw
import pyra.vault.reader as vr
import pyra.vault.writer as vw
import pyra.security.injection as si
import pyra.config.manager as cm
b.VAULT_PATH = fake_home / "vault"
b.BLOCKED_PREFIXES = [b.VAULT_PATH]
mi._MEMORY_ROOT = fake_home / "memory"
mi._INDEX_FILE = fake_home / "memory" / "MEMORY_INDEX.md"
mr._MEMORY_ROOT = fake_home / "memory"
mw._MEMORY_ROOT = fake_home / "memory"
vr._KEYS_FILE = fake_home / "vault" / "secrets" / "api_keys.json"
vw._KEYS_FILE = fake_home / "vault" / "secrets" / "api_keys.json"
si._LOG_FILE = fake_home / "security.log"
cm._CONFIG_PATH = fake_home / "config.yaml"
# Bootstrap the directory structure
from pyra.config.dirs import bootstrap
(fake_home / "vault").mkdir(parents=True)
(fake_home / "vault" / "secrets").mkdir()
(fake_home / "vault" / ".vault_lock").touch(mode=0o400)
(fake_home / "memory" / "user").mkdir(parents=True)
(fake_home / "memory" / "context").mkdir()
(fake_home / "memory" / "knowledge").mkdir()
return fake_home
@pytest.fixture
def lmstudio_available():
"""Skip test if LM Studio is not reachable."""
import httpx
try:
r = httpx.get("http://localhost:1234/v1/models", timeout=2.0)
r.raise_for_status()
return True
except Exception:
pytest.skip("LM Studio not reachable at localhost:1234")