"""Issue C — inbox pseudo-project + cross-project (project="") entities.""" import pytest from fastapi.testclient import TestClient from atocore.engineering.service import ( create_entity, get_entities, init_engineering_schema, promote_entity, ) from atocore.main import app from atocore.projects.registry import ( GLOBAL_PROJECT, INBOX_PROJECT, is_reserved_project, register_project, resolve_project_name, update_project, ) @pytest.fixture def seeded_db(tmp_data_dir, tmp_path, monkeypatch): # Isolate the project registry so "p05" etc. don't canonicalize # to aliases inherited from the host registry. 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_engineering_schema() # Audit table lives in the memory schema — bring it up so audit rows # don't spam warnings during retargeting tests. from atocore.models.database import init_db init_db() yield tmp_data_dir def test_inbox_is_reserved(): assert is_reserved_project("inbox") is True assert is_reserved_project("INBOX") is True assert is_reserved_project("p05-interferometer") is False assert is_reserved_project("") is False def test_resolve_project_name_preserves_inbox(): assert resolve_project_name("inbox") == "inbox" assert resolve_project_name("INBOX") == "inbox" assert resolve_project_name("") == "" def test_cannot_register_inbox(tmp_path, monkeypatch): monkeypatch.setenv( "ATOCORE_PROJECT_REGISTRY_PATH", str(tmp_path / "registry.json"), ) from atocore import config config.settings = config.Settings() with pytest.raises(ValueError, match="reserved"): register_project( project_id="inbox", ingest_roots=[{"source": "vault", "subpath": "incoming/inbox"}], ) def test_cannot_update_inbox(tmp_path, monkeypatch): monkeypatch.setenv( "ATOCORE_PROJECT_REGISTRY_PATH", str(tmp_path / "registry.json"), ) from atocore import config config.settings = config.Settings() with pytest.raises(ValueError, match="reserved"): update_project(project_name="inbox", description="hijack attempt") def test_create_entity_with_empty_project_is_global(seeded_db): e = create_entity(entity_type="material", name="Invar", project="") assert e.project == "" def test_create_entity_in_inbox(seeded_db): e = create_entity(entity_type="vendor", name="Zygo", project="inbox") assert e.project == "inbox" def test_get_entities_inbox_scope(seeded_db): create_entity(entity_type="vendor", name="Zygo", project="inbox") create_entity(entity_type="material", name="Invar", project="") create_entity(entity_type="component", name="Mirror", project="p05") inbox = get_entities(project=INBOX_PROJECT, scope_only=True) assert {e.name for e in inbox} == {"Zygo"} def test_get_entities_global_scope(seeded_db): create_entity(entity_type="vendor", name="Zygo", project="inbox") create_entity(entity_type="material", name="Invar", project="") create_entity(entity_type="component", name="Mirror", project="p05") globals_ = get_entities(project=GLOBAL_PROJECT, scope_only=True) assert {e.name for e in globals_} == {"Invar"} def test_real_project_includes_global_by_default(seeded_db): create_entity(entity_type="material", name="Invar", project="") create_entity(entity_type="component", name="Mirror", project="p05") create_entity(entity_type="component", name="Other", project="p06") p05 = get_entities(project="p05") names = {e.name for e in p05} assert "Mirror" in names assert "Invar" in names, "cross-project material should bleed in by default" assert "Other" not in names def test_real_project_scope_only_excludes_global(seeded_db): create_entity(entity_type="material", name="Invar", project="") create_entity(entity_type="component", name="Mirror", project="p05") p05 = get_entities(project="p05", scope_only=True) assert {e.name for e in p05} == {"Mirror"} def test_api_post_entity_with_null_project_stores_global(seeded_db): client = TestClient(app) r = client.post("/entities", json={ "entity_type": "material", "name": "Titanium", "project": None, }) assert r.status_code == 200 globals_ = get_entities(project=GLOBAL_PROJECT, scope_only=True) assert any(e.name == "Titanium" for e in globals_) def test_api_get_entities_scope_only(seeded_db): create_entity(entity_type="material", name="Invar", project="") create_entity(entity_type="component", name="Mirror", project="p05") client = TestClient(app) mixed = client.get("/entities?project=p05").json() scoped = client.get("/entities?project=p05&scope_only=true").json() assert mixed["count"] == 2 assert scoped["count"] == 1 def test_promote_with_target_project_retargets(seeded_db): e = create_entity( entity_type="vendor", name="ZygoLead", project="inbox", status="candidate", ) ok = promote_entity(e.id, target_project="p05") assert ok is True from atocore.engineering.service import get_entity promoted = get_entity(e.id) assert promoted.status == "active" assert promoted.project == "p05" def test_promote_without_target_project_keeps_project(seeded_db): e = create_entity( entity_type="vendor", name="ZygoStay", project="inbox", status="candidate", ) ok = promote_entity(e.id) assert ok is True from atocore.engineering.service import get_entity promoted = get_entity(e.id) assert promoted.status == "active" assert promoted.project == "inbox" def test_api_promote_with_target_project(seeded_db): e = create_entity( entity_type="vendor", name="ZygoApi", project="inbox", status="candidate", ) client = TestClient(app) r = client.post( f"/entities/{e.id}/promote", json={"target_project": "p05"}, ) assert r.status_code == 200 body = r.json() assert body["status"] == "promoted" assert body["target_project"] == "p05"