test(mcp-gate): the eval for an MCP surface over OKF bundles, written RED

No capability lands in this commit. `tools/okf_mcp_gate.py` measures six
rows against a server that does not exist, and the reason is the
interpreter's own words rather than this file's opinion: `python -c 'import
llm_ingestion_okf.mcp_server'` raises ModuleNotFoundError, printed in row 1.

Today: 1 (0/7) - 2 (0/0) - 3 (1/4) - 4 (0/9) - 5 (0/3) - 6 (0/6),
`GATE RED: rows 1, 2, 3, 4, 5, 6`, exit 1.

Row 3 is not 0, and that is a measurement rather than a slip. The order
predicted 0 of N on every row; the incumbent one-to-one skill already
refuses out loud when its bundle is rebuilt, because `okf check`'s
`bundle_mismatch` rule exists and compares the declared ref against the
payload's. One of the four artefact classes therefore passes the loudness
clause today -- at the cost the row also prints: one artefact to recreate
and one manual step per consuming project.

The server is a SUBPROCESS and never an import. Every row speaks
newline-delimited JSON-RPC over stdin/stdout starting at `initialize`, with
a client written narrowly here: a client built from the server's own framing
helpers would agree with the server by construction. Nothing in the gate
ranks, serves or writes a bundle.

Denominators are pinned in the gate and recounted a second time in the
tests. `REQUIRED_TOOLS` (7 across two variants), `DRILL_ARTEFACTS` (4),
`DISCOVERY_BUNDLES x DISCOVERY_CHECKS` (9), `CROSS_CHECKS` (3) and
`HOSTILE_CASES` (6) do not come from the run -- a row counting what the
server happened to offer goes green by offering less, which is the defect
rows 2 and 3 of the retrieval gate were caught on.

Row 2's denominator DOES come from the frozen graded question set, at run
time, and the set is never committed here: `--sett` plus `--frys`, with the
sha256 verified against the freeze file and a freeze version below the
ordered one refused as a usage error, not measured. Without them the row is
`0 of 0` with the reason stated. Its machinery carries a known-positive over
the synthetic corpus this file invents, and a green test proves those quotes
really are in the fixture on disk without asking any server -- so a 0 there
means the surface could not reach text that is demonstrably present.

Six tests are red, every one of them on an assert about behaviour: 0 of 7
tools answered, 1 of 4 artefacts loud, 0 of 9 discovery checks, 0 of 3
cross-bundle checks, 0 of 6 hostile cases, and `find_spec` returning None
rather than an ImportError. Eight instrument tests are green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-20 09:51:06 +02:00
commit 5f1772e832
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 1415 additions and 0 deletions

224
tests/test_mcp_gate.py Normal file
View file

@ -0,0 +1,224 @@
"""The MCP gate's own tests (`tools/okf_mcp_gate.py`).
Two halves, and the split is the point.
THE CAPABILITY TESTS ARE RED UNTIL THE SERVER EXISTS, and each one is red on
an ASSERT about behaviour -- never on an import, never on an AttributeError.
A test that dies with `ModuleNotFoundError` proves that a name is missing; a
test that dies with `0 == 7` proves that seven required tools did not answer
over a real protocol, which is the claim the order asked to be able to fell.
THE INSTRUMENT TESTS ARE GREEN NOW, and every denominator they pin is counted
a SECOND time inside the test. A guard that read the gate's own total would
agree with any total the gate happened to hold -- the defect this repository
has already met twice, most recently in the retrieval gate's rows 2 and 3,
where a row counted what the run produced and went green by producing less.
THE SYNTHETIC CORPUS IS THE ROW-2 MACHINERY'S KNOWN-POSITIVE, and the test
below proves the quotes really are in the bundles ON DISK, without asking any
server. Without it, `0 of 4` could mean the fixture never carried the text.
"""
from __future__ import annotations
import importlib.util
import json
import sys
import tempfile
from pathlib import Path
import pytest
TOOLS = Path(__file__).resolve().parents[1] / "tools"
sys.path.insert(0, str(TOOLS))
import okf_mcp_gate as gate # noqa: E402
@pytest.fixture()
def scratch() -> Path:
with tempfile.TemporaryDirectory(prefix="okf-mcp-gate-test-") as directory:
yield Path(directory)
# --- the instrument ---------------------------------------------------------
def test_the_required_tool_roster_is_pinned_and_not_taken_from_a_run() -> None:
"""Counted again here, by name. A roster the gate could shrink is a
denominator the capability could satisfy by offering less."""
one_to_one = ("okf_describe", "okf_ask", "okf_fetch")
one_to_many = ("okf_list", "okf_describe", "okf_ask", "okf_fetch")
assert gate.REQUIRED_TOOLS["one-to-one"] == one_to_one
assert gate.REQUIRED_TOOLS["one-to-many"] == one_to_many
assert sum(len(names) for names in gate.REQUIRED_TOOLS.values()) == len(one_to_one) + len(
one_to_many
)
def test_every_pinned_denominator_is_recounted_here() -> None:
"""Four artefact classes, three discovery checks over three bundles, three
cross-bundle checks, six hostile cases -- each listed again."""
assert gate.DRILL_ARTEFACTS == (
"mcp-one-to-one",
"mcp-one-to-many",
"skill-one-to-one",
"skill-one-to-many",
)
assert gate.DISCOVERY_BUNDLES * len(gate.DISCOVERY_CHECKS) == 3 * 3
assert len(gate.CROSS_CHECKS) == 3
assert gate.HOSTILE_CASES == (
"traversal-in-bundle-id",
"traversal-in-concept-id",
"symlink-out-of-root",
"broken-manifest",
"oversized-concept",
"unknown-bundle-id",
)
assert gate.OVERSIZED_BYTES == 10 * 1024 * 1024
def test_a_row_that_counted_nothing_is_never_green() -> None:
"""`0 of 0` is the shape a green verdict hides behind."""
assert gate._row(1, "n", 0, 0, "r", []).status == gate.RED
assert gate._row(1, "n", 3, 3, "r", []).status == gate.GREEN
assert gate._row(1, "n", 2, 3, "r", []).status == gate.RED
def test_the_synthetic_quotes_are_really_in_the_bundles_on_disk(scratch: Path) -> None:
"""Row 2's known-positive, proved without a server.
If this is green and row 2's known-positive is 0 of 4, the surface could
not reach text that is demonstrably there.
"""
bundles = gate.corpus(scratch)
for bundle_id, anchor, quote in gate.SYNTHETIC_ANCHORS:
concept = bundles[bundle_id] / f"{anchor}.md"
assert concept.is_file(), f"{bundle_id}/{anchor} is not in the fixture"
assert quote in concept.read_text(encoding="utf-8")
def test_the_two_synthetic_bundles_share_no_question_token(scratch: Path) -> None:
"""Row 5 asks for an answer spanning two bundles. If the bundles shared
vocabulary, one of them could carry the whole answer and the row would
pass without ever crossing."""
from llm_ingestion_okf import consume
bundles = gate.corpus(scratch)
words = {
name: {
token
for path in sorted(root.rglob("*.md"))
if path.name != "index.md"
for token in consume.normalise(path.read_text(encoding="utf-8"))
}
for name, root in bundles.items()
}
shared = words["bridge-notes"] & words["kitchen-notes"]
assert "spennvidde" not in shared
assert "surdeig" not in shared
def test_a_question_set_whose_bytes_moved_is_a_usage_error_not_a_red_row(
scratch: Path,
) -> None:
"""A gate that measured a moving set would report on nothing. Driven from
BOTH sides: the matching pair is accepted, the moved one is refused."""
questions = scratch / "sporsmal.json"
payload = {
"sporsmal": [
{
"bundle": "bridge-notes",
"atomer": [{"kilde_anker": "spennvidde", "kilde_sitat": "24 meter"}],
}
]
}
questions.write_text(json.dumps(payload), encoding="utf-8")
import hashlib
digest = hashlib.sha256(questions.read_bytes()).hexdigest()
freeze = scratch / "frys.json"
freeze.write_text(
json.dumps({"versjon": 4, "sha256": {"sporsmal.json": digest}}), encoding="utf-8"
)
anchors = gate.read_anchor_set(questions, freeze, want_version=4)
assert anchors.pairs == (("bridge-notes", "spennvidde", "24 meter"),)
questions.write_text(json.dumps(payload) + " ", encoding="utf-8")
with pytest.raises(gate.GateUsage, match="is not the file"):
gate.read_anchor_set(questions, freeze, want_version=4)
def test_an_older_freeze_version_is_refused_rather_than_measured(scratch: Path) -> None:
"""The set is versioned by its scoring regime. Measuring version 3 and
reporting it as the ordered version 4 is how a number outlives its own
definition."""
import hashlib
questions = scratch / "sporsmal.json"
questions.write_text(json.dumps({"sporsmal": []}), encoding="utf-8")
freeze = scratch / "frys.json"
freeze.write_text(
json.dumps(
{
"versjon": 3,
"sha256": {"sporsmal.json": hashlib.sha256(questions.read_bytes()).hexdigest()},
}
),
encoding="utf-8",
)
with pytest.raises(gate.GateUsage, match="declares version 3"):
gate.read_anchor_set(questions, freeze, want_version=4)
def test_the_gate_reads_bundle_and_concept_ids_at_any_depth() -> None:
"""An `ask` answer carries one id per excerpt and a `list` answer one per
bundle. A rule reading the top level only would score the shape."""
answer = {"excerpts": [{"bundle_id": "a", "concept_id": "x"}, {"bundle_id": "b"}]}
assert gate._bundle_ids(answer) == {"a", "b"}
assert gate._source_ids(answer) == {"x"}
assert gate._bundle_ids({"excerpts": []}) == set()
# --- the capability: red until the server exists -----------------------------
def test_the_server_module_exists() -> None:
"""Red on an ASSERT, never on an import: the claim is that the module is
reachable, and a `ModuleNotFoundError` would prove only that this file
spelled a name."""
assert importlib.util.find_spec(gate.SERVER_MODULE) is not None, (
f"{gate.SERVER_MODULE} is not importable; the MCP surface has not been built"
)
def _row(number: int, scratch: Path) -> gate.Row:
rows = {row.number: row for row in gate.evaluate(scratch)}
return rows[number]
def test_every_required_tool_answers_over_real_stdio(scratch: Path) -> None:
row = _row(1, scratch)
assert (row.k, row.m) == (row.m, 7), f"{row.k} of {row.m}: {row.details}"
def test_a_rebuilt_bundle_never_gets_yesterdays_answer(scratch: Path) -> None:
"""Scenario 1. A silent stale answer is red regardless of every other
number, so this asserts the whole row rather than the MCP arms alone."""
row = _row(3, scratch)
assert row.k == row.m == len(gate.DRILL_ARTEFACTS), f"{row.k} of {row.m}: {row.details}"
def test_a_bundle_added_while_the_server_runs_is_discovered(scratch: Path) -> None:
row = _row(4, scratch)
assert row.k == row.m == 9, f"{row.k} of {row.m}: {row.details}"
def test_one_documented_sequence_answers_from_two_bundles(scratch: Path) -> None:
row = _row(5, scratch)
assert row.k == row.m == 3, f"{row.k} of {row.m}: {row.details}"
def test_hostile_input_is_refused_out_loud(scratch: Path) -> None:
row = _row(6, scratch)
assert row.k == row.m == 6, f"{row.k} of {row.m}: {row.details}"

1191
tools/okf_mcp_gate.py Normal file

File diff suppressed because it is too large Load diff