from __future__ import annotations from dataclasses import FrozenInstanceError from hashlib import sha256 import pytest from mdpolish import ( DocumentSnapshot, ErrorStage, ProposedChange, ResidualProposal, RunError, RunStatus, TextEdit, TextSpan, TransformResult, ) from mdpolish.models import ProposalReference def test_snapshot_preserves_exact_markdown_and_computes_hash() -> None: markdown = "标题\r\nCafe\u0301\n🙂\n" snapshot = DocumentSnapshot(markdown) assert snapshot.markdown == markdown assert snapshot.sha256 == sha256(markdown.encode("utf-8")).hexdigest() def test_empty_snapshot_is_valid_and_hash_cannot_be_supplied() -> None: snapshot = DocumentSnapshot("") assert snapshot.sha256 == sha256(b"").hexdigest() with pytest.raises(TypeError): DocumentSnapshot("", sha256="0" * 64) # type: ignore[call-arg] def test_snapshot_is_frozen() -> None: snapshot = DocumentSnapshot("original") with pytest.raises(FrozenInstanceError): snapshot.markdown = "changed" # type: ignore[misc] @pytest.mark.parametrize( ("start", "end", "error_type"), [ (-1, 0, ValueError), (2, 1, ValueError), (True, 1, TypeError), ], ) def test_span_rejects_invalid_indexes(start: int, end: int, error_type: type[Exception]) -> None: with pytest.raises(error_type): TextSpan(start, end) def test_text_edit_supports_insert_delete_and_replace() -> None: snapshot = DocumentSnapshot("中文abc") insertion = TextEdit(snapshot.sha256, TextSpan(2, 2), "", "!") deletion = TextEdit(snapshot.sha256, TextSpan(2, 3), "a", "") replacement = TextEdit(snapshot.sha256, TextSpan(3, 5), "bc", "BC") assert insertion.span.is_empty assert deletion.replacement == "" assert replacement.expected_text == "bc" def test_text_edit_rejects_bad_digest_length_mismatch_and_no_op() -> None: digest = DocumentSnapshot("abc").sha256 with pytest.raises(ValueError, match="SHA-256"): TextEdit("bad", TextSpan(0, 1), "a", "b") with pytest.raises(ValueError, match="length"): TextEdit(digest, TextSpan(0, 2), "a", "b") with pytest.raises(ValueError, match="must change"): TextEdit(digest, TextSpan(0, 1), "a", "a") def test_proposal_requires_reason_edits_and_one_matching_digest() -> None: snapshot = DocumentSnapshot("abc") other = DocumentSnapshot("xyz") edit = TextEdit(snapshot.sha256, TextSpan(0, 1), "a", "A") other_edit = TextEdit(other.sha256, TextSpan(0, 1), "x", "X") with pytest.raises(ValueError, match="reason"): ProposedChange(snapshot.sha256, " ", (edit,)) with pytest.raises(ValueError, match="non-empty tuple"): ProposedChange(snapshot.sha256, "reason", ()) with pytest.raises(ValueError, match="proposal digest"): ProposedChange(snapshot.sha256, "reason", (other_edit,)) def test_transform_result_enforces_status_specific_output_fields() -> None: snapshot = DocumentSnapshot("abc") error = RunError("component", "1.0.0", 0, ErrorStage.TRANSFORM, "ExampleError", "safe") edit = TextEdit(snapshot.sha256, TextSpan(0, 1), "a", "A") proposal = ProposedChange(snapshot.sha256, "reason", (edit,)) residual = ResidualProposal( component_id="component", component_version="1.0.0", component_position=0, proposal_ref=ProposalReference(0, snapshot.sha256, 0), proposal=proposal, ) success = TransformResult( status=RunStatus.SUCCESS, input_sha256=snapshot.sha256, current_sha256=snapshot.sha256, output_markdown=snapshot.markdown, ) failed = TransformResult( status=RunStatus.FAILED, input_sha256=snapshot.sha256, current_sha256=snapshot.sha256, errors=(error,), partial_markdown=snapshot.markdown, ) unstable = TransformResult( status=RunStatus.UNSTABLE, input_sha256=snapshot.sha256, current_sha256=snapshot.sha256, residual_proposals=(residual,), partial_markdown=snapshot.markdown, ) assert success.output_markdown == "abc" assert failed.partial_markdown == "abc" assert unstable.residual_proposals == (residual,) with pytest.raises(ValueError, match="successful result"): TransformResult( status=RunStatus.SUCCESS, input_sha256=snapshot.sha256, current_sha256=snapshot.sha256, errors=(error,), output_markdown=snapshot.markdown, ) with pytest.raises(ValueError, match="failed result"): TransformResult( status=RunStatus.FAILED, input_sha256=snapshot.sha256, current_sha256=snapshot.sha256, partial_markdown=snapshot.markdown, )