portfolio-optimiser/tests/test_console_entry_points.py
Kjell Tore Guttormsen 38df79126f
feat(toolbox): the first four doors out of the toolbox, without a chat client on the way
B-gate row 1's premise, made callable. Every path through the framework CLI constructs a chat
client, so an outside caller -- a human at a terminal, or an agent that is NOT po -- could not
reach a single run-path step without paying for a model. These four steps need no model at all.

One CLI, four subcommands, one core call each:

  navigate-bundle  --bundle-dir                         -> okf.navigate_bundle
  cost-baseline    --bundle-dir --project-id            -> okf.derive_cost_baseline
  retrieve-chunks  --query --docs-dir [--top-k]         -> datasource.retrieve_chunks
  prepass-admit    --payload --bundle-dir [--dimension] -> prepass.admit_payload

Each handler is a thin adapter: strings in, the SAME function the run path calls, JSON on stdout,
and an exit code that says what happened (0 ran, 2 malformed call, 3 the step refused, named).
A handler that computed anything of its own would be a second implementation of a run-path step,
and the outside caller would stop getting what the debate gets.

Dispatch is an explicit branch per command, not argparse's `set_defaults(handler=...)`: the table
hides the one thing a reader wants to see, and B-gate row 1 asks the same question of the source
(it walks the call graph from `main` down to the step's symbol), where a callable in a Namespace
is a hop neither can follow.

Probes (`tests/test_toolbox_doors.py`, 10 arms): each starts the door as a SUBPROCESS with the
subcommand in argv and asserts on what it wrote -- never by importing the core function, which is
the whole difference the gate exists to measure. The yardstick is outside the door in every arm:
the filesystem (navigate-bundle, including the one deliberate outside-bundle link), a table
transcribed from the priced fixture (cost-baseline), the in-process seam it must equal byte for
byte (retrieve-chunks), and the producer's own checked-in payload (prepass-admit). Every refusal
arm has an rc-0 control beside it.

`portfolio-optimiser-toolbox` is the THIRD console script, and the pin test now says why: it is
the door the other two cannot be used for. README and CLAUDE.md updated with the command and the
reason it exists; every documented invocation was run.

Row 1: 1 -> 5 of 17 (four subcommands + `gate`, which the class fix in e47be68 stopped rejecting
on a name technicality). No other row moved; exit 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 08:11:58 +02:00

90 lines
4.4 KiB
Python

"""P4 pkt. 5 — the console entry points are part of the FROZEN install surface.
The README's central claim is "download -> run". Until now every documented invocation went
through ``uv run python -m portfolio_optimiser.<module>``, which works but is not an install
surface: nothing in the distribution metadata promised a command. This test pins the two commands
that the demo and the framework CLI are reached by.
Why read ``importlib.metadata`` and not ``pyproject.toml``: a ``[project.scripts]`` line that has
never been ``uv sync``-ed is a claim, not a command. The distribution metadata is what a fresh
clone materializes after ``uv sync``, so it is the only reading that can fail when the surface is
merely *declared*. The declaration is checked too (the TOML is the source the metadata is built
from), but the metadata assert is the load-bearing one.
Scope, stated so it is a decision and not an oversight: exactly THREE commands are exposed.
``run`` is the framework CLI (three documented modes), ``simulation`` is the offline end-to-end
proof the README points a newcomer at, and ``toolbox`` (added 2026-09-20) is the door an outside
caller reaches the run-path steps through WITHOUT a chat client — B-gate row 1's premise, and the
one thing the other two cannot be used for, since every path through ``run`` constructs a client.
``costsim`` / ``hitl`` / ``preflight`` keep the ``-m`` form — they are operator utilities, not the
product's front door, and every name added here is a name the freeze has to carry.
"""
from __future__ import annotations
import importlib
import importlib.metadata
from pathlib import Path
import pytest
_DIST = "portfolio-optimiser"
# command name -> "module:function" target, verbatim as it must appear in the metadata.
_EXPECTED: dict[str, str] = {
"portfolio-optimiser": "portfolio_optimiser.run:main",
"portfolio-optimiser-demo": "portfolio_optimiser.simulation:main",
"portfolio-optimiser-toolbox": "portfolio_optimiser.toolbox:main",
}
def _console_scripts() -> dict[str, str]:
"""The installed distribution's console scripts, as ``{name: "module:function"}``."""
return {
ep.name: ep.value
for ep in importlib.metadata.distribution(_DIST).entry_points
if ep.group == "console_scripts"
}
@pytest.mark.parametrize(("name", "target"), sorted(_EXPECTED.items()))
def test_console_script_is_installed(name: str, target: str) -> None:
"""T-P4.5a: the command exists in the INSTALLED distribution and points at the right target.
RED when the ``[project.scripts]`` entry is removed (or when it is added to the TOML without a
re-sync — which is the same failure a fresh clone would hit for real).
"""
scripts = _console_scripts()
assert name in scripts, (
f"console script {name!r} is not installed; found {sorted(scripts)}. "
"Declare it under [project.scripts] in pyproject.toml and re-run `uv sync`."
)
assert scripts[name] == target
@pytest.mark.parametrize("name", sorted(_EXPECTED))
def test_console_script_target_resolves(name: str) -> None:
"""T-P4.5b: the INSTALLED target actually imports and is callable — a typo'd module or function
name installs a command that only fails when the operator runs it, which on demo day is on
stage. Resolves what the distribution says, not what this file expects: resolving ``_EXPECTED``
would only ever re-check a constant against itself."""
target = _console_scripts()[name]
module_name, _, func_name = target.partition(":")
module = importlib.import_module(module_name)
entry = getattr(module, func_name, None)
assert callable(entry), f"{target} (behind {name}) does not resolve to a callable"
def test_pyproject_declares_exactly_these_scripts() -> None:
"""T-P4.5c: the declaration in ``pyproject.toml`` matches the installed set exactly.
Guards the drift direction the metadata assert cannot see: a script installed from an older
sync but since deleted from the TOML would leave a command that a fresh clone never gets.
"""
pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml"
try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - Python 3.10 has no tomllib
pytest.skip("tomllib is 3.11+; the installed-metadata asserts cover the same surface")
declared = tomllib.loads(pyproject.read_text(encoding="utf-8"))["project"]["scripts"]
assert declared == _EXPECTED