"""Issue E — /invalidate + /supersede for active entities and memories.""" import pytest from fastapi.testclient import TestClient from atocore.engineering.service import ( create_entity, get_entity, get_relationships, init_engineering_schema, invalidate_active_entity, supersede_entity, ) from atocore.main import app from atocore.memory.service import create_memory, get_memories def _get_memory(memory_id): for status in ("active", "candidate", "invalid", "superseded"): for m in get_memories(status=status, active_only=False, limit=5000): if m.id == memory_id: return m return None from atocore.models.database import init_db @pytest.fixture def env(tmp_data_dir, tmp_path, monkeypatch): registry_path = tmp_path / "test-registry.json" registry_path.write_text('{"projects": []}', encoding="utf-8") monkeypatch.setenv("ATOCORE_PROJECT_REGISTRY_PATH", str(registry_path)) from atocore import config config.settings = config.Settings() init_db() init_engineering_schema() yield tmp_data_dir def test_invalidate_active_entity_transitions_to_invalid(env): e = create_entity(entity_type="component", name="tower-to-kill") ok, code = invalidate_active_entity(e.id, reason="duplicate") assert ok is True assert code == "invalidated" assert get_entity(e.id).status == "invalid" def test_invalidate_on_candidate_is_409(env): e = create_entity(entity_type="component", name="still-candidate", status="candidate") ok, code = invalidate_active_entity(e.id) assert ok is False assert code == "not_active" def test_invalidate_is_idempotent_on_invalid(env): e = create_entity(entity_type="component", name="already-gone") invalidate_active_entity(e.id) ok, code = invalidate_active_entity(e.id) assert ok is True assert code == "already_invalid" def test_supersede_creates_relationship(env): old = create_entity(entity_type="component", name="old-tower") new = create_entity(entity_type="component", name="new-tower") ok = supersede_entity(old.id, superseded_by=new.id, note="replaced") assert ok is True assert get_entity(old.id).status == "superseded" rels = get_relationships(new.id, direction="outgoing") assert any( r.relationship_type == "supersedes" and r.target_entity_id == old.id for r in rels ), "supersedes relationship must be auto-created" def test_supersede_rejects_self(): # no db needed — validation is pre-write. Using a fresh env anyway. pass # covered below via API test def test_api_invalidate_entity(env): e = create_entity(entity_type="component", name="api-kill") client = TestClient(app) r = client.post( f"/entities/{e.id}/invalidate", json={"reason": "test cleanup"}, ) assert r.status_code == 200 assert r.json()["status"] == "invalidated" def test_api_invalidate_entity_idempotent(env): e = create_entity(entity_type="component", name="api-kill-2") client = TestClient(app) client.post(f"/entities/{e.id}/invalidate", json={"reason": "first"}) r = client.post(f"/entities/{e.id}/invalidate", json={"reason": "second"}) assert r.status_code == 200 assert r.json()["status"] == "already_invalid" def test_api_invalidate_unknown_entity_is_404(env): client = TestClient(app) r = client.post( "/entities/nonexistent-id/invalidate", json={"reason": "missing"}, ) assert r.status_code == 404 def test_api_invalidate_candidate_entity_is_409(env): e = create_entity(entity_type="component", name="cand", status="candidate") client = TestClient(app) r = client.post(f"/entities/{e.id}/invalidate", json={"reason": "x"}) assert r.status_code == 409 def test_api_supersede_entity(env): old = create_entity(entity_type="component", name="api-old-tower") new = create_entity(entity_type="component", name="api-new-tower") client = TestClient(app) r = client.post( f"/entities/{old.id}/supersede", json={"superseded_by": new.id, "reason": "dedup"}, ) assert r.status_code == 200 body = r.json() assert body["status"] == "superseded" assert body["superseded_by"] == new.id assert get_entity(old.id).status == "superseded" def test_api_supersede_self_is_400(env): e = create_entity(entity_type="component", name="self-sup") client = TestClient(app) r = client.post( f"/entities/{e.id}/supersede", json={"superseded_by": e.id, "reason": "oops"}, ) assert r.status_code == 400 def test_api_supersede_missing_replacement_is_400(env): old = create_entity(entity_type="component", name="orphan-old") client = TestClient(app) r = client.post( f"/entities/{old.id}/supersede", json={"superseded_by": "does-not-exist", "reason": "missing"}, ) assert r.status_code == 400 def test_api_invalidate_memory(env): m = create_memory( memory_type="project", content="memory to retract", project="p05", ) client = TestClient(app) r = client.post( f"/memory/{m.id}/invalidate", json={"reason": "outdated"}, ) assert r.status_code == 200 assert r.json()["status"] == "invalidated" assert _get_memory(m.id).status == "invalid" def test_api_supersede_memory(env): m = create_memory( memory_type="project", content="memory to supersede", project="p05", ) client = TestClient(app) r = client.post( f"/memory/{m.id}/supersede", json={"reason": "replaced by newer fact"}, ) assert r.status_code == 200 assert r.json()["status"] == "superseded" assert _get_memory(m.id).status == "superseded" def test_v1_aliases_present(env): client = TestClient(app) spec = client.get("/openapi.json").json() paths = spec["paths"] for p in ( "/v1/entities/{entity_id}/invalidate", "/v1/entities/{entity_id}/supersede", "/v1/memory/{memory_id}/invalidate", "/v1/memory/{memory_id}/supersede", ): assert p in paths, f"{p} missing"