225 lines
9.1 KiB
Python
225 lines
9.1 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||
|
||
|
||
class AckSkillContentTests(unittest.TestCase):
|
||
def test_ack_skill_owns_resources_without_modifying_agent_instructions(self) -> None:
|
||
content = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(encoding="utf-8")
|
||
|
||
for expected in (
|
||
"pouch init ack --project <project-root>",
|
||
".pouch/ack/project.md",
|
||
".pouch/ack/tasks.yaml",
|
||
".pouch/ack/knowledge.yaml",
|
||
".pouch/ack/delivery.yaml",
|
||
".pouch/ack/regression.yaml",
|
||
"tasks: []",
|
||
"validate_tasks.py",
|
||
"validate_knowledge.py",
|
||
"validate_delivery.py",
|
||
"validate_regression.py",
|
||
"select_tasks.py",
|
||
"select_knowledge.py",
|
||
"select_regression.py",
|
||
"references/kickoff.md",
|
||
"agents.yaml",
|
||
"validate_agents.py",
|
||
"model: null",
|
||
"不要修改项目的 `AGENTS.md`",
|
||
"当前会话担任 Coordinator",
|
||
"intents.testEnvironment",
|
||
"运行测试环境",
|
||
"运行版本发布",
|
||
"运行回归",
|
||
"via: deployer",
|
||
"## ack 初始化:完成 | 部分完成 | 阻塞",
|
||
"加载 deployer skill 的「初始化」",
|
||
):
|
||
self.assertIn(expected, content)
|
||
|
||
def test_ack_skill_is_explicit_only(self) -> None:
|
||
metadata = (REPO_ROOT / "skills" / "ack" / "agents" / "openai.yaml").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
|
||
self.assertIn('display_name: "ACK"', metadata)
|
||
self.assertIn("allow_implicit_invocation: false", metadata)
|
||
|
||
def test_model_routing_is_fingerprint_bound_and_does_not_trust_receipt_reuse(self) -> None:
|
||
content = (
|
||
REPO_ROOT / "skills" / "ack" / "references" / "model-routing.md"
|
||
).read_text(encoding="utf-8")
|
||
|
||
self.assertIn("--expected-launch-fingerprint", content)
|
||
self.assertIn("allowlist-v1", content)
|
||
self.assertIn("禁止根据持久化 receipt 自动复用", content)
|
||
self.assertIn("launcher 身份证明", content)
|
||
|
||
def test_worker_reuse_requires_idle_state_and_verified_history_reset(self) -> None:
|
||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
adapter = (
|
||
REPO_ROOT / "skills" / "ack" / "references" / "orca-adapter.md"
|
||
).read_text(encoding="utf-8")
|
||
|
||
self.assertIn("空闲", skill)
|
||
self.assertIn("清理历史消息", skill)
|
||
self.assertIn("无法确认清理成功时创建 fresh worker", skill)
|
||
self.assertIn("角色、profile、worktree", adapter)
|
||
self.assertIn("不得复用仍在工作", adapter)
|
||
self.assertIn("或运行状态不明的 worker", adapter)
|
||
|
||
def test_coordinator_reclaims_only_verified_task_terminals_at_run_end(self) -> None:
|
||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
kickoff = (
|
||
REPO_ROOT / "skills" / "ack" / "references" / "kickoff.md"
|
||
).read_text(encoding="utf-8")
|
||
|
||
self.assertIn("整轮任务完成", skill)
|
||
self.assertIn("回收所有只属于 `verified` 任务的 worker", skill)
|
||
self.assertIn("终端,并核对关闭回执", skill)
|
||
self.assertIn("不设置 TTL", skill)
|
||
self.assertIn("blocked", kickoff)
|
||
self.assertIn("failed_retest", kickoff)
|
||
self.assertIn("leftover", kickoff)
|
||
|
||
def test_coordinator_reclaims_role_terminal_after_success(self) -> None:
|
||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
adapter = (
|
||
REPO_ROOT / "skills" / "ack" / "references" / "orca-adapter.md"
|
||
).read_text(encoding="utf-8")
|
||
|
||
self.assertIn("--completed-task-id", skill)
|
||
self.assertIn("--completed-role", skill)
|
||
self.assertIn("--completed-role developer", adapter)
|
||
self.assertIn("--completed-role test", adapter)
|
||
self.assertIn("orca terminal close --tab", adapter)
|
||
|
||
def test_environment_failures_are_reported_without_consuming_retest_rounds(self) -> None:
|
||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
optimization = (
|
||
REPO_ROOT / "skills" / "ack" / "references" / "optimization-method.md"
|
||
).read_text(encoding="utf-8")
|
||
schema = json.loads(
|
||
(REPO_ROOT / "skills" / "ack" / "templates" / "tasks.schema.json")
|
||
.read_text(encoding="utf-8")
|
||
)
|
||
|
||
self.assertIn("环境失败不占复验轮次", skill)
|
||
self.assertIn("userAction", optimization)
|
||
self.assertIn("environmentIncidents", schema["definitions"]["task"]["properties"]["dispatch"]["properties"])
|
||
|
||
def test_validation_ready_hands_off_a_deployed_test_environment(self) -> None:
|
||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
delivery = (
|
||
REPO_ROOT / "skills" / "ack" / "references" / "delivery.md"
|
||
).read_text(encoding="utf-8")
|
||
|
||
self.assertIn("不能停在", skill)
|
||
self.assertIn("`verified` 却声称整轮 ACK 已结束", skill)
|
||
self.assertIn("validation_ready", delivery)
|
||
self.assertIn("访问地址和用户下一步", delivery)
|
||
|
||
def test_coordinator_keeps_looping_and_chains_developer_to_test(self) -> None:
|
||
skill = (REPO_ROOT / "skills" / "ack" / "SKILL.md").read_text(
|
||
encoding="utf-8"
|
||
)
|
||
|
||
required = (
|
||
"这不是 fire-and-forget",
|
||
"不得在 `launch` / `dispatch` 后只回复“已派发”并结束当前会话",
|
||
"`check --wait`",
|
||
"`worker_probe.py`",
|
||
"默认 60 秒窗口",
|
||
"60–120 秒节奏",
|
||
"Developer 回报 `worker_done` 或 `escalation`",
|
||
"写回 `fixed_by_dev`",
|
||
"主动派发 Test",
|
||
"继续等待 `retest_result`",
|
||
)
|
||
for expected in required:
|
||
self.assertIn(expected, skill)
|
||
|
||
dispatch_pos = skill.index("主动派发 Test")
|
||
dev_done_pos = skill.index("Developer 回报 `worker_done` 或 `escalation`")
|
||
test_done_pos = skill.index("继续等待 `retest_result`")
|
||
self.assertLess(dev_done_pos, dispatch_pos)
|
||
self.assertLess(dispatch_pos, test_done_pos)
|
||
|
||
def test_ack_knowledge_resources_and_version_are_present(self) -> None:
|
||
ack_dir = REPO_ROOT / "skills" / "ack"
|
||
|
||
for relative_path in (
|
||
"templates/knowledge.template.yaml",
|
||
"templates/knowledge.schema.json",
|
||
"examples/knowledge.example.yaml",
|
||
"scripts/validate_knowledge.py",
|
||
"scripts/select_tasks.py",
|
||
"scripts/select_knowledge.py",
|
||
"scripts/select_regression.py",
|
||
"scripts/validate_regression.py",
|
||
"templates/regression.template.yaml",
|
||
"templates/regression.schema.json",
|
||
"examples/regression.example.yaml",
|
||
"references/regression.md",
|
||
"scripts/run_verification.py",
|
||
"scripts/worker_profiles.py",
|
||
"scripts/launch_worker.py",
|
||
"scripts/validate_agents.py",
|
||
"templates/agents.schema.json",
|
||
"templates/agents.template.yaml",
|
||
"examples/agents.example.yaml",
|
||
"templates/delivery.template.yaml",
|
||
"templates/delivery.schema.json",
|
||
"examples/delivery.example.yaml",
|
||
"references/delivery.md",
|
||
):
|
||
self.assertTrue((ack_dir / relative_path).is_file(), relative_path)
|
||
version = (ack_dir / "VERSION").read_text(encoding="utf-8").strip()
|
||
self.assertEqual(version, "0.20.0")
|
||
self.assertIn(
|
||
f'ackVersion: "{version}"',
|
||
(ack_dir / "examples" / "tasks.example.yaml").read_text(encoding="utf-8"),
|
||
)
|
||
self.assertIn(
|
||
f"ack v{version}",
|
||
(ack_dir / "examples" / "project.example.md").read_text(encoding="utf-8"),
|
||
)
|
||
self.assertIn(
|
||
'ackVersion: "<接入时的 ack skill 版本>"',
|
||
(ack_dir / "templates" / "tasks.template.yaml").read_text(
|
||
encoding="utf-8"
|
||
),
|
||
)
|
||
self.assertTrue((ack_dir / "references" / "feishu-bug-intake.md").is_file())
|
||
|
||
def test_task_template_derives_project_files_without_persisted_root_paths(self) -> None:
|
||
template = (
|
||
REPO_ROOT / "skills" / "ack" / "templates" / "tasks.template.yaml"
|
||
).read_text(encoding="utf-8")
|
||
|
||
# v0.19 起 allowedWorktrees 白名单废弃:模板只保留废弃说明注释,不再生成该字段
|
||
self.assertNotIn("allowedWorktrees:", template)
|
||
self.assertIn("allowedWorktrees", template)
|
||
self.assertIn('knowledgeFile: ".pouch/ack/knowledge.yaml"', template)
|
||
self.assertIn('regressionFile: ".pouch/ack/regression.yaml"', template)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|