mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
* [haik]: ckpt, added in the test runner skelton i made in another repo -> still gotta make tweaks to fold it onto the current repo * [haik]: restructure backend test layout: move 37 test files from backend/tests/ into backend/tests/test_cases/, add backend/tests/run.sh one-command launcher that auto-provisions both runner and test venvs with stamp-based caching, update runner config.json to point test_paths at test_cases/ and fix venv_python path, revise runner README with run.sh usage and clearer setup instructions, and gitignore the .runner-venv directory * [haik]: refactor: reorganize backend/tests/test_cases from flat structure into domain subdirectories — moved 36 test files into auth/, browser/, labeling/, service/, settings/, and web_search/ for better discoverability and grouping * [haik]: overhaul test picker UI and add post-run rerun loop: replace checkbox glyphs with fzf-style row recolouring (coral=full, lighter coral=partial) and a right-pinned selection dot, add config-driven icon tiers (nerd/emoji/unicode/ascii) with per-glyph graceful degradation, replace the modal -k keyword screen with an inline live-filtering search bar that prunes the tree on every keystroke, add warm Anthropic-dark coral theme, toolbar flag chips replacing the old status line, and a floating help badge overlay; add rerun_prompt.py with inline Textual pill prompt (rerun all/failed/passed/exit) shown after each TTY run, wire it into main.py as a post-run loop; change run_tests to return (exit_code, RunSummary) tracking collected/passed/failed node IDs via new Dashboard methods; add icons field to config.py and config.json (set to nerd), document icon tiers and Nerd Font setup in README, set Hack Nerd Font in .vscode/settings.json, add .runner-venv to linter excludes
209 lines
7.7 KiB
Python
209 lines
7.7 KiB
Python
"""Post-run action prompt with sleek, focusable pills.
|
|
|
|
Shown on a TTY after every run. Drops a compact bubble inline at the bottom of
|
|
the terminal (Textual owns the mouse + keyboard) offering four actions: rerun
|
|
all, rerun failed, rerun passed, or exit. Inline mode keeps the run's scrollback
|
|
above it instead of taking over the whole screen.
|
|
|
|
Navigate the pills with the arrow keys and press enter to choose, click a pill,
|
|
or hit its letter shortcut. Pills whose set is empty (no failures, no passes)
|
|
are dimmed and skipped. Styling mirrors the test picker so the surfaces read as
|
|
one tool. Kept separate from ``run.py`` so the Rich run path stays Textual-free.
|
|
|
|
``prompt_actions(...)`` returns the node IDs to rerun, or None to exit.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from rich.text import Text
|
|
from textual.app import App, ComposeResult
|
|
from textual.binding import Binding
|
|
from textual.containers import Horizontal, Vertical
|
|
from textual.screen import Screen
|
|
from textual.widgets import Static
|
|
|
|
from tests.runner.picker import _THEME
|
|
|
|
# Keep the inline bubble small: list only a few failures, the rest collapse into
|
|
# an "and N more" line. The pill counts still reflect the true totals.
|
|
_MAX_LISTED = 6
|
|
|
|
# On-brand accents for the failure list (theme vars aren't reachable from Rich
|
|
# markup, so mirror the picker palette here): coral mark + warm-grey path.
|
|
_MARK = "#d97757" # primary coral
|
|
_PATH = "#b0aea6" # secondary warm grey
|
|
|
|
|
|
class Pill(Static):
|
|
"""A compact, focusable, clickable action pill.
|
|
|
|
Mirrors the picker's chip look but joins the focus ring so the arrow keys
|
|
can move between pills; ``enter`` (or a click) fires the screen's choose.
|
|
Empty actions are passed ``enabled=False`` -> disabled, dimmed, and skipped
|
|
by focus navigation.
|
|
"""
|
|
|
|
def __init__(self, key_hint: str, result: str, label: str, *, enabled: bool,
|
|
primary: bool = False, id: str | None = None) -> None:
|
|
cls = "pill" + (" primary" if primary else "")
|
|
super().__init__(id=id, classes=cls)
|
|
self.key_hint = key_hint
|
|
self.result = result
|
|
self.label_text = label
|
|
self.can_focus = enabled
|
|
self.disabled = not enabled
|
|
|
|
def render(self) -> Text:
|
|
return Text.assemble((f"{self.key_hint} ", "bold"), (self.label_text, ""))
|
|
|
|
def on_click(self) -> None:
|
|
if not self.disabled:
|
|
self.screen.choose(self.result)
|
|
|
|
|
|
class ActionScreen(Screen):
|
|
CSS = """
|
|
ActionScreen { height: auto; }
|
|
#bubble {
|
|
height: auto;
|
|
width: auto;
|
|
max-width: 100%;
|
|
margin: 0 1;
|
|
padding: 0 1;
|
|
background: $panel;
|
|
border: round $primary;
|
|
border-title-align: left;
|
|
}
|
|
#failbox { height: auto; padding: 0 1; }
|
|
#pills { height: auto; }
|
|
.pill {
|
|
width: auto;
|
|
height: 1;
|
|
margin: 1 1 0 0;
|
|
padding: 0 1;
|
|
color: $text-muted;
|
|
background: $surface-lighten-1;
|
|
}
|
|
.pill:focus { background: $primary; color: $background; text-style: bold; }
|
|
.pill:hover { background: $accent; color: $background; text-style: bold; }
|
|
.pill:disabled { color: $text-disabled; background: $surface; text-opacity: 60%; }
|
|
#hint {
|
|
height: 1;
|
|
margin: 1 0 0 1;
|
|
color: $text-muted;
|
|
text-style: italic;
|
|
}
|
|
"""
|
|
|
|
BINDINGS = [
|
|
Binding("left", "move(-1)", "Prev", show=False),
|
|
Binding("up", "move(-1)", "Prev", show=False),
|
|
Binding("right", "move(1)", "Next", show=False),
|
|
Binding("down", "move(1)", "Next", show=False),
|
|
Binding("enter", "activate", "Select", priority=True),
|
|
Binding("a", "pick('all')", "Rerun all", show=False),
|
|
Binding("f", "pick('failed')", "Rerun failed", show=False),
|
|
Binding("s", "pick('passed')", "Rerun passed", show=False),
|
|
Binding("q", "pick('exit')", "Exit", show=False, priority=True),
|
|
Binding("escape", "pick('exit')", "Exit", show=False, priority=True),
|
|
]
|
|
|
|
def __init__(self, all_ids: list[str], failed_ids: list[str],
|
|
passed_ids: list[str]) -> None:
|
|
super().__init__()
|
|
self._all = all_ids
|
|
self._failed = failed_ids
|
|
self._passed = passed_ids
|
|
self._enabled = {
|
|
"all": bool(all_ids),
|
|
"failed": bool(failed_ids),
|
|
"passed": bool(passed_ids),
|
|
"exit": True,
|
|
}
|
|
|
|
def compose(self) -> ComposeResult:
|
|
failed = self._failed
|
|
body = Text()
|
|
listed = failed[:_MAX_LISTED]
|
|
for i, nid in enumerate(listed):
|
|
body.append("\u2717 ", style=_MARK)
|
|
body.append(nid, style=_PATH)
|
|
if i < len(listed) - 1:
|
|
body.append("\n")
|
|
hidden = len(failed) - _MAX_LISTED
|
|
if hidden > 0:
|
|
body.append(f"\n\u2026 and {hidden} more", style="dim italic")
|
|
with Vertical(id="bubble"):
|
|
if failed:
|
|
yield Static(body, id="failbox")
|
|
with Horizontal(id="pills"):
|
|
yield Pill("a", "all", f"rerun all ({len(self._all)})",
|
|
enabled=self._enabled["all"], id="all")
|
|
yield Pill("f", "failed", f"rerun failed ({len(self._failed)})",
|
|
enabled=self._enabled["failed"], id="failed")
|
|
yield Pill("s", "passed", f"rerun passed ({len(self._passed)})",
|
|
enabled=self._enabled["passed"], id="passed")
|
|
yield Pill("q", "exit", "exit", enabled=True, id="exit")
|
|
yield Static("\u2190/\u2192 move \u00b7 enter select \u00b7 esc exit", id="hint")
|
|
|
|
def on_mount(self) -> None:
|
|
total, npass, nfail = len(self._all), len(self._passed), len(self._failed)
|
|
self.query_one("#bubble", Vertical).border_title = (
|
|
f"\U0001f4a1 {total} run \u00b7 {npass} passed \u00b7 {nfail} failed"
|
|
)
|
|
# Land focus on the most useful default: failures if any, else rerun-all.
|
|
default = "failed" if self._failed else "all"
|
|
self.query_one(f"#{default}", Pill).focus()
|
|
|
|
def action_move(self, delta: int) -> None:
|
|
# Step focus between pills; disabled (empty) pills are skipped because
|
|
# they're not in the focus chain. Wraps at the ends.
|
|
if delta < 0:
|
|
self.focus_previous()
|
|
else:
|
|
self.focus_next()
|
|
|
|
def choose(self, result: str) -> None:
|
|
self.app.exit(result)
|
|
|
|
def action_pick(self, result: str) -> None:
|
|
if self._enabled.get(result, False):
|
|
self.choose(result)
|
|
|
|
def action_activate(self) -> None:
|
|
focused = self.focused
|
|
if isinstance(focused, Pill) and not focused.disabled:
|
|
self.choose(focused.result)
|
|
|
|
|
|
class ActionPrompt(App):
|
|
def __init__(self, all_ids: list[str], failed_ids: list[str],
|
|
passed_ids: list[str]) -> None:
|
|
super().__init__()
|
|
self._all = all_ids
|
|
self._failed = failed_ids
|
|
self._passed = passed_ids
|
|
|
|
def on_mount(self) -> None:
|
|
self.register_theme(_THEME)
|
|
self.theme = "picker"
|
|
self.push_screen(ActionScreen(self._all, self._failed, self._passed))
|
|
|
|
|
|
def prompt_actions(all_ids: list[str], failed_ids: list[str],
|
|
passed_ids: list[str]) -> list[str] | None:
|
|
"""Show the inline post-run prompt.
|
|
|
|
Returns the node IDs to rerun (rerun-all / failed / passed) or None to exit.
|
|
"""
|
|
if not all_ids:
|
|
return None
|
|
# inline=True renders a compact region at the bottom of the existing
|
|
# terminal instead of taking over the whole screen (the alternate buffer).
|
|
choice = ActionPrompt(all_ids, failed_ids, passed_ids).run(inline=True)
|
|
return {
|
|
"all": all_ids,
|
|
"failed": failed_ids,
|
|
"passed": passed_ids,
|
|
}.get(choice)
|