Rows 2 and 3 require the build's inventory to EQUAL the witness's, so what the witness does not count, nothing can lose visibly. An independent review put a header and a comment in a docx, measured 0 of either in the bundle, and the accounting still read "2 of 2 carried". Thirteen classes are now counted, each with a red test written first: docx header/footer, comment, endnote and text box (a box's paragraphs are its own, or the text is booked twice) - pptx speaker note and hidden slide (`show="0"`, no longer counted as an ordinary slide) - xlsx formula and hidden sheet (the state lives in `workbook.xml` and is reached through the relationship id, so the sheet part itself says nothing about it) - odt header/footer from `styles.xml` and annotation (counted as prose, it made the accounting demand a reader carry a note the author wrote to themselves) - STS `mixed-citation`, `mml:math`, `fig` and its caption, measured by the review at 4.1 % of N200's source text and 3.9 % of N100's. M-2: the two STS witnesses had ONE role map between them, so row 5 -- "two witnesses agree" -- could not see a hole in it. `_sts_role_xml` and `_sts_role_json` are written apart, each for its own delivery, and a test holds them apart. M-3: 20 of 63 element types had a count of ZERO in their only fixture. Seven hand-built documents close it, every element type now occurs at least once (a test asserts it), and ALL TWENTY documents carry a hand count read off the fixture's own bytes (four did before). `.xlsx image` -- the operator's own proposed exception -- could not be exercised at all until now. Every witness also states WHAT IT STILL DOES NOT COUNT, per file type, and the gate prints that list on every run. THE FIXTURE ROWS ARE RED NOW, AND THAT IS THE POINT. Row 2 red on .docx, .odt, .pptx, .xlsx and .xml; row 3 at u = 25, d = 2 over the new classes, including a footnote and four spreadsheet cells the build genuinely drops. `0 claimed and not found` on the same run: nothing the build DOES book as carried failed the bundle check, so the red is the build's and not the instrument's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
878 lines
31 KiB
Python
878 lines
31 KiB
Python
"""The content-accounting gate's own tests (`tools/okf_accounting_gate.py`).
|
|
|
|
The gate was written RED at `0b00de4`, before `okf build` had a source
|
|
inventory or a per-element account. These tests are GREEN and prove three things.
|
|
|
|
1. The fasit is independent. `tools/okf_witness.py` imports no
|
|
`llm_ingestion_okf` module -- checked on the live import graph of a process
|
|
that ran every witness, with a control that shows the check fires -- and
|
|
the committed inventories are exactly what the witness counts today, pinned
|
|
again to hand counts on four documents.
|
|
2. Every row CAN turn green and CAN turn red, each boundary driven from both
|
|
sides with synthetic build output (the door the capability must open).
|
|
3. Run against the real `okf build`, the fixture rows are green. They were
|
|
red at `0b00de4` on the three defects the gate was ordered for (no
|
|
inventory, a file carried AND rejected, a rejected document logged as "0
|
|
carried of 0 found"); `test_a_file_carried_through_a_document_and_rejected_
|
|
is_double_booked` and the row 4 tests keep those defects detectable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
TOOLS = Path(__file__).resolve().parents[1] / "tools"
|
|
sys.path.insert(0, str(TOOLS))
|
|
|
|
import okf_accounting_gate as gate # noqa: E402
|
|
import okf_witness as witness # noqa: E402
|
|
|
|
TABLE = [
|
|
".csv",
|
|
".docx",
|
|
".htm",
|
|
".html",
|
|
".json",
|
|
".md",
|
|
".odt",
|
|
".pdf",
|
|
".pptx",
|
|
".rtf",
|
|
".txt",
|
|
".xlsx",
|
|
".xml",
|
|
]
|
|
|
|
|
|
# --- 1. the fasit ------------------------------------------------------------
|
|
|
|
_IMPORT_PROBE = """
|
|
import sys
|
|
sys.path.insert(0, {tools!r})
|
|
{preload}
|
|
import okf_witness as w
|
|
from pathlib import Path
|
|
fixtures = Path({fixtures!r})
|
|
w.witness_inbox(fixtures / "corpus")
|
|
w.witness_inbox(fixtures / "rejected")
|
|
w.count_sts_json((fixtures / "witness" / "prosess-84-sts.twin.json").read_bytes())
|
|
w.pdf_poppler(fixtures / "corpus" / "prosess-84-tabell.pdf")
|
|
print(sorted(m for m in sys.modules if m.split(".")[0] == "llm_ingestion_okf"))
|
|
"""
|
|
|
|
|
|
def _package_modules_after_witness(preload: str) -> list[str]:
|
|
script = _IMPORT_PROBE.format(tools=str(TOOLS), fixtures=str(gate.FIXTURES), preload=preload)
|
|
out = subprocess.run(
|
|
[sys.executable, "-c", script], capture_output=True, text=True, check=True
|
|
).stdout
|
|
result: list[str] = json.loads(out.strip().splitlines()[-1].replace("'", '"'))
|
|
return result
|
|
|
|
|
|
def test_the_witness_imports_no_module_of_the_package_it_judges() -> None:
|
|
pytest.importorskip("pdfplumber")
|
|
assert _package_modules_after_witness("") == []
|
|
|
|
|
|
def test_the_import_check_fires_when_the_package_is_loaded() -> None:
|
|
pytest.importorskip("pdfplumber")
|
|
assert "llm_ingestion_okf" in _package_modules_after_witness("import llm_ingestion_okf")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("inbox", "committed"),
|
|
[(gate.CORPUS, gate.INVENTORY), (gate.REJECTED, gate.REJECTED_INVENTORY)],
|
|
)
|
|
def test_the_committed_fasit_is_what_the_witness_counts(inbox: Path, committed: Path) -> None:
|
|
pytest.importorskip("pdfplumber")
|
|
assert witness.witness_inbox(inbox) == gate.load_inventory(committed)
|
|
|
|
|
|
def test_the_fixture_corpus_covers_every_readme_file_type() -> None:
|
|
inventory = gate.load_inventory(gate.INVENTORY)
|
|
assert gate.readme_types() == TABLE
|
|
assert {entry["suffix"] for entry in inventory["documents"].values()} == set(TABLE)
|
|
|
|
|
|
# Counted by hand from the fixture bytes, not by the witness. Seven of the
|
|
# thirteen documents were added 2026-09-18 because 20 of 63 element types had
|
|
# a count of ZERO in their only fixture, and a type that cannot appear cannot
|
|
# be lost visibly either (independent review, M-3).
|
|
HAND_COUNTS = {
|
|
"notat.md": {
|
|
"code_block": 1,
|
|
"heading": 2,
|
|
"image": 1,
|
|
"paragraph": 2,
|
|
"table": 1,
|
|
"table_row": 2,
|
|
},
|
|
"side.htm": {"cell": 4, "heading": 2, "image": 1, "list_item": 2, "paragraph": 1, "table": 1},
|
|
"figur.html": {
|
|
"cell": 4,
|
|
"heading": 1,
|
|
"image": 1,
|
|
"list_item": 2,
|
|
"paragraph": 1,
|
|
"table": 1,
|
|
},
|
|
"krav-rikt-tekstformat.rtf": {"cell": 56, "image": 0, "paragraph": 3, "table_row": 24},
|
|
"bilde.rtf": {"cell": 0, "image": 1, "paragraph": 2, "table_row": 0},
|
|
"prosess-84-sts.xml": {
|
|
"cell": 0,
|
|
"citation": 0,
|
|
"figure": 0,
|
|
"figure_caption": 0,
|
|
"footnote": 0,
|
|
"image": 2,
|
|
"list_item": 0,
|
|
"math": 0,
|
|
"paragraph": 2,
|
|
"section": 2,
|
|
"section_label": 2,
|
|
"table": 0,
|
|
"table_label": 0,
|
|
"title": 2,
|
|
},
|
|
"sts-rikt.xml": {
|
|
"cell": 4,
|
|
"citation": 1,
|
|
"figure": 1,
|
|
"figure_caption": 1,
|
|
"footnote": 1,
|
|
"image": 1,
|
|
"list_item": 1,
|
|
"math": 1,
|
|
"paragraph": 5,
|
|
"section": 1,
|
|
"section_label": 1,
|
|
"table": 1,
|
|
"table_label": 1,
|
|
"title": 1,
|
|
},
|
|
"topptekst-og-kommentar.docx": {
|
|
"cell": 2,
|
|
"comment": 1,
|
|
"endnote": 1,
|
|
"footnote": 1,
|
|
"header_footer": 2,
|
|
"heading": 1,
|
|
"image": 0,
|
|
"paragraph": 3,
|
|
"table": 1,
|
|
"text_box": 1,
|
|
},
|
|
"notater-og-skjult.pptx": {
|
|
"cell": 4,
|
|
"hidden_slide": 1,
|
|
"image": 0,
|
|
"note": 1,
|
|
"paragraph": 2,
|
|
"slide": 1,
|
|
"table": 2,
|
|
"title": 2,
|
|
},
|
|
"skjult-ark-og-formel.xlsx": {
|
|
"cell": 7,
|
|
"formula": 1,
|
|
"hidden_sheet": 1,
|
|
"image": 1,
|
|
"row": 3,
|
|
"sheet": 1,
|
|
},
|
|
"logg.txt": {"line": 4, "paragraph": 3},
|
|
"mengder.csv": {"cell": 6, "header_cell": 3, "row": 2},
|
|
"parametre.json": {"key": 6, "value": 6},
|
|
"prosess-84-tabell.pdf": {"image": 2, "page": 1},
|
|
"prosess-84-web.html": {
|
|
"cell": 0,
|
|
"heading": 1,
|
|
"image": 3,
|
|
"list_item": 0,
|
|
"paragraph": 3,
|
|
"table": 0,
|
|
},
|
|
"prosess-84-notat.docx": {
|
|
"cell": 0,
|
|
"comment": 0,
|
|
"endnote": 0,
|
|
"footnote": 0,
|
|
"header_footer": 0,
|
|
"heading": 0,
|
|
"image": 1,
|
|
"paragraph": 2,
|
|
"table": 0,
|
|
"text_box": 0,
|
|
},
|
|
"prosess-84-presentasjon.pptx": {
|
|
"cell": 0,
|
|
"hidden_slide": 0,
|
|
"image": 1,
|
|
"note": 0,
|
|
"paragraph": 0,
|
|
"slide": 1,
|
|
"table": 0,
|
|
"title": 1,
|
|
},
|
|
"prisark.xlsx": {
|
|
"cell": 18,
|
|
"formula": 0,
|
|
"hidden_sheet": 0,
|
|
"image": 0,
|
|
"row": 9,
|
|
"sheet": 2,
|
|
},
|
|
"krav-tekstdokument.odt": {
|
|
"annotation": 0,
|
|
"cell": 56,
|
|
"header_footer": 0,
|
|
"heading": 1,
|
|
"image": 0,
|
|
"list_item": 0,
|
|
"paragraph": 2,
|
|
"table": 2,
|
|
},
|
|
"liste-og-bilde.odt": {
|
|
"annotation": 1,
|
|
"cell": 2,
|
|
"header_footer": 2,
|
|
"heading": 1,
|
|
"image": 1,
|
|
"list_item": 2,
|
|
"paragraph": 4,
|
|
"table": 1,
|
|
},
|
|
}
|
|
|
|
|
|
def test_every_element_type_the_witness_counts_occurs_in_a_fixture() -> None:
|
|
"""M-3: 20 of 63 element types had a count of 0 in their only fixture, so
|
|
six of seven witness mutants survived -- a witness cannot be wrong about
|
|
something it never sees."""
|
|
pytest.importorskip("pdfplumber")
|
|
inventory = gate.load_inventory(gate.INVENTORY)
|
|
seen: dict[str, int] = {}
|
|
for entry in inventory["documents"].values():
|
|
for role, number in entry["elements"].items():
|
|
key = f"{entry['suffix']} {role}"
|
|
seen[key] = seen.get(key, 0) + number
|
|
absent = sorted(key for key, number in seen.items() if number == 0)
|
|
assert absent == [], f"element types with no occurrence in any fixture: {absent}"
|
|
|
|
|
|
def test_the_hand_counts_cover_every_document_of_the_corpus() -> None:
|
|
"""Row 1's fasit is the witness's own output, so a hand count is the only
|
|
thing in the loop that the witness did not produce. Four of thirteen
|
|
documents had one; every document has one now."""
|
|
inventory = gate.load_inventory(gate.INVENTORY)
|
|
assert sorted(HAND_COUNTS) == sorted(inventory["documents"])
|
|
|
|
|
|
@pytest.mark.parametrize("name", sorted(HAND_COUNTS))
|
|
def test_the_witness_matches_a_hand_count(name: str) -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / name)
|
|
assert inventory.elements == HAND_COUNTS[name]
|
|
|
|
|
|
def test_a_fenced_heading_is_not_a_heading_to_the_witness() -> None:
|
|
count, _ = witness.count_markdown("# Real\n\n```bash\n# not one\n```\n")
|
|
assert count.counts["heading"] == 1
|
|
assert count.counts["code_block"] == 1
|
|
assert count.texts["heading"] == [["# Real"]]
|
|
|
|
|
|
def test_the_sts_image_reference_resolves_through_the_graphics_directory() -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.STS_FIXTURE)
|
|
assert [ref.target for ref in inventory.images] == [
|
|
"graphics/tabell-84-2.png",
|
|
"graphics/figur-84-1.png",
|
|
]
|
|
|
|
|
|
def test_a_reference_above_the_document_resolves_to_nothing(tmp_path: Path) -> None:
|
|
(tmp_path / "secret.png").write_bytes(b"x")
|
|
(tmp_path / "docs").mkdir()
|
|
document = tmp_path / "docs" / "a.html"
|
|
assert witness.resolve_local(tmp_path, document, "../secret.png") is None
|
|
|
|
|
|
def test_the_witness_refuses_a_doctype() -> None:
|
|
with pytest.raises(witness.WitnessRefused):
|
|
witness.count_sts_xml(b'<!DOCTYPE x [<!ENTITY a "b">]><standard/>')
|
|
|
|
|
|
# --- M-1 / M-2: what the witness could not see -------------------------------
|
|
#
|
|
# Written RED 2026-09-18. Rows 2 and 3 require the build's inventory to EQUAL
|
|
# the witness's, so what the witness does not count, nothing can account for:
|
|
# an independent review put a header and a comment in a docx, measured 0 of
|
|
# either in the bundle, and the accounting still read "2 of 2 carried".
|
|
|
|
|
|
def test_the_docx_witness_counts_a_header_a_comment_and_a_text_box() -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / "topptekst-og-kommentar.docx")
|
|
for role in ("header_footer", "comment", "endnote", "text_box"):
|
|
assert inventory.elements[role] > 0, role
|
|
assert "Utkast" in " ".join(
|
|
piece for pieces in inventory.texts["header_footer"] for piece in pieces
|
|
)
|
|
|
|
|
|
def test_the_pptx_witness_counts_notes_and_does_not_call_a_hidden_slide_a_slide() -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / "notater-og-skjult.pptx")
|
|
assert inventory.elements["note"] > 0
|
|
assert inventory.elements["hidden_slide"] == 1
|
|
|
|
|
|
def test_the_xlsx_witness_counts_a_formula_and_a_hidden_sheet() -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / "skjult-ark-og-formel.xlsx")
|
|
assert inventory.elements["formula"] > 0
|
|
assert inventory.elements["hidden_sheet"] == 1
|
|
|
|
|
|
def test_the_odt_witness_counts_a_header_and_does_not_call_an_annotation_prose() -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / "liste-og-bilde.odt")
|
|
assert inventory.elements["header_footer"] > 0
|
|
assert inventory.elements["annotation"] == 1
|
|
|
|
|
|
def test_the_sts_witness_counts_citations_formulas_and_figure_captions() -> None:
|
|
inventory = witness.witness_file(gate.CORPUS, gate.CORPUS / "sts-rikt.xml")
|
|
for role in ("citation", "math", "figure", "figure_caption"):
|
|
assert inventory.elements[role] > 0, role
|
|
|
|
|
|
def test_the_two_sts_role_maps_are_written_twice_and_not_shared() -> None:
|
|
"""M-2: both STS witnesses went through ONE `_sts_role`, so row 5 could
|
|
never see a hole in it. Two maps, each written for its own delivery."""
|
|
assert witness._sts_role_xml is not witness._sts_role_json
|
|
assert "_sts_role_json" not in witness._sts_role_xml.__code__.co_names
|
|
assert "_sts_role_xml" not in witness._sts_role_json.__code__.co_names
|
|
|
|
|
|
def test_every_witnessed_type_says_what_it_does_not_count() -> None:
|
|
assert set(witness.NOT_COUNTED) == set(witness.WITNESSED_SUFFIXES)
|
|
assert all(witness.NOT_COUNTED[suffix] for suffix in witness.WITNESSED_SUFFIXES)
|
|
|
|
|
|
def test_the_gate_prints_what_each_witness_does_not_count() -> None:
|
|
rendered = gate.render([])
|
|
assert "not counted" in rendered
|
|
for suffix in witness.WITNESSED_SUFFIXES:
|
|
assert suffix in rendered
|
|
|
|
|
|
# --- 2. every row can go both ways -------------------------------------------
|
|
|
|
|
|
#: The two headings of `a.md` as a bundle would carry them. The gate verifies
|
|
#: a booked `carried` against THIS, never against the declaration.
|
|
_BUNDLE_TEXT = (
|
|
"# Foerste overskrift\n\nProsa.\n\n"
|
|
"\n_Source: x.png_\n\n# Andre overskrift\n"
|
|
)
|
|
|
|
|
|
def _inventory() -> dict[str, Any]:
|
|
return {
|
|
"documents": {
|
|
"a.md": {
|
|
"suffix": ".md",
|
|
"elements": {"heading": 2, "image": 1},
|
|
"texts": {
|
|
"heading": ["Foerste overskrift", "Andre overskrift"],
|
|
"image": [""],
|
|
},
|
|
"images": [{"kind": "local", "ref": "graphics/x.png", "target": "graphics/x.png"}],
|
|
},
|
|
},
|
|
"files": {"graphics/x.png": {"pointed_at_by": ["a.md"]}},
|
|
}
|
|
|
|
|
|
def _assets(*paths: Path) -> dict[str, str]:
|
|
"""The `assets/` directory a build wrote for these source files."""
|
|
return {f"{gate._sha12(p)}-{p.name}": gate._sha256(p) for p in paths}
|
|
|
|
|
|
def _build(
|
|
*,
|
|
accounting: dict[str, Any] | None = None,
|
|
sources: set[str] | None = None,
|
|
assets: dict[str, str] | None = None,
|
|
log: str = "",
|
|
exit_code: int = 0,
|
|
bundle_text: str = _BUNDLE_TEXT,
|
|
) -> gate.Build:
|
|
return gate.Build(
|
|
exit_code=exit_code,
|
|
log=log,
|
|
accounting=accounting,
|
|
source_files={"a.md"} if sources is None else sources,
|
|
assets=assets or {},
|
|
bundle_text=bundle_text,
|
|
)
|
|
|
|
|
|
def _declared(heading: int = 2, image: int = 1, fate: str = "rejected") -> dict[str, Any]:
|
|
return {
|
|
"accounting_version": 1,
|
|
"documents": [
|
|
{
|
|
"source_file": "a.md",
|
|
"status": "persisted",
|
|
"code": None,
|
|
"inventory": {"heading": 2, "image": 1},
|
|
"fates": {
|
|
"heading": {"carried": heading},
|
|
"image": {"pointer": image},
|
|
},
|
|
}
|
|
],
|
|
"files": [{"source_file": "graphics/x.png", "fate": fate, "code": "extractor_unknown"}],
|
|
}
|
|
|
|
|
|
def _corpus(tmp_path: Path) -> Path:
|
|
(tmp_path / "graphics").mkdir()
|
|
(tmp_path / "graphics" / "x.png").write_bytes(b"png bytes")
|
|
return tmp_path
|
|
|
|
|
|
def test_row1_is_green_when_every_type_has_a_fasit() -> None:
|
|
inventory = {
|
|
"documents": {f"f{s}": {"suffix": s, "elements": {}} for s in TABLE},
|
|
"files": {},
|
|
}
|
|
row = gate.row1(TABLE, inventory, inventory)
|
|
assert (row.k, row.m, row.status) == (13, 13, gate.GREEN)
|
|
|
|
|
|
def test_row1_is_red_when_one_type_lacks_a_fasit() -> None:
|
|
inventory = {
|
|
"documents": {f"f{s}": {"suffix": s, "elements": {}} for s in TABLE[:-1]},
|
|
"files": {},
|
|
}
|
|
row = gate.row1(TABLE, inventory, inventory)
|
|
assert (row.k, row.m, row.status) == (12, 13, gate.RED)
|
|
|
|
|
|
def test_row1_is_red_when_the_committed_fasit_is_stale() -> None:
|
|
inventory = {"documents": {"f.md": {"suffix": ".md", "elements": {"heading": 1}}}}
|
|
fresh = {"documents": {"f.md": {"suffix": ".md", "elements": {"heading": 2}}}}
|
|
row = gate.row1([".md"], inventory, fresh)
|
|
assert (row.k, row.status) == (0, gate.RED)
|
|
|
|
|
|
def test_row2_is_red_without_the_door() -> None:
|
|
row = gate.row2([".md"], _inventory(), _build(), door=False)
|
|
assert (row.k, row.m, row.status) == (0, 1, gate.RED)
|
|
|
|
|
|
def test_row2_is_green_when_the_declared_inventory_equals_the_witness() -> None:
|
|
row = gate.row2([".md"], _inventory(), _build(accounting=_declared()), door=True)
|
|
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
|
|
|
|
|
|
def test_row2_is_red_when_the_declared_inventory_is_one_off() -> None:
|
|
declared = _declared()
|
|
declared["documents"][0]["inventory"]["heading"] = 3
|
|
row = gate.row2([".md"], _inventory(), _build(accounting=declared), door=True)
|
|
assert (row.k, row.status) == (0, gate.RED)
|
|
|
|
|
|
def test_row3_is_green_when_every_element_and_file_has_one_fate(tmp_path: Path) -> None:
|
|
units = gate.account(_inventory(), _build(accounting=_declared()), _corpus(tmp_path))
|
|
row = gate.row3(units, door=True)
|
|
assert (row.k, row.m, row.status) == (2, 2, gate.GREEN)
|
|
|
|
|
|
@pytest.mark.parametrize(("heading", "u", "d"), [(1, 1, 0), (3, 0, 1)])
|
|
def test_row3_is_red_one_element_either_side(tmp_path: Path, heading: int, u: int, d: int) -> None:
|
|
units = gate.account(
|
|
_inventory(), _build(accounting=_declared(heading=heading)), _corpus(tmp_path)
|
|
)
|
|
document = units[0]
|
|
assert (document.unaccounted, document.double) == (u, d)
|
|
assert gate.row3(units, door=True).status == gate.RED
|
|
|
|
|
|
def test_row3_is_red_when_no_fate_is_declared(tmp_path: Path) -> None:
|
|
units = gate.account(_inventory(), _build(), _corpus(tmp_path))
|
|
assert units[0].unaccounted == 3
|
|
assert gate.row3(units, door=False).status == gate.RED
|
|
|
|
|
|
def test_a_file_carried_through_a_document_and_rejected_is_double_booked(tmp_path: Path) -> None:
|
|
corpus = _corpus(tmp_path)
|
|
carried = _assets(corpus / "graphics" / "x.png")
|
|
units = gate.account(_inventory(), _build(accounting=_declared(), assets=carried), corpus)
|
|
assert (units[1].unaccounted, units[1].double) == (0, 1)
|
|
|
|
|
|
def test_a_file_carried_through_a_document_and_declared_carried_is_clean(tmp_path: Path) -> None:
|
|
corpus = _corpus(tmp_path)
|
|
carried = _assets(corpus / "graphics" / "x.png")
|
|
build = _build(accounting=_declared(fate="carried"), assets=carried)
|
|
assert gate.account(_inventory(), build, corpus)[1].clean
|
|
|
|
|
|
def test_a_file_declared_carried_without_its_bytes_is_unaccounted(tmp_path: Path) -> None:
|
|
build = _build(accounting=_declared(fate="carried"))
|
|
unit = gate.account(_inventory(), build, _corpus(tmp_path))[1]
|
|
assert (unit.unaccounted, unit.double) == (1, 0)
|
|
|
|
|
|
def test_an_unpointed_file_sharing_bytes_with_a_carried_one_is_not_carried(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
corpus = _corpus(tmp_path)
|
|
(corpus / "graphics" / "twin.png").write_bytes(b"png bytes")
|
|
inventory = _inventory()
|
|
inventory["files"]["graphics/twin.png"] = {"pointed_at_by": []}
|
|
carried = _assets(corpus / "graphics" / "x.png")
|
|
units = gate.account(inventory, _build(assets=carried), corpus)
|
|
assert [(u.name, u.double) for u in units[1:]] == [
|
|
("graphics/twin.png", 0),
|
|
("graphics/x.png", 1),
|
|
]
|
|
|
|
|
|
def test_without_the_door_double_booking_is_derived_from_conservation(tmp_path: Path) -> None:
|
|
corpus = _corpus(tmp_path)
|
|
carried = _assets(corpus / "graphics" / "x.png")
|
|
assert gate.account(_inventory(), _build(assets=carried), corpus)[1].double == 1
|
|
assert gate.account(_inventory(), _build(), corpus)[1].clean
|
|
|
|
|
|
# --- B-1: the judge opens the bundle itself ----------------------------------
|
|
#
|
|
# Written RED 2026-09-18 against the hardening order. At 864570b the gate
|
|
# compared BOOKED NUMBERS with the witness's counts and never opened a concept
|
|
# file, so a report that booked every element of every document as carried was
|
|
# `GATE GREEN` over a bundle holding nothing (independent review, B-1).
|
|
|
|
|
|
def _all_carried(headings: int = 2, images: int = 1) -> dict[str, Any]:
|
|
"""A report that books everything as carried, the cheat's shape."""
|
|
return {
|
|
"accounting_version": 1,
|
|
"documents": [
|
|
{
|
|
"source_file": "a.md",
|
|
"status": "persisted",
|
|
"code": None,
|
|
"inventory": {"heading": 2, "image": 1},
|
|
"fates": {
|
|
"heading": {"carried": headings},
|
|
"image": {"carried": images},
|
|
},
|
|
}
|
|
],
|
|
"files": [
|
|
{"source_file": "graphics/x.png", "fate": "rejected", "code": "extractor_unknown"}
|
|
],
|
|
}
|
|
|
|
|
|
def test_carried_text_the_bundle_does_not_hold_is_unverified(tmp_path: Path) -> None:
|
|
build = _build(accounting=_all_carried(images=0), bundle_text="")
|
|
unit = gate.account(_inventory(), build, _corpus(tmp_path))[0]
|
|
assert unit.unverified == 2
|
|
assert not unit.clean
|
|
|
|
|
|
def test_carried_text_the_bundle_holds_verifies(tmp_path: Path) -> None:
|
|
corpus = _corpus(tmp_path)
|
|
build = _build(accounting=_all_carried(images=0), assets=_assets(corpus / "graphics" / "x.png"))
|
|
unit = gate.account(_inventory(), build, corpus)[0]
|
|
assert (unit.unverified, unit.verified) == (0, 2)
|
|
|
|
|
|
def test_one_heading_carried_of_two_in_the_bundle_is_unverified(tmp_path: Path) -> None:
|
|
build = _build(accounting=_all_carried(images=0), bundle_text="# Foerste overskrift\n")
|
|
unit = gate.account(_inventory(), build, _corpus(tmp_path))[0]
|
|
assert unit.unverified == 1
|
|
|
|
|
|
def test_an_image_booked_carried_without_its_bytes_is_unverified(tmp_path: Path) -> None:
|
|
"""The image element has no text of its own, so the only proof it was
|
|
carried is the asset. Without it the booking is not verifiable, and an
|
|
unverifiable booking is never clean."""
|
|
unit = gate.account(_inventory(), _build(accounting=_all_carried()), _corpus(tmp_path))[0]
|
|
assert unit.unverified >= 1
|
|
assert not unit.clean
|
|
|
|
|
|
def test_a_negative_booking_is_never_clean(tmp_path: Path) -> None:
|
|
declared = _all_carried()
|
|
declared["documents"][0]["fates"]["heading"] = {"carried": 25, "rejected": {"x": -15}}
|
|
unit = gate.account(_inventory(), _build(accounting=declared), _corpus(tmp_path))[0]
|
|
assert unit.invalid >= 1
|
|
assert not unit.clean
|
|
|
|
|
|
def test_a_document_declared_persisted_that_is_not_in_the_bundle_is_never_clean(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
build = _build(accounting=_all_carried(), sources=set())
|
|
unit = gate.account(_inventory(), build, _corpus(tmp_path))[0]
|
|
assert unit.invalid >= 1
|
|
|
|
|
|
def test_everything_rejected_is_never_clean_for_a_document_the_build_persisted(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
declared = _all_carried()
|
|
declared["documents"][0]["fates"] = {
|
|
"heading": {"rejected": {"fail_secure": 2}},
|
|
"image": {"rejected": {"fail_secure": 1}},
|
|
}
|
|
unit = gate.account(_inventory(), _build(accounting=declared), _corpus(tmp_path))[0]
|
|
assert unit.invalid >= 1
|
|
assert "persisted" in "; ".join(unit.notes)
|
|
|
|
|
|
def test_everything_rejected_is_clean_for_a_document_the_build_refused(tmp_path: Path) -> None:
|
|
declared = _all_carried()
|
|
declared["documents"][0]["status"] = "rejected"
|
|
declared["documents"][0]["code"] = "fail_secure"
|
|
declared["documents"][0]["fates"] = {
|
|
"heading": {"rejected": {"fail_secure": 2}},
|
|
"image": {"rejected": {"fail_secure": 1}},
|
|
}
|
|
unit = gate.account(
|
|
_inventory(), _build(accounting=declared, sources=set()), _corpus(tmp_path)
|
|
)[0]
|
|
assert unit.clean
|
|
|
|
|
|
def test_a_rejection_code_outside_the_closed_list_is_never_clean(tmp_path: Path) -> None:
|
|
declared = _all_carried()
|
|
declared["documents"][0]["status"] = "rejected"
|
|
declared["documents"][0]["code"] = "because_i_said_so"
|
|
declared["documents"][0]["fates"] = {
|
|
"heading": {"rejected": {"because_i_said_so": 2}},
|
|
"image": {"rejected": {"because_i_said_so": 1}},
|
|
}
|
|
unit = gate.account(
|
|
_inventory(), _build(accounting=declared, sources=set()), _corpus(tmp_path)
|
|
)[0]
|
|
assert unit.invalid >= 1
|
|
|
|
|
|
def test_an_accounting_version_the_gate_does_not_read_is_never_clean(tmp_path: Path) -> None:
|
|
declared = _all_carried()
|
|
declared["accounting_version"] = 2
|
|
units = gate.account(_inventory(), _build(accounting=declared), _corpus(tmp_path))
|
|
assert not any(u.clean for u in units)
|
|
|
|
|
|
def test_an_asset_with_the_right_name_and_the_wrong_bytes_is_not_carried(tmp_path: Path) -> None:
|
|
"""m-1: the check was a NAME check, so a zero-byte file called
|
|
`<sha12>-x.png` proved a carry."""
|
|
corpus = _corpus(tmp_path)
|
|
source = corpus / "graphics" / "x.png"
|
|
lying = {f"{gate._sha12(source)}-x.png": gate._sha256_bytes(b"")}
|
|
build = _build(accounting=_declared(fate="carried"), assets=lying)
|
|
assert gate.account(_inventory(), build, corpus)[1].unaccounted == 1
|
|
|
|
|
|
def test_the_cheat_that_books_everything_carried_makes_row3_red(tmp_path: Path) -> None:
|
|
"""The review's `MODE=carried`: a report that changes not one byte of the
|
|
bundle and books every element as carried."""
|
|
units = gate.account(
|
|
_inventory(), _build(accounting=_all_carried(), bundle_text=""), _corpus(tmp_path)
|
|
)
|
|
assert gate.row3(units, door=True).status == gate.RED
|
|
|
|
|
|
_HONEST_LOG = (
|
|
"* **Images**: 0 carried of 1 found, written to `assets/`.\n"
|
|
"* a.md: 3 elements found in the source, 0 carried: document rejected `fail_secure`\n"
|
|
)
|
|
|
|
|
|
def _rejected_inventory() -> dict[str, Any]:
|
|
inventory = _inventory()
|
|
inventory["documents"]["a.md"]["images"] = [{"kind": "local"}]
|
|
return inventory
|
|
|
|
|
|
def test_row4_is_green_when_the_log_names_what_the_rejected_document_held() -> None:
|
|
row = gate.row4(_rejected_inventory(), _build(sources=set(), log=_HONEST_LOG))
|
|
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"log",
|
|
[
|
|
_HONEST_LOG.replace("0 carried of 1 found", "0 carried of 0 found"),
|
|
_HONEST_LOG.replace("3 elements", "2 elements"),
|
|
_HONEST_LOG.replace(" `fail_secure`", ""),
|
|
],
|
|
)
|
|
def test_row4_is_red_when_the_log_understates_the_rejected_document(log: str) -> None:
|
|
row = gate.row4(_rejected_inventory(), _build(sources=set(), log=log))
|
|
assert (row.k, row.status) == (0, gate.RED)
|
|
|
|
|
|
def test_row4_cannot_be_green_when_nothing_was_rejected() -> None:
|
|
row = gate.row4(_rejected_inventory(), _build(log=_HONEST_LOG))
|
|
assert (row.m, row.status) == (0, gate.RED)
|
|
|
|
|
|
def test_row5_is_green_when_the_witnesses_agree() -> None:
|
|
row = gate.row5([("pair", gate.compare({"p": 3}, {"p": 3}))], [])
|
|
assert (row.k, row.m, row.status) == (1, 1, gate.GREEN)
|
|
|
|
|
|
def test_row5_is_red_with_both_numbers_when_they_disagree_by_one() -> None:
|
|
row = gate.row5([("pair", gate.compare({"p": 3}, {"p": 4}))], [])
|
|
assert row.status == gate.RED
|
|
assert row.details == ["pair: p: 3 vs 4"]
|
|
|
|
|
|
def test_row5_is_red_when_a_witness_is_missing() -> None:
|
|
assert gate.row5([("pair", gate.compare({"p": 3}, None))], []).status == gate.RED
|
|
|
|
|
|
def test_row6_without_its_source_is_red_locally_and_skipped_in_ci(tmp_path: Path) -> None:
|
|
missing = tmp_path / "absent"
|
|
local = gate.row6(missing, ci=False)
|
|
ci = gate.row6(missing, ci=True)
|
|
assert (local.status, local.fails) == (gate.RED, True)
|
|
assert (ci.status, ci.fails) == (gate.SKIPPED, False)
|
|
assert "source missing" in ci.reason
|
|
|
|
|
|
def test_the_proposed_exceptions_are_not_applied() -> None:
|
|
assert "NOT APPROVED" in gate.render([])
|
|
assert not {suffix for suffix, _ in gate.APPROVED_EXCEPTIONS} & {
|
|
item["suffix"] for item in gate.PROPOSED_EXCEPTIONS
|
|
}, "an exception cannot be both proposed and approved"
|
|
|
|
|
|
def test_the_operator_approved_the_pdf_exception_and_nothing_else() -> None:
|
|
"""Operator 2026-09-17, answering the gate's three proposals: the PDF one
|
|
only. It moves no number -- no witness counts a heading in a PDF -- so what
|
|
it changes is that the gap is a stated limit rather than an open question.
|
|
"""
|
|
assert gate.APPROVED_EXCEPTIONS == frozenset(
|
|
{(".pdf", "heading"), (".pdf", "paragraph"), (".pdf", "table")}
|
|
)
|
|
assert [item["suffix"] for item in gate.PROPOSED_EXCEPTIONS] == [
|
|
".xlsx",
|
|
".md .txt .csv .json .odt .rtf",
|
|
]
|
|
assert gate.APPROVED_ON in gate.render([])
|
|
|
|
|
|
def test_bad_usage_exits_two() -> None:
|
|
with pytest.raises(SystemExit) as exc:
|
|
gate.main(["--no-such-flag"])
|
|
assert exc.value.code == 2
|
|
|
|
|
|
# --- 3. the real build at this commit ----------------------------------------
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def real_rows() -> list[gate.Row]:
|
|
pytest.importorskip("pdfplumber")
|
|
pytest.importorskip("pypandoc")
|
|
return gate.evaluate(r761=None, ci=True, consume=False)
|
|
|
|
|
|
def _cheating_report(inventory: dict[str, Any], mode: str) -> dict[str, Any]:
|
|
"""The review's `cheat.py`, as data: a report that changes not one byte of
|
|
the bundle and books every element as carried (or as rejected)."""
|
|
documents = []
|
|
for name, entry in inventory["documents"].items():
|
|
if mode == "carried":
|
|
fates = {kind: {"carried": n} for kind, n in entry["elements"].items()}
|
|
else:
|
|
fates = {
|
|
kind: {"rejected": {"extractor_unknown": n}} if n else {"rejected": {}}
|
|
for kind, n in entry["elements"].items()
|
|
}
|
|
documents.append(
|
|
{
|
|
"source_file": name,
|
|
"status": "persisted",
|
|
"code": None,
|
|
"inventory": dict(entry["elements"]),
|
|
"fates": fates,
|
|
}
|
|
)
|
|
files = [{"source_file": name, "fate": "carried", "code": None} for name in inventory["files"]]
|
|
return {"accounting_version": 1, "documents": documents, "files": files}
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["carried", "empty"])
|
|
def test_a_report_the_build_did_not_write_cannot_make_row3_green(mode: str) -> None:
|
|
"""B-1, end to end on the real fixture bundle. Until 2026-09-18 both modes
|
|
gave `GATE GREEN`, exit 0: the gate compared the report's numbers with the
|
|
witness's and never opened a concept file."""
|
|
pytest.importorskip("pdfplumber")
|
|
pytest.importorskip("pypandoc")
|
|
inventory = gate.load_inventory(gate.INVENTORY)
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
build = gate.run_build(gate.CORPUS, Path(tmp), door=True)
|
|
build.accounting = _cheating_report(inventory, mode)
|
|
units = gate.account(inventory, build, gate.CORPUS)
|
|
assert gate.row3(units, door=True).status == gate.RED
|
|
|
|
|
|
def test_the_door_exists() -> None:
|
|
assert gate.door_available()
|
|
|
|
|
|
def test_the_real_gate_names_what_the_build_does_not_account_for(
|
|
real_rows: list[gate.Row],
|
|
) -> None:
|
|
"""Rows 1-5 against the real `okf build`; row 6 needs R761 and is skipped
|
|
here.
|
|
|
|
Rows 2 and 3 were GREEN at `864570b` and are RED now, and that is the
|
|
hardening working rather than a regression: the witness counts thirteen
|
|
classes of content it could not see before, the build accounts for none
|
|
of them, and a number nobody counts is a loss nobody can report. The
|
|
named classes are the raw material for the next capability order.
|
|
"""
|
|
assert [(r.number, r.status) for r in real_rows] == [
|
|
(1, gate.GREEN),
|
|
(2, gate.RED),
|
|
(3, gate.RED),
|
|
(4, gate.GREEN),
|
|
(5, gate.GREEN),
|
|
(6, gate.SKIPPED),
|
|
]
|
|
unaccounted = " ".join(real_rows[2].details)
|
|
for element in (
|
|
"annotation",
|
|
"citation",
|
|
"comment",
|
|
"endnote",
|
|
"figure",
|
|
"figure_caption",
|
|
"formula",
|
|
"header_footer",
|
|
"hidden_sheet",
|
|
"hidden_slide",
|
|
"math",
|
|
"note",
|
|
"text_box",
|
|
):
|
|
assert f"{element}: 0 booked of" in unaccounted, element
|
|
# And no false red from the gate's own reading: every element the build
|
|
# DOES book as carried was found in the bundle.
|
|
assert "0 claimed and not found" in real_rows[2].details[0]
|