242 lines
11 KiB
Python
242 lines
11 KiB
Python
"""The tab title carries how many agents are working (task 39).
|
|||
|
|
|
||
|
|
A running agent was visible on the board and nowhere else, so the moment
|
||
|
|
you switched tabs — the normal thing to do while an agent works for
|
||
|
|
minutes — bench said the same string whether three agents ran or none.
|
||
|
|
The count now leads the title, ahead of the project, because a truncated
|
||
|
|
tab must still show it.
|
||
|
|
|
||
|
|
Two halves, as elsewhere for board.html. The behaviour — what the string
|
||
|
|
is for 0, 1 and N agents, which set is counted, and that document.title
|
||
|
|
is written only when it changed — is exercised for real: the pieces are
|
||
|
|
lifted out of the page and run in node, skipped where node is absent.
|
||
|
|
The wiring (both readers of "running" going through one function) is a
|
||
|
|
source-level invariant, board.html being a single file with inline JS and
|
||
|
|
no frontend test runner.
|
||
|
|
|
||
|
|
python3 -m unittest discover -s tests -v
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import json
|
||
|
|
import re
|
||
|
|
import shutil
|
||
|
|
import subprocess
|
||
|
|
import unittest
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
BOARD = Path(__file__).resolve().parents[1] / "manager" / "core" / "board.html"
|
||
|
|
|
||
|
|
# The pieces the title is made of, lifted from the page as written.
|
||
|
|
PARTS = (
|
||
|
|
r"function runningAgents\(\) \{.*?\n\}",
|
||
|
|
r"const VIEW_TITLES = \{[^\n]*\};",
|
||
|
|
r"const WORKING_MARK = '[^']+';",
|
||
|
|
r"function tabTitle\(project, view, working\) \{.*?\n\}",
|
||
|
|
r"let shownTitle = null;",
|
||
|
|
r"function renderTitle\(\) \{.*?\n\}",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _harness() -> str:
|
||
|
|
html = BOARD.read_text(encoding="utf-8")
|
||
|
|
out = []
|
||
|
|
for pattern in PARTS:
|
||
|
|
m = re.search(pattern, html, re.S)
|
||
|
|
if m is None:
|
||
|
|
raise AssertionError(f"board.html no longer defines {pattern!r}")
|
||
|
|
out.append(m.group(0))
|
||
|
|
return "\n".join(out)
|
||
|
|
|
||
|
|
|
||
|
|
def _running(n: int) -> list:
|
||
|
|
"""n agent records the board would call running, plus noise that is not:
|
||
|
|
a finished run and a failed one."""
|
||
|
|
agents = [{"status": "running", "started": 1000 + i} for i in range(n)]
|
||
|
|
return agents + [{"status": "done"}, {"status": "failed"}]
|
||
|
|
|
||
|
|
|
||
|
|
class TitleBehaviour(unittest.TestCase):
|
||
|
|
"""What the tab actually says, run as the browser would run it."""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls):
|
||
|
|
cls.node = shutil.which("node")
|
||
|
|
if not cls.node:
|
||
|
|
raise unittest.SkipTest("node not available — tab title unrun")
|
||
|
|
cls.src = _harness()
|
||
|
|
|
||
|
|
def titles(self, frames: list) -> list:
|
||
|
|
"""document.title after each frame — a frame being the state and
|
||
|
|
view the page would render — with every write recorded, so a title
|
||
|
|
assigned twice with the same string shows up as two entries."""
|
||
|
|
script = (self.src + """
|
||
|
|
var writes = [];
|
||
|
|
var document = { set title(v) { writes.push(v); }, get title() {
|
||
|
|
return writes.length ? writes[writes.length - 1] : null; } };
|
||
|
|
var S = {};
|
||
|
|
for (const frame of """ + json.dumps(frames) + """) {
|
||
|
|
S = { state: frame.state, view: frame.view || 'board' };
|
||
|
|
renderTitle();
|
||
|
|
}
|
||
|
|
console.log(JSON.stringify(writes));
|
||
|
|
""")
|
||
|
|
out = subprocess.run([self.node, "-e", script],
|
||
|
|
capture_output=True, text=True)
|
||
|
|
self.assertEqual(out.returncode, 0, out.stderr)
|
||
|
|
return json.loads(out.stdout)
|
||
|
|
|
||
|
|
def title(self, state: dict, view: str = "board") -> str:
|
||
|
|
"""The one title a single frame produces."""
|
||
|
|
writes = self.titles([{"state": state, "view": view}])
|
||
|
|
self.assertEqual(len(writes), 1, f"expected one write, got {writes}")
|
||
|
|
return writes[0]
|
||
|
|
|
||
|
|
def test_a_quiet_board_is_the_plain_title(self):
|
||
|
|
"""Byte for byte what it was before this existed — no prefix, no
|
||
|
|
separator, no space. A board with nothing running looks untouched."""
|
||
|
|
self.assertEqual(self.title({"project": "bench", "agents": []}),
|
||
|
|
"bench · bench")
|
||
|
|
self.assertEqual(
|
||
|
|
self.title({"project": "bench", "agents": _running(0)}),
|
||
|
|
"bench · bench")
|
||
|
|
self.assertEqual(self.title({"project": "bench"}), "bench · bench")
|
||
|
|
|
||
|
|
def test_the_count_leads_the_title(self):
|
||
|
|
"""Ahead of the project, because a tab truncated to a few characters
|
||
|
|
must still answer the question this card exists for."""
|
||
|
|
title = self.title({"project": "bench", "agents": _running(3)})
|
||
|
|
self.assertTrue(title.startswith("3"),
|
||
|
|
f"the count must lead the title, got {title!r}")
|
||
|
|
self.assertEqual(title, "3◌ · bench · bench")
|
||
|
|
|
||
|
|
def test_one_agent_reads_as_one(self):
|
||
|
|
"""A count and a mark rather than a word, so "1 agents" is not a
|
||
|
|
string this code can produce."""
|
||
|
|
title = self.title({"project": "bench", "agents": _running(1)})
|
||
|
|
self.assertEqual(title, "1◌ · bench · bench")
|
||
|
|
self.assertNotIn("agents", title)
|
||
|
|
|
||
|
|
def test_only_running_agents_are_counted(self):
|
||
|
|
"""The same filter the header chip applies — see the wiring test
|
||
|
|
below for why they cannot drift apart."""
|
||
|
|
state = {"project": "bench",
|
||
|
|
"agents": [{"status": "done"}, {"status": "failed"},
|
||
|
|
{"status": "running"}]}
|
||
|
|
self.assertEqual(self.title(state), "1◌ · bench · bench")
|
||
|
|
|
||
|
|
def test_the_project_still_names_which_bench_this_is(self):
|
||
|
|
"""The thing the tab already did well survives the prefix."""
|
||
|
|
a = self.title({"project": "projectA", "agents": _running(2)})
|
||
|
|
b = self.title({"project": "projectB", "agents": _running(2)})
|
||
|
|
self.assertEqual(a, "2◌ · projectA · bench")
|
||
|
|
self.assertNotEqual(a, b)
|
||
|
|
|
||
|
|
def test_every_view_carries_the_count(self):
|
||
|
|
"""It describes the board, not the view, so switching views changes
|
||
|
|
only the view word."""
|
||
|
|
state = {"project": "bench", "agents": _running(2)}
|
||
|
|
self.assertEqual(
|
||
|
|
[self.title(state, v) for v in ("board", "flight", "focus")],
|
||
|
|
["2◌ · bench · bench", "2◌ · bench · sessions",
|
||
|
|
"2◌ · bench · focus"])
|
||
|
|
|
||
|
|
def test_a_finished_run_returns_the_tab_to_the_plain_title(self):
|
||
|
|
"""No reload: the frame after the last agent stops is a plain
|
||
|
|
title again."""
|
||
|
|
busy = {"project": "bench", "agents": _running(1)}
|
||
|
|
quiet = {"project": "bench", "agents": _running(0)}
|
||
|
|
self.assertEqual(
|
||
|
|
self.titles([{"state": busy}, {"state": quiet}]),
|
||
|
|
["1◌ · bench · bench", "bench · bench"])
|
||
|
|
|
||
|
|
def test_a_stateless_page_keeps_the_served_title(self):
|
||
|
|
"""Before state arrives there is nothing better to say than what the
|
||
|
|
server rendered, so nothing is written at all."""
|
||
|
|
self.assertEqual(self.titles([{"state": None}, {"state": {}}]), [])
|
||
|
|
|
||
|
|
def test_the_title_is_written_only_when_it_changed(self):
|
||
|
|
"""render() runs on every SSE frame; a burst with nothing moving
|
||
|
|
must not touch document.title once."""
|
||
|
|
busy = {"project": "bench", "agents": _running(2)}
|
||
|
|
frames = [{"state": busy} for _ in range(20)]
|
||
|
|
self.assertEqual(self.titles(frames), ["2◌ · bench · bench"])
|
||
|
|
|
||
|
|
def test_a_changed_count_is_written_again(self):
|
||
|
|
"""The cache is a cache, not a latch: every distinct title lands,
|
||
|
|
including a return to one seen before."""
|
||
|
|
def frame(n, view="board"):
|
||
|
|
return {"state": {"project": "bench", "agents": _running(n)},
|
||
|
|
"view": view}
|
||
|
|
self.assertEqual(
|
||
|
|
self.titles([frame(1), frame(1), frame(2), frame(2),
|
||
|
|
frame(2, "focus"), frame(0), frame(1)]),
|
||
|
|
["1◌ · bench · bench", "2◌ · bench · bench",
|
||
|
|
"2◌ · bench · focus", "bench · bench", "1◌ · bench · bench"])
|
||
|
|
|
||
|
|
|
||
|
|
class Wiring(unittest.TestCase):
|
||
|
|
"""board.html's own half: what keeps the tab and the header chip from
|
||
|
|
counting two different things, and the mark from becoming an emoji."""
|
||
|
|
|
||
|
|
@classmethod
|
||
|
|
def setUpClass(cls):
|
||
|
|
cls.html = BOARD.read_text(encoding="utf-8")
|
||
|
|
|
||
|
|
def test_one_filter_answers_how_many_are_running(self):
|
||
|
|
"""Both counters go through runningAgents(); a second inline filter
|
||
|
|
is how the tab and the chip would start disagreeing. (Asking which
|
||
|
|
agent holds one card is a different question and stays where it
|
||
|
|
is.)"""
|
||
|
|
filters = re.findall(r"\.filter\(a => a\.status === 'running'\)",
|
||
|
|
self.html)
|
||
|
|
self.assertEqual(len(filters), 1,
|
||
|
|
"only runningAgents() may filter for running agents, "
|
||
|
|
f"found {len(filters)} such filters")
|
||
|
|
body = re.search(r"function runningAgents\(\) \{(.*?)\n\}",
|
||
|
|
self.html, re.DOTALL)
|
||
|
|
self.assertIsNotNone(body, "board.html lost runningAgents()")
|
||
|
|
self.assertIn("a.status === 'running'", body.group(1))
|
||
|
|
|
||
|
|
def test_the_header_chip_counts_that_same_list(self):
|
||
|
|
chip = re.search(r"function renderChip\(\) \{(.*?)\n\}",
|
||
|
|
self.html, re.DOTALL)
|
||
|
|
self.assertIsNotNone(chip, "board.html lost renderChip()")
|
||
|
|
self.assertIn("runningAgents()", chip.group(1))
|
||
|
|
|
||
|
|
def test_the_tab_counts_that_same_list(self):
|
||
|
|
title = re.search(r"function renderTitle\(\) \{(.*?)\n\}",
|
||
|
|
self.html, re.DOTALL)
|
||
|
|
self.assertIsNotNone(title, "board.html lost renderTitle()")
|
||
|
|
self.assertIn("runningAgents()", title.group(1))
|
||
|
|
|
||
|
|
def test_the_mark_is_the_boards_own_in_flight_glyph(self):
|
||
|
|
"""Not an emoji: the same ◌ the CI and copilot chips wear while
|
||
|
|
something is in flight."""
|
||
|
|
mark = re.search(r"const WORKING_MARK = '([^']+)';", self.html)
|
||
|
|
self.assertIsNotNone(mark, "board.html lost WORKING_MARK")
|
||
|
|
self.assertEqual(mark.group(1), "◌")
|
||
|
|
self.assertGreater(len(re.findall("◌", self.html)), 1,
|
||
|
|
"the mark should already be in use on the chips")
|
||
|
|
|
||
|
|
def test_the_prefix_is_the_only_thing_ahead_of_the_project(self):
|
||
|
|
"""One writer, and everything it can put before the project name is
|
||
|
|
the count and the mark — no view name, no generic string."""
|
||
|
|
writes = re.findall(r"document\.title\s*=\s*([^\n;]+)", self.html)
|
||
|
|
self.assertEqual(len(writes), 1,
|
||
|
|
f"expected one document.title assignment, got {writes}")
|
||
|
|
built = re.search(r"function tabTitle\(project, view, working\) \{"
|
||
|
|
r"(.*?)\n\}", self.html, re.DOTALL)
|
||
|
|
self.assertIsNotNone(built, "board.html lost tabTitle()")
|
||
|
|
before, _, after = built.group(1).partition("project +")
|
||
|
|
self.assertTrue(after, "tabTitle must put the project in the title")
|
||
|
|
self.assertNotIn("VIEW_TITLES", before,
|
||
|
|
"the view name may never precede the project")
|
||
|
|
self.assertIn("working", before,
|
||
|
|
"only the running count precedes the project")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
unittest.main()
|