"""The drawer renders a wrapped list item as one item (task 41).
`md()` in board.html used to treat every *physical* line inside a block as
a unit. Task files are hard-wrapped at ~74 columns, so the second line of
an item became its own bullet, `- [ ]` rendered as a literal bracket pair,
nested lists flattened, and paragraphs kept the author's ragged edge via
` `.
board.html is a single file with inline JS and no frontend test runner, so
this suite lifts `esc()` and `md()` straight out of the page and runs them
under node — the renderer is a pure function of its input, so its actual
output is what to assert on. Node is not a dependency of bench itself, so
those checks skip when it is absent; the source-level invariants at the
bottom always run and are in the same style as the board's other
`test_*.py` checks on board.html.
python3 -m unittest discover -s tests -v
"""
from __future__ import annotations
import re
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BOARD = ROOT / "manager" / "core" / "board.html"
NODE = shutil.which("node")
HTML = BOARD.read_text(encoding="utf-8")
def lift(pattern: str, what: str) -> str:
m = re.search(pattern, HTML, re.M | re.S)
assert m, f"board.html lost {what}"
return m.group(0)
HARNESS = (
lift(r"^const esc = \(s\) =>.*?\n.*?\n", "its esc() helper")
+ lift(r"^function md\(src\) \{\n.*?\n\}\n", "its md() renderer")
+ "process.stdout.write(md(require('fs').readFileSync(0, 'utf8')));\n"
)
class RendererCase(unittest.TestCase):
"""Base: run the page's own md() over a markdown string."""
@classmethod
def setUpClass(cls):
if not NODE:
return
cls._dir = tempfile.TemporaryDirectory()
cls.js = Path(cls._dir.name) / "md.js"
cls.js.write_text(HARNESS, encoding="utf-8")
@classmethod
def tearDownClass(cls):
if NODE:
cls._dir.cleanup()
def render(self, src: str) -> str:
out = subprocess.run([NODE, str(self.js)], input=src, text=True,
capture_output=True)
self.assertEqual(out.returncode, 0, out.stderr)
return out.stdout
INNERMOST = re.compile(r"<(ul|ol)>((?:(?!<(?:ul|ol)>).)*?)\1>", re.S)
def items(self, html: str) -> list[str]:
"""The text of each top-level
: nested lists dropped, markup
stripped, so a test can assert on what the reader sees."""
body = re.sub(r"(ul|ol)>\s*$", "", re.sub(r"^\s*<(ul|ol)>", "", html))
while self.INNERMOST.search(body): # peel nested lists off
body = self.INNERMOST.sub("", body)
body = re.sub(r'', "", body) # the tick glyph
return [re.sub(r"<[^>]+>", "", li).strip()
for li in re.findall(r"
]*>(.*?)
", body, re.S)]
@unittest.skipUnless(NODE, "node is needed to run the page's own md()")
class WrappedItemsTests(RendererCase):
"""One bullet per item, however the author wrapped it."""
def test_a_wrapped_item_is_one_item(self):
"""The live bug: 'serves the built landing page' / 'over' were two
bullets because the source line broke between them."""
html = self.render(
"- Given a request for the site, when it is served, then the\n"
" worker serves the built landing page\n"
"- A second item\n")
self.assertEqual(html.count("
", html)
def test_ordered_lists_group_the_same_way(self):
html = self.render(
"1. The board creates a git worktree on a new\n"
" branch from the newest main it can see\n"
"2. The agent works in the worktree\n")
self.assertTrue(html.startswith(""), html[:40])
self.assertEqual(len(self.items(html)), 2)
@unittest.skipUnless(NODE, "node is needed to run the page's own md()")
class TaskListTests(RendererCase):
"""`- [ ]` / `- [x]` become checkboxes, and only ever glyphs."""
SRC = ("- [ ] Given an Acceptance list whose items wrap, then there\n"
" is exactly one bullet per item\n"
"- [x] Fenced code blocks are unchanged\n")
def test_no_bracket_survives_as_text(self):
html = self.render(self.SRC)
text = " ".join(self.items(html))
self.assertNotIn("[", text)
self.assertNotIn("]", text)
def test_the_item_text_survives_beside_the_box(self):
self.assertEqual(
self.items(self.render(self.SRC)),
["Given an Acceptance list whose items wrap, then there "
"is exactly one bullet per item",
"Fenced code blocks are unchanged"])
def test_ticked_and_unticked_are_distinguishable(self):
html = self.render(self.SRC)
lis = re.findall(r"
]*)>", html)
self.assertEqual(len(lis), 2)
self.assertIn('class="tick"', lis[0]) # open: neutral
self.assertIn('class="tick on"', lis[1]) # done: settled
self.assertEqual(html.count(' and never a handler: clicking it can
do nothing, so it cannot quietly edit the file."""
html = self.render(self.SRC + "\n- [X] upper case counts as ticked\n")
self.assertNotIn("]*)>", html)[-1])
def test_a_bracket_that_is_not_a_checkbox_is_left_alone(self):
items = self.items(self.render("- [see the spec](../ref.md) explains it\n"))
self.assertEqual(items, ["see the spec explains it"])
self.assertIn('href="../ref.md"', self.render(
"- [see the spec](../ref.md) explains it\n"))
@unittest.skipUnless(NODE, "node is needed to run the page's own md()")
class NestingTests(RendererCase):
"""Children indent under their parent instead of flattening beside it."""
def test_a_nested_list_is_a_child_of_its_parent_item(self):
html = self.render(
"- parent one\n"
" - child a\n"
" - child b\n"
"- parent two\n")
self.assertEqual(self.items(html), ["parent one", "parent two"])
self.assertRegex(html, r"parent one
", html)
@unittest.skipUnless(NODE, "node is needed to run the page's own md()")
class ReflowTests(RendererCase):
"""Prose wraps to the drawer, not to the author's editor."""
def test_a_paragraph_has_no_hard_break(self):
html = self.render(
"The renderer is line-based and the task files are\n"
"hard-wrapped, so almost every list on the board comes\n"
"out wrong.\n")
self.assertNotIn(" ", html)
self.assertIn("task files are hard-wrapped", html)
def test_a_blockquote_reflows_too(self):
html = self.render("> a quoted line\n> and its continuation\n"
.replace(">", ">"))
self.assertNotIn(" ", html)
self.assertIn("a quoted line and its continuation", html)
def test_no_br_survives_anywhere_in_the_corpus(self):
"""Every card on the board plus AGENTS.md: the author's wrap column
must not reach the browser."""
docs = sorted(ROOT.glob("tasks/*/*.md")) + [ROOT / "AGENTS.md"]
self.assertGreater(len(docs), 5, "no task files found to render")
for doc in docs:
with self.subTest(doc=doc.relative_to(ROOT).as_posix()):
self.assertNotIn(" ", self.render(
doc.read_text(encoding="utf-8")))
def source_bullets(src: str) -> int:
"""How many logical list items a document contains, counted from the
source the way a reader counts them: markers only, fences skipped, and
only in blocks that actually open with one."""
marker = re.compile(r"^[ \t]*(?:[-*]|\d+\.)[ \t]+")
total, fence = 0, False
for block in re.split(r"\n{2,}", src):
ticks = block.count("```")
if fence or block.startswith("```"):
fence = (ticks % 2 == 0) if fence else (ticks % 2 == 1)
continue
lines = block.rstrip().split("\n")
if len(lines) >= 2 and re.match(r"^\s*\|.*\|\s*$", lines[0]) \
and re.match(r"^\s*\|[\s:|-]+\|\s*$", lines[1]):
continue # a table, not a list
if marker.match(lines[0]):
total += sum(1 for l in lines if marker.match(l))
return total
@unittest.skipUnless(NODE, "node is needed to run the page's own md()")
class CorpusTests(RendererCase):
"""The whole board, not a fixture: one bullet per marker, no more."""
def docs(self) -> list[Path]:
found = sorted(ROOT.glob("tasks/*/*.md")) + [ROOT / "AGENTS.md"]
self.assertGreater(len(found), 5, "no task files found to render")
return found
def test_every_document_renders_one_bullet_per_marker(self):
"""Before the fix a hard-wrapped item produced a bullet per source
line; this is the acceptance criterion applied to every card."""
for doc in self.docs():
with self.subTest(doc=doc.relative_to(ROOT).as_posix()):
src = doc.read_text(encoding="utf-8")
self.assertEqual(self.render(src).count("
'), html.count("
(.*?)", html, re.S).group(1)
self.assertEqual(body.rstrip("\n").split("\n"), [
".task-manager/",
"├── AGENTS.md ← This file",
"│ ├── VERSION, board.py",
"└── manager/",
])
def test_a_fence_spanning_blank_lines_still_closes(self):
"""The fence state machine spans blocks — a blank line inside a
fence must not end it, and a bullet inside must stay literal."""
html = self.render("```\nfirst\n\n- not a bullet\n```\n\nafter\n")
self.assertEqual(html.count("
"), 1)
self.assertNotIn("
", html)
self.assertIn("
after
", html)
def test_a_table_after_a_list_is_still_a_table(self):
"""Card 30's wrong/right table is the live case."""
html = self.render(
"- a bullet that wraps\n onto a second line\n\n"
"| Turn 1 says | bench actually |\n| --- | --- |\n"
"| `bench.toml` | `manager/local/.env` |\n")
self.assertIn("
", html)
self.assertIn("
Turn 1 says
", html)
self.assertIn("
bench.toml
", html)
self.assertEqual(html.count("
What to build", html)
self.assertIn("", html)
self.assertIn("
item
", html)
def test_html_in_the_source_is_still_escaped(self):
html = self.render("- an item with in it\n")
self.assertNotIn("