192 lines
6.7 KiB
Python
192 lines
6.7 KiB
Python
"""CLI help presentation tests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from io import StringIO
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import click
|
|
from rich.console import Console
|
|
from typer import rich_utils
|
|
from typer.testing import CliRunner
|
|
|
|
from opensquilla.cli.main import app
|
|
from opensquilla.ui import ACCENT, questionary_style
|
|
|
|
runner = CliRunner()
|
|
|
|
|
|
def test_rich_help_uses_opensquilla_accent() -> None:
|
|
assert rich_utils.STYLE_OPTIONS_PANEL_BORDER == ACCENT
|
|
assert rich_utils.STYLE_COMMANDS_PANEL_BORDER == ACCENT
|
|
assert rich_utils.STYLE_OPTION == f"bold {ACCENT}"
|
|
assert rich_utils.STYLE_COMMANDS_TABLE_FIRST_COLUMN == f"bold {ACCENT}"
|
|
|
|
|
|
def test_questionary_checkbox_selection_avoids_reverse_row_highlight() -> None:
|
|
style = questionary_style()
|
|
assert style is not None
|
|
rules = dict(style.style_rules)
|
|
|
|
for token in ("pointer", "highlighted", "selected"):
|
|
assert "noreverse" in rules[token]
|
|
assert "bg:" not in rules[token]
|
|
|
|
|
|
def test_onboard_help_keeps_router_option_readable() -> None:
|
|
result = runner.invoke(app, ["onboard", "--help"], terminal_width=100)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
output = click.unstyle(result.output)
|
|
assert "--router" in output
|
|
assert "--router MODE" in output
|
|
assert "Router profile: recommended," in output
|
|
assert "openrouter-mix, or" in output
|
|
assert "disabled." in output
|
|
assert "TEXT recommended | openrouter-mix" not in output
|
|
|
|
|
|
def test_onboard_help_uses_compact_option_columns() -> None:
|
|
result = runner.invoke(app, ["onboard", "--help"], terminal_width=100)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
output = click.unstyle(result.output)
|
|
assert "--provider TEXT" in output
|
|
assert "--router MODE" in output
|
|
assert not re.search(r"--provider\s{12,}TEXT", output)
|
|
|
|
|
|
def test_onboard_help_explains_first_run_inputs() -> None:
|
|
result = runner.invoke(app, ["onboard", "--help"], terminal_width=110)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
output = click.unstyle(result.output)
|
|
assert "OpenSquilla setup cockpit" in output
|
|
assert "SquillaRouter" in output
|
|
assert "Provider id to configure" in output
|
|
assert "Model id for the provider" in output
|
|
assert "Read the provider key from this environment" in output
|
|
assert "variable." in output
|
|
assert "Only run the wizard when required setup is" in output
|
|
assert "incomplete." in output
|
|
assert "Leave channel setup for later" in output
|
|
assert "Leave optional Web search setup for later" in output
|
|
assert "Leave optional image generation setup for later" in output
|
|
|
|
|
|
def test_configure_help_explains_section_specific_inputs() -> None:
|
|
result = runner.invoke(app, ["configure", "--help"], terminal_width=110)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
output = click.unstyle(result.output)
|
|
normalized = " ".join(output.replace("│", " ").split())
|
|
assert (
|
|
"Reconfigure provider, router, ensemble, channels, search, image generation, "
|
|
"or memory" in normalized
|
|
)
|
|
assert "Usage:" in output
|
|
assert "[SECTION]" in output
|
|
assert "SECTION_ARG" not in output
|
|
assert "Section to configure" in output
|
|
assert "Text provider id for provider setup" in normalized
|
|
assert "Search provider id" in normalized
|
|
assert "Image provider id" in normalized
|
|
assert "Model id for provider or remote memory embedding" in normalized
|
|
assert "Channel type such as slack, discord, feishu" in normalized
|
|
assert "Image model id" in normalized
|
|
assert "Memory embedding provider" in output
|
|
|
|
|
|
def test_help_theme_supports_click_make_metavar_without_context(monkeypatch) -> None:
|
|
def legacy_make_metavar(self: click.Option) -> str:
|
|
if self.is_bool_flag:
|
|
return "BOOLEAN"
|
|
return self.metavar or self.type.name.upper()
|
|
|
|
monkeypatch.setattr(click.Option, "make_metavar", legacy_make_metavar)
|
|
|
|
result = runner.invoke(app, ["onboard", "--help"], terminal_width=100)
|
|
|
|
assert result.exit_code == 0, result.output
|
|
output = click.unstyle(result.output)
|
|
assert "--provider TEXT" in output
|
|
assert "--router MODE" in output
|
|
|
|
|
|
def test_help_theme_accepts_typer_vendored_click_parameters() -> None:
|
|
option = SimpleNamespace(
|
|
param_type_name="option",
|
|
name="provider",
|
|
opts=["--provider"],
|
|
secondary_opts=[],
|
|
metavar=None,
|
|
type=SimpleNamespace(name="text"),
|
|
required=False,
|
|
help="Provider id to configure.",
|
|
)
|
|
argument = SimpleNamespace(
|
|
param_type_name="argument",
|
|
name="section_arg",
|
|
opts=["section_arg"],
|
|
secondary_opts=[],
|
|
metavar="SECTION",
|
|
type=SimpleNamespace(name="text"),
|
|
required=False,
|
|
help="Section to configure.",
|
|
)
|
|
console = Console(file=StringIO(), force_terminal=False, color_system=None)
|
|
|
|
rich_utils._print_options_panel(
|
|
name="Options",
|
|
params=[option, argument],
|
|
ctx=click.Context(click.Command("demo")),
|
|
markup_mode="rich",
|
|
console=console,
|
|
)
|
|
|
|
output = click.unstyle(console.file.getvalue())
|
|
assert "--provider TEXT" in output
|
|
assert "SECTION" in output
|
|
|
|
|
|
def test_cli_brand_surfaces_do_not_use_cyan() -> None:
|
|
cli_files = [*Path("src/opensquilla/cli").rglob("*.py"), Path("src/opensquilla/ui.py")]
|
|
forbidden = (
|
|
"bold cyan",
|
|
"[cyan]",
|
|
"[/cyan]",
|
|
"typer.colors.CYAN",
|
|
'style="cyan"',
|
|
)
|
|
|
|
offenders: list[str] = []
|
|
for path in cli_files:
|
|
text = path.read_text(encoding="utf-8")
|
|
for needle in forbidden:
|
|
if needle in text:
|
|
offenders.append(f"{path}:{needle}")
|
|
|
|
assert offenders == []
|
|
|
|
|
|
def test_opentui_theme_accents_are_not_cyan() -> None:
|
|
# The brand's no-cyan rule is about the ACCENT, not secondary semantics: every
|
|
# OpenTUI theme's brand accent must be warm/orange, never cyan. (OpenSquilla's
|
|
# own --info token is cyan-ish #56C2E6 and is allowed for the route/info role.)
|
|
theme_mjs = Path("src/opensquilla/cli/tui/opentui/package/src/theme.mjs").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
block = theme_mjs.split("PALETTES = Object.freeze({", 1)[1].split("});", 1)[0]
|
|
accents = re.findall(r"\baccent(?:Secondary)?:\s*\"(#[0-9a-fA-F]{6})\"", block)
|
|
|
|
def is_cyan(hex_value: str) -> bool:
|
|
h = hex_value.lstrip("#")
|
|
r, g, b = (int(h[i : i + 2], 16) for i in (0, 2, 4))
|
|
return r < 0x80 and g > 0xB0 and b > 0xB0 and abs(g - b) < 0x40
|
|
|
|
assert accents, "could not parse theme accents from theme.mjs"
|
|
offenders = [hex_value for hex_value in accents if is_cyan(hex_value)]
|
|
assert offenders == [], f"theme brand accent must not be cyan: {offenders}"
|