Files
playbook/test/test_template_contracts.py
T
csh 699b431cac feat(workflow): adopt Matt Pocock ticket workflow
Replace the Superpowers plan pipeline with grill-with-docs, specs, local tickets, and ticket-native execution.

BREAKING CHANGE: Remove the legacy Plan CLI, prompt templates, and Superpowers skills.
2026-08-10 16:55:46 +08:00

311 lines
14 KiB
Python

import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TEMPLATES = ROOT / "templates"
LEGACY_FLOW_TERMS = (
"using-superpowers",
"brainstorming",
"writing-plans",
"executing-plans",
"docs/superpowers/",
"plan-status",
"旧开发流程目录",
"## 禁止旧流程",
"旧开发队列",
"单文件开发计划主链",
"active-context.md",
"progress.md",
"decisions.md",
)
class TemplateContractsTests(unittest.TestCase):
def test_templates_define_only_the_matt_ticket_workflow(self):
combined = "\n".join(
path.read_text(encoding="utf-8")
for path in sorted(TEMPLATES.rglob("*.md"))
)
for required in (
"setup-matt-pocock-skills",
"grill-with-docs",
"to-spec",
"to-tickets",
".scratch/",
"main_loop.py claim",
"main_loop.py integrate",
):
self.assertIn(required, combined)
for legacy in LEGACY_FLOW_TERMS:
self.assertNotIn(legacy, combined)
def test_project_templates_drop_legacy_language_placeholders(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", templates_readme)
self.assertNotIn("{{LANGUAGE_1}}", templates_readme)
self.assertNotIn("docs/workflows/", templates_readme)
self.assertNotIn("templates/workflows/", templates_readme)
self.assertNotIn("docs/superpowers/", templates_readme)
self.assertIn(".scratch/<feature>/spec.md", templates_readme)
self.assertNotIn("docs/prompts/", templates_readme)
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
encoding="utf-8"
)
self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template)
self.assertIn("AGENT_RULES.md", agents_template)
self.assertIn("memory-bank/project-brief.md", agents_template)
self.assertIn("memory-bank/tech-context.md", agents_template)
self.assertIn("memory-bank/system-patterns.md", agents_template)
self.assertIn(".scratch/queue.md", agents_template)
self.assertNotIn("docs/prompts/", agents_template)
tech_context_template = (
TEMPLATES / "memory-bank" / "tech-context.template.md"
).read_text(encoding="utf-8")
self.assertNotIn("{{MAIN_LANGUAGE}}", tech_context_template)
self.assertNotIn("{{LANGUAGE_1}}", tech_context_template)
self.assertNotIn("**主要语言**", tech_context_template)
self.assertIn("## 不可假设项", tech_context_template)
project_brief_template = (
TEMPLATES / "memory-bank" / "project-brief.template.md"
).read_text(encoding="utf-8")
self.assertIn("## 成功定义", project_brief_template)
def test_agents_template_nests_agents_block_inside_framework_block(self):
agents_template = (TEMPLATES / "AGENTS.template.md").read_text(
encoding="utf-8"
)
lines = [line.strip() for line in agents_template.splitlines()]
for marker in (
"<!-- playbook:framework:start -->",
"<!-- playbook:framework:end -->",
"<!-- playbook:agents:start -->",
"<!-- playbook:agents:end -->",
):
self.assertIn(marker, lines, msg=f"missing marker: {marker}")
framework_start = lines.index("<!-- playbook:framework:start -->")
framework_end = lines.index("<!-- playbook:framework:end -->")
agents_start = lines.index("<!-- playbook:agents:start -->")
agents_end = lines.index("<!-- playbook:agents:end -->")
nesting_reason = (
"sync_agents_template replaces the whole framework block; the agents "
"sub-block must stay inside it so preserve_agents_subblock can carry "
"the deployed language rules across the replacement"
)
self.assertLess(framework_start, agents_start, msg=nesting_reason)
self.assertLess(agents_start, agents_end, msg=nesting_reason)
self.assertLess(agents_end, framework_end, msg=nesting_reason)
def test_templates_readme_separates_ownership_from_deployment_config(self):
templates_readme = (TEMPLATES / "README.md").read_text(encoding="utf-8")
classification = templates_readme.split("## 模板分类", 1)[1].split(
"## 模板说明", 1
)[0]
self.assertIn("### 1. 入口导航", classification)
self.assertIn("### 2. 初始化后由项目维护", classification)
self.assertIn("### 3. 参考模板", classification)
self.assertIn("项目新增的 `memory-bank/*`", classification)
self.assertNotIn("[sync_", classification)
self.assertNotIn("force", classification)
self.assertNotIn("no_backup", classification)
def test_memory_bank_contains_only_stable_project_knowledge(self):
memory_templates = {
path.name for path in (TEMPLATES / "memory-bank").glob("*.template.md")
}
self.assertEqual(
memory_templates,
{
"project-brief.template.md",
"tech-context.template.md",
"system-patterns.template.md",
},
)
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
stable_paths = (
"memory-bank/project-brief.md",
"memory-bank/tech-context.md",
"memory-bank/system-patterns.md",
)
for path in stable_paths:
self.assertIn(path, rules)
self.assertIn("进入 `grill-with-docs` 或本地 ticket 执行协议前", rules)
normalized_rules = " ".join(rules.split())
for required in (
"只记录下一 session 仍需要的稳定知识",
"写入 `tech-context.md` 的命令和环境事实必须已经验证",
"关键取舍及理由写入 `docs/adr/`",
"不把聊天流水、未验证猜测或短期进度写入 `CONTEXT.md`",
"没有长期价值的信息时不更新这些文件",
):
self.assertIn(required, normalized_rules)
def test_prompt_templates_are_not_part_of_the_workflow(self):
self.assertFalse(TEMPLATES.joinpath("prompts").exists())
def test_agent_rules_template_defines_ticket_and_integration_contracts(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
self.assertIn("Matt Pocock 工程流程与 ticket 执行约束", rules)
self.assertIn("{{PLAYBOOK_ROOT}}", rules)
self.assertIn("Playbook 模板/供应商目录", rules)
self.assertIn("setup-matt-pocock-skills", rules)
self.assertIn("grill-with-docs", rules)
self.assertIn("to-spec", rules)
self.assertIn("to-tickets", rules)
self.assertIn("一次可以先生成多个 feature", rules)
self.assertIn("`.scratch/queue.md`", rules)
self.assertIn("多个 frontier tickets 可在 worktree 模式并发执行", rules)
self.assertIn("不创建额外 worktree", rules)
self.assertIn("共享同一文件系统", rules)
self.assertIn("跨机器或独立 clone", " ".join(rules.split()))
self.assertIn("只有 `reclaim` 可以接管", rules)
self.assertIn("先提交实现", rules)
self.assertIn("Standards/Spec 双轴审查", rules)
self.assertIn("三个独立门禁,不能互相替代", rules)
for legacy in LEGACY_FLOW_TERMS:
self.assertNotIn(legacy, rules)
def test_agent_rules_routes_each_matt_on_ramp_to_its_real_destination(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
on_ramps = rules.split("## On-ramps 与 detours", 1)[1].split(
"## Phase boundaries", 1
)[0]
self.assertIn("`wayfinder`", on_ramps)
self.assertIn("`to-spec -> to-tickets`", on_ramps)
self.assertIn("`research`", on_ramps)
self.assertIn("先进入 `grill-with-docs`", on_ramps)
self.assertIn("`handoff` 到独立目录运行", on_ramps)
self.assertIn("`prototype`", on_ramps)
self.assertIn("再 `handoff` 结论回原设计会话", on_ramps)
bug_route = rules.split("### 已明确预期行为的 bug", 1)[1].split(
"## 正式工程主链", 1
)[0]
self.assertIn("`diagnosing-bugs` 完整执行 Phase 1-6", bug_route)
self.assertIn("不再进入 `implement` 或重复 `tdd`", bug_route)
self.assertNotIn("to-spec", bug_route)
def test_agent_rules_defines_a_local_ticket_execution_adapter(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
main_flow = rules.split("## 正式工程主链", 1)[1].split(
"## On-ramps 与 detours", 1
)[0]
adapter = rules.split("## 本地 Ticket 执行协议", 1)[1].split(
"## 文档职责", 1
)[0]
normalized_adapter = " ".join(adapter.split())
self.assertIn("本地 ticket 执行协议", main_flow)
self.assertNotIn("implement + tdd", main_flow)
self.assertIn("不直接调用上游 `implement`", normalized_adapter)
ordered_steps = (
"读取已领取 ticket 的 spec",
"按 `tdd`",
"提交全部实现",
"运行 `code-review`",
"调用 `main_loop.py finish`",
)
positions = [adapter.index(step) for step in ordered_steps]
self.assertEqual(positions, sorted(positions))
def test_agent_rules_defines_mechanical_review_inputs_and_pass_mapping(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
review_contract = rules.split("### Review 适配契约", 1)[1].split(
"### Ticket 完成或状态转换", 1
)[0]
normalized_review_contract = " ".join(review_contract.split())
for required in (
"fixed point",
"`.scratch/<feature>/spec.md`",
"`.scratch/<feature>/issues/<ticket>-*.md`",
"零个未解决的硬 finding",
"不得据此填写 `standards=pass` 或 `spec=pass`",
):
self.assertIn(required, normalized_review_contract)
def test_agent_rules_reads_claimed_context_after_claim_and_defines_lease_policy(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
startup = rules.split("## 会话启动", 1)[1].split("## 任务入口", 1)[0]
claim = rules.split("### 领取", 1)[1].split("### 心跳和接管", 1)[0]
lease = rules.split("### 心跳和接管", 1)[1].split(
"### Review 适配契约", 1
)[0]
self.assertNotIn("当前 `.scratch/<feature>/spec.md`", startup)
self.assertNotIn("当前 `.scratch/<feature>/issues/<ticket>.md`", startup)
self.assertIn("领取成功后立即读取", claim)
self.assertIn("全局唯一", claim)
self.assertIn("每 10 分钟", lease)
self.assertIn("固定为 30 分钟", lease)
def test_agent_rules_preserves_to_spec_seam_and_phase_boundary_contracts(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
main_flow = rules.split("## 正式工程主链", 1)[1].split(
"## On-ramps 与 detours", 1
)[0]
self.assertIn("不重新进行已经完成的需求采访", main_flow)
self.assertIn("完成 seam confirmation", main_flow)
self.assertIn("`tdd` 不得在未经确认的 seam 上开始", main_flow)
phase_boundaries = rules.split("## Phase boundaries", 1)[1].split(
"## 文档职责", 1
)[0]
ordered_options = (
"继续当前 session",
"使用 `clear`",
"使用 `handoff`",
"交给 subagent",
"使用 `compact`",
)
positions = [phase_boundaries.index(option) for option in ordered_options]
self.assertEqual(positions, sorted(positions))
self.assertIn("上下文过长不等于必须 `handoff`", phase_boundaries)
def test_agent_rules_binds_state_and_evidence_to_claimed_git_context(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
execution = rules.split("### 领取", 1)[1]
after_claim_output = execution.split("stdout 返回", 1)[1]
self.assertIn("绝对 `STATE_ROOT`", execution)
self.assertIn("`CONTROL_ROOT` 是共享 Git control checkout", execution)
self.assertIn("`WORKSPACE` 是当前 ticket 的代码工作区", execution)
self.assertNotIn("--state-root .scratch", after_claim_output)
self.assertIn("--review-base <同一feature HEAD>", execution)
self.assertIn(
'commit=<HEAD>; base=<review-base>; standards=pass; spec=pass',
execution,
)
self.assertIn("把该 `FEATURE_HEAD` 合入 ticket branch", execution)
self.assertIn(
"`--feature-head`、`--review-base` 和 review evidence 的 `base`",
execution,
)
self.assertIn(
"base=<最新main HEAD>; standards=pass; spec=pass",
execution,
)
def test_agent_rules_limits_main_loop_to_shared_local_markdown_state(self):
rules = (TEMPLATES / "AGENT_RULES.template.md").read_text(encoding="utf-8")
isolation = rules.split("## 执行隔离", 1)[1].split(
"## 主循环命令", 1
)[0]
self.assertIn("`main_loop.py` 只支持 local Markdown tracker", isolation)
self.assertIn("没有远程 tracker adapter", isolation)
self.assertIn("跨机器或独立\nclone 的并发不受支持", isolation)
self.assertNotIn("必须改用具备远程", isolation)
if __name__ == "__main__":
unittest.main()