Merge the highest-value slots from a fuller development-task template into bench's lean one: an optional Depends on header line, an Affected areas line in Context, Out of scope bullets in What to build, edge-case and Given/When/Then guidance in Acceptance, and a Risks slot in Notes. Each slot says who consumes it and is marked deletable — empty boilerplate is worse than absence. The load-bearing contracts are unchanged: exact Status values, the Open questions heading the NOT READY gate keys off, Acceptance as the review target, and the template staying off the board. Organisation process (staging, sign-offs, approvals, contacts) is deliberately absent; a new contract test pins both the gained slots and what must not grow back. CLAUDE.md's Task file format section documents the Depends on line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
"""The task template's load-bearing contract. Its headings are machinery,
|
|
not prose convention: the body becomes the work agent's brief verbatim,
|
|
Acceptance is what review agents judge against, and the Open questions
|
|
heading is what the NOT READY gate keys off — so the template must keep
|
|
them, exactly, in order. Task 07 enriched the template; these tests pin
|
|
both what it gained and what it must never grow.
|
|
|
|
python3 -m unittest discover -s tests -v
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
TEMPLATE = (Path(__file__).resolve().parents[1]
|
|
/ "tasks" / "task-template.md")
|
|
|
|
LOAD_BEARING_HEADINGS = [
|
|
"Context", "What to build", "Acceptance", "Open questions", "Notes",
|
|
]
|
|
|
|
# Organisation process the board either does mechanically or doesn't own —
|
|
# deliberately absent from the template, and it must not grow back.
|
|
ORG_PROCESS_TERMS = (
|
|
"staging", "sign-off", "signoff", "qa ", "product owner",
|
|
"browser", "device matrix", "contact",
|
|
)
|
|
|
|
# The slots task 07 added, each of which earns its keep on the card.
|
|
ENRICHED_SLOTS = (
|
|
"**Depends on:**",
|
|
"**Affected areas:**",
|
|
"**Out of scope**",
|
|
"**Risks**",
|
|
"Given <",
|
|
"Edge case",
|
|
)
|
|
|
|
|
|
class TemplateContract(unittest.TestCase):
|
|
def setUp(self):
|
|
self.text = TEMPLATE.read_text()
|
|
|
|
def test_load_bearing_headings_survive_in_order(self):
|
|
headings = re.findall(r"^## (.+)$", self.text, re.MULTILINE)
|
|
self.assertEqual(headings, LOAD_BEARING_HEADINGS)
|
|
|
|
def test_status_is_an_exact_board_value(self):
|
|
self.assertIn("**Status:** Backlog\n", self.text)
|
|
|
|
def test_open_questions_section_names_the_gate(self):
|
|
section = self.text.split("## Open questions", 1)[1]
|
|
section = section.split("## ", 1)[0]
|
|
self.assertIn("`NOT READY`", section)
|
|
|
|
def test_enriched_slots_are_present(self):
|
|
for slot in ENRICHED_SLOTS:
|
|
with self.subTest(slot=slot):
|
|
self.assertIn(slot, self.text)
|
|
|
|
def test_new_slots_say_they_are_deletable(self):
|
|
# "delete" appears with each optional slot, so authors trim rather
|
|
# than leave hollow sections behind.
|
|
for slot in ("**Depends on:**", "**Affected areas:**",
|
|
"**Out of scope**", "**Risks**"):
|
|
with self.subTest(slot=slot):
|
|
paragraph = self.text.split(slot, 1)[1].split("\n\n", 1)[0]
|
|
self.assertIn("delete", paragraph.lower())
|
|
|
|
def test_no_organisation_process_creeps_in(self):
|
|
lowered = self.text.lower()
|
|
for term in ORG_PROCESS_TERMS:
|
|
with self.subTest(term=term):
|
|
self.assertNotIn(term, lowered)
|
|
|
|
def test_template_sits_outside_every_stage_directory(self):
|
|
# The board lists stage directories only; the template must stay a
|
|
# sibling of them, not a card.
|
|
self.assertEqual(TEMPLATE.parent.name, "tasks")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|