195 lines
6.0 KiB
Python
195 lines
6.0 KiB
Python
|
|
"""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"
|