portfolio-optimiser/tests/test_demo_stderr_quiet_loadbearing.py
Kjell Tore Guttormsen ab7f45aa95 feat(cli): console entry points + the demo's stderr damping (P4 pkt. 5 og 2)
Two commands are now part of the install surface a fresh clone gets from `uv sync`:
`portfolio-optimiser` (run:main) and `portfolio-optimiser-demo` (simulation:main).
Deliberately two of five main()s — costsim/hitl/preflight stay module-invoked; every
name here is a name the freeze has to carry. Pinned against the INSTALLED distribution's
metadata, not the TOML: a [project.scripts] line that has never been synced is a claim,
not a command. Measured: stdout is byte-identical across both invocation forms.

stderr (P4 pkt. 2), the session's open decision, resolved by measurement rather than by
preference. Damped: the round-cap notice only, via a filter on the emitting logger, keyed
on the message and installed by main() — never at import, so a library consumer keeps its
own logging config. NOT damped: the two ExperimentalWarnings. They fire while the package
__init__ imports run -> agent_framework, always before simulation's own imports and under
both invocation forms, so silencing them would mean filtering warnings inside the library
package on every consumer's behalf; they are pinned in pkt. 3 instead. A console-script
wrapper was rejected for a second reason: the two forms would then write different stderr,
and a byte-fasit would pin the command rather than the program. stderr 6 -> 4 lines.

A first implementation wrapped simulation's own agent_framework import in a scoped mute.
Measurement showed it can never fire — the package __init__ has already imported
agent_framework by then — so it was removed rather than left as a green-but-dead seam.

Load-bearing MEASURED against the whole suite, five mutations all red + green control:
remove [project.scripts] · typo the target · detach the main() call · make the filter drop
everything · install the filter at import time. The typo mutation also felled a test: the
resolve-assert re-checked the expected constant against itself, and now resolves what the
distribution actually installs. 775 -> 785 passed / 4 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C2bxLcCRguxXzpM4priTMn
2026-08-09 15:13:09 +02:00

116 lines
5.4 KiB
Python

"""P4 pkt. 2 — the demo's round-cap notice is damped, and the damping is NARROW by construction.
Measured on 2026-08-09, the demo wrote six stderr lines: two ``ExperimentalWarning``s from
``agent_framework`` (import time), two ``GroupChatOrchestrator reached max_rounds=3; forcing
completion.`` notices (``logging``, reaching stderr via ``logging.lastResort``), a blank line, and
the deliberately non-deterministic ``arbeidskopi:`` line.
**Only the round-cap notices are damped, and this file only tests those.** The two import-time
warnings fire while ``portfolio_optimiser/__init__.py`` imports ``run`` — always before
``simulation``'s own imports, under both invocation forms — so damping them would mean filtering
warnings inside the library package on every consumer's behalf. They are pinned in pkt. 3 instead.
Measured, not assumed: see the decision recorded at the top of ``simulation.py``.
**Why narrowness is the property under test, not the silence.** Plan P4 pkt. 3 pins stderr to a
byte-fasit so a new warning after a MAF bump or a subtree pull TRIPS the pin. A damping keyed on the
logger rather than the message would swallow that new warning too, leaving a pin that can no longer
fail for the reason it exists. So the drop-assert here is paired with a control proving an
unmeasured message from the very same logger still gets through — a filter that can only ever say
"drop" proves nothing.
"""
from __future__ import annotations
import logging
import subprocess
import sys
from portfolio_optimiser.simulation import ROUND_CAP_LOGGER, quiet_expected_round_cap_notice
_REAL_ROUND_CAP_MESSAGE = "GroupChatOrchestrator reached max_rounds=3; forcing completion."
class _Recorder(logging.Handler):
"""Collects whatever survives the logger's own filters."""
def __init__(self) -> None:
super().__init__()
self.messages: list[str] = []
def emit(self, record: logging.LogRecord) -> None:
self.messages.append(record.getMessage())
def _record_through_real_logger(message: str, *, quiet: bool) -> list[str]:
"""Log ``message`` through the REAL emitting logger and return what reached a handler.
Logger-level filters run in ``Logger.handle`` BEFORE ``callHandlers``, so a dropped record never
reaches the recorder — the same point at which ``logging.lastResort`` would otherwise have
written it to stderr in the demo process.
"""
logger = logging.getLogger(ROUND_CAP_LOGGER)
recorder = _Recorder()
logger.addHandler(recorder)
installed: logging.Filter | None = None
try:
if quiet:
installed = quiet_expected_round_cap_notice()
logger.warning("%s", message)
finally:
logger.removeHandler(recorder)
if installed is not None:
logger.removeFilter(installed)
return recorder.messages
def test_round_cap_notice_reaches_stderr_without_the_damping() -> None:
"""T-P4.2a (the RED-proof for the test itself): the notice really is emitted through this
logger name. The negative assert below is worthless unless the event provably happens first."""
assert _record_through_real_logger(_REAL_ROUND_CAP_MESSAGE, quiet=False) == [
_REAL_ROUND_CAP_MESSAGE
]
def test_round_cap_notice_is_dropped_by_the_damping() -> None:
"""T-P4.2b: with the damping installed, the expected round-cap notice never reaches a handler."""
assert _record_through_real_logger(_REAL_ROUND_CAP_MESSAGE, quiet=True) == []
def test_unrelated_warning_from_the_same_logger_still_surfaces() -> None:
"""T-P4.2c (control): the damping is keyed on the message, not on the logger. A different
warning from the very same logger still gets through — otherwise the pin in pkt. 3 could never
catch a genuine new orchestration problem."""
other = "GroupChatOrchestrator: participant 'checker' returned no message."
assert _record_through_real_logger(other, quiet=True) == [other]
def test_the_demo_run_emits_no_round_cap_notice() -> None:
"""T-P4.2e: the damping is WIRED — the real demo process writes no round-cap line.
Without this, the three asserts above would all pass with the ``main()`` call detached: they
install the filter themselves, so they measure the filter and not the demo. Runs the module form
(``-m``) rather than the console script, because that needs no assumption about PATH; both forms
were measured to write identical stderr when the entry point was added, and the console script
has its own tests in ``test_console_entry_points``.
"""
proc = subprocess.run(
[sys.executable, "-m", "portfolio_optimiser.simulation"],
capture_output=True,
text=True,
check=False,
)
assert proc.returncode == 0, proc.stderr
assert "forcing completion" not in proc.stderr, (
"the round-cap notice reached stderr; is quiet_expected_round_cap_notice() still called in "
f"main()? stderr was:\n{proc.stderr}"
)
# Control on the same output: the run really did happen, so the absence above is a damped line
# and not an unrun demo.
assert "LÆRINGSSLØYFA ER LUKKET" in proc.stdout
def test_damping_is_not_installed_at_import_time() -> None:
"""T-P4.2d: importing the module must not reconfigure logging for a library consumer — the
filter is runtime state installed by ``main()``. RED if the install call is moved to module
scope."""
assert logging.getLogger(ROUND_CAP_LOGGER).filters == []