feat(ship): enforce test manifest handoff

This commit is contained in:
2026-10-14 12:16:55 +08:00
parent d7ebb6ee85
commit 8d48d2d335
5 changed files with 163 additions and 2 deletions
+3
View File
@@ -34,6 +34,9 @@ description: >-
新的 ship。
- tester / builder / deployer 的 SSOT 仍在各自目录。ship 只追加指针。本轮跑过的
测试、obtain、部署若没写入 `current.yaml` 的 `evidence`,该阶段不算完成。
- 新建 ship 的任务带 `testPlan`,开发 worker 把新增/修改测试写入
`2-develop/test-manifest.yaml`;verify 按 manifest 的 selector 执行,regress 再叠加
所有 tester catalog 根和项目原生完整测试。没有测试时必须有明确批准的 `testGap`。
- 项目构建/部署契约仍在 `.pouch/ack/delivery.yaml` 和 builder/deployer 配置里,不要
复制进每个 ship。
- 旧的 `.pouch/specifier/features/<id>/` 在继续该需求时迁到
+10
View File
@@ -16,6 +16,7 @@
current.yaml # 总账:kind、状态、stopAt、指针
spec.md # 短需求说明(short 和 long 都有)
tasks.yaml # 本 ship 的任务 DAG
2-develop/test-manifest.yaml # 开发者新增/修改测试的机器清单
status.md # 给人看的一页摘要
1-spec/ # 规格、评审、根因和 handoff
2-develop/ # 开发任务和变更
@@ -131,6 +132,12 @@ evidence:
`tasks.yaml` 只描述本 ship 的工作项,不是 ACK 缺陷板。
新建 ship 的任务默认带 `testPlan`。开发 worker 在 manifest 中记录测试的稳定 id、
runner、target、selector、覆盖的验收项和 `added|changed` 状态。`verify` 必须逐条执行
manifest;`regress` 在完整 tester catalog 和项目原生测试之外再次确认这些条目通过。
仅文档或明确无行为变化的任务可使用 `policy: not_applicable`,但必须填写被主会话接受的
`testGap.reason` 与 `acceptedBy`。
```yaml
shipId: add-export-button
tasks:
@@ -141,6 +148,9 @@ tasks:
- 登录用户在列表页看到「导出」
verification:
- 浏览器打开列表页,按钮可见且可点
testPlan:
policy: required
manifest: 2-develop/test-manifest.yaml
status: open
stopAt: review_ready
```
+11 -2
View File
@@ -21,9 +21,18 @@ scripts/append_evidence.py 追加后跑 scripts/validate_ship.py。
验收:见 spec.md 与 tasks.yaml 里该任务的 expected / verification。
授权:<本步允许的 git/PR/部署动作;未写的不要做>
测试交接:`implement` / `implement_task` 必须在 ship 的 `2-develop/test-manifest.yaml`
登记本次新增或修改的测试。每条记录至少包含稳定 `id`、`kind`、`runner`、`target`、
`selector`、`covers` 和 `change: added|changed`。若确实没有行为变化或无法补测,必须
填写 `testGap.reason`,并由主会话明确写入 `acceptedBy`;不能留空后声称已验证。
测试边界:`verify` / `verify_task` 只负责开发阶段验证,执行任务相关的单元测试、
新增功能集成测试和静态检查,不执行合并后的完整 tester 回归;`regress` 只在合并后
负责完整回归,必须覆盖发现的每个 tester catalog 根和项目声明的原生测试套件。
新增功能集成测试和静态检查,不执行合并后的完整 tester 回归。验证前先使用 ship skill
的 `scripts/validate_test_manifest.py <ship-dir>/2-develop/test-manifest.yaml`
带上当前 ship id、任务的 testPlan.policy 执行 `--mode verify`;逐条执行 manifest 中的
测试 selector,并在证据中记录测试 id。
`regress` 只在合并后负责完整回归,除 manifest 中的新增/修改测试外,必须覆盖发现的每个
tester catalog 根和项目声明的原生测试套件。
若步骤是 `verify_task` 或 `regress` 且 skill 是 `tester`:先执行
`python3 <tester-skill>/scripts/catalog.py roots --root <project-root>`。`verify_task` 按
+39
View File
@@ -19,6 +19,7 @@ from ship_lib import (
check_ship_id,
load_yaml,
)
from validate_test_manifest import validate_manifest
def _issue(issues: list[str], msg: str) -> None:
@@ -144,6 +145,7 @@ def validate_ship_dir(ship_dir: Path) -> list[str]:
_issue(issues, f"缺少 {tasks_rel}")
else:
issues.extend(_validate_tasks(tasks_path, ship_id, kind))
issues.extend(_validate_manifest_pointers(tasks_path, ship_dir, ship_id))
delivery = data.get("delivery")
if delivery is not None:
@@ -247,6 +249,43 @@ def _validate_tasks(path: Path, ship_id: Any, kind: Any) -> list[str]:
return issues
def _validate_manifest_pointers(path: Path, ship_dir: Path, ship_id: str) -> list[str]:
issues: list[str] = []
try:
data = load_yaml(path)
except Exception:
return issues
tasks = data.get("tasks") if isinstance(data, dict) else None
if not isinstance(tasks, list):
return issues
for index, item in enumerate(tasks):
if not isinstance(item, dict):
continue
plan = item.get("testPlan")
if not isinstance(plan, dict):
continue
manifest = plan.get("manifest")
if not isinstance(manifest, str) or not manifest.strip():
continue
target = (ship_dir / manifest).resolve()
try:
target.relative_to(ship_dir.resolve())
except ValueError:
_issue(issues, f"tasks[{index}].testPlan.manifest 必须位于 ship 目录内: {manifest}")
continue
if not target.is_file():
_issue(issues, f"tasks[{index}].testPlan.manifest 不存在: {manifest}")
continue
manifest_issues, _ = validate_manifest(
target,
expected_ship_id=ship_id,
policy=str(plan.get("policy") or "required"),
mode="schema",
)
issues.extend(f"tasks[{index}].{issue}" for issue in manifest_issues)
return issues
def _validate_evidence(evidence: dict[str, Any], project_root: Path) -> list[str]:
issues: list[str] = []
for key in ("testRuns", "artifacts", "deploys", "prs", "commits"):