llm-ingestion-okf/tests/test_gate_refusal_is_loud.py
Kjell Tore Guttormsen 404bed26e2
feat(build): name the documents the gate refused whole, and close as 1.0.0
Measured 2026-09-20 on an official documentation corpus of 594 sources built
with the shipped default gate `guard-trusted-source`: 17 sources were refused
OUTRIGHT -- `fail_secure` 3, `quarantine_review` 14 -- and 16 of them were
among 197 official documentation pages, the pages on hooks, skills,
permissions, errors, env-vars and authentication among them. The summary said
only `fail_secure`: 3/594. Three of the four facts a reader needs were missing:
the COUNT of documents the gate dropped (the existing `rejected (coded)` line
sums gate refusals and extraction failures, two failures with two different
remedies), the NAMES, and the way out. Rebuilt with `--gate none`, all 17 went
through untouched, so the refusal is the gate and not the readers.

`okf build` now prints a `Documents the gate refused WHOLE` section directly
under the denominator, carrying all four: the count with its denominator, the
names capped at ten with the rest in the bundle's `log.md`, the codes, and
`--gate none` for a source you vouch for yourself. The same fact goes to stderr
in one line, built from the same field, because `okf build > report.txt` is an
ordinary thing to do. `log.md` gains one bullet naming every refused document,
uncapped.

The exit code deliberately does not move. The build is valid -- every refusal
is coded, the conservation identity holds, and the bundle is a true record of
what the gate allowed. What was wrong was the silence.

A run the gate refused nothing from is byte-identical in both places, which is
the known-negative in the new suite: no bundle this repository ships was built
with a gate refusal, so this cannot have moved a byte measured here.

Also, and measuring nothing new:

- README gains `Known limitations` high up -- the gate's refusals and the way
  out, the absent ceiling on what one run pays for images (a 70 KB PDF with 16
  images under the declared limit reached 851 MB peak RSS; RLIMIT_AS is not
  enforceable on this platform, so the 512 MiB per-link budget is the whole
  bound), the three gates of this repository that are RED today (retrieval
  5/7/8/9, MCP 2, accounting 2/3/6 -- all three re-run on this commit), what
  the content accounting does not count, and the rough edges nothing is
  planned for.
- The two `pip install` lines under "Install in detail" install `[extract]`.
  The first screen does; those two did not, so the two recipes produced
  different installations and the detailed one reports `resolved converter
  path: unresolved (extractor_extra_missing)`.
- Version `1.0.0`, synced across pyproject, `__version__`, `uv.lock`, the four
  README install lines, the install prose, the current-tag entry and the
  CHANGELOG, where the two "after the 0.10.1 notes were written, untagged"
  sections are folded in. It adds no capability over `v0.10.1`; what it adds
  is that the tool says what it does not do.

Suite: 2325 passed, 2 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 21:44:49 +02:00

196 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""A document the gate refuses WHOLE is named in the run's own summary.
Measured on 2026-09-20 against a real documentation corpus (594 source files,
197 official documentation pages) built with the shipped default gate: 17
sources -- 16 of the 197 pages -- were refused outright, `fail_secure` 3 and
`quarantine_review` 14, and the pages lost were hooks, skills, permissions,
errors, env-vars and authentication. The build exited 0 and said so like this:
- `fail_secure`: 3/594
- `quarantine_review`: 14/594
Three of the four facts a reader needs were absent. The code was there (c);
the COUNT of documents the gate refused whole was not (the `rejected (coded)`
line sums gate refusals and extraction failures into one number), the NAMES
were not (b), and the way out -- `--gate none` for a source you vouch for
yourself -- was not (d). So a bundle could lose the pages it exists for while
its summary read like a clean run.
The exit code deliberately does NOT move. The build is valid: every refusal is
coded, the conservation identity holds, and the bundle is a true record of what
the gate allowed. What was wrong was the silence, not the status.
The fixture is a two-document inbox, never the real corpus: `CARRIER` carries a
zero-width space inside a word and is measured `fail_secure` under BOTH guard
presets, and `BENIGN` clears both, so the run has a refusal AND a survivor and
cannot pass by refusing everything.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from llm_ingestion_okf import cli, corpus
BUNDLE_ID = "gate-loud-fixture"
OKF_VERSION = "0.2"
CARRIER = "# Kostnader\n\nEn merknad med et nullbreddetegn i seg.\n"
BENIGN = "# Kostnader\n\nKvartalstall for plattformgruppen, uten funn.\n"
def _inbox(root: Path, documents: dict[str, str]) -> Path:
inbox = root / "inbox"
inbox.mkdir(parents=True, exist_ok=True)
for name, body in documents.items():
(inbox / name).write_text(body, encoding="utf-8", newline="")
return inbox
def _build(inbox: Path, bundle: Path, *extra: str) -> int:
return cli.main(
[
"build",
str(inbox),
"--bundle",
str(bundle),
"--bundle-id",
BUNDLE_ID,
"--okf-version",
OKF_VERSION,
*extra,
]
)
def _section(out: str) -> str:
"""Exactly the refusal section, bounded at the next heading.
Taken to the end of the report, the slice would also hold `## Rejection
codes`, and an assertion about how many names the section lists would count
that section's totals too.
"""
after = out.split(corpus.REFUSED_HEADING, 1)[1]
end = after.find("\n## ")
return after if end < 0 else after[:end]
def _run(
tmp_path: Path, documents: dict[str, str], capsys: pytest.CaptureFixture[str]
) -> tuple[int, str, str]:
pytest.importorskip("llm_ingestion_guard")
inbox = _inbox(tmp_path, documents)
code = _build(inbox, tmp_path / "bundle")
captured = capsys.readouterr()
return code, captured.out, captured.err
def test_the_refused_documents_are_counted_with_their_denominator(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""(a) how many documents the gate refused whole -- its own number.
`rejected (coded): 1/2` already existed and is not this number: it also
counts a file the extractor could not read, which is a different fact about
a different failure and points at a different remedy.
"""
code, out, _ = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
assert code == 0, "a build whose gate refused one of two documents is still a valid build"
assert corpus.REFUSED_HEADING in out
assert "1 of 2" in out
def test_the_refused_documents_are_named(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""(b) the file names, and (c) the code beside each one."""
_, out, _ = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
section = _section(out)
assert "carrier.md" in section
assert "fail_secure" in section
assert "benign.md" not in section, "a document that reached the bundle is not a refusal"
def test_the_way_out_is_stated_in_the_summary(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""(d) the exact command, not a hint that one exists."""
_, out, _ = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
section = _section(out)
assert "--gate none" in section
assert "trust" in section.lower()
def test_a_run_the_gate_refused_nothing_from_says_nothing(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The known-positive: the section must be earned, not unconditional.
Without this, every assertion above is satisfied by a constant string and
the test says nothing about whether a refusal was observed.
"""
code, out, err = _run(tmp_path, {"benign.md": BENIGN}, capsys)
assert code == 0
assert corpus.REFUSED_HEADING not in out
assert "--gate none" not in err
def test_the_refusal_also_reaches_stderr(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Loud where a redirected stdout cannot hide it.
`okf build > report.txt` is an ordinary thing to do, and it puts the whole
summary in a file the reader opens later, if at all.
"""
_, _, err = _run(tmp_path, {"carrier.md": CARRIER, "benign.md": BENIGN}, capsys)
banner = [line for line in err.splitlines() if "refused" in line]
assert banner, "stderr must carry the refusal"
assert "carrier.md" in banner[0]
assert "--gate none" in banner[0]
def test_a_long_list_is_capped_and_points_at_the_whole_one(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""A capped list is only honest if the rest is somewhere a reader can go.
The bundle's own `log.md` is that somewhere: SPEC section 9 already holds
this run's denominator and gate name, and a document the gate dropped is
the same class of fact -- the one thing about the run the bundle cannot
otherwise recover.
"""
# Distinct titles, not thirteen copies: two documents reducing to one
# concept name hit the section 3 collision refusal BEFORE the gate, which
# would make this row green over a set the gate never saw.
documents = {
f"carrier-{index:02d}.md": CARRIER.replace("Kostnader", f"Kostnader {index:02d}")
for index in range(corpus.REFUSED_NAME_CAP + 3)
}
documents["benign.md"] = BENIGN
_, out, _ = _run(tmp_path, documents, capsys)
section = _section(out)
assert section.count("`fail_secure`") == corpus.REFUSED_NAME_CAP + 1, (
"the per-name list is capped; the count line carries the code totals"
)
assert "3 more" in section
assert corpus.LOG_NAME in section
log = (tmp_path / "bundle" / corpus.LOG_NAME).read_text(encoding="utf-8")
for name in sorted(documents):
if name != "benign.md":
assert name in log, "log.md names every refused document, uncapped"
def test_the_log_is_unchanged_when_the_gate_refused_nothing(
tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""Exposure, measured rather than argued: a clean run's bytes do not move.
Every bundle this repository ships was built without a gate refusal, so a
bullet written only when there is one cannot have moved a byte of them.
"""
_run(tmp_path, {"benign.md": BENIGN}, capsys)
log = (tmp_path / "bundle" / corpus.LOG_NAME).read_text(encoding="utf-8")
assert "refused" not in log.lower()