lllin000_PaperForge/tests/test_status.py

383 lines
13 KiB
Python

"""Tests for status.py — run_status with canonical index, run_doctor Index Health.
Phase 25-01 Task 2: run_status() reads from canonical index
Phase 25-01 Task 3: run_doctor() Index Health section
"""
from __future__ import annotations
import json
from pathlib import Path
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _minimal_vault(tmp_path: Path) -> Path:
"""Create a minimal vault with paperforge.json for path resolution."""
vault = tmp_path / "test_vault"
vault.mkdir(parents=True, exist_ok=True)
pf_json = vault / "paperforge.json"
pf_json.write_text(
json.dumps(
{
"version": "1.2.0",
"vault_config": {
"system_dir": "99_System",
"resources_dir": "03_Resources",
"literature_dir": "Literature",
"control_dir": "LiteratureControl",
"base_dir": "05_Bases",
"skill_dir": ".opencode/skills",
},
},
indent=2,
),
encoding="utf-8",
)
return vault
def _ensure_domain_config(vault: Path) -> None:
"""Create domain config so load_domain_config returns a valid config."""
from paperforge.config import paperforge_paths as _pp
paths = _pp(vault)
config_dir = paths["paperforge"] / "config"
config_dir.mkdir(parents=True, exist_ok=True)
config_path = config_dir / "domain-collections.json"
config_path.write_text(
json.dumps(
{
"collections": {},
"domains": [],
},
indent=2,
),
encoding="utf-8",
)
def _write_index(vault: Path, items: list[dict]) -> None:
"""Write a canonical index envelope to the vault."""
from paperforge.worker.asset_index import atomic_write_index, build_envelope, get_index_path
idx_path = get_index_path(vault)
idx_path.parent.mkdir(parents=True, exist_ok=True)
envelope = build_envelope(items)
atomic_write_index(idx_path, envelope)
def _ensure_library_records(vault: Path) -> Path:
"""Ensure library_records dir exists and return its path."""
from paperforge.worker._utils import pipeline_paths
paths = pipeline_paths(vault)
records_dir = paths["library_records"]
records_dir.mkdir(parents=True, exist_ok=True)
return records_dir
def _ensure_exports(vault: Path) -> None:
"""Create empty exports dir (needed for run_status filesystem counts)."""
from paperforge.worker._utils import pipeline_paths
paths = pipeline_paths(vault)
paths["exports"].mkdir(parents=True, exist_ok=True)
# ---------------------------------------------------------------------------
# Task 2: status --json reads from canonical index
# ---------------------------------------------------------------------------
class TestStatusJsonIndexSource:
"""run_status(json_output=True) with canonical index"""
def test_status_json_includes_lifecycle_health_maturity(self, tmp_path: Path) -> None:
"""JSON output includes lifecycle/health/maturity aggregates from index."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
# Create a canonical index with 2 entries
items = [
{
"zotero_key": "AAA",
"lifecycle": "ai_context_ready",
"health": {
"pdf_health": "healthy",
"ocr_health": "healthy",
"note_health": "healthy",
"asset_health": "healthy",
},
"maturity": {"level": 6},
},
{
"zotero_key": "BBB",
"lifecycle": "indexed",
"health": {
"pdf_health": "healthy",
"ocr_health": "healthy",
"note_health": "Formal note missing",
"asset_health": "Missing workspace paths",
},
"maturity": {"level": 1},
},
]
_write_index(vault, items)
code = run_status(vault, json_output=True)
assert code == 0
def test_status_json_fallback_when_index_missing(self, tmp_path: Path, capsys) -> None:
"""When no canonical index, lifecycle/health/maturity fields are None."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
code = run_status(vault, json_output=True)
captured = capsys.readouterr().out
assert code == 0
envelope = json.loads(captured)
data = envelope["data"]
assert data["lifecycle_level_counts"] == {}
assert data["health_aggregate"] == {}
assert data["maturity_distribution"] == {}
def test_status_json_includes_structured_ocr_health(self, tmp_path: Path, capsys) -> None:
"""JSON output includes structured_ocr_health key."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "HLTH001"
ocr_dir.mkdir(parents=True)
health_dir = ocr_dir / "health"
health_dir.mkdir(parents=True)
(health_dir / "ocr_health.json").write_text(
json.dumps({"overall": "green", "page_count": 3, "blocks_count": 50}),
encoding="utf-8",
)
code = run_status(vault, json_output=True)
captured = capsys.readouterr().out
assert code == 0
envelope = json.loads(captured)
data = envelope["data"]
assert "structured_ocr_health" in data
assert len(data["structured_ocr_health"]) == 1
assert data["structured_ocr_health"][0]["key"] == "HLTH001"
assert data["structured_ocr_health"][0]["overall"] == "green"
def test_status_text_output_includes_index_section(self, tmp_path: Path, capsys) -> None:
"""Text output contains 'lifecycle:' line when index is present."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
_write_index(vault, [])
code = run_status(vault, json_output=False)
captured = capsys.readouterr().out
assert code == 0
assert "lifecycle:" in captured
assert "health:" in captured
# ---------------------------------------------------------------------------
# Task 3: doctor Index Health section
# ---------------------------------------------------------------------------
class TestDoctorIndexHealth:
"""run_doctor() Index Health section"""
def _run_doctor(self, vault: Path) -> str:
"""Run doctor and return captured stdout."""
import io
import sys
from paperforge.worker.status import run_doctor
old_stdout = sys.stdout
sys.stdout = buf = io.StringIO()
try:
run_doctor(vault)
finally:
sys.stdout = old_stdout
return buf.getvalue()
def test_doctor_includes_index_health_section(self, tmp_path: Path) -> None:
"""Doctor output includes 'Index Health' section when index present."""
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_write_index(vault, [])
output = self._run_doctor(vault)
assert "Index Health" in output
def test_doctor_index_health_with_mixed_health(self, tmp_path: Path) -> None:
"""Mixed health entries produce correct counts."""
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
items = [
{
"zotero_key": "AAA",
"lifecycle": "ai_context_ready",
"health": {
"pdf_health": "healthy",
"ocr_health": "healthy",
"note_health": "healthy",
"asset_health": "healthy",
},
"maturity": {"level": 6},
},
{
"zotero_key": "BBB",
"lifecycle": "indexed",
"health": {
"pdf_health": "healthy",
"ocr_health": "OCR failed",
"note_health": "Formal note missing",
"asset_health": "healthy",
},
"maturity": {"level": 1},
},
]
_write_index(vault, items)
output = self._run_doctor(vault)
assert "Index Health" in output
# PDF Health: both healthy
assert "PDF Health" in output
# OCR Health: one unhealthy
assert "OCR Health" in output
# Note Health: one unhealthy
assert "Note Health" in output
def test_doctor_index_health_no_index(self, tmp_path: Path) -> None:
"""When no index, doctor shows info message instead of crashing."""
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
output = self._run_doctor(vault)
assert "Index Health" in output
assert "No canonical index" in output
# ---------------------------------------------------------------------------
# Structured OCR Health in status
# ---------------------------------------------------------------------------
def test_status_text_structured_ocr_health(tmp_path: Path, capsys) -> None:
"""run_status() text output shows structured OCR health."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "HLTH001"
ocr_dir.mkdir(parents=True)
health_dir = ocr_dir / "health"
health_dir.mkdir(parents=True)
(health_dir / "ocr_health.json").write_text(
json.dumps({"overall": "yellow", "page_count": 5, "blocks_count": 100}),
encoding="utf-8",
)
code = run_status(vault)
captured = capsys.readouterr().out
assert code == 0
assert "structured_ocr_health" in captured
assert "HLTH001" in captured
assert "yellow" in captured
# ---------------------------------------------------------------------------
# OCR version runtime state in status
# ---------------------------------------------------------------------------
def test_status_json_includes_ocr_version_state(tmp_path: Path, capsys) -> None:
"""JSON output includes ocr_version_state with version info."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "VST001"
ocr_dir.mkdir(parents=True)
meta = {
"zotero_key": "VST001",
"raw_version": {"ocr_model": "PaddleOCR-VL-1.6", "ocr_raw_schema_version": "1.0.0"},
"derived_version": {"renderer_version": "2.0.0"},
"raw_upgradable": False,
"derived_stale": False,
}
(ocr_dir / "meta.json").write_text(json.dumps(meta), encoding="utf-8")
code = run_status(vault, json_output=True)
assert code == 0
captured = capsys.readouterr().out
envelope = json.loads(captured)
payload = envelope["data"]
assert "ocr_version_state" in payload
assert payload["ocr_version_state"]["total_papers"] == 1
assert payload["ocr_version_state"]["derived_stale_count"] == 0
assert payload["ocr_version_state"]["raw_upgradable_count"] == 0
def test_status_text_ocr_version_state(tmp_path: Path, capsys) -> None:
"""Text output includes ocr_version_state line."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "VST001"
ocr_dir.mkdir(parents=True)
meta = {
"zotero_key": "VST001",
"raw_version": {"ocr_model": "PaddleOCR-VL-1.5", "ocr_raw_schema_version": "1.0.0"},
"derived_version": {"renderer_version": "1.0.0-compat"},
"raw_upgradable": True,
"derived_stale": True,
}
(ocr_dir / "meta.json").write_text(json.dumps(meta), encoding="utf-8")
code = run_status(vault)
assert code == 0
captured = capsys.readouterr().out
assert "ocr_version_state" in captured
def test_status_text_legacy_backfilled(tmp_path: Path, capsys) -> None:
"""Text output includes legacy_backfilled count when papers are backfilled."""
from paperforge.worker.status import run_status
vault = _minimal_vault(tmp_path)
_ensure_domain_config(vault)
_ensure_exports(vault)
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "LEGACY001"
ocr_dir.mkdir(parents=True)
meta = {
"zotero_key": "LEGACY001",
"ocr_status": "done",
"is_backfilled": True,
}
(ocr_dir / "meta.json").write_text(json.dumps(meta), encoding="utf-8")
code = run_status(vault)
assert code == 0
captured = capsys.readouterr().out
assert "legacy_backfilled" in captured