from __future__ import annotations from pathlib import Path from typing import Any, cast import pytest from reviewer.server import ReviewArtifactError, ReviewArtifacts from tests.reviewer_fixture import create_review_run, read_json, write_json def test_reviewer_returns_full_comparison_and_all_component_stages(tmp_path: Path) -> None: fixture = create_review_run(tmp_path) repository = ReviewArtifacts(fixture["run_directory"]) run = cast(dict[str, Any], repository.run_summary()) comparison = cast(dict[str, Any], repository.document_comparison("paper")) changed = cast(dict[str, Any], repository.component_stage("paper", 0)) unchanged = cast(dict[str, Any], repository.component_stage("paper", 1)) assert run["summary"] == { "document_count": 1, "success_count": 1, "failed_count": 0, "unstable_count": 0, "change_count": 1, } assert run["documents"][0]["source_available"] is True assert comparison["original_markdown"] == fixture["original"] assert comparison["cleaned_markdown"] == fixture["cleaned"] assert comparison["changes"][0]["editor_range"] == {"start": 4, "end": 7} assert changed["before_markdown"] == fixture["original"] assert changed["after_markdown"] == fixture["cleaned"] assert unchanged["before_markdown"] == fixture["cleaned"] assert unchanged["after_markdown"] == fixture["cleaned"] assert unchanged["changes"] == [] @pytest.mark.parametrize("status", ["failed", "unstable"]) def test_non_success_documents_expose_audit_but_no_formal_output(tmp_path: Path, status: str) -> None: fixture = create_review_run(tmp_path, status=status) repository = ReviewArtifacts(fixture["run_directory"]) comparison = cast(dict[str, Any], repository.document_comparison("paper")) assert comparison["original_markdown"] == fixture["original"] assert comparison["cleaned_markdown"] is None if status == "failed": assert comparison["errors"] assert comparison["residual_proposals"] == [] else: assert comparison["errors"] == [] assert comparison["residual_proposals"] with pytest.raises(ReviewArtifactError, match="success"): repository.component_stage("paper", 0) def test_historical_run_without_locator_reports_unavailable_source(tmp_path: Path) -> None: fixture = create_review_run(tmp_path, with_locator=False) repository = ReviewArtifacts(fixture["run_directory"]) summary = cast(dict[str, Any], repository.run_summary()) assert summary["documents"][0]["source_available"] is False assert "review-locator.json" in summary["documents"][0]["availability_error"] comparison = cast(dict[str, Any], repository.document_comparison("paper")) assert comparison["original_markdown"] is None assert comparison["cleaned_markdown"] == fixture["cleaned"] assert comparison["changes"][0]["editor_range"] is None with pytest.raises(ReviewArtifactError, match=r"review-locator\.json"): repository.component_stage("paper", 0) def test_source_hash_change_is_not_silently_displayed(tmp_path: Path) -> None: fixture = create_review_run(tmp_path) repository = ReviewArtifacts(fixture["run_directory"]) source_path = fixture["source_path"] assert isinstance(source_path, Path) source_path.write_text("changed\n", encoding="utf-8") summary = cast(dict[str, Any], repository.run_summary()) assert summary["documents"][0]["source_available"] is False assert "哈希" in summary["documents"][0]["availability_error"] comparison = cast(dict[str, Any], repository.document_comparison("paper")) assert comparison["original_markdown"] is None assert comparison["changes"][0]["editor_range"] is None with pytest.raises(ReviewArtifactError, match="哈希"): repository.component_stage("paper", 0) def test_reviewer_rejects_unknown_schema_and_artifact_path_escape(tmp_path: Path) -> None: fixture = create_review_run(tmp_path) manifest_path = fixture["manifest_path"] assert isinstance(manifest_path, Path) manifest = read_json(manifest_path) manifest["schema_version"] = 2 write_json(manifest_path, manifest) with pytest.raises(ReviewArtifactError) as unknown: ReviewArtifacts(fixture["run_directory"]) assert unknown.value.code == "unsupported_schema" second = create_review_run(tmp_path / "second") second_manifest_path = second["manifest_path"] assert isinstance(second_manifest_path, Path) second_manifest = read_json(second_manifest_path) second_manifest["documents"][0]["result_path"] = "../outside.json" write_json(second_manifest_path, second_manifest) with pytest.raises(ReviewArtifactError, match="路径"): ReviewArtifacts(second["run_directory"]) def test_reviewer_rejects_tampered_snapshot_chain_and_unknown_identity(tmp_path: Path) -> None: fixture = create_review_run(tmp_path) result_path = fixture["result_path"] assert isinstance(result_path, Path) result = read_json(result_path) result["changes"][0]["after_sha256"] = "0" * 64 write_json(result_path, result) repository = ReviewArtifacts(fixture["run_directory"]) with pytest.raises(ReviewArtifactError) as replay_error: repository.document_comparison("paper") assert replay_error.value.code == "untrusted_replay" with pytest.raises(ReviewArtifactError) as document_error: repository.document_comparison("missing") assert document_error.value.http_status == 404 with pytest.raises(ReviewArtifactError) as component_error: repository.component_stage("paper", 99) assert component_error.value.http_status == 404