feat(card): a folder shows every bundle under it, as the server does
`okf card <folder>` prints `okf_list` and `okf_describe` with no bundle named, joined by `mcp_server.overview`, and computes nothing of its own: one source, two doors. Chosen over a separate `--root` flag because the skill's first step must be the same command whether it was pointed at a bundle or at a folder of them; the rule that decides is discovery's own (a directory carrying an `index.md` is a bundle). A bundle path prints its card as before. v1.1 order F, part F1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3d149f955a
commit
df83c65e32
5 changed files with 212 additions and 4 deletions
139
tests/test_folder_of_bundles.py
Normal file
139
tests/test_folder_of_bundles.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
"""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"}
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue