"""Issue F — binary asset store + artifact entity + wiki rendering.""" from io import BytesIO import pytest from fastapi.testclient import TestClient from PIL import Image from atocore.assets import ( AssetTooLarge, AssetTypeNotAllowed, get_asset, get_asset_binary, get_thumbnail, invalidate_asset, list_orphan_assets, store_asset, ) from atocore.engineering.service import ( ENTITY_TYPES, create_entity, create_relationship, init_engineering_schema, ) from atocore.main import app from atocore.models.database import init_db def _png_bytes(color=(255, 0, 0), size=(64, 48)) -> bytes: buf = BytesIO() Image.new("RGB", size, color).save(buf, format="PNG") return buf.getvalue() @pytest.fixture def assets_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_artifact_is_in_entity_types(): assert "artifact" in ENTITY_TYPES def test_store_asset_happy_path(assets_env): data = _png_bytes() asset = store_asset(data=data, mime_type="image/png", caption="red square") assert asset.hash_sha256 assert asset.size_bytes == len(data) assert asset.width == 64 assert asset.height == 48 assert asset.mime_type == "image/png" from pathlib import Path assert Path(asset.stored_path).exists() def test_store_asset_is_idempotent_on_hash(assets_env): data = _png_bytes() a = store_asset(data=data, mime_type="image/png") b = store_asset(data=data, mime_type="image/png", caption="different caption") assert a.id == b.id, "same content should dedup to the same asset id" def test_store_asset_rejects_unknown_mime(assets_env): with pytest.raises(AssetTypeNotAllowed): store_asset(data=b"hello", mime_type="text/plain") def test_store_asset_rejects_oversize(assets_env, monkeypatch): monkeypatch.setattr( "atocore.config.settings.assets_max_upload_bytes", 10, raising=False, ) with pytest.raises(AssetTooLarge): store_asset(data=_png_bytes(), mime_type="image/png") def test_get_asset_binary_roundtrip(assets_env): data = _png_bytes(color=(0, 255, 0)) asset = store_asset(data=data, mime_type="image/png") _, roundtrip = get_asset_binary(asset.id) assert roundtrip == data def test_thumbnail_generates_and_caches(assets_env): data = _png_bytes(size=(800, 600)) asset = store_asset(data=data, mime_type="image/png") _, thumb1 = get_thumbnail(asset.id, size=120) _, thumb2 = get_thumbnail(asset.id, size=120) assert thumb1 == thumb2 # Must be a valid JPEG and smaller than the source assert thumb1[:3] == b"\xff\xd8\xff" assert len(thumb1) < len(data) def test_orphan_list_excludes_referenced(assets_env): referenced = store_asset(data=_png_bytes((1, 1, 1)), mime_type="image/png") lonely = store_asset(data=_png_bytes((2, 2, 2)), mime_type="image/png") create_entity( entity_type="artifact", name="ref-test", properties={"kind": "image", "asset_id": referenced.id}, ) orphan_ids = {o.id for o in list_orphan_assets()} assert lonely.id in orphan_ids assert referenced.id not in orphan_ids def test_invalidate_refuses_referenced_asset(assets_env): asset = store_asset(data=_png_bytes((3, 3, 3)), mime_type="image/png") create_entity( entity_type="artifact", name="pinned", properties={"kind": "image", "asset_id": asset.id}, ) assert invalidate_asset(asset.id) is False assert get_asset(asset.id).status == "active" def test_invalidate_orphan_succeeds(assets_env): asset = store_asset(data=_png_bytes((4, 4, 4)), mime_type="image/png") assert invalidate_asset(asset.id) is True assert get_asset(asset.id).status == "invalid" def test_api_upload_and_fetch(assets_env): client = TestClient(app) png = _png_bytes((7, 7, 7)) r = client.post( "/assets", files={"file": ("red.png", png, "image/png")}, data={"project": "p05", "caption": "unit test upload"}, ) assert r.status_code == 200, r.text body = r.json() assert body["mime_type"] == "image/png" assert body["caption"] == "unit test upload" asset_id = body["id"] r2 = client.get(f"/assets/{asset_id}") assert r2.status_code == 200 assert r2.headers["content-type"].startswith("image/png") assert r2.content == png r3 = client.get(f"/assets/{asset_id}/thumbnail?size=100") assert r3.status_code == 200 assert r3.headers["content-type"].startswith("image/jpeg") r4 = client.get(f"/assets/{asset_id}/meta") assert r4.status_code == 200 assert r4.json()["id"] == asset_id def test_api_upload_rejects_bad_mime(assets_env): client = TestClient(app) r = client.post( "/assets", files={"file": ("notes.txt", b"hello", "text/plain")}, ) assert r.status_code == 415 def test_api_get_entity_evidence_returns_artifacts(assets_env): asset = store_asset(data=_png_bytes((9, 9, 9)), mime_type="image/png") artifact = create_entity( entity_type="artifact", name="cap-001", properties={ "kind": "image", "asset_id": asset.id, "caption": "tower base", }, ) tower = create_entity(entity_type="component", name="tower") create_relationship( source_entity_id=tower.id, target_entity_id=artifact.id, relationship_type="evidenced_by", ) client = TestClient(app) r = client.get(f"/entities/{tower.id}/evidence") assert r.status_code == 200 body = r.json() assert body["count"] == 1 ev = body["evidence"][0] assert ev["kind"] == "image" assert ev["caption"] == "tower base" assert ev["asset"]["id"] == asset.id def test_v1_assets_aliases_present(assets_env): client = TestClient(app) spec = client.get("/openapi.json").json() paths = spec["paths"] for p in ( "/v1/assets", "/v1/assets/{asset_id}", "/v1/assets/{asset_id}/thumbnail", "/v1/assets/{asset_id}/meta", "/v1/entities/{entity_id}/evidence", ): assert p in paths, f"{p} missing from /v1 alias set" def test_wiki_renders_evidence_strip(assets_env): from atocore.engineering.wiki import render_entity asset = store_asset(data=_png_bytes((10, 10, 10)), mime_type="image/png") artifact = create_entity( entity_type="artifact", name="cap-ev-01", properties={ "kind": "image", "asset_id": asset.id, "caption": "viewport", }, ) tower = create_entity(entity_type="component", name="tower-wiki") create_relationship( source_entity_id=tower.id, target_entity_id=artifact.id, relationship_type="evidenced_by", ) html = render_entity(tower.id) assert "Visual evidence" in html assert f"/assets/{asset.id}/thumbnail" in html assert "viewport" in html def test_wiki_renders_artifact_full_image(assets_env): from atocore.engineering.wiki import render_entity asset = store_asset(data=_png_bytes((11, 11, 11)), mime_type="image/png") artifact = create_entity( entity_type="artifact", name="cap-full-01", properties={ "kind": "image", "asset_id": asset.id, "caption": "detail shot", "capture_context": "narrator: here's the base plate close-up", }, ) html = render_entity(artifact.id) assert f"/assets/{asset.id}/thumbnail?size=1024" in html assert "Capture context" in html assert "narrator" in html