This commit is contained in:
LASTA_DEV01\lasta
2026-05-19 20:31:52 +09:00
parent 00407e7a08
commit e260e5f218
104 changed files with 12898 additions and 1709 deletions

View File

@@ -137,6 +137,14 @@ def test_info_shape() -> None:
assert "text-to-triples" in body["capabilities"]
def test_phase0_does_not_mount_future_extraction_route() -> None:
"""Phase 0 app startup must not depend on Phase 1 extraction packages."""
ctx = _make_mock_context([])
with _client_with_context(ctx) as client:
response = client.post("/api/v1/extract/url", params={"url": "https://example.com"})
assert response.status_code == 404
# ─── /flush ───────────────────────────────────────────────────────────────
def test_flush_requires_confirmation_token() -> None:
ctx = _make_mock_context([])

View File

@@ -0,0 +1,76 @@
"""Phase 3 crawl job API tests."""
from __future__ import annotations
import importlib
from collections.abc import Generator
from contextlib import asynccontextmanager
from fastapi.testclient import TestClient
from ont_platform.api import db_deps
from ont_platform.storage.models import Base
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
main_module = importlib.import_module("ont_platform.api.main")
@asynccontextmanager
async def _noop_lifespan(app):
yield
def _phase3_client(monkeypatch) -> TestClient:
monkeypatch.setenv("PHASE", "3")
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)
def override_get_db() -> Generator[Session, None, None]:
db = session_factory()
try:
yield db
finally:
db.close()
app = main_module.create_app()
app.router.lifespan_context = _noop_lifespan
app.dependency_overrides[db_deps.get_db] = override_get_db
return TestClient(app, raise_server_exceptions=True, backend="asyncio")
def test_phase3_crawl_job_accepts_inline_html_and_persists_progress(monkeypatch) -> None:
html = """
<html lang="en">
<head><title>Ontology Job</title></head>
<body><article><h1>Ontology Job</h1><p>Alice works at Acme in Berlin.</p></article></body>
</html>
"""
with _phase3_client(monkeypatch) as client:
response = client.post(
"/api/v1/crawl/jobs",
json={
"project_id": "proj_crawl",
"url": "https://example.test/job",
"html": html,
"profile": "dynamic_page",
},
)
body = response.json()
job_id = body["job"]["id"]
status = client.get(f"/api/v1/crawl/jobs/{job_id}")
assert response.status_code == 200, response.text
assert body["job"]["status"] == "completed"
assert body["job"]["document_id"].startswith("doc_")
assert body["job"]["entity_count"] >= 1
assert body["job"]["metadata"]["progress"]["profile"] == "dynamic_page"
assert body["job"]["metadata"]["progress"]["pages_completed"] == 1
assert status.status_code == 200
assert status.json()["job"]["id"] == job_id

View File

@@ -0,0 +1,111 @@
"""Phase 6 maintenance API tests."""
from __future__ import annotations
import importlib
from collections.abc import Generator
from contextlib import asynccontextmanager
from datetime import datetime
from fastapi.testclient import TestClient
from ont_platform.api import db_deps
from ont_platform.storage.candidate_repository import CandidateRepository
from ont_platform.storage.models import Base, SourceDocument
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
main_module = importlib.import_module("ont_platform.api.main")
@asynccontextmanager
async def _noop_lifespan(app):
yield
def _phase6_client(monkeypatch) -> TestClient:
monkeypatch.setenv("PHASE", "6")
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)
def override_get_db() -> Generator[Session, None, None]:
db = session_factory()
if db.get(SourceDocument, "doc_api_phase6") is None:
_seed(db)
db.commit()
try:
yield db
finally:
db.close()
app = main_module.create_app()
app.router.lifespan_context = _noop_lifespan
app.dependency_overrides[db_deps.get_db] = override_get_db
return TestClient(app, raise_server_exceptions=True, backend="asyncio")
def _seed(db: Session) -> None:
db.add(
SourceDocument(
id="doc_api_phase6",
project_id="proj_api_phase6",
source_url="https://example.test/api-phase6",
document_type="html",
title="API Phase 6 Source",
text="Acme appears in a source.",
content_hash="hash_api_phase6",
fingerprint="fp_api_phase6",
retrieved_at=datetime.utcnow(),
extracted_by="trafilatura",
)
)
CandidateRepository(db).save_lightweight_result(
project_id="proj_api_phase6",
document_id="doc_api_phase6",
result={
"entities": [
{
"id": "E_api_phase6",
"label": "Acme",
"type": "org",
"confidence": 0.4,
"evidence_ids": [],
}
],
"relations": [],
"evidence_spans": [],
},
)
def test_phase6_api_runs_loop_and_reviews_proposal(monkeypatch) -> None:
with _phase6_client(monkeypatch) as client:
run_response = client.post(
"/api/v1/maintenance/runs",
json={
"project_id": "proj_api_phase6",
"requested_by": "ops",
"actor_role": "admin",
},
)
proposal_response = client.get(
"/api/v1/maintenance/proposals",
params={"project_id": "proj_api_phase6", "status": "pending_review"},
)
proposal_id = proposal_response.json()["proposals"][0]["id"]
review_response = client.post(
f"/api/v1/maintenance/proposals/{proposal_id}/review",
json={"reviewed_by": "admin", "actor_role": "admin", "approve": True},
)
assert run_response.status_code == 200, run_response.text
assert run_response.json()["run"]["summary"]["direct_mutations"] == 0
assert proposal_response.status_code == 200
assert proposal_response.json()["proposals"]
assert review_response.status_code == 200, review_response.text
assert review_response.json()["proposal"]["status"] == "approved"

View File

@@ -0,0 +1,123 @@
"""Phase 2 review queue API tests."""
from __future__ import annotations
import importlib
from collections.abc import Generator
from contextlib import asynccontextmanager
from fastapi.testclient import TestClient
from ont_platform.api import db_deps
from ont_platform.storage.models import Base
from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
main_module = importlib.import_module("ont_platform.api.main")
@asynccontextmanager
async def _noop_lifespan(app):
yield
def _phase2_client(monkeypatch) -> TestClient:
monkeypatch.setenv("PHASE", "2")
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)
def override_get_db() -> Generator[Session, None, None]:
db = session_factory()
try:
yield db
finally:
db.close()
app = main_module.create_app()
app.router.lifespan_context = _noop_lifespan
app.dependency_overrides[db_deps.get_db] = override_get_db
return TestClient(app, raise_server_exceptions=True, backend="asyncio")
def test_review_api_ingests_approves_and_promotes_candidate(monkeypatch) -> None:
with _phase2_client(monkeypatch) as client:
ingest = client.post(
"/api/v1/review/ingest/lightweight",
json={
"project_id": "proj_api",
"document_id": "doc_api",
"entities": [
{
"id": "E_api",
"label": "Acme",
"type": "org",
"confidence": 0.93,
"evidence_ids": ["EV_api"],
}
],
"relations": [],
"evidence_spans": [
{
"id": "EV_api",
"text": "Acme is mentioned in the source.",
"start_offset": 0,
"end_offset": 32,
}
],
"source_trust": 0.9,
"validation_passed": True,
},
)
approve = client.post(
"/api/v1/review/candidates/entity/E_api/approve",
json={"reviewed_by": "lasta", "reason": "verified"},
)
promote = client.post("/api/v1/review/promote", params={"project_id": "proj_api"})
assert ingest.status_code == 200, ingest.text
assert ingest.json()["source_type"] == "lightweight"
assert approve.status_code == 200, approve.text
assert approve.json()["decision"]["new_status"] == "approved"
assert promote.status_code == 200, promote.text
plan = promote.json()["promotion_plan"]
assert plan["entity_count"] == 1
assert plan["blocked_count"] == 0
assert plan["entities"][0]["id"] == "E_api"
def test_review_api_blocks_approval_without_evidence(monkeypatch) -> None:
with _phase2_client(monkeypatch) as client:
ingest = client.post(
"/api/v1/review/ingest/ontocast",
json={
"project_id": "proj_api",
"document_id": "doc_api",
"entities": [
{
"id": "E_no_evidence_api",
"label": "Unsupported",
"entity_type": "concept",
"confidence": 0.99,
"evidence_ids": [],
}
],
"relations": [],
"evidence_spans": [],
"source_trust": 0.95,
"validation_passed": True,
},
)
approve = client.post(
"/api/v1/review/candidates/entity/E_no_evidence_api/approve",
json={"reviewed_by": "lasta"},
)
assert ingest.status_code == 200, ingest.text
assert ingest.json()["source_type"] == "ontocast"
assert approve.status_code == 422
assert "without valid evidence" in approve.json()["detail"]

View File

@@ -0,0 +1,87 @@
"""Phase 1 URL/HTML ingestion API tests."""
from __future__ import annotations
import importlib
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi.testclient import TestClient
from ont_platform.storage.dedup_cache import reset_default_dedup_cache
main_module = importlib.import_module("ont_platform.api.main")
@asynccontextmanager
async def _noop_lifespan(app):
yield
def _phase1_client(monkeypatch) -> TestClient:
monkeypatch.setenv("PHASE", "1")
reset_default_dedup_cache()
app = main_module.create_app()
app.router.lifespan_context = _noop_lifespan
return TestClient(app, raise_server_exceptions=True, backend="asyncio")
def test_extract_url_accepts_html_payload_and_returns_source_document(
monkeypatch,
fixtures_dir: Path,
) -> None:
html = (fixtures_dir / "korean" / "news_yonhap.html").read_text(encoding="utf-8")
with _phase1_client(monkeypatch) as client:
response = client.post(
"/api/v1/extract/url",
json={
"url": "https://example.test/news/data-quality",
"html": html,
"project_id": "proj_phase1",
},
)
assert response.status_code == 200, response.text
body = response.json()
assert body["status"] == "success"
assert body["source_document"]["source_url"] == "https://example.test/news/data-quality"
assert body["source_document"]["language"] == "ko"
assert body["source_document"]["content_hash"]
assert body["evidence_spans"]
assert body["content_unit"]["doc_iri"].startswith("urn:source:doc_")
assert body["dedup"]["is_duplicate"] is False
def test_process_url_skips_duplicate_payload_by_fingerprint(
monkeypatch,
fixtures_dir: Path,
) -> None:
html = (fixtures_dir / "korean" / "blog_naver.html").read_text(encoding="utf-8")
with _phase1_client(monkeypatch) as client:
first = client.post(
"/process/url",
json={
"url": "https://example.test/blog/original",
"html": html,
"project_id": "proj_phase1",
},
)
second = client.post(
"/process/url",
json={
"url": "https://example.test/blog/mirror",
"html": html,
"project_id": "proj_phase1",
},
)
assert first.status_code == 200, first.text
assert second.status_code == 200, second.text
first_body = first.json()
second_body = second.json()
assert first_body["dedup"]["is_duplicate"] is False
assert second_body["dedup"]["is_duplicate"] is True
assert second_body["dedup"]["existing_document_id"] == first_body["source_document"]["id"]
assert second_body["entity_count"] == 0
assert "Duplicate source document skipped" in second_body["warnings"][0]