mike-backend / backend /python-service /tests /test_track_changes.py
bardd's picture
Deploy Mike backend-only Space
06671aa verified
Raw History Blame Contribute Delete
29.3 kB
import os
import shutil
import sys
import tempfile
import zipfile
from pathlib import Path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from docx_comments import apply_docx_comments
from models import ReviewFinding
def _finding(
*,
finding_id: str = "test_finding_1",
paragraph_id: str = "P_0008",
target_text: str = "",
anchor_mode: str = "paragraph",
op_type: str = "COMMENT",
new_text: str | None = None,
insert_text: str | None = None,
position: str | None = None,
rewrite_mode: str | None = None,
old_span: str | None = None,
new_span: str | None = None,
) -> ReviewFinding:
return ReviewFinding(
id=finding_id,
risk_level="high",
clause_type="Indemnification",
issue="Test issue",
comment="This is a test comment from the Mike system.",
review_reasoning={"clause_section": "test", "violated_standard": "test", "zamil_exposure": "Manual test run"},
target_text=target_text,
anchor_mode=anchor_mode,
start_paragraph_id=paragraph_id,
end_paragraph_id=paragraph_id,
start_anchor=target_text[:220],
end_anchor=target_text[-220:] if target_text else "",
before_anchor="",
after_anchor="",
confidence=1.0,
op_type=op_type,
new_text=new_text,
insert_text=insert_text,
position=position,
rewrite_mode=rewrite_mode,
old_span=old_span,
new_span=new_span,
)
def _sample_docx() -> Path:
root = Path(__file__).resolve().parents[1]
sample = root / "data" / "uploads" / "run_20260508_120049_e4b2552b55" / "test_contract.docx"
if not sample.exists():
import pytest
pytest.skip(f"Missing test DOCX fixture: {sample}")
return sample
def _require_dotnet():
if shutil.which("dotnet") is None:
import pytest
pytest.skip("dotnet is required for the DOCX commenter integration test")
def _write_minimal_docx(path: Path, body_paragraphs: list[str]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
para_xml = "".join(
f'<w:p><w:r><w:t>{t}</w:t></w:r></w:p>' for t in body_paragraphs
)
with zipfile.ZipFile(path, "w") as zf:
zf.writestr(
"[Content_Types].xml",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/>'
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>'
'</Types>',
)
zf.writestr(
"_rels/.rels",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>'
'</Relationships>',
)
zf.writestr(
"word/_rels/document.xml.rels",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'</Relationships>',
)
zf.writestr(
"word/document.xml",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
f'<w:body>{para_xml}<w:sectPr/></w:body></w:document>',
)
def _docx_has_tracked_changes(path: Path) -> bool:
with zipfile.ZipFile(path) as zf:
doc_xml = zf.read("word/document.xml").decode("utf-8")
return "w:ins" in doc_xml or "w:del" in doc_xml
def _docx_has_comments(path: Path) -> bool:
with zipfile.ZipFile(path) as zf:
names = zf.namelist()
return "word/comments.xml" in names
# ---------------------------------------------------------------------------
# Step 16: COMMENT-only backward compatibility
# ---------------------------------------------------------------------------
def test_comment_only_mode_backward_compatible():
_require_dotnet()
sample = _sample_docx()
with tempfile.TemporaryDirectory() as tmpdir:
out = Path(tmpdir) / "output.docx"
results = apply_docx_comments(
input_docx=sample,
output_docx=out,
findings=[_finding(target_text="")],
work_dir=Path(tmpdir) / "work",
track_changes=False,
)
assert results
assert results[0].status == "applied"
assert results[0].failure_code == "editable"
assert not _docx_has_tracked_changes(out)
def test_comment_only_mode_with_track_changes_false():
_require_dotnet()
sample = _sample_docx()
with tempfile.TemporaryDirectory() as tmpdir:
out = Path(tmpdir) / "output.docx"
results = apply_docx_comments(
input_docx=sample,
output_docx=out,
findings=[_finding(target_text="")],
work_dir=Path(tmpdir) / "work",
track_changes=False,
)
assert results[0].status == "applied"
assert not _docx_has_tracked_changes(out)
# ---------------------------------------------------------------------------
# Step 17: REWRITE on body paragraphs
# ---------------------------------------------------------------------------
def test_rewrite_paragraph_produces_tracked_changes():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["First paragraph.", "Second paragraph to rewrite.", "Third paragraph."])
findings = [_finding(
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
new_text="Second paragraph rewritten content.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
assert _docx_has_tracked_changes(out)
assert _docx_has_comments(out)
def test_rewrite_paragraph_missing_new_text_fails():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["First paragraph.", "Second paragraph.", "Third paragraph."])
findings = [_finding(
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
new_text=None,
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "not_applied"
assert results[0].failure_code == "missing_new_text"
def test_rewrite_span_mode_produces_tracked_changes():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["The Contractor shall pay thirty (30) calendar days after receipt."])
findings = [_finding(
paragraph_id="P_0001",
op_type="REWRITE_PARAGRAPH",
rewrite_mode="span",
old_span="thirty (30)",
new_span="forty-five (45)",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
assert _docx_has_tracked_changes(out)
def test_rewrite_span_missing_fields_fails():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["Some text."])
findings = [_finding(
paragraph_id="P_0001",
op_type="REWRITE_PARAGRAPH",
rewrite_mode="span",
old_span="Some",
new_span=None,
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "not_applied"
assert results[0].failure_code == "missing_span_fields"
# ---------------------------------------------------------------------------
# Step 18: REWRITE on table-cell paragraphs
# ---------------------------------------------------------------------------
def test_rewrite_table_cell_paragraph():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "table.docx"
out = tmp / "table_out.docx"
_write_minimal_table_docx(inp)
findings = [_finding(
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
new_text="Rewritten table cell.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
assert _docx_has_tracked_changes(out)
# ---------------------------------------------------------------------------
# Step 19: INSERT
# ---------------------------------------------------------------------------
def test_insert_paragraph_after_anchor():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["First paragraph.", "Second paragraph."])
findings = [_finding(
paragraph_id="P_0001",
op_type="INSERT",
insert_text="Force Majeure. Neither party shall be liable for failure to perform due to unforeseen events.",
position="after",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
assert _docx_has_tracked_changes(out)
def test_insert_paragraph_before_anchor():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["First paragraph.", "Second paragraph."])
findings = [_finding(
paragraph_id="P_0002",
op_type="INSERT",
insert_text="New clause inserted before second paragraph.",
position="before",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
def test_insert_missing_insert_text_fails():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["First paragraph."])
findings = [_finding(
paragraph_id="P_0001",
op_type="INSERT",
insert_text=None,
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "not_applied"
assert results[0].failure_code == "missing_insert_text"
# ---------------------------------------------------------------------------
# Step 20: DELETE
# ---------------------------------------------------------------------------
def test_delete_paragraph():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["First paragraph.", "Second paragraph to delete.", "Third paragraph."])
findings = [_finding(
paragraph_id="P_0002",
op_type="DELETE_PARAGRAPH",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
assert _docx_has_tracked_changes(out)
# ---------------------------------------------------------------------------
# Step 21: Mixed output — comments + redlines in one file
# ---------------------------------------------------------------------------
def test_mixed_comment_and_rewrite():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, [
"First paragraph.",
"Second paragraph with thirty days notice period.",
"Third paragraph.",
])
findings = [
_finding(
finding_id="comment_1",
paragraph_id="P_0001",
op_type="COMMENT",
),
_finding(
finding_id="rewrite_1",
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
rewrite_mode="span",
old_span="thirty days",
new_span="forty-five days",
),
]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert len(results) == 2
assert all(r.status == "applied" for r in results)
assert _docx_has_tracked_changes(out)
assert _docx_has_comments(out)
def test_mixed_comment_rewrite_insert_delete():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, [
"Paragraph one for comment.",
"Paragraph two to rewrite with some text.",
"Paragraph three to delete entirely.",
"Paragraph four as insert anchor.",
])
findings = [
_finding(finding_id="c1", paragraph_id="P_0001", op_type="COMMENT"),
_finding(finding_id="r1", paragraph_id="P_0002", op_type="REWRITE_PARAGRAPH", new_text="Paragraph two rewritten completely."),
_finding(finding_id="d1", paragraph_id="P_0003", op_type="DELETE_PARAGRAPH"),
_finding(finding_id="i1", paragraph_id="P_0004", op_type="INSERT", insert_text="New force majeure clause.", position="after"),
]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert len(results) == 4
applied_count = sum(1 for r in results if r.status == "applied")
assert applied_count == 4
assert _docx_has_tracked_changes(out)
assert _docx_has_comments(out)
# ---------------------------------------------------------------------------
# Selective invariant check
# ---------------------------------------------------------------------------
def test_selective_invariant_untouched_paragraphs_preserved():
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, [
"First paragraph stays unchanged.",
"Second paragraph will be rewritten.",
"Third paragraph also stays unchanged.",
])
findings = [_finding(
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
new_text="Second paragraph now different.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
def test_strict_invariant_still_works_for_comments_only():
_require_dotnet()
sample = _sample_docx()
with tempfile.TemporaryDirectory() as tmpdir:
out = Path(tmpdir) / "output.docx"
results = apply_docx_comments(
input_docx=sample,
output_docx=out,
findings=[_finding(target_text="")],
work_dir=Path(tmpdir) / "work",
track_changes=False,
)
assert results[0].status == "applied"
# ---------------------------------------------------------------------------
# Helper: minimal table DOCX (shared with original test file)
# ---------------------------------------------------------------------------
def _write_minimal_table_docx(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as zf:
zf.writestr(
"[Content_Types].xml",
"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>""",
)
zf.writestr(
"_rels/.rels",
"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>""",
)
zf.writestr(
"word/_rels/document.xml.rels",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'</Relationships>',
)
zf.writestr(
"word/document.xml",
"""<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><w:body><w:p><w:r><w:t>Body paragraph.</w:t></w:r></w:p><w:tbl><w:tr><w:tc><w:p><w:r><w:t>Table cell clause.</w:t></w:r></w:p></w:tc></w:tr></w:tbl><w:sectPr/></w:body></w:document>""",
)
def _write_multi_run_docx(path: Path) -> None:
"""Create a DOCX where P_0001 has multiple runs with different RunProperties (bold + normal)."""
path.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(path, "w") as zf:
zf.writestr(
"[Content_Types].xml",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">'
'<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'
'<Default Extension="xml" ContentType="application/xml"/>'
'<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>'
'</Types>',
)
zf.writestr(
"_rels/.rels",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>'
'</Relationships>',
)
zf.writestr(
"word/_rels/document.xml.rels",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'
'</Relationships>',
)
# Paragraph with a bold run followed by a normal run
zf.writestr(
"word/document.xml",
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
'<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"'
' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">'
'<w:body>'
'<w:p>'
'<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">Defined Term: </w:t></w:r>'
'<w:r><w:t xml:space="preserve">means the agreement between the parties.</w:t></w:r>'
'</w:p>'
'<w:p><w:r><w:t>Second paragraph.</w:t></w:r></w:p>'
'<w:sectPr/></w:body></w:document>',
)
def _read_doc_xml(path: Path) -> str:
with zipfile.ZipFile(path) as zf:
return zf.read("word/document.xml").decode("utf-8")
# ---------------------------------------------------------------------------
# New tests: Clone-and-wrap rewrite preserves formatting
# ---------------------------------------------------------------------------
def test_rewrite_produces_single_del_and_single_ins():
"""Full-paragraph rewrite should produce exactly one w:del and one w:ins."""
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["Original text here.", "Keep this."])
findings = [_finding(
paragraph_id="P_0001",
op_type="REWRITE_PARAGRAPH",
new_text="Replacement text here.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
doc_xml = _read_doc_xml(out)
# Exactly one DeletedRun and one InsertedRun in the rewritten paragraph
assert doc_xml.count("w:del ") == 1 or doc_xml.count("w:del>") >= 1
assert "w:ins " in doc_xml or "w:ins>" in doc_xml
# The deleted text should contain the original text
assert "w:delText" in doc_xml
assert "Original text here." in doc_xml
# The inserted text should contain the new text
assert "Replacement text here." in doc_xml
def test_rewrite_multi_run_preserves_per_run_formatting():
"""When a paragraph has multiple runs with different formatting (bold + normal),
rewriting should preserve the original per-run RunProperties inside w:del."""
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "multi_run.docx"
out = tmp / "output.docx"
_write_multi_run_docx(inp)
findings = [_finding(
paragraph_id="P_0001",
op_type="REWRITE_PARAGRAPH",
new_text="New Definition: refers to the updated contract terms.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
doc_xml = _read_doc_xml(out)
# The deleted content should preserve the bold RunProperties (w:b) from
# the first original run inside the DeletedRun.
# Note: OpenXML SDK serializes self-closing tags as '<w:b />' (with space)
assert ("<w:b/>" in doc_xml or "<w:b " in doc_xml), "Bold RunProperties should be preserved in deleted content"
assert "w:delText" in doc_xml
# Both original text fragments should appear as deleted text
assert "Defined Term" in doc_xml
assert "means the agreement" in doc_xml
# New text should appear in an InsertedRun
assert "New Definition" in doc_xml
def test_rewrite_does_not_modify_untouched_paragraphs():
"""Paragraphs not targeted by rewrite should remain untouched."""
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, [
"Untouched paragraph one.",
"This will be rewritten.",
"Untouched paragraph three.",
])
findings = [_finding(
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
new_text="This has been rewritten.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
assert results[0].status == "applied"
doc_xml = _read_doc_xml(out)
# Untouched paragraphs should have normal w:t text, not w:delText
assert "Untouched paragraph one." in doc_xml
assert "Untouched paragraph three." in doc_xml
# The rewritten paragraph should have del + ins
assert "This will be rewritten." in doc_xml # as deleted text
assert "This has been rewritten." in doc_xml # as inserted text
def test_rewrite_empty_visible_text_rejects():
"""Rewrite on an empty paragraph should be rejected (either as no visible text
or anchor_not_found if the empty paragraph isn't indexed)."""
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
out = tmp / "output.docx"
# Create a docx with an effectively empty paragraph
_write_minimal_docx(inp, ["Real text.", ""])
findings = [_finding(
paragraph_id="P_0002",
op_type="REWRITE_PARAGRAPH",
new_text="Should not apply.",
)]
results = apply_docx_comments(
input_docx=inp,
output_docx=out,
findings=findings,
work_dir=tmp / "work",
track_changes=True,
)
# Empty paragraphs are either not indexed (anchor_not_found) or rejected
# as having no visible text (paragraph_no_visible_text)
assert results[0].status == "not_applied"
assert results[0].failure_code in ("paragraph_no_visible_text", "anchor_not_found")
def test_rewrite_with_existing_comment_preserves_it():
"""If a paragraph already has a comment, rewrite should preserve it via anchor rescue."""
_require_dotnet()
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
inp = tmp / "input.docx"
mid = tmp / "mid.docx"
out = tmp / "output.docx"
_write_minimal_docx(inp, ["Clause to comment and rewrite.", "Other clause."])
# Step 1: Apply a comment first
comment_results = apply_docx_comments(
input_docx=inp,
output_docx=mid,
findings=[_finding(
finding_id="comment_first",
paragraph_id="P_0001",
op_type="COMMENT",
)],
work_dir=tmp / "work1",
track_changes=True,
)
assert comment_results[0].status == "applied"
assert _docx_has_comments(mid)
# Step 2: Now rewrite the same paragraph on the commented document
rewrite_results = apply_docx_comments(
input_docx=mid,
output_docx=out,
findings=[_finding(
finding_id="rewrite_second",
paragraph_id="P_0001",
op_type="REWRITE_PARAGRAPH",
new_text="Clause rewritten after comment.",
)],
work_dir=tmp / "work2",
track_changes=True,
)
assert rewrite_results[0].status == "applied"
assert _docx_has_tracked_changes(out)
# The original comment should still be present
assert _docx_has_comments(out)
if __name__ == "__main__":
test_rewrite_paragraph_produces_tracked_changes()