All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
Fixes critical bug in 1033-25: user-uploaded עריכה-*.docx files were
orphaned on disk while exports kept rebuilding from stale DB blocks.
New architecture:
- User-uploaded DOCX becomes the source of truth (cases.active_draft_path)
- System edits via XML surgery with real Word <w:ins>/<w:del> revisions
- User can Accept/Reject each change from within Word
Components:
- docx_reviser.py: XML surgery for Track Changes (15 tests)
- docx_retrofit.py: retroactive bookmark injection with Hebrew marker
detection + heading heuristic (9 tests)
- docx_exporter.py: emits bookmarks around each of the 12 blocks
- 3 new MCP tools: apply_user_edit, list_bookmarks, revise_draft
- 4 new/updated endpoints: upload (auto-registers active draft),
/exports/revise, /exports/bookmarks, /exports/{filename}/retrofit,
/active-draft
- DB migration: cases.active_draft_path column
- UI: correct banner using real v-numbers, "מקור האמת" badge,
detailed upload toast with bookmarks_added/missing_blocks
- agents: legal-exporter (3 export modes), legal-ceo (stage G for
revision handling), legal-writer (revision mode)
Multi-tenancy:
- Works for both CMP (1xxx cases) and CMPA (8xxx/9xxx cases)
- New revise-draft skill added to both companies
- deploy-track-changes.sh syncs skills CMP ↔ CMPA
- retrofit_case.py: one-off retrofit of existing files
Tests: 34 passing (15 reviser + 9 retrofit + 4 exporter bookmarks + 6 e2e)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
"""בדיקות ל-bookmark helpers ב-docx_exporter.
|
||
|
||
הבדיקות מתרכזות ב-helper functions בלבד (לא בכל ה-export flow שדורש DB).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import zipfile
|
||
from pathlib import Path
|
||
|
||
from docx import Document
|
||
from lxml import etree
|
||
|
||
from legal_mcp.services.docx_exporter import (
|
||
_BOOKMARK_ID_START,
|
||
_insert_bookmark_end,
|
||
_insert_bookmark_start,
|
||
_wrap_block_with_bookmarks,
|
||
)
|
||
from legal_mcp.services.docx_reviser import NSMAP, _w, list_bookmarks
|
||
|
||
|
||
def test_insert_bookmark_helpers_create_valid_xml(tmp_path: Path) -> None:
|
||
doc = Document()
|
||
p = doc.add_paragraph("תוכן בלוק י")
|
||
_insert_bookmark_start(p, "block-yod", 10001)
|
||
_insert_bookmark_end(p, 10001)
|
||
|
||
out = tmp_path / "out.docx"
|
||
doc.save(str(out))
|
||
|
||
# Verify via list_bookmarks (uses the same XML)
|
||
assert list_bookmarks(out) == ["block-yod"]
|
||
|
||
|
||
def test_wrap_block_with_bookmarks_wraps_multiple_paragraphs(tmp_path: Path) -> None:
|
||
doc = Document()
|
||
doc.add_paragraph("ראשון — לפני") # noise before
|
||
|
||
bm_counter = [_BOOKMARK_ID_START]
|
||
|
||
def writer() -> None:
|
||
doc.add_paragraph("בלוק — פסקה 1")
|
||
doc.add_paragraph("בלוק — פסקה 2")
|
||
doc.add_paragraph("בלוק — פסקה 3")
|
||
|
||
_wrap_block_with_bookmarks(doc, "block-yod", writer, bm_counter)
|
||
doc.add_paragraph("אחרי — אחרון") # noise after
|
||
|
||
out = tmp_path / "out.docx"
|
||
doc.save(str(out))
|
||
|
||
# The bookmark should wrap exactly the 3 middle paragraphs
|
||
with zipfile.ZipFile(out, "r") as zf:
|
||
tree = etree.fromstring(zf.read("word/document.xml"))
|
||
|
||
paragraphs = tree.findall(".//w:p", NSMAP)
|
||
# Find para index of bookmarkStart and bookmarkEnd
|
||
start_idx = end_idx = None
|
||
for i, p in enumerate(paragraphs):
|
||
if p.find(".//w:bookmarkStart", NSMAP) is not None:
|
||
start_idx = i
|
||
if p.find(".//w:bookmarkEnd", NSMAP) is not None:
|
||
end_idx = i
|
||
assert start_idx is not None
|
||
assert end_idx is not None
|
||
# The paragraph containing start must be the first new one ("פסקה 1")
|
||
start_text = "".join(paragraphs[start_idx].itertext())
|
||
end_text = "".join(paragraphs[end_idx].itertext())
|
||
assert "פסקה 1" in start_text
|
||
assert "פסקה 3" in end_text
|
||
|
||
|
||
def test_wrap_block_skipped_when_writer_adds_nothing(tmp_path: Path) -> None:
|
||
doc = Document()
|
||
bm_counter = [_BOOKMARK_ID_START]
|
||
_wrap_block_with_bookmarks(doc, "block-empty", lambda: None, bm_counter)
|
||
out = tmp_path / "out.docx"
|
||
doc.save(str(out))
|
||
assert list_bookmarks(out) == []
|
||
|
||
|
||
def test_multiple_blocks_get_unique_bookmark_ids(tmp_path: Path) -> None:
|
||
doc = Document()
|
||
bm_counter = [_BOOKMARK_ID_START]
|
||
for name in ("block-alef", "block-bet", "block-gimel"):
|
||
_wrap_block_with_bookmarks(
|
||
doc, name,
|
||
lambda n=name: doc.add_paragraph(f"תוכן של {n}"),
|
||
bm_counter,
|
||
)
|
||
out = tmp_path / "out.docx"
|
||
doc.save(str(out))
|
||
|
||
with zipfile.ZipFile(out, "r") as zf:
|
||
tree = etree.fromstring(zf.read("word/document.xml"))
|
||
|
||
ids = [el.get(_w("id")) for el in tree.iterfind(".//w:bookmarkStart", NSMAP)]
|
||
assert len(ids) == 3
|
||
assert len(set(ids)) == 3
|
||
|
||
names = list_bookmarks(out)
|
||
assert set(names) == {"block-alef", "block-bet", "block-gimel"}
|