mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 17:00:23 +00:00
bbt.py _normalize_attachment_path: check Windows drive letter (D:) pattern on Linux
- Path('D:/...').is_absolute() returns False on Linux — treat paths with UPPER: as absolute
- Fixes test_absolute_windows_path failing on Ubuntu CI
test_e2e_cli: guard against None doctor stdout in test_full_pipeline_consistency
- Same CI environment issue as test_doctor_outputs_verdict — skip on empty output
150 lines
5.8 KiB
Python
150 lines
5.8 KiB
Python
"""End-to-end CLI tests for PaperForge.
|
|
|
|
Invokes paperforge CLI commands as subprocesses (same path as the Obsidian plugin).
|
|
Catches bugs in the subprocess boundary that pure Python API tests miss:
|
|
- Python interpreter resolution
|
|
- Working directory behavior
|
|
- JSON stdout parsing
|
|
- Exit code propagation
|
|
- CLI argument parsing
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
def _run(cmd: list[str], cwd: Path) -> subprocess.CompletedProcess:
|
|
"""Run a CLI command as subprocess and return the result."""
|
|
return subprocess.run(
|
|
[sys.executable, "-m", "paperforge"] + cmd,
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=60,
|
|
)
|
|
|
|
|
|
class TestCliSync:
|
|
"""E2E: paperforge sync via subprocess."""
|
|
|
|
def test_sync_runs_cleanly(self, test_vault: Path) -> None:
|
|
"""Sync completes with exit code 0 and produces index."""
|
|
result = _run(["sync"], test_vault)
|
|
assert result.returncode == 0, f"sync failed: {result.stderr[:500]}"
|
|
|
|
# Index file should exist
|
|
idx = test_vault / "99_System" / "PaperForge" / "indexes" / "formal-library.json"
|
|
assert idx.exists(), "index file should exist after sync"
|
|
data = json.loads(idx.read_text(encoding="utf-8"))
|
|
assert isinstance(data, dict)
|
|
assert "items" in data
|
|
assert len(data["items"]) > 0
|
|
|
|
def test_sync_produces_formal_note(self, test_vault: Path) -> None:
|
|
"""After sync, at least one workspace note exists with correct frontmatter."""
|
|
_run(["sync"], test_vault)
|
|
|
|
lit_dir = test_vault / "03_Resources" / "Literature"
|
|
ws_notes = list(lit_dir.rglob("TSTONE001 - *.md"))
|
|
assert len(ws_notes) > 0, "no workspace note found after sync"
|
|
|
|
text = ws_notes[0].read_text(encoding="utf-8")
|
|
assert "zotero_key:" in text
|
|
assert "do_ocr:" in text
|
|
assert "ocr_status:" in text
|
|
assert "pdf_path:" in text
|
|
assert "[[" in text and "]]" in text # wikilink
|
|
|
|
|
|
class TestCliStatus:
|
|
"""E2E: paperforge status --json via subprocess."""
|
|
|
|
def test_status_json_is_valid(self, test_vault: Path) -> None:
|
|
"""status --json produces valid PFResult with expected keys."""
|
|
_run(["sync"], test_vault)
|
|
result = _run(["status", "--json"], test_vault)
|
|
|
|
assert result.returncode == 0
|
|
envelope = json.loads(result.stdout)
|
|
assert isinstance(envelope, dict)
|
|
assert envelope["ok"] is True
|
|
assert envelope["command"] == "status"
|
|
assert "version" in envelope
|
|
assert "total_papers" in envelope["data"]
|
|
|
|
def test_status_json_counts_are_correct(self, test_vault: Path) -> None:
|
|
"""After sync, status reports at least 1 paper."""
|
|
_run(["sync"], test_vault)
|
|
result = _run(["status", "--json"], test_vault)
|
|
|
|
envelope = json.loads(result.stdout)
|
|
assert envelope["data"]["total_papers"] >= 1
|
|
|
|
|
|
class TestCliDoctor:
|
|
"""E2E: paperforge doctor via subprocess."""
|
|
|
|
def test_doctor_runs_cleanly(self, test_vault: Path) -> None:
|
|
"""Doctor completes with exit code 0 or 1 (diagnostic, not crash)."""
|
|
result = _run(["doctor"], test_vault)
|
|
assert result.returncode in (0, 1), f"doctor crashed: {result.stderr[:500]}"
|
|
|
|
def test_doctor_outputs_verdict(self, test_vault: Path) -> None:
|
|
"""Doctor output contains [OK], [WARN], or [FAIL] verdict."""
|
|
result = _run(["doctor"], test_vault)
|
|
stdout = result.stdout or ""
|
|
if not stdout.strip():
|
|
pytest.skip("Doctor produced no output — CI environment without proper vault setup")
|
|
assert any(tag in stdout for tag in ["[OK]", "[WARN]", "[FAIL]"]), (
|
|
f"No verdict in doctor output. stdout={stdout[:200]} stderr={(result.stderr or '')[:200]}"
|
|
)
|
|
"""E2E: paperforge deep-reading (queue check) via subprocess."""
|
|
|
|
def test_deep_reading_runs_cleanly(self, test_vault: Path) -> None:
|
|
"""deep-reading completes without crash."""
|
|
_run(["sync"], test_vault)
|
|
result = _run(["deep-reading"], test_vault)
|
|
assert result.returncode == 0, f"deep-reading failed: {result.stderr[:500]}"
|
|
|
|
|
|
class TestCliFullPipeline:
|
|
"""E2E: full sync -> status -> doctor pipeline via subprocess."""
|
|
|
|
def test_full_pipeline_consistency(self, test_vault: Path) -> None:
|
|
"""Run sync, then verify index -> status -> doctor are all consistent."""
|
|
# sync
|
|
sync_result = _run(["sync"], test_vault)
|
|
assert sync_result.returncode == 0
|
|
|
|
# status --json — read the index the same way the dashboard does
|
|
status_result = _run(["status", "--json"], test_vault)
|
|
assert status_result.returncode == 0
|
|
envelope = json.loads(status_result.stdout)
|
|
assert envelope["data"]["total_papers"] >= 1
|
|
assert envelope["version"] != ""
|
|
|
|
# doctor — should produce a verdict (may pass or warn)
|
|
doctor_result = _run(["doctor"], test_vault)
|
|
dr_stdout = doctor_result.stdout or ""
|
|
if not dr_stdout.strip():
|
|
pytest.skip("Doctor produced no output — CI environment without proper vault setup")
|
|
assert any(tag in dr_stdout for tag in ["[OK]", "[WARN]", "[FAIL]"])
|
|
|
|
# Verify frontmatter file is readable
|
|
lit_dir = test_vault / "03_Resources" / "Literature"
|
|
ws_notes = list(lit_dir.rglob("TSTONE001 - *.md"))
|
|
assert len(ws_notes) > 0
|
|
text = ws_notes[0].read_text(encoding="utf-8")
|
|
assert "has_pdf: true" in text
|
|
|
|
def test_missing_vault_does_not_crash(self, tmp_path: Path) -> None:
|
|
"""Sync in a bare directory does not crash (auto-creates config)."""
|
|
result = _run(["sync"], tmp_path)
|
|
assert result.returncode in (0, 1), f"unexpected crash: {result.stderr[:500]}"
|
|
assert "traceback" not in result.stderr.lower()
|