llm-ingestion-okf/tests/test_folder_of_bundles.py
Kjell Tore Guttormsen 718c064279 feat(consume): one call over a folder asks every bundle under it
`okf consume <folder>` is the server's `okf_ask` with no bundle named,
byte for byte: no ranking of its own, every excerpt carrying its bundle
id. `--bundle-id` asks one bundle under the folder. A flag that acts on
one bundle's cut is refused by name over a folder rather than dropped,
because the server takes none of them. A bundle path reads as before.

v1.1 order F, part F2.

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

216 lines
8 KiB
Python

"""One folder, every bundle under it: the command line's own door (v1.1 F).
The server has read a FOLDER of bundles since `okf mcp --root`; the generic
skill read one bundle at a time, the one it was pointed at, and could not see
the others. These tests hold the two command-line entries the skill now uses
-- `okf card <folder>` and `okf consume <folder>` -- to the server's OWN
functions: one source, two doors. Both corpora are invented here.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
from llm_ingestion_okf import mcp_server
from llm_ingestion_okf.cli import build
BAKERY = {
"surdeig.md": (
"# Surdeig\n\n"
"## Heving\n\n"
"Surdeigen hever i tolv timer ved romtemperatur før den formes.\n\n"
"## Steking\n\n"
"Brødet stekes i førti minutter på to hundre og tretti grader.\n"
),
}
GARDEN = {
"tomater.md": (
"# Tomater\n\n"
"## Vanning\n\n"
"Tomatene vannes hver morgen, og jorda skal aldri tørke helt ut.\n\n"
"## Oppbinding\n\n"
"Plantene bindes opp til en stokk når de er tretti centimeter høye.\n"
),
}
def _bundle(tmp_path: Path, folder: Path, name: str, documents: dict[str, str]) -> Path:
source = tmp_path / f"src-{name}"
source.mkdir()
for file_name, text in documents.items():
(source / file_name).write_text(text, encoding="utf-8")
target = folder / name
build(source, target, bundle_id=name, okf_version="0.2")
return target
@pytest.fixture(scope="module")
def folder(tmp_path_factory: pytest.TempPathFactory) -> Path:
tmp_path = tmp_path_factory.mktemp("folder-of-bundles")
root = tmp_path / "samlinger"
root.mkdir()
_bundle(tmp_path, root, "bakeri", BAKERY)
_bundle(tmp_path, root, "hage", GARDEN)
return root
def _okf(*argv: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-m", "llm_ingestion_okf.cli", *argv],
capture_output=True,
text=True,
check=False,
)
def _surface(folder: Path) -> mcp_server.Surface:
return mcp_server.build_surface(bundle=None, roots=[folder])
# --- F1: the overview -------------------------------------------------------
def test_the_card_of_a_folder_names_every_bundle_under_it(folder: Path) -> None:
run = _okf("card", str(folder))
assert run.returncode == 0, run.stderr
overview = json.loads(run.stdout)
assert [card["bundle_id"] for card in overview["cards"]] == ["bakeri", "hage"]
assert [entry["bundle_id"] for entry in overview["bundles"]] == ["bakeri", "hage"]
assert overview["unreadable"] == []
assert overview["shape"] == "one-to-many"
# Each card carries the map, which is what the working method reads first.
assert all(card["map"] for card in overview["cards"])
def test_the_card_of_a_folder_is_the_servers_own_listing_and_description(folder: Path) -> None:
"""One source: the bytes the command prints are the server's two replies."""
surface = _surface(folder)
listing = mcp_server.call_list(surface, {})
described = mcp_server.call_describe(surface, {})
overview = json.loads(_okf("card", str(folder)).stdout)
assert overview["bundles"] == listing["bundles"]
assert overview["unreadable"] == listing["unreadable"]
assert overview["cards"] == described["cards"]
def test_the_card_of_one_bundle_is_unchanged(folder: Path) -> None:
"""Pointed at one bundle, the command prints that bundle's card, as before."""
bundle = folder / "bakeri"
run = _okf("card", str(bundle))
assert run.returncode == 0, run.stderr
card = json.loads(run.stdout)
assert card == mcp_server.card(bundle.resolve(), profile=mcp_server.okf_consume.DEFAULT_PROFILE)
assert "cards" not in card
def test_a_folder_holding_no_bundle_is_refused_rather_than_empty(tmp_path: Path) -> None:
empty = tmp_path / "tom"
empty.mkdir()
run = _okf("card", str(empty))
assert run.returncode == 1
assert "bundle_none_served" in run.stderr
def test_a_broken_bundle_under_the_folder_is_reported(folder: Path, tmp_path: Path) -> None:
root = tmp_path / "med-feil"
root.mkdir()
for name in ("bakeri", "hage"):
(root / name).symlink_to(folder / name) # never followed: not listed
broken = root / "odelagt"
broken.mkdir()
(broken / "index.md").write_text("---\ntitle: x\n---\n", encoding="utf-8")
real = root / "ekte"
real.mkdir()
for source in (folder / "hage").rglob("*"):
target = real / source.relative_to(folder / "hage")
if source.is_dir():
target.mkdir(parents=True, exist_ok=True)
else:
target.write_bytes(source.read_bytes())
overview = json.loads(_okf("card", str(root)).stdout)
assert [card["bundle_id"] for card in overview["cards"]] == ["hage"]
assert overview["unreadable"] == [
{"directory": "odelagt", "reason": "index.md declares no bundle_id"}
]
# --- F2: one call across the folder ------------------------------------------
QUESTIONS = ("hvor lenge hever surdeigen", "hvor ofte vannes tomatene")
def _ask(folder: Path, *extra: str) -> subprocess.CompletedProcess[str]:
argv = ["consume", str(folder)]
for question in QUESTIONS:
argv += ["--question", question]
return _okf(*argv, *extra)
def test_one_call_over_a_folder_answers_from_every_bundle(folder: Path) -> None:
run = _ask(folder)
assert run.returncode == 0, run.stderr
reply = json.loads(run.stdout)
assert reply["asked"] == ["bakeri", "hage"]
assert reply["questions"] == list(QUESTIONS)
by_bundle = {answer["bundle_id"]: answer["payload"] for answer in reply["answers"]}
assert set(by_bundle) == {"bakeri", "hage"}
# Every excerpt names the bundle it came from, and it is the right one.
for bundle_id, payload in by_bundle.items():
assert payload["excerpts"], bundle_id
assert {excerpt["bundle_id"] for excerpt in payload["excerpts"]} == {bundle_id}
delivered = {
answer["bundle_id"]: " ".join(excerpt["text"] for excerpt in answer["payload"]["excerpts"])
for answer in reply["answers"]
}
assert "tolv timer" in delivered["bakeri"]
assert "hver morgen" in delivered["hage"]
def test_one_call_over_a_folder_is_the_servers_own_ask(folder: Path) -> None:
"""No ranking of its own: the bytes are `okf_ask`'s with no bundle named."""
reply = json.loads(_ask(folder).stdout)
assert reply == mcp_server.call_ask(_surface(folder), {"questions": list(QUESTIONS)})
def test_naming_one_bundle_under_the_folder_asks_only_that_one(folder: Path) -> None:
reply = json.loads(_ask(folder, "--bundle-id", "hage").stdout)
assert reply["asked"] == ["hage"]
assert reply == mcp_server.call_ask(
_surface(folder), {"questions": list(QUESTIONS), "bundle_id": "hage"}
)
def test_an_unknown_bundle_name_is_refused(folder: Path) -> None:
run = _ask(folder, "--bundle-id", "finnes-ikke")
assert run.returncode == 1
assert "bundle_unknown" in run.stderr
def test_a_reading_flag_the_server_does_not_take_is_refused_over_a_folder(folder: Path) -> None:
"""A flag that would be silently dropped is refused: the folder door reads
exactly as the server reads, and a flag it ignored would make the caller
believe in a cut that never happened."""
run = _ask(folder, "--no-source-quota")
assert run.returncode == 2
assert "--no-source-quota" in run.stderr
def test_bundle_id_on_one_bundle_is_refused(folder: Path) -> None:
run = _okf("consume", str(folder / "hage"), "--question", "vanning", "--bundle-id", "hage")
assert run.returncode == 2
assert "--bundle-id" in run.stderr
def test_one_bundle_is_read_as_before(folder: Path) -> None:
"""Pointed at one bundle, the payload is the single-bundle payload."""
run = _okf("consume", str(folder / "hage"), "--question", "vanning")
assert run.returncode == 0, run.stderr
payload = json.loads(run.stdout)
assert "answers" not in payload
assert payload["bundle"]["bundle_id"] == "hage"