Files
bench/tests/test_board_title.py
istos 72a0d4195f board: put the running-agent count in the tab title
A running agent was visible on the board and nowhere else, so switching
tabs — the normal thing to do while one works for minutes — left bench
saying the same string whether three agents ran or none.

The count now leads the title (`2◌ · bench · bench`), ahead of even the
project: the reasoning that put the project first (truncation eats the
tail) applies harder to the one state you want from a tab you are not
looking at. The mark is the in-flight glyph the CI and copilot chips
already wear, not an emoji, and a quiet board keeps the plain title byte
for byte.

Both the tab and the header chip now count through one runningAgents(),
so they cannot disagree, and the title is written only when the string
actually changed — render() runs on every SSE frame.

Behaviour is covered in node (skipped where node is absent); the wiring
that keeps one filter and one writer is a source-level invariant, as
elsewhere for board.html.
2026-07-31 16:12:47 +02:00

174 lines
7.6 KiB
Python

"""The tab names its project (task 22): the title is "<project> · bench",
project first, so two benches side by side are told apart at tab-bar width.
Two halves are tested here. The server half — config resolving the project
name and httpd rendering it into the served page — runs in fresh
interpreters, because config reads its settings at import and BOARD_TITLE
is the thing under test. The browser half lives in board.html's inline JS
with no frontend test runner, so it is checked as source-level invariants:
the ones that, if broken, would let the tab drift back to a generic string
or put the view name before the project.
python3 -m unittest discover -s tests -v
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
CORE = REPO / "manager" / "core"
BOARD = CORE / "board.html"
# BOARD_TITLE unset in the process environment is "nothing configured":
# process env beats local/.env, so this also neutralizes a developer's own
# override leaking into the defaults test.
UNSET = {"BOARD_TITLE": ""}
def _probe(expression: str, settings: dict) -> object:
"""Evaluate an expression against config/httpd in a fresh interpreter,
with the given settings in the environment config reads at import."""
env = dict(os.environ)
env.update(UNSET)
env.update(settings)
out = subprocess.check_output(
[sys.executable, "-c",
"import sys, json; sys.path.insert(0, sys.argv[1]); "
"import config, httpd; print(json.dumps(eval(sys.argv[2])))",
str(CORE), expression],
env=env, text=True)
return json.loads(out)
def _title_of(settings: dict) -> str:
"""The <title> text of the page the board would serve."""
page = _probe("httpd.page_bytes().decode('utf-8')", settings)
match = re.search(r"<title>(.*?)</title>", page, re.DOTALL)
assert match, "the served page lost its <title>"
return match.group(1)
class ProjectName(unittest.TestCase):
def test_defaults_to_the_repo_directory_name(self):
"""The name nobody has to configure: what the checkout is called."""
self.assertEqual(_probe("config.PROJECT", {}), REPO.name)
def test_board_title_overrides_it(self):
"""For people whose checkout directories are all called "app"."""
self.assertEqual(_probe("config.PROJECT", {"BOARD_TITLE": "payments"}),
"payments")
def test_a_blank_setting_is_not_a_blank_title(self):
"""An empty or whitespace-only value means "not configured", not
"call this board nothing"."""
self.assertEqual(_probe("config.PROJECT", {"BOARD_TITLE": " "}),
REPO.name)
def test_the_state_payload_carries_it(self):
"""The browser needs it too — the view switcher rewrites the title
without refetching the page."""
self.assertEqual(_probe("httpd.state_payload()['project']",
{"BOARD_TITLE": "payments"}), "payments")
class ServedTitle(unittest.TestCase):
def test_the_title_is_rendered_into_the_page(self):
"""Server-rendered, so the tab is right on first paint rather than
flickering from generic to named on every refresh."""
self.assertEqual(_title_of({"BOARD_TITLE": "payments"}),
"payments · bench")
def test_two_projects_get_two_titles(self):
"""The whole point: distinguishable in the tab bar, in cmd-tab and
in history — and distinguishable by their *first* word."""
a = _title_of({"BOARD_TITLE": "projectA"})
b = _title_of({"BOARD_TITLE": "projectB"})
self.assertNotEqual(a, b)
self.assertTrue(a.startswith("projectA") and b.startswith("projectB"),
f"the project must lead the title, got {a!r} / {b!r}")
def test_the_default_title_names_this_checkout(self):
self.assertEqual(_title_of({}), f"{REPO.name} · bench")
def test_a_project_name_cannot_inject_markup(self):
"""The name comes from a directory or a settings file, both of which
can hold anything — it is escaped, not spliced."""
title = _title_of({"BOARD_TITLE": "<script>x</script>"})
self.assertNotIn("<script>", title)
self.assertIn("&lt;script&gt;", title)
def test_nothing_else_in_the_page_is_disturbed(self):
"""Only the title element is rewritten; the rest is the file."""
page = _probe("httpd.page_bytes().decode('utf-8')",
{"BOARD_TITLE": "payments"})
source = BOARD.read_text(encoding="utf-8")
strip = lambda s: re.sub(r"<title>.*?</title>", "", s, count=1,
flags=re.DOTALL)
self.assertEqual(strip(page), strip(source))
class PageInvariants(unittest.TestCase):
"""board.html's own half: the fallback title and the code that keeps it
in step with the view switcher."""
@classmethod
def setUpClass(cls):
cls.html = BOARD.read_text(encoding="utf-8")
def test_the_shipped_title_is_not_the_old_generic_string(self):
""""Bench — task board" in two tabs was the bug."""
self.assertNotIn("task board", self.html.lower())
def test_the_title_is_only_ever_written_from_the_project(self):
"""One writer, and the string it writes is built by tabTitle() from
the project — so no code path can revert to the generic string. That
the project leads everything but the running count (task 39) is
tested in test_tab_agents.py."""
writes = re.findall(r"document\.title\s*=\s*([^\n;]+)", self.html)
self.assertEqual(len(writes), 1,
f"expected one document.title assignment, got {writes}")
body = re.search(r"function renderTitle\(\)\s*\{(.*?)\n\}",
self.html, re.DOTALL)
self.assertIsNotNone(body, "board.html lost renderTitle()")
self.assertIn("tabTitle(S.state.project", body.group(1),
"the title must be built from the project")
def test_a_stateless_page_keeps_the_served_title(self):
"""Before the first state load there is nothing better to say than
what the server already rendered."""
body = re.search(r"function renderTitle\(\)\s*\{(.*?)\n\}",
self.html, re.DOTALL)
self.assertIsNotNone(body, "board.html lost renderTitle()")
self.assertIn("if (!S.state?.project) return;", body.group(1),
"renderTitle must bail out rather than write a "
"project-less title")
def test_every_view_has_a_tail(self):
"""The switcher may suffix, but the board view keeps the name people
bookmarked: "<project> · bench"."""
table = re.search(r"const VIEW_TITLES = \{(.*?)\};", self.html, re.DOTALL)
self.assertIsNotNone(table, "board.html lost VIEW_TITLES")
tails = dict(re.findall(r"(\w+): '([^']+)'", table.group(1)))
views = set(re.findall(r'data-view="(\w+)"', self.html))
self.assertEqual(set(tails), views,
"every view in the switcher needs a title tail")
self.assertEqual(tails["board"], "bench")
def test_the_title_follows_every_render(self):
"""render() runs on state loads and on view switches alike, so
hanging renderTitle off it covers both."""
body = re.search(r"function render\(\)\s*\{(.*?)\n\}", self.html, re.DOTALL)
self.assertIsNotNone(body, "board.html lost render()")
self.assertIn("renderTitle();", body.group(1))
if __name__ == "__main__":
unittest.main()