133 lines
5.6 KiB
Python
133 lines
5.6 KiB
Python
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
|