llm-ingestion-okf/tests/test_k2_office_fixtures.py
Kjell Tore Guttormsen a7b050b569 test(fixtures): a synthetic K2 denominator for pptx, odt and rtf
`docs/2026-09-04-k2-pptx-odt-rtf.md` measured the corpus denominator for
these three office rows and found it ZERO: `K2/trinn1` holds 43 files and
not one of them is a `pptx`, an `odt` or an `rtf`. So `extract._EVIDENCE`
calls those rows `unmeasured` in the strongest sense available -- they
work by construction and had never met a document at all.

This is the smallest thing that changes that without inventing a corpus.
One authored document -- a title, an intro, a 20-row label/value table, a
caption and a 4x4 grid -- laid out three times in three containers, so
the container and its reader are the only variable between the three
measurements. `k2-office-fasit.json` carries the hand count taken from
the AUTHORED content rather than from any converter's output: 56 cells,
20 pairs, 59 distinct strings, shared by all three. It is committed here,
before the measurement runs, because a fasit written afterwards is a
description of a result rather than a denominator for it.

No converter wrote any of these files. `make_k2_office.py` lays every
part by hand, for the reason `make_fixtures.py` already states and this
set inherits: a file written by the converter and then read by the
converter proves only that the converter agrees with itself, and stays
green through any conversion defect that is symmetric. The commissioning
order offered pandoc as one generator option; the committed fixture
policy forbids it and the policy wins.

Two converter behaviours were measured while laying the RTF out, both of
them structurally plausible input read silently wrong, exit code 0 and no
warning. Without `\pard\intbl` on cell paragraphs, consecutive
`\trowd...\row` rows come back as each row NESTED inside the previous
one: five label/value rows read as five levels of nested table, 2076
characters where 117 were expected. And the `\uN?` unicode escape -- the
form Word emits -- loses the character after it: `A\u248?BC` reads back
as `AoC` with the `B` gone, `A\u248?xBC` reads back as `AoBC`. The
fixture writes `\uN ?` with an explicit space, which round-trips. Neither
is worked around anywhere in `src/`.

The generator and the fasit live one level above `k2-office/` and that is
not tidiness: Door B walks its drop directory recursively, so anything
parked beside the three documents would enter the run and N would stop
being 3.

Three synthetic documents in one house style are not a corpus. The rows
stay `unmeasured` and the suite asserts that they do.

Suite 1132 passed (1127 + 5), `ruff check` and `ruff format --check`
clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 05:17:18 +02:00

116 lines
4.4 KiB
Python

"""The K2 denominator for `pptx`/`odt`/`rtf`: a synthetic fixture set, N = 3.
`docs/2026-09-04-k2-pptx-odt-rtf.md` measured the corpus denominator for these
three office rows and found it ZERO -- `K2/trinn1` holds 43 files and not one
of them is a `pptx`, an `odt` or an `rtf`. So those three rows in
`extract._EVIDENCE` read `unmeasured`: they work by construction and had never
met a document anyone wrote.
This module is the smallest thing that changes that without inventing a corpus:
three hand-laid documents carrying the SAME content in three containers, and a
hand-counted fasit committed beside them. It pins the one property a report
cannot assert for itself -- that all three go through Door B, offline, and land
as concepts -- so the numbers in
`docs/2026-09-07-k2-pptx-odt-rtf-fixtures.md` stay re-measurable rather than
becoming a memory of a run.
WHAT IT DOES NOT DO. Three synthetic documents in one house style are not a
corpus. A green run here says the row is exercised, never that the format is
covered; `_EVIDENCE` therefore stays `unmeasured` and this suite asserts that
it does.
"""
from __future__ import annotations
import json
from dataclasses import dataclass
from pathlib import Path
import pytest
from llm_ingestion_okf.extract import _EVIDENCE
from llm_ingestion_okf.inbox import GateDecision, process_inbox
pytest.importorskip("pypandoc", reason="office conversion needs the [extract] extra")
INGESTED_AT = "2026-09-07T00:00:00Z"
WARN = "warn"
FIXTURES = Path(__file__).parent / "fixtures"
DROP = FIXTURES / "k2-office"
FASIT = FIXTURES / "k2-office-fasit.json"
#: The three containers, one per row this fixture set exists to reach.
EXPECTED_FILES = (
"krav-presentasjon.pptx",
"krav-rikt-tekstformat.rtf",
"krav-tekstdokument.odt",
)
@dataclass
class StubGuard:
"""The pinned guard's surface, offline. A TEST DOUBLE -- never in `src/`."""
def __call__(self, text: str) -> GateDecision:
return GateDecision(sanitized_text=text, disposition=WARN, reasons=())
def test_the_drop_directory_holds_exactly_the_three_containers() -> None:
"""N = 3, and N is read off the directory rather than asserted from memory.
The generator and the fasit live one level UP on purpose: Door B walks this
directory recursively, so anything else parked here would enter the run and
the denominator would stop being three.
"""
assert sorted(path.name for path in DROP.iterdir()) == list(EXPECTED_FILES)
def test_all_three_go_through_door_b(tmp_path: Path) -> None:
"""3/3 merged, offline, through the same call the operator makes."""
result = process_inbox(
DROP,
tmp_path / "bundle",
INGESTED_AT,
okf_type="note",
gate=StubGuard(), # type: ignore[arg-type]
)
assert [item.source_file for item in result.persisted] == list(EXPECTED_FILES)
assert (result.quarantined, result.rejected, result.failed) == ((), (), ())
def test_every_persisted_concept_carries_the_documents_own_strings(tmp_path: Path) -> None:
"""A concept that arrived empty would still count as persisted.
So the count is not the whole assertion: each written concept has to carry
the title, a label from the pairing table and a cell from the grid -- one
string from each of the three structures the fixture is built out of.
"""
bundle = tmp_path / "bundle"
result = process_inbox(
DROP,
bundle,
INGESTED_AT,
okf_type="note",
gate=StubGuard(), # type: ignore[arg-type]
)
for item in result.persisted:
text = (bundle / item.path).read_text(encoding="utf-8")
assert "Kravspesifikasjon for tunnelbelysning" in text
assert "Årsdøgntrafikk:" in text
assert "Klasse C" in text
def test_the_fasit_is_hand_counted_and_committed_beside_the_fixtures() -> None:
"""The denominator the report divides by, frozen before the measurement ran."""
fasit = json.loads(FASIT.read_text(encoding="utf-8"))
assert sorted(fasit["documents"]) == list(EXPECTED_FILES)
for name in EXPECTED_FILES:
entry = fasit["documents"][name]
assert entry["cells"] == fasit["shared"]["cells"]
assert entry["pairs"] == fasit["shared"]["pairs"]
def test_three_synthetic_documents_do_not_make_a_row_measured() -> None:
"""One house style, N = 3, written by us -- that is exercise, not coverage."""
assert [_EVIDENCE[key] for key in (".pptx", ".odt", ".rtf")] == ["unmeasured"] * 3