Phase 1 - Ingestion hardening: - Encoding fallback (UTF-8/UTF-8-sig/Latin-1/CP1252) - Delete detection: purge DB/vector entries for removed files - Ingestion stats endpoint (GET /stats) Phase 5 - Trusted Project State: - project_state table with categories (status, decision, requirement, contact, milestone, fact, config) - CRUD API: POST/GET/DELETE /project/state - Upsert semantics, invalidation (supersede) support - Context builder integrates project state at highest trust precedence - Project state gets 20% budget allocation, appears first in context - Trust precedence: Project State > Retrieved Chunks (per Master Plan) 33/33 tests passing. Validated end-to-end with GigaBIT M1 project data. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
128 lines
4.0 KiB
Python
128 lines
4.0 KiB
Python
"""Tests for Trusted Project State."""
|
|
|
|
import pytest
|
|
|
|
from atocore.context.project_state import (
|
|
CATEGORIES,
|
|
ensure_project,
|
|
format_project_state,
|
|
get_state,
|
|
init_project_state_schema,
|
|
invalidate_state,
|
|
set_state,
|
|
)
|
|
from atocore.models.database import init_db
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def setup_db(tmp_data_dir):
|
|
"""Initialize DB and project state schema for every test."""
|
|
init_db()
|
|
init_project_state_schema()
|
|
|
|
|
|
def test_ensure_project_creates():
|
|
"""Test creating a new project."""
|
|
pid = ensure_project("test-project", "A test project")
|
|
assert pid
|
|
# Second call returns same ID
|
|
pid2 = ensure_project("test-project")
|
|
assert pid == pid2
|
|
|
|
|
|
def test_set_state_creates_entry():
|
|
"""Test creating a project state entry."""
|
|
entry = set_state("myproject", "status", "phase", "Phase 0.5 — PoC complete")
|
|
assert entry.category == "status"
|
|
assert entry.key == "phase"
|
|
assert entry.value == "Phase 0.5 — PoC complete"
|
|
assert entry.status == "active"
|
|
|
|
|
|
def test_set_state_upserts():
|
|
"""Test that setting same key updates the value."""
|
|
set_state("myproject", "status", "phase", "Phase 0")
|
|
entry = set_state("myproject", "status", "phase", "Phase 1")
|
|
assert entry.value == "Phase 1"
|
|
|
|
# Only one entry should exist
|
|
entries = get_state("myproject", category="status")
|
|
assert len(entries) == 1
|
|
assert entries[0].value == "Phase 1"
|
|
|
|
|
|
def test_set_state_invalid_category():
|
|
"""Test that invalid category raises ValueError."""
|
|
with pytest.raises(ValueError, match="Invalid category"):
|
|
set_state("myproject", "invalid_category", "key", "value")
|
|
|
|
|
|
def test_get_state_all():
|
|
"""Test getting all state entries for a project."""
|
|
set_state("proj", "status", "phase", "Phase 1")
|
|
set_state("proj", "decision", "database", "SQLite for v1")
|
|
set_state("proj", "requirement", "latency", "<2 seconds")
|
|
|
|
entries = get_state("proj")
|
|
assert len(entries) == 3
|
|
categories = {e.category for e in entries}
|
|
assert categories == {"status", "decision", "requirement"}
|
|
|
|
|
|
def test_get_state_by_category():
|
|
"""Test filtering by category."""
|
|
set_state("proj", "status", "phase", "Phase 1")
|
|
set_state("proj", "decision", "database", "SQLite")
|
|
set_state("proj", "decision", "vectordb", "ChromaDB")
|
|
|
|
entries = get_state("proj", category="decision")
|
|
assert len(entries) == 2
|
|
assert all(e.category == "decision" for e in entries)
|
|
|
|
|
|
def test_get_state_nonexistent_project():
|
|
"""Test getting state for a project that doesn't exist."""
|
|
entries = get_state("nonexistent")
|
|
assert entries == []
|
|
|
|
|
|
def test_invalidate_state():
|
|
"""Test marking a state entry as superseded."""
|
|
set_state("invalidate-test", "decision", "approach", "monolith")
|
|
success = invalidate_state("invalidate-test", "decision", "approach")
|
|
assert success
|
|
|
|
# Active entries should be empty
|
|
entries = get_state("invalidate-test", active_only=True)
|
|
assert len(entries) == 0
|
|
|
|
# But entry still exists if we include inactive
|
|
entries = get_state("invalidate-test", active_only=False)
|
|
assert len(entries) == 1
|
|
assert entries[0].status == "superseded"
|
|
|
|
|
|
def test_invalidate_nonexistent():
|
|
"""Test invalidating a nonexistent entry."""
|
|
success = invalidate_state("proj", "decision", "nonexistent")
|
|
assert not success
|
|
|
|
|
|
def test_format_project_state():
|
|
"""Test formatting state entries for context injection."""
|
|
set_state("proj", "status", "phase", "Phase 1")
|
|
set_state("proj", "decision", "database", "SQLite", source="Build Spec V1")
|
|
entries = get_state("proj")
|
|
|
|
formatted = format_project_state(entries)
|
|
assert "--- Trusted Project State ---" in formatted
|
|
assert "--- End Project State ---" in formatted
|
|
assert "phase: Phase 1" in formatted
|
|
assert "database: SQLite" in formatted
|
|
assert "(source: Build Spec V1)" in formatted
|
|
|
|
|
|
def test_format_empty():
|
|
"""Test formatting empty state."""
|
|
assert format_project_state([]) == ""
|