feat(gateway): opt-in runtime-metadata footer on final replies (#17026)

Append a compact 'model · 68% · ~/projects/hermes' footer to the FINAL
message of each turn, disabled by default (display.runtime_footer.enabled).
Answers the Telegram-side parity ask: runtime context that the CLI status
bar already shows is now available in messaging replies when enabled.

Wiring:
- gateway/runtime_footer.py: resolve_footer_config + format_runtime_footer +
  build_footer_line. Pure-function renderer; per-platform overrides under
  display.platforms.<platform>.runtime_footer.
- gateway/run.py: appends footer to response right after reasoning prepend
  so it lands only on the final message (never tool progress or streaming
  chunks). When streaming already delivered the body (already_sent), the
  footer is sent as a small trailing message instead.
- agent_result now exposes context_length alongside last_prompt_tokens so
  the footer can compute the pct; both gateway return paths updated.
- /footer [on|off|status] slash command, wired in CLI (cli.py) and gateway
  (gateway/run.py both running-agent bypass and main dispatch). Global
  toggle only; per-platform overrides via config.yaml.

Graceful degradation:
- Missing context_length (unknown model) → pct field silently dropped
  (no '?%' artifact).
- Empty final_response → no footer appended.
- Unknown field names in config → silently ignored.

Tests: 25-case unit suite (tests/gateway/test_runtime_footer.py) plus E2E
harness covering streaming vs non-streaming branches, per-platform override,
and the exact argument contract gateway/run.py uses.

Co-authored-by: teknium1 <teknium@users.noreply.github.com>
This commit is contained in:
Teknium 2026-04-28 06:50:04 -07:00 committed by GitHub
parent 6085d7a93e
commit e123f4ecf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 610 additions and 0 deletions

54
cli.py
View File

@ -6232,6 +6232,8 @@ class HermesCLI:
self._console_print(f" Status bar {state}") self._console_print(f" Status bar {state}")
elif canonical == "verbose": elif canonical == "verbose":
self._toggle_verbose() self._toggle_verbose()
elif canonical == "footer":
self._handle_footer_command(cmd_original)
elif canonical == "yolo": elif canonical == "yolo":
self._toggle_yolo() self._toggle_yolo()
elif canonical == "reasoning": elif canonical == "reasoning":
@ -6859,6 +6861,58 @@ class HermesCLI:
if self._apply_tui_skin_style(): if self._apply_tui_skin_style():
print(" Prompt + TUI colors updated.") print(" Prompt + TUI colors updated.")
def _handle_footer_command(self, cmd_original: str) -> None:
"""Toggle or inspect ``display.runtime_footer.enabled`` from the CLI.
Usage:
/footer toggle
/footer on|off explicit
/footer status show current state
"""
from hermes_cli.config import load_config
from hermes_cli.colors import Colors as _Colors
# Parse arg
arg = ""
try:
parts = (cmd_original or "").strip().split(None, 1)
if len(parts) > 1:
arg = parts[1].strip().lower()
except Exception:
arg = ""
cfg = load_config() or {}
footer_cfg = ((cfg.get("display") or {}).get("runtime_footer") or {})
current = bool(footer_cfg.get("enabled", False))
fields = footer_cfg.get("fields") or ["model", "context_pct", "cwd"]
if arg in ("status", "?"):
state = "ON" if current else "OFF"
_cprint(
f" {_Colors.BOLD}Runtime footer:{_Colors.RESET} {state}\n"
f" Fields: {', '.join(fields)}"
)
return
if arg in ("on", "enable", "true", "1"):
new_state = True
elif arg in ("off", "disable", "false", "0"):
new_state = False
elif arg == "":
new_state = not current
else:
_cprint(" Usage: /footer [on|off|status]")
return
if save_config_value("display.runtime_footer.enabled", new_state):
state = (
f"{_Colors.GREEN}ON{_Colors.RESET}" if new_state
else f"{_Colors.DIM}OFF{_Colors.RESET}"
)
_cprint(f" Runtime footer: {state}")
else:
_cprint(" Failed to save runtime_footer setting to config.yaml")
def _toggle_verbose(self): def _toggle_verbose(self):
"""Cycle tool progress mode: off → new → all → verbose → off.""" """Cycle tool progress mode: off → new → all → verbose → off."""
cycle = ["off", "new", "all", "verbose"] cycle = ["off", "new", "all", "verbose"]

View File

@ -3905,6 +3905,8 @@ class GatewayRunner:
return await self._handle_yolo_command(event) return await self._handle_yolo_command(event)
if _cmd_def_inner.name == "verbose": if _cmd_def_inner.name == "verbose":
return await self._handle_verbose_command(event) return await self._handle_verbose_command(event)
if _cmd_def_inner.name == "footer":
return await self._handle_footer_command(event)
# Gateway-handled info/control commands with dedicated # Gateway-handled info/control commands with dedicated
# running-agent handlers. # running-agent handlers.
@ -4125,6 +4127,9 @@ class GatewayRunner:
if canonical == "verbose": if canonical == "verbose":
return await self._handle_verbose_command(event) return await self._handle_verbose_command(event)
if canonical == "footer":
return await self._handle_footer_command(event)
if canonical == "yolo": if canonical == "yolo":
return await self._handle_yolo_command(event) return await self._handle_yolo_command(event)
@ -5224,6 +5229,27 @@ class GatewayRunner:
display_reasoning = last_reasoning.strip() display_reasoning = last_reasoning.strip()
response = f"💭 **Reasoning:**\n```\n{display_reasoning}\n```\n\n{response}" response = f"💭 **Reasoning:**\n```\n{display_reasoning}\n```\n\n{response}"
# Runtime-metadata footer — only on the FINAL message of the turn.
# Off by default (display.runtime_footer.enabled=false). When
# streaming already delivered the body, we can't mutate the sent
# text, so we fire a separate trailing send below.
_footer_line = ""
try:
from gateway.runtime_footer import build_footer_line as _bfl
_footer_line = _bfl(
user_config=_load_gateway_config(),
platform_key=_platform_config_key(source.platform),
model=agent_result.get("model"),
context_tokens=agent_result.get("last_prompt_tokens", 0) or 0,
context_length=agent_result.get("context_length") or None,
cwd=os.environ.get("TERMINAL_CWD", ""),
)
except Exception as _footer_err:
logger.debug("runtime_footer build failed: %s", _footer_err)
_footer_line = ""
if _footer_line and response and not agent_result.get("already_sent"):
response = f"{response}\n\n{_footer_line}"
# Emit agent:end hook # Emit agent:end hook
await self.hooks.emit("agent:end", { await self.hooks.emit("agent:end", {
**hook_ctx, **hook_ctx,
@ -5394,6 +5420,17 @@ class GatewayRunner:
await self._deliver_media_from_response( await self._deliver_media_from_response(
response, event, _media_adapter, response, event, _media_adapter,
) )
# Streaming already delivered the body text, but the footer was
# intentionally held back (see the `not already_sent` gate above).
# Send it now as a small trailing message so Telegram/Discord/etc.
# still surface the runtime metadata on the final reply.
if _footer_line:
try:
_foot_adapter = self.adapters.get(source.platform)
if _foot_adapter:
await _foot_adapter.send(source.chat_id, _footer_line)
except Exception as _e:
logger.debug("trailing footer send failed: %s", _e)
return None return None
return response return response
@ -7451,6 +7488,98 @@ class GatewayRunner:
logger.warning("Failed to save tool_progress mode: %s", e) logger.warning("Failed to save tool_progress mode: %s", e)
return f"{descriptions[new_mode]}\n_(could not save to config: {e})_" return f"{descriptions[new_mode]}\n_(could not save to config: {e})_"
async def _handle_footer_command(self, event: MessageEvent) -> str:
"""Handle /footer command — toggle the runtime-metadata footer.
Usage:
/footer toggle on/off
/footer on enable globally
/footer off disable globally
/footer status show current state + fields
The footer is saved to ``display.runtime_footer.enabled`` (global).
Per-platform overrides under ``display.platforms.<platform>.runtime_footer``
are respected but not modified here edit config.yaml directly for
per-platform control.
"""
import yaml
from gateway.runtime_footer import resolve_footer_config
config_path = _hermes_home / "config.yaml"
platform_key = _platform_config_key(event.source.platform)
# --- parse argument -------------------------------------------------
arg = ""
try:
text = (getattr(event, "message", None) or "").strip()
if text.startswith("/"):
parts = text.split(None, 1)
if len(parts) > 1:
arg = parts[1].strip().lower()
except Exception:
arg = ""
# --- load config ----------------------------------------------------
user_config: dict = {}
try:
if config_path.exists():
with open(config_path, encoding="utf-8") as f:
user_config = yaml.safe_load(f) or {}
except Exception as e:
return f"⚠️ Could not read config.yaml: {e}"
effective = resolve_footer_config(user_config, platform_key)
if arg in ("status", "?"):
state = "ON" if effective["enabled"] else "OFF"
fields = ", ".join(effective.get("fields") or [])
return (
f"📎 Runtime footer: **{state}**\n"
f"Fields: `{fields}`\n"
f"Platform: `{platform_key}`"
)
if arg in ("on", "enable", "true", "1"):
new_state = True
elif arg in ("off", "disable", "false", "0"):
new_state = False
elif arg == "":
new_state = not effective["enabled"]
else:
return "Usage: `/footer [on|off|status]`"
# --- write global flag ---------------------------------------------
try:
if not isinstance(user_config.get("display"), dict):
user_config["display"] = {}
display = user_config["display"]
if not isinstance(display.get("runtime_footer"), dict):
display["runtime_footer"] = {}
display["runtime_footer"]["enabled"] = new_state
atomic_yaml_write(config_path, user_config)
except Exception as e:
logger.warning("Failed to save runtime_footer.enabled: %s", e)
return f"⚠️ Could not save config: {e}"
state = "ON" if new_state else "OFF"
example = ""
if new_state:
# Show a preview using current agent state if available.
from gateway.runtime_footer import format_runtime_footer
preview = format_runtime_footer(
model=_resolve_gateway_model(user_config) or None,
context_tokens=0,
context_length=None,
fields=effective.get("fields") or ["model", "context_pct", "cwd"],
)
if preview:
example = f"\nExample: `{preview}`"
return (
f"📎 Runtime footer: **{state}**"
f"{example}\n"
f"_(saved globally — takes effect on next message)_"
)
async def _handle_compress_command(self, event: MessageEvent) -> str: async def _handle_compress_command(self, event: MessageEvent) -> str:
"""Handle /compress command -- manually compress conversation context. """Handle /compress command -- manually compress conversation context.
@ -10810,11 +10939,13 @@ class GatewayRunner:
_last_prompt_toks = 0 _last_prompt_toks = 0
_input_toks = 0 _input_toks = 0
_output_toks = 0 _output_toks = 0
_context_length = 0
_agent = agent_holder[0] _agent = agent_holder[0]
if _agent and hasattr(_agent, "context_compressor"): if _agent and hasattr(_agent, "context_compressor"):
_last_prompt_toks = getattr(_agent.context_compressor, "last_prompt_tokens", 0) _last_prompt_toks = getattr(_agent.context_compressor, "last_prompt_tokens", 0)
_input_toks = getattr(_agent, "session_prompt_tokens", 0) _input_toks = getattr(_agent, "session_prompt_tokens", 0)
_output_toks = getattr(_agent, "session_completion_tokens", 0) _output_toks = getattr(_agent, "session_completion_tokens", 0)
_context_length = getattr(_agent.context_compressor, "context_length", 0) or 0
_resolved_model = getattr(_agent, "model", None) if _agent else None _resolved_model = getattr(_agent, "model", None) if _agent else None
if not final_response: if not final_response:
@ -10831,6 +10962,7 @@ class GatewayRunner:
"input_tokens": _input_toks, "input_tokens": _input_toks,
"output_tokens": _output_toks, "output_tokens": _output_toks,
"model": _resolved_model, "model": _resolved_model,
"context_length": _context_length,
} }
# Scan tool results for MEDIA:<path> tags that need to be delivered # Scan tool results for MEDIA:<path> tags that need to be delivered
@ -10935,6 +11067,7 @@ class GatewayRunner:
"input_tokens": _input_toks, "input_tokens": _input_toks,
"output_tokens": _output_toks, "output_tokens": _output_toks,
"model": _resolved_model, "model": _resolved_model,
"context_length": _context_length,
"session_id": effective_session_id, "session_id": effective_session_id,
"response_previewed": result.get("response_previewed", False), "response_previewed": result.get("response_previewed", False),
} }

150
gateway/runtime_footer.py Normal file
View File

@ -0,0 +1,150 @@
"""Gateway runtime-metadata footer.
Renders a compact footer showing runtime state (model, context %, cwd) and
appends it to the FINAL message of an agent turn when enabled. Off by default
to keep replies minimal.
Config (``~/.hermes/config.yaml``)::
display:
runtime_footer:
enabled: true # off by default
fields: [model, context_pct, cwd] # order shown; drop any to hide
Per-platform overrides live under ``display.platforms.<platform>.runtime_footer``.
Users can toggle the global setting with ``/footer on|off`` from both the CLI
and any gateway platform.
The footer is appended to the final response text in ``gateway/run.py`` right
before returning the response to the adapter send path so it only lands on
the final message a user sees, not on tool-progress updates or streaming
partials. When streaming is on and the final text has already been delivered
piecemeal, the footer is sent as a separate trailing message via
``send_trailing_footer()``.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import Any, Iterable, Optional
_DEFAULT_FIELDS: tuple[str, ...] = ("model", "context_pct", "cwd")
_SEP = " · "
def _home_relative_cwd(cwd: str) -> str:
"""Return *cwd* with ``$HOME`` collapsed to ``~``. Empty string if unset."""
if not cwd:
return ""
try:
home = os.path.expanduser("~")
p = os.path.abspath(cwd)
if home and (p == home or p.startswith(home + os.sep)):
return "~" + p[len(home):]
return p
except Exception:
return cwd
def _model_short(model: Optional[str]) -> str:
"""Drop ``vendor/`` prefix for readability (``openai/gpt-5.4`` → ``gpt-5.4``)."""
if not model:
return ""
return model.rsplit("/", 1)[-1]
def resolve_footer_config(
user_config: dict[str, Any] | None,
platform_key: str | None = None,
) -> dict[str, Any]:
"""Resolve effective runtime-footer config for *platform_key*.
Merge order (later wins):
1. Built-in defaults (enabled=False)
2. ``display.runtime_footer``
3. ``display.platforms.<platform_key>.runtime_footer``
"""
resolved = {"enabled": False, "fields": list(_DEFAULT_FIELDS)}
cfg = (user_config or {}).get("display") or {}
global_cfg = cfg.get("runtime_footer")
if isinstance(global_cfg, dict):
if "enabled" in global_cfg:
resolved["enabled"] = bool(global_cfg.get("enabled"))
if isinstance(global_cfg.get("fields"), list) and global_cfg["fields"]:
resolved["fields"] = [str(f) for f in global_cfg["fields"]]
if platform_key:
platforms = cfg.get("platforms") or {}
plat_cfg = platforms.get(platform_key)
if isinstance(plat_cfg, dict):
plat_footer = plat_cfg.get("runtime_footer")
if isinstance(plat_footer, dict):
if "enabled" in plat_footer:
resolved["enabled"] = bool(plat_footer.get("enabled"))
if isinstance(plat_footer.get("fields"), list) and plat_footer["fields"]:
resolved["fields"] = [str(f) for f in plat_footer["fields"]]
return resolved
def format_runtime_footer(
*,
model: Optional[str],
context_tokens: int,
context_length: Optional[int],
cwd: Optional[str] = None,
fields: Iterable[str] = _DEFAULT_FIELDS,
) -> str:
"""Render the footer line, or return "" if no fields have data.
Fields are skipped silently when their underlying data is missing a
partially-populated footer is better than a line with ``?%`` or empty slots.
"""
parts: list[str] = []
for field in fields:
if field == "model":
m = _model_short(model)
if m:
parts.append(m)
elif field == "context_pct":
if context_length and context_length > 0 and context_tokens >= 0:
pct = max(0, min(100, round((context_tokens / context_length) * 100)))
parts.append(f"{pct}%")
elif field == "cwd":
rel = _home_relative_cwd(cwd or os.environ.get("TERMINAL_CWD", ""))
if rel:
parts.append(rel)
# Unknown field names are silently ignored.
if not parts:
return ""
return _SEP.join(parts)
def build_footer_line(
*,
user_config: dict[str, Any] | None,
platform_key: str | None,
model: Optional[str],
context_tokens: int,
context_length: Optional[int],
cwd: Optional[str] = None,
) -> str:
"""Top-level entry point used by gateway/run.py.
Returns the footer text (empty string when disabled or no data). Callers
append this to the final response themselves, preserving a single blank
line of separation.
"""
cfg = resolve_footer_config(user_config, platform_key)
if not cfg.get("enabled"):
return ""
return format_runtime_footer(
model=model,
context_tokens=context_tokens,
context_length=context_length,
cwd=cwd,
fields=cfg.get("fields") or _DEFAULT_FIELDS,
)

View File

@ -115,6 +115,9 @@ COMMAND_REGISTRY: list[CommandDef] = [
CommandDef("verbose", "Cycle tool progress display: off -> new -> all -> verbose", CommandDef("verbose", "Cycle tool progress display: off -> new -> all -> verbose",
"Configuration", cli_only=True, "Configuration", cli_only=True,
gateway_config_gate="display.tool_progress_command"), gateway_config_gate="display.tool_progress_command"),
CommandDef("footer", "Toggle gateway runtime-metadata footer on final replies",
"Configuration", args_hint="[on|off|status]",
subcommands=("on", "off", "status")),
CommandDef("yolo", "Toggle YOLO mode (skip all dangerous command approvals)", CommandDef("yolo", "Toggle YOLO mode (skip all dangerous command approvals)",
"Configuration"), "Configuration"),
CommandDef("reasoning", "Manage reasoning effort and display", "Configuration", CommandDef("reasoning", "Manage reasoning effort and display", "Configuration",

View File

@ -707,6 +707,14 @@ DEFAULT_CONFIG = {
"tool_progress_overrides": {}, # DEPRECATED — use display.platforms instead "tool_progress_overrides": {}, # DEPRECATED — use display.platforms instead
"tool_preview_length": 0, # Max chars for tool call previews (0 = no limit, show full paths/commands) "tool_preview_length": 0, # Max chars for tool call previews (0 = no limit, show full paths/commands)
"platforms": {}, # Per-platform display overrides: {"telegram": {"tool_progress": "all"}, "slack": {"tool_progress": "off"}} "platforms": {}, # Per-platform display overrides: {"telegram": {"tool_progress": "all"}, "slack": {"tool_progress": "off"}}
# Gateway runtime-metadata footer appended to the FINAL message of a turn
# (disabled by default to keep replies minimal). When enabled, renders
# e.g. `model · 68% · ~/projects/hermes`. Per-platform overrides go under
# display.platforms.<platform>.runtime_footer.
"runtime_footer": {
"enabled": False,
"fields": ["model", "context_pct", "cwd"], # Order shown; drop any to hide
},
}, },
# Web dashboard settings # Web dashboard settings

View File

@ -0,0 +1,262 @@
"""Unit tests for gateway.runtime_footer — the opt-in runtime-metadata footer
appended to final gateway replies."""
from __future__ import annotations
import os
import pytest
from gateway.runtime_footer import (
_home_relative_cwd,
_model_short,
build_footer_line,
format_runtime_footer,
resolve_footer_config,
)
# ---------------------------------------------------------------------------
# _model_short + _home_relative_cwd
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"model,expected",
[
("openai/gpt-5.4", "gpt-5.4"),
("anthropic/claude-sonnet-4.6", "claude-sonnet-4.6"),
("gpt-5.4", "gpt-5.4"),
("", ""),
(None, ""),
],
)
def test_model_short_drops_vendor_prefix(model, expected):
assert _model_short(model) == expected
def test_home_relative_cwd_collapses_home(tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path))
sub = tmp_path / "projects" / "hermes"
sub.mkdir(parents=True)
result = _home_relative_cwd(str(sub))
assert result == "~/projects/hermes"
def test_home_relative_cwd_leaves_abs_path_alone(tmp_path, monkeypatch):
monkeypatch.setenv("HOME", str(tmp_path / "other"))
result = _home_relative_cwd(str(tmp_path / "outside" / "dir"))
assert result == str(tmp_path / "outside" / "dir")
def test_home_relative_cwd_empty_returns_empty():
assert _home_relative_cwd("") == ""
# ---------------------------------------------------------------------------
# format_runtime_footer
# ---------------------------------------------------------------------------
def test_format_footer_all_fields(monkeypatch, tmp_path):
monkeypatch.setenv("HOME", str(tmp_path))
monkeypatch.setenv("TERMINAL_CWD", str(tmp_path / "projects" / "hermes"))
(tmp_path / "projects" / "hermes").mkdir(parents=True)
out = format_runtime_footer(
model="openrouter/openai/gpt-5.4",
context_tokens=68000,
context_length=100000,
cwd=None, # falls back to TERMINAL_CWD env var
fields=("model", "context_pct", "cwd"),
)
assert out == "gpt-5.4 · 68% · ~/projects/hermes"
def test_format_footer_skips_missing_context_length():
out = format_runtime_footer(
model="openai/gpt-5.4",
context_tokens=500,
context_length=None,
cwd="/tmp/wd",
fields=("model", "context_pct", "cwd"),
)
# context_pct dropped silently; no "?%" artifact
assert "%" not in out
assert "gpt-5.4" in out
assert "/tmp/wd" in out
def test_format_footer_context_pct_clamped_to_100():
out = format_runtime_footer(
model="m",
context_tokens=500_000, # way over
context_length=100_000,
cwd="",
fields=("context_pct",),
)
assert out == "100%"
def test_format_footer_context_pct_never_negative():
out = format_runtime_footer(
model="m",
context_tokens=-50,
context_length=100,
cwd="",
fields=("context_pct",),
)
# Negative input => no field emitted (we require context_tokens >= 0)
assert out == ""
def test_format_footer_empty_fields_returns_empty():
out = format_runtime_footer(
model="m", context_tokens=0, context_length=100,
cwd="/x", fields=(),
)
assert out == ""
def test_format_footer_drops_cwd_when_empty(monkeypatch):
monkeypatch.delenv("TERMINAL_CWD", raising=False)
out = format_runtime_footer(
model="openai/gpt-5.4",
context_tokens=50, context_length=100,
cwd="",
fields=("model", "context_pct", "cwd"),
)
# cwd silently dropped; model + pct remain
assert out == "gpt-5.4 · 50%"
def test_format_footer_custom_field_order():
out = format_runtime_footer(
model="openai/gpt-5.4",
context_tokens=50, context_length=100,
cwd="/opt/project",
fields=("context_pct", "model"), # swapped + no cwd
)
assert out == "50% · gpt-5.4"
def test_format_footer_unknown_field_silently_ignored():
out = format_runtime_footer(
model="openai/gpt-5.4",
context_tokens=50, context_length=100,
cwd="/x",
fields=("model", "bogus", "context_pct"),
)
assert out == "gpt-5.4 · 50%"
# ---------------------------------------------------------------------------
# resolve_footer_config
# ---------------------------------------------------------------------------
def test_resolve_defaults_off_empty_config():
cfg = resolve_footer_config({}, "telegram")
assert cfg == {"enabled": False, "fields": ["model", "context_pct", "cwd"]}
def test_resolve_global_enable():
user = {"display": {"runtime_footer": {"enabled": True}}}
cfg = resolve_footer_config(user, "telegram")
assert cfg["enabled"] is True
assert cfg["fields"] == ["model", "context_pct", "cwd"]
def test_resolve_platform_override_wins():
user = {
"display": {
"runtime_footer": {"enabled": True, "fields": ["model"]},
"platforms": {
"slack": {"runtime_footer": {"enabled": False}},
},
},
}
# Telegram picks up the global enable
assert resolve_footer_config(user, "telegram")["enabled"] is True
# Slack overrides to off
assert resolve_footer_config(user, "slack")["enabled"] is False
def test_resolve_platform_can_add_fields_only():
user = {
"display": {
"runtime_footer": {"enabled": True},
"platforms": {
"discord": {"runtime_footer": {"fields": ["context_pct"]}},
},
},
}
tg = resolve_footer_config(user, "telegram")
assert tg["enabled"] is True
assert tg["fields"] == ["model", "context_pct", "cwd"]
dc = resolve_footer_config(user, "discord")
assert dc["enabled"] is True
assert dc["fields"] == ["context_pct"]
def test_resolve_ignores_malformed_config():
# Non-dict runtime_footer shouldn't crash
user = {"display": {"runtime_footer": "on"}}
cfg = resolve_footer_config(user, "telegram")
assert cfg["enabled"] is False
# ---------------------------------------------------------------------------
# build_footer_line — top-level entry point used by gateway/run.py
# ---------------------------------------------------------------------------
def test_build_footer_empty_when_disabled():
out = build_footer_line(
user_config={},
platform_key="telegram",
model="openai/gpt-5.4",
context_tokens=10, context_length=100,
cwd="/tmp",
)
assert out == ""
def test_build_footer_returns_rendered_when_enabled(monkeypatch, tmp_path):
monkeypatch.setenv("HOME", str(tmp_path))
out = build_footer_line(
user_config={"display": {"runtime_footer": {"enabled": True}}},
platform_key="telegram",
model="openai/gpt-5.4",
context_tokens=25, context_length=100,
cwd=str(tmp_path / "proj"),
)
(tmp_path / "proj").mkdir(exist_ok=True)
assert "gpt-5.4" in out
assert "25%" in out
def test_build_footer_per_platform_off_suppresses():
user = {
"display": {
"runtime_footer": {"enabled": True},
"platforms": {"slack": {"runtime_footer": {"enabled": False}}},
},
}
out = build_footer_line(
user_config=user,
platform_key="slack",
model="openai/gpt-5.4",
context_tokens=10, context_length=100,
cwd="/tmp",
)
assert out == ""
def test_build_footer_no_data_returns_empty_even_when_enabled():
# Enabled, but context_length is None AND cwd empty AND model empty ⇒ no fields
out = build_footer_line(
user_config={"display": {"runtime_footer": {"enabled": True}}},
platform_key="telegram",
model="",
context_tokens=0, context_length=None,
cwd="",
)
# With no TERMINAL_CWD env either
if not os.environ.get("TERMINAL_CWD"):
assert out == ""