Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/agent-review-runtime-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ on:
- "tests/test_noema_two_phase_handoff.py"
- "tests/test_noema_refreshed_app_identity.py"
- "tests/test_noema_token_lifetime_stale_run_contract.py"
- "scripts/ci/noema_review_document.py"
- "scripts/ci/noema_hwp_mcp_reader.mjs"
- "scripts/ci/noema-document-reader/package.json"
- "scripts/ci/noema-document-reader/package-lock.json"
- "tests/test_noema_document_review_context.py"
- "docs/doctoring/noema-review-token-lifetime.md"
- "docs/product-technical-gap-baseline.md"
- ".github/workflows/opencode-review-dispatch.yml"
Expand Down Expand Up @@ -185,6 +190,11 @@ jobs:
tests/test_noema_two_phase_handoff.py|\
tests/test_noema_refreshed_app_identity.py|\
tests/test_noema_token_lifetime_stale_run_contract.py|\
scripts/ci/noema_review_document.py|\
scripts/ci/noema_hwp_mcp_reader.mjs|\
scripts/ci/noema-document-reader/package.json|\
scripts/ci/noema-document-reader/package-lock.json|\
tests/test_noema_document_review_context.py|\
docs/doctoring/noema-review-token-lifetime.md)
noema_suite=true
;;
Expand Down Expand Up @@ -349,13 +359,15 @@ jobs:
tests/test_noema_reviewer_token_lifetime.py \
tests/test_noema_two_phase_handoff.py \
tests/test_noema_refreshed_app_identity.py \
tests/test_noema_token_lifetime_stale_run_contract.py
tests/test_noema_token_lifetime_stale_run_contract.py \
tests/test_noema_document_review_context.py
python -m compileall -q \
.github/actions/noema-review/two_phase.py \
tests/test_noema_reviewer_token_lifetime.py \
tests/test_noema_two_phase_handoff.py \
tests/test_noema_refreshed_app_identity.py \
tests/test_noema_token_lifetime_stale_run_contract.py
tests/test_noema_token_lifetime_stale_run_contract.py \
tests/test_noema_document_review_context.py

- name: Verify OpenCode Rust coverage toolchain contract
if: steps.affected_suites.outputs.opencode == 'true'
Expand Down
5 changes: 4 additions & 1 deletion scripts/ci/noema_hwp_mcp_reader.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ try {
const document = await documentModule.openDocument(filePath);
documentModule.closeDocument(document);
const text = await toolsModule.readHwp({ file_path: filePath });
if (!text || /(?:파일 읽기 오류|File not found|text extraction error)/i.test(text)) {
if (
!text ||
/^(?:파일 읽기 오류|File not found|텍스트 추출 오류|text extraction error)/i.test(text)
) {
throw new Error("hwp-mcp returned an extraction error");
}
process.stdout.write(`${text}\n`);
Expand Down
3 changes: 2 additions & 1 deletion scripts/ci/noema_review_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pathlib import PurePosixPath

from defusedxml import ElementTree as ET
from defusedxml.common import DefusedXmlException


MAX_DOCUMENT_BYTES = 8 * 1024 * 1024
Expand Down Expand Up @@ -79,7 +80,7 @@ def _extract_docx(raw: bytes) -> str:

try:
root = ET.fromstring(document_xml)
except ET.ParseError as exc:
except (ET.ParseError, DefusedXmlException) as exc:
raise DocumentReadError("DOCX document.xml is malformed") from exc

body = root.find(f"{W}body")
Expand Down
28 changes: 28 additions & 0 deletions tests/test_noema_document_review_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ def _docx_bytes(*, malformed: bool = False) -> bytes:
return output.getvalue()


def _docx_entity_bytes() -> bytes:
"""Build a DOCX whose entity declaration must be rejected safely."""
xml = """<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE w:document [<!ENTITY expansion "blocked">]>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body><w:p><w:r><w:t>&expansion;</w:t></w:r></w:p></w:body>
</w:document>"""
output = io.BytesIO()
with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive:
archive.writestr("word/document.xml", xml)
return output.getvalue()


def _pr() -> dict[str, object]:
return {
"headRefOid": "head",
Expand Down Expand Up @@ -75,6 +88,15 @@ def test_hosted_reader_bundle_is_pinned_and_local():
assert "python3 -m pip install --quiet --require-hashes --no-deps" in workflow
assert "requirements-noema-document-ci-hashes.txt" in quality_workflow
assert "Install exact Noema document dependencies" in quality_workflow
for path in (
"scripts/ci/noema_review_document.py",
"scripts/ci/noema_hwp_mcp_reader.mjs",
"scripts/ci/noema-document-reader/package.json",
"scripts/ci/noema-document-reader/package-lock.json",
"tests/test_noema_document_review_context.py",
):
assert path in quality_workflow
assert "tests/test_noema_document_review_context.py" in quality_workflow


def test_docx_text_reaches_the_actual_reviewer_payload(monkeypatch):
Expand Down Expand Up @@ -148,6 +170,12 @@ def test_malformed_docx_is_explicit_in_review_context(monkeypatch):
assert "not a zip archive" not in context


def test_forbidden_docx_entities_are_explicitly_rejected():
"""Defused XML entity failures become the same bounded reader error."""
with pytest.raises(document.DocumentReadError, match="DOCX document.xml is malformed"):
document.extract_review_document("docs/entity.docx", _docx_entity_bytes())


def test_hwp_reader_contract_is_local_and_fail_closed(monkeypatch):
"""HWP/HWPX use the configured local adapter and reject failed readers."""
monkeypatch.setenv(document.HWP_READER_ENV, "/trusted/hwp-mcp-source")
Expand Down
Loading