161 lines
4.8 KiB
Python
161 lines
4.8 KiB
Python
|
|
"""PATCH /entities/{id} — edit mutable fields without cloning (sprint P1)."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
from atocore.engineering.service import (
|
||
|
|
create_entity,
|
||
|
|
get_entity,
|
||
|
|
init_engineering_schema,
|
||
|
|
update_entity,
|
||
|
|
)
|
||
|
|
from atocore.main import app
|
||
|
|
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_update_entity_description(env):
|
||
|
|
e = create_entity(entity_type="component", name="t", description="old desc")
|
||
|
|
updated = update_entity(e.id, description="new desc")
|
||
|
|
assert updated.description == "new desc"
|
||
|
|
assert get_entity(e.id).description == "new desc"
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_entity_properties_merge(env):
|
||
|
|
e = create_entity(
|
||
|
|
entity_type="component",
|
||
|
|
name="t2",
|
||
|
|
properties={"color": "red", "kg": 5},
|
||
|
|
)
|
||
|
|
updated = update_entity(
|
||
|
|
e.id, properties_patch={"color": "blue", "material": "invar"},
|
||
|
|
)
|
||
|
|
assert updated.properties == {"color": "blue", "kg": 5, "material": "invar"}
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_entity_properties_null_deletes_key(env):
|
||
|
|
e = create_entity(
|
||
|
|
entity_type="component",
|
||
|
|
name="t3",
|
||
|
|
properties={"color": "red", "kg": 5},
|
||
|
|
)
|
||
|
|
updated = update_entity(e.id, properties_patch={"color": None})
|
||
|
|
assert "color" not in updated.properties
|
||
|
|
assert updated.properties.get("kg") == 5
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_entity_confidence_bounds(env):
|
||
|
|
e = create_entity(entity_type="component", name="t4", confidence=0.5)
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
update_entity(e.id, confidence=1.5)
|
||
|
|
with pytest.raises(ValueError):
|
||
|
|
update_entity(e.id, confidence=-0.1)
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_entity_source_refs_append_dedup(env):
|
||
|
|
e = create_entity(
|
||
|
|
entity_type="component",
|
||
|
|
name="t5",
|
||
|
|
source_refs=["session:a", "session:b"],
|
||
|
|
)
|
||
|
|
updated = update_entity(
|
||
|
|
e.id, append_source_refs=["session:b", "session:c"],
|
||
|
|
)
|
||
|
|
assert updated.source_refs == ["session:a", "session:b", "session:c"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_update_entity_returns_none_for_unknown(env):
|
||
|
|
assert update_entity("nonexistent", description="x") is None
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_happy_path(env):
|
||
|
|
e = create_entity(
|
||
|
|
entity_type="component",
|
||
|
|
name="tower",
|
||
|
|
description="old",
|
||
|
|
properties={"material": "steel"},
|
||
|
|
confidence=0.6,
|
||
|
|
)
|
||
|
|
client = TestClient(app)
|
||
|
|
r = client.patch(
|
||
|
|
f"/entities/{e.id}",
|
||
|
|
json={
|
||
|
|
"description": "three-stage tower",
|
||
|
|
"properties": {"material": "invar", "height_mm": 1200},
|
||
|
|
"confidence": 0.9,
|
||
|
|
"source_refs": ["session:s1"],
|
||
|
|
"note": "from voice session",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
assert r.status_code == 200, r.text
|
||
|
|
body = r.json()
|
||
|
|
assert body["description"] == "three-stage tower"
|
||
|
|
assert body["properties"]["material"] == "invar"
|
||
|
|
assert body["properties"]["height_mm"] == 1200
|
||
|
|
assert body["confidence"] == 0.9
|
||
|
|
assert "session:s1" in body["source_refs"]
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_omitted_fields_unchanged(env):
|
||
|
|
e = create_entity(
|
||
|
|
entity_type="component",
|
||
|
|
name="keep-desc",
|
||
|
|
description="keep me",
|
||
|
|
)
|
||
|
|
client = TestClient(app)
|
||
|
|
r = client.patch(
|
||
|
|
f"/entities/{e.id}",
|
||
|
|
json={"confidence": 0.7},
|
||
|
|
)
|
||
|
|
assert r.status_code == 200
|
||
|
|
assert r.json()["description"] == "keep me"
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_404_on_missing(env):
|
||
|
|
client = TestClient(app)
|
||
|
|
r = client.patch("/entities/does-not-exist", json={"description": "x"})
|
||
|
|
assert r.status_code == 404
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_rejects_bad_confidence(env):
|
||
|
|
e = create_entity(entity_type="component", name="bad-conf")
|
||
|
|
client = TestClient(app)
|
||
|
|
r = client.patch(f"/entities/{e.id}", json={"confidence": 2.0})
|
||
|
|
assert r.status_code == 400
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_aliased_under_v1(env):
|
||
|
|
e = create_entity(entity_type="component", name="v1-patch")
|
||
|
|
client = TestClient(app)
|
||
|
|
r = client.patch(
|
||
|
|
f"/v1/entities/{e.id}",
|
||
|
|
json={"description": "via v1"},
|
||
|
|
)
|
||
|
|
assert r.status_code == 200
|
||
|
|
assert get_entity(e.id).description == "via v1"
|
||
|
|
|
||
|
|
|
||
|
|
def test_api_patch_audit_row_written(env):
|
||
|
|
from atocore.engineering.service import get_entity_audit
|
||
|
|
|
||
|
|
e = create_entity(entity_type="component", name="audit-check")
|
||
|
|
client = TestClient(app)
|
||
|
|
client.patch(
|
||
|
|
f"/entities/{e.id}",
|
||
|
|
json={"description": "new", "note": "manual edit"},
|
||
|
|
)
|
||
|
|
audit = get_entity_audit(e.id)
|
||
|
|
actions = [a["action"] for a in audit]
|
||
|
|
assert "updated" in actions
|