Files
codestable__codestable/tests/test_skill_workflow_scenarios.py
2026-07-17 17:59:18 +08:00

1519 lines
67 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
SKILLS = ROOT / "plugins/codestable/skills"
AFFECTED_SKILLS = {
"cs",
"cs-feat",
"cs-feat-design",
"cs-feat-design-review",
"cs-feat-impl",
"cs-feat-qa",
"cs-feat-accept",
"cs-feat-ff",
"cs-issue",
"cs-issue-report",
"cs-issue-analyze",
"cs-issue-fix",
"cs-refactor",
"cs-refactor-ff",
"cs-docs",
"cs-doc-tutorial",
"cs-doc-api",
"cs-docs-neat",
"cs-epic",
"cs-feedback",
"cs-roadmap",
"cs-roadmap-review",
"cs-roadmap-impl-goal",
"cs-code-review",
"cs-brainstorm",
"cs-req",
"cs-onboard",
}
SCENARIO_COVERAGE = {
"router": {"cs", "cs-brainstorm", "cs-req", "cs-onboard"},
"feature-long-range": {
"cs-feat",
"cs-feat-design",
"cs-feat-design-review",
"cs-feat-impl",
"cs-feat-qa",
"cs-feat-accept",
"cs-feat-ff",
"cs-code-review",
},
"epic-long-range": {
"cs-epic",
"cs-roadmap",
"cs-roadmap-review",
"cs-roadmap-impl-goal",
"cs-feat",
"cs-code-review",
},
"issue": {"cs-issue", "cs-issue-report", "cs-issue-analyze", "cs-issue-fix", "cs-code-review"},
"refactor": {"cs-refactor", "cs-refactor-ff", "cs-code-review"},
"docs": {"cs-docs", "cs-doc-tutorial", "cs-doc-api", "cs-docs-neat"},
"feedback": {"cs-feedback"},
"goal-driver": {"cs-onboard", "cs-feat", "cs-epic"},
}
COMPATIBILITY_ENTRIES = {
"cs-feat-design": ("cs-feat", "requested_stage", "design"),
"cs-feat-design-review": ("cs-feat", "requested_stage", "design-review"),
"cs-feat-impl": ("cs-feat", "requested_stage", "implementation"),
"cs-feat-qa": ("cs-feat", "requested_stage", "qa"),
"cs-feat-accept": ("cs-feat", "requested_stage", "acceptance"),
"cs-feat-ff": ("cs-feat", "requested_mode", "fastforward"),
"cs-issue-report": ("cs-issue", "requested_stage", "report"),
"cs-issue-analyze": ("cs-issue", "requested_stage", "analyze"),
"cs-issue-fix": ("cs-issue", "requested_stage", "fix"),
"cs-refactor-ff": ("cs-refactor", "requested_mode", "fastforward"),
"cs-doc-tutorial": ("cs-docs", "requested_mode", "tutorial"),
"cs-doc-api": ("cs-docs", "requested_mode", "api"),
"cs-roadmap": ("cs-epic", "requested_stage", "planning"),
"cs-roadmap-review": ("cs-epic", "requested_stage", "review"),
"cs-roadmap-impl-goal": ("cs-epic", "requested_stage", "goal-package"),
}
@dataclass(frozen=True)
class Action:
name: str
target: str
def skill_text(skill: str, rel_path: str = "SKILL.md") -> str:
return (SKILLS / skill / rel_path).read_text(encoding="utf-8")
def assert_doc_contains(skill: str, rel_path: str, *phrases: str) -> None:
text = skill_text(skill, rel_path)
missing = [phrase for phrase in phrases if phrase not in text]
assert missing == [], f"{skill}/{rel_path} missing {missing}"
def write(path: Path, text: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(text, encoding="utf-8")
def git(repo: Path, *args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["git", *args],
cwd=repo,
check=True,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
def init_isolated_repo(tmp_path: Path) -> Path:
repo = tmp_path / "isolated-repo"
repo.mkdir()
git(repo, "init", "-b", "main")
git(repo, "config", "user.name", "Workflow Test")
git(repo, "config", "user.email", "workflow-test@example.invalid")
write(repo / ".codestable/attention.md", "# Attention\n\n- Report language: zh.\n")
write(repo / ".codestable/reference/execution-conventions.md", skill_text("cs-onboard", "references/execution-conventions.md"))
write(repo / ".codestable/reference/agent-conventions.md", skill_text("cs-onboard", "references/agent-conventions.md"))
write(repo / "src/app.py", "def hello():\n return 'hello'\n")
git(repo, "add", ".")
git(repo, "commit", "-m", "baseline")
return repo
def frontmatter(path: Path) -> dict[str, str]:
if not path.exists():
return {}
text = path.read_text(encoding="utf-8")
if not text.startswith("---"):
return {}
block = text.split("---", 2)[1]
result = {}
for line in block.splitlines():
if ":" in line:
key, value = line.split(":", 1)
result[key.strip()] = value.strip().strip('"')
return result
def top_level_yaml(path: Path) -> dict[str, str]:
result = {}
if not path.exists():
return result
for line in path.read_text(encoding="utf-8").splitlines():
if line.startswith(" ") or ":" not in line:
continue
key, value = line.split(":", 1)
result[key.strip()] = value.strip().strip('"')
return result
def replace_status(path: Path, status: str) -> None:
text = path.read_text(encoding="utf-8")
text = re.sub(r"(?m)^status: .+$", f"status: {status}", text, count=1)
path.write_text(text, encoding="utf-8")
def feature_dir(repo: Path, slug: str) -> Path:
return repo / ".codestable/features" / f"2026-07-02-{slug}"
def write_feature_design(
repo: Path,
slug: str,
status: str = "draft",
roadmap: str | None = None,
execution_lane: str = "standard",
) -> Path:
directory = feature_dir(repo, slug)
roadmap_fields = ""
if roadmap:
roadmap_fields = f"roadmap: {roadmap}\nroadmap_item: {slug}\n"
write(
directory / f"{slug}-design.md",
f"---\ndoc_type: feature-design\nfeature: 2026-07-02-{slug}\n{roadmap_fields}"
f"execution_lane: {execution_lane}\nstatus: {status}\n---\n# Design\n",
)
write(
directory / f"{slug}-checklist.yaml",
f"feature: 2026-07-02-{slug}\nsteps:\n - id: step-1\n status: pending\nchecks:\n - id: check-1\n status: pending\n",
)
return directory
def write_feature_design_review(repo: Path, slug: str, status: str = "passed") -> None:
directory = feature_dir(repo, slug)
write(
directory / f"{slug}-design-review.md",
f"---\ndoc_type: feature-design-review\nfeature: 2026-07-02-{slug}\nstatus: {status}\n---\n# Design Review\n",
)
def write_feature_goal_state(repo: Path, slug: str, stage: str, status: str, driver: str = "none") -> None:
directory = feature_dir(repo, slug)
driver_id = f"{driver}-123" if driver != "none" else ""
write(
directory / "goal-state.yaml",
f"feature: {slug}\nstatus: {status}\nbaseline_ref: baseline\nstage: {stage}\ndriver_kind: {driver}\ndriver_id: \"{driver_id}\"\n",
)
write(directory / "goal-plan.md", "# Goal Plan\n")
write(directory / "goal-protocol.md", "# Goal Protocol\n")
def normalized_review_state(path: Path) -> str:
meta = frontmatter(path)
state = meta.get("review_state")
valid = {"passed", "changes-requested", "awaiting-reviewer", "needs-owner-approval", "reviewer-failed", "blocked"}
if state:
return state if state in valid else "invalid"
status = meta.get("status")
if status == "passed":
return "passed"
if status in {"changes-requested", "blocking"}:
return "changes-requested"
if status == "blocked":
return "legacy-blocked"
return "missing" if status is None else "invalid"
# Compact scenario model only. Production conformance is asserted by
# test_codestable_workflow_next.py against codestable-workflow-next.py directly.
def feature_next(repo: Path, slug: str) -> Action:
directory = feature_dir(repo, slug)
design = directory / f"{slug}-design.md"
review = directory / f"{slug}-design-review.md"
goal_state = directory / "goal-state.yaml"
if not design.exists():
return Action("load-reference", "cs-feat/references/design/protocol.md")
design_meta = frontmatter(design)
design_status = design_meta.get("status")
execution_lane = design_meta.get("execution_lane", "standard")
review_state = normalized_review_state(review)
if design_status == "approved" and review_state != "passed":
return Action("blocked", "invalid-approved-design-review")
if review_state == "changes-requested":
return Action("load-reference", "cs-feat/references/design/protocol.md")
if review_state == "awaiting-reviewer":
return Action("awaiting", "feature-design-reviewer")
if review_state == "needs-owner-approval":
return Action("user-checkpoint", "feature-design-review-fallback")
if review_state in {"invalid", "legacy-blocked", "reviewer-failed", "blocked"}:
return Action("blocked", "feature-design-review-block")
if design_status == "draft" and review_state != "passed":
return Action("load-reference", "cs-feat/references/design-review/protocol.md")
if review_state == "passed" and design_status != "approved":
return Action("user-checkpoint", "feature-design-confirmation")
if design_status == "approved" and not goal_state.exists():
if execution_lane == "goal":
return Action("load-reference", "cs-feat/references/goal/protocol.md")
return Action("load-reference", "cs-feat/references/implementation/protocol.md")
state = top_level_yaml(goal_state)
stage = state.get("stage")
status = state.get("status")
if (stage, status) == ("complete", "passed"):
return Action("complete", "CS_FEATURE_GOAL_COMPLETE")
if (stage, status) == ("handoff", "blocked"):
return Action("user-checkpoint", "CS_FEATURE_GOAL_HANDOFF")
if state.get("driver_kind") in {"host-agent", "paseo", "native"} and state.get("driver_id"):
return Action("report-visible-driver", state["driver_id"])
if (stage, status) == ("implementation", "ready-to-dispatch"):
return Action("dispatch-goal-driver", "feature")
if (stage, status) == ("implementation", "running"):
return Action("load-reference", "cs-feat/references/implementation/protocol.md")
if (stage, status) == ("review", "ready"):
return Action("load-skill", "cs-code-review")
if (stage, status) == ("review", "fixing"):
return Action("load-reference", "cs-feat/references/implementation/protocol.md#review-fix")
if (stage, status) == ("qa", "ready"):
return Action("load-reference", "cs-feat/references/qa/protocol.md")
if (stage, status) == ("qa", "fixing"):
return Action("load-reference", "cs-feat/references/implementation/protocol.md#qa-fix")
if (stage, status) == ("acceptance", "ready"):
return Action("load-reference", "cs-feat/references/acceptance/protocol.md")
return Action("blocked", "unknown-feature-state")
def roadmap_dir(repo: Path, slug: str) -> Path:
return repo / ".codestable/roadmap" / slug
def write_roadmap(repo: Path, slug: str, status: str = "draft") -> Path:
directory = roadmap_dir(repo, slug)
write(directory / f"{slug}-roadmap.md", f"---\ndoc_type: roadmap\nslug: {slug}\nstatus: {status}\n---\n# Roadmap\n")
write(
directory / f"{slug}-items.yaml",
f"roadmap: {slug}\nitems:\n - slug: api-seed\n status: planned\n feature: null\n - slug: ui-seed\n status: planned\n feature: null\n",
)
return directory
def write_roadmap_review(repo: Path, slug: str, status: str = "passed") -> None:
directory = roadmap_dir(repo, slug)
write(directory / f"{slug}-roadmap-review.md", f"---\ndoc_type: roadmap-review\nstatus: {status}\n---\n# Review\n")
def roadmap_item_slugs(repo: Path, slug: str) -> list[str]:
items = roadmap_dir(repo, slug) / f"{slug}-items.yaml"
return re.findall(r"(?m)^\s*-\s+slug:\s+([A-Za-z0-9_-]+)\s*$", items.read_text(encoding="utf-8"))
def write_epic_goal_state(repo: Path, slug: str, status: str, driver: str = "none") -> None:
directory = roadmap_dir(repo, slug)
driver_id = f"{driver}-roadmap-123" if driver != "none" else ""
write(
directory / "goal-state.yaml",
f"roadmap: {slug}\nstatus: {status}\nbaseline_ref: baseline\ndriver_kind: {driver}\ndriver_id: \"{driver_id}\"\ncurrent_feature_index: 0\nfeatures:\n - slug: api-seed\n status: pending\n",
)
write(directory / "goal-plan.md", "# Goal Plan\n")
write(directory / "goal-protocol.md", "# Goal Protocol\n")
def epic_next(repo: Path, slug: str) -> Action:
directory = roadmap_dir(repo, slug)
roadmap = directory / f"{slug}-roadmap.md"
review = directory / f"{slug}-roadmap-review.md"
goal_state = directory / "goal-state.yaml"
if not roadmap.exists():
return Action("load-reference", "cs-epic/references/planning/protocol.md")
roadmap_status = frontmatter(roadmap).get("status")
review_state = normalized_review_state(review)
if roadmap_status == "active" and review_state == "missing":
return Action("blocked", "invalid-active-roadmap-review")
if review_state == "changes-requested":
return Action("load-reference", "cs-epic/references/planning/protocol.md")
if review_state == "awaiting-reviewer":
return Action("awaiting", "roadmap-reviewer")
if review_state == "needs-owner-approval":
return Action("user-checkpoint", "roadmap-review-fallback")
if review_state in {"invalid", "legacy-blocked", "reviewer-failed", "blocked"}:
return Action("blocked", "roadmap-review-block")
if review_state != "passed":
return Action("load-reference", "cs-epic/references/review/protocol.md")
if roadmap_status != "active":
return Action("user-checkpoint", "epic-roadmap-confirmation")
child_designs = sorted((repo / ".codestable/features").glob("*/**/*-design.md"))
roadmap_child_designs = [path for path in child_designs if frontmatter(path).get("roadmap") == slug]
child_items = {
frontmatter(path).get("roadmap_item") or path.name.removesuffix("-design.md") for path in roadmap_child_designs
}
if any(item_slug not in child_items for item_slug in roadmap_item_slugs(repo, slug)):
return Action("load-skill", "cs-feat design/design-review")
if not roadmap_child_designs:
return Action("load-skill", "cs-feat design/design-review")
child_review_statuses = []
child_design_statuses = []
for design in roadmap_child_designs:
child_slug = design.name.removesuffix("-design.md")
child_review = design.parent / f"{child_slug}-design-review.md"
child_review_statuses.append(frontmatter(child_review).get("status"))
child_design_statuses.append(frontmatter(design).get("status"))
if any(status != "passed" for status in child_review_statuses):
return Action("load-skill", "cs-feat design/design-review")
if any(status != "approved" for status in child_design_statuses):
return Action("user-checkpoint", "all-feature-designs-confirmation")
if not goal_state.exists():
return Action("load-reference", "cs-epic/references/goal/protocol.md")
state = top_level_yaml(goal_state)
if state.get("status") in {"complete", "completed"}:
return Action("complete", "CS_ROADMAP_GOAL_COMPLETE")
if state.get("status") == "handoff":
return Action("user-checkpoint", "CS_ROADMAP_GOAL_HANDOFF")
if state.get("driver_kind") in {"host-agent", "paseo", "native"} and state.get("driver_id"):
return Action("report-visible-driver", state["driver_id"])
if state.get("status") == "ready-to-dispatch":
return Action("dispatch-goal-driver", "epic")
return Action("blocked", "unknown-epic-state")
def issue_dir(repo: Path, slug: str) -> Path:
return repo / ".codestable/issues" / f"2026-07-02-{slug}"
def write_issue_doc(repo: Path, slug: str, name: str, status: str = "draft") -> None:
write(
issue_dir(repo, slug) / f"{slug}-{name}.md",
f"---\ndoc_type: issue-{name}\nissue: 2026-07-02-{slug}\nstatus: {status}\n---\n# {name}\n",
)
def issue_next(repo: Path, slug: str) -> Action:
directory = issue_dir(repo, slug)
report = directory / f"{slug}-report.md"
if not report.exists():
return Action("load-reference", "cs-issue/references/report/protocol.md")
if frontmatter(report).get("status") != "confirmed":
return Action("user-checkpoint", "issue-report-confirmation")
analysis = directory / f"{slug}-analysis.md"
if not analysis.exists():
return Action("load-reference", "cs-issue/references/analyze/protocol.md")
if frontmatter(analysis).get("status") != "confirmed":
return Action("user-checkpoint", "issue-fix-plan-confirmation")
if not (directory / f"{slug}-fix-note.md").exists():
return Action("load-reference", "cs-issue/references/fix/protocol.md")
review_status = frontmatter(directory / f"{slug}-review.md").get("status")
if not review_status:
return Action("load-skill", "cs-code-review")
if review_status in {"changes-requested", "blocked"}:
return Action("load-reference", "cs-issue/references/fix/protocol.md#review-fix")
if review_status == "passed":
approval = directory / "approval-report.md"
approval_status = frontmatter(approval).get("status")
if not approval_status:
return Action("load-reference", "cs-issue/references/fix/protocol.md#completion-checkpoint")
if approval_status == "pending":
return Action("user-checkpoint", "issue-fix-completion")
if approval_status == "approved":
return Action("complete", "issue-reviewed")
return Action("blocked", "issue-fix-completion-rejected")
return Action("blocked", "unknown-issue-state")
def refactor_dir(repo: Path, slug: str) -> Path:
return repo / ".codestable/refactors" / f"2026-07-02-{slug}"
def write_refactor_doc(repo: Path, slug: str, name: str, status: str = "draft") -> None:
write(
refactor_dir(repo, slug) / f"{slug}-{name}.md",
f"---\ndoc_type: refactor-{name}\nrefactor: 2026-07-02-{slug}\nstatus: {status}\n---\n# {name}\n",
)
def refactor_next(repo: Path, slug: str, mode: str | None = None, small_scope: bool = False) -> Action:
if mode == "fastforward" and small_scope:
return Action("load-reference", "cs-refactor/references/fastforward/protocol.md")
directory = refactor_dir(repo, slug)
scan = directory / f"{slug}-scan.md"
design = directory / f"{slug}-refactor-design.md"
apply_notes = directory / f"{slug}-apply-notes.md"
if not scan.exists():
return Action("load-reference", "cs-refactor/references/standard/protocol.md#scan")
if "selected: true" not in scan.read_text(encoding="utf-8"):
return Action("user-checkpoint", "refactor-scan-selection")
if not design.exists():
return Action("load-reference", "cs-refactor/references/standard/protocol.md#design")
if frontmatter(design).get("status") != "approved":
return Action("user-checkpoint", "refactor-design-confirmation")
if not apply_notes.exists():
return Action("load-reference", "cs-refactor/references/standard/protocol.md#apply")
apply_state = apply_notes.read_text(encoding="utf-8")
if "step_state: applied-awaiting-human" in apply_state:
return Action("user-checkpoint", "refactor-human-validation")
if "step_state: verified" not in apply_state:
return Action("load-reference", "cs-refactor/references/standard/protocol.md#apply")
review_status = frontmatter(directory / f"{slug}-review.md").get("status")
if review_status != "passed":
return Action("load-skill", "cs-code-review")
return Action("complete", "refactor-reviewed")
def docs_next(repo: Path, request: str, mode: str | None = None) -> Action:
if "sync" in request or "neat" in request or "memory" in request:
return Action("load-skill", "cs-docs-neat")
if mode == "api" or "api" in request:
return Action("load-reference", "cs-docs/references/api/protocol.md")
guide = repo / "docs/dev/widget-guide.md"
if guide.exists() and frontmatter(guide).get("status") == "current" and "small edit" in request:
return Action("focused-edit", "docs/dev/widget-guide.md")
return Action("load-reference", "cs-docs/references/tutorial/protocol.md")
def code_review_next(repo: Path, feature_slug: str | None = None, range_arg: str | None = None) -> Action:
if range_arg:
diff = subprocess.run(["git", "diff", range_arg], cwd=repo, text=True, stdout=subprocess.PIPE, check=True).stdout
return Action("ad-hoc-range-review", range_arg) if diff else Action("blocked", "empty-range")
if feature_slug:
directory = feature_dir(repo, feature_slug)
review = directory / f"{feature_slug}-review.md"
if frontmatter(review).get("status") == "passed":
diff = git(repo, "diff", "--stat").stdout
if not diff:
design = directory / f"{feature_slug}-design.md"
lane = frontmatter(design).get("execution_lane", "standard")
downstream = "cs-feat QA" if lane == "goal" else "cs-feat acceptance-inline"
return Action("downstream", downstream)
diff = git(repo, "diff", "--stat").stdout
if diff:
return Action("run-review", f".codestable/features/2026-07-02-{feature_slug}")
return Action("blocked", "no-review-scope")
def goal_driver_decision(
*,
host_agent: bool = False,
visible: bool = False,
nested_reviewer: bool = False,
) -> Action:
if host_agent and visible and nested_reviewer:
return Action("dispatch", "host-agent")
return Action("fallback", "fenced-/goal")
def test_scenario_matrix_covers_every_refactored_skill() -> None:
covered = set().union(*SCENARIO_COVERAGE.values())
assert AFFECTED_SKILLS - covered == set()
def test_router_classifies_intake_before_selecting_a_target() -> None:
assert_doc_contains(
"cs",
"SKILL.md",
"入口模式先于路由目标",
"data Ambiguity = MissingActionIntent | RouteChoice [SkillName]",
"data IntakeMode = Execute | Advise | Explain | Ambiguous Ambiguity",
"Completed Recommendation",
"Completed Overview",
"NeedsHuman ActionIntentMissing",
"HumanCheckpoint ClarifyRoute",
"preflight 只记录缺失状态",
)
def test_router_execute_mode_continues_in_the_current_run() -> None:
assert_doc_contains(
"cs",
"SKILL.md",
"RoutedTo target",
"当前 run 继续",
"原始诉求原样传递",
"一个请求同一时刻只转交一个主入口",
)
def test_router_keeps_route_brief_small_and_mode_specific() -> None:
text = skill_text("cs")
_, separator, body = text.partition("\n---\n")
assert separator
assert "Dispatch: continuing-current-run | recommendation-only" in text
assert "route brief 只用于 Execute / Advise" in text
assert "L0-L4" not in body
assert "Route Level Quick Reference" not in body
def test_router_priority_and_recovery_boundaries_are_explicit() -> None:
assert_doc_contains(
"cs",
"SKILL.md",
"专用 workflow 优先于 `cs-goal`",
"已知优化目标",
"主动扫描未知问题",
"capability / requirement",
"canonical 决策",
"复用经验",
"features/issues/roadmap/goals/refactors/audits/brainstorms/feedback",
"`cs-onboard` 是串行前置 gate",
"目标 skill 无法加载",
)
def test_shared_dispatch_conventions_distinguish_exit_confirmation() -> None:
text = skill_text("cs-onboard", "references/execution-conventions.md")
for phrase in (
"## Skill 间同轮转交",
"已确认出口",
"待确认出口",
"不要求重新调用命令",
"转交本身不授权",
"data PendingHandoff = PendingHandoff Skill Context",
"| AwaitOwner PendingHandoff",
"resumeHandoff :: PendingHandoff -> OwnerDecision -> TargetAvailability -> HandoffOutcome",
"ApproveExit TargetAvailable = LoadAndContinue target ctx",
"DeclineExit _ = Stay ctx",
"缺输入或能力用 `NeedsHuman`",
"只需等待的外部工作用 `Awaiting`",
"才用 `HumanCheckpoint`",
):
assert phrase in text
assert "OneCheckpoint Skill" not in text
decline = "resumeHandoff (PendingHandoff _ ctx) DeclineExit _"
unavailable = "resumeHandoff (PendingHandoff target _) ApproveExit TargetUnavailable"
approved = "resumeHandoff (PendingHandoff target ctx) ApproveExit TargetAvailable"
assert text.index(decline) < text.index(unavailable) < text.index(approved)
def test_brainstorm_secondary_routes_keep_one_confirmation_gate() -> None:
text = skill_text("cs-brainstorm")
for phrase in (
"data Readiness = Fuzzy | Clear | Ready",
'route (Feature Clear) = Handoff PendingExit "cs-feat"',
'route (Feature Ready) = Handoff ConfirmedExit "cs-feat"',
'route (Epic Clear) = Handoff PendingExit "cs-epic"',
'route (Epic Ready) = Handoff ConfirmedExit "cs-epic"',
"`Ready` 只来自 owner 明确表示现在进入下一阶段",
"用户确认后在当前 run 加载 `cs-feat`",
"直接在当前 run 加载 `cs-epic`",
):
assert phrase in text
assert re.search(r"(?m)^confirmed\b", text) is None
assert "route Clear" not in text
assert "停下来等用户触发 design" not in text
assert "准备好了就触发 `cs-epic`" not in text
def test_review_outcomes_do_not_treat_waiting_or_missing_input_as_approval() -> None:
text = skill_text("cs-code-review")
protocol = skill_text("cs-code-review", "references/independent-review/protocol.md")
for phrase in (
"| Awaiting ReviewWait",
"| NeedsHuman ReviewBlocker",
"not s.specFinalized -> NeedsHuman SpecNotFinalized",
"not s.diffAttributed -> NeedsHuman DiffNotAttributable",
"Just lane <- firstLaunchableLane s -> Launching lane",
"Just wait <- firstPendingLane s -> Awaiting wait",
"HumanCheckpoint SelfReviewDowngrade",
):
assert phrase in text
for forbidden in (
"HumanCheckpoint SpecNotFinalized",
"HumanCheckpoint DiffNotAttributable",
"HumanCheckpoint LaneStillPending",
):
assert forbidden not in text
guard_order = (
"not s.specFinalized",
"not s.diffAttributed",
"anyLaneFailed s",
"firstLaunchableLane s",
"firstPendingLane s",
"focusedClosureEligible s && hasBlocking s",
"focusedClosureEligible s -> FocusedClosure Passed",
"laneAMissing s",
"hasBlocking s -> ReviewWritten ChangesRequested",
)
assert [text.index(guard) for guard in guard_order] == sorted(
text.index(guard) for guard in guard_order
)
for phrase in (
"mergeGate (Launch agent config) _ = MergeLaunch LaneA (TaskCommand agent config)",
"mergeGate _ (OcrReady command) = MergeLaunch LaneB (OcrLaneCommand command)",
"mergeGate (Await ref) _ = MergeAwaiting (LaneStillPending LaneA (TaskRunRef ref))",
"mergeGate _ (OcrActive ref) = MergeAwaiting (LaneStillPending LaneB (OcrRunRef ref))",
"mergeGate (NeedOwnerApproval reason) _ = MergeNeedsOwnerApproval reason",
):
assert phrase in protocol
merge_order = (
"mergeGate (Blocked reason) _",
"mergeGate _ laneB | failed laneB",
"mergeGate (Launch agent config) _",
"mergeGate _ (OcrReady command)",
"mergeGate (Await ref) _",
"mergeGate _ (OcrActive ref)",
"mergeGate (NeedOwnerApproval reason) _",
)
assert [protocol.index(guard) for guard in merge_order] == sorted(
protocol.index(guard) for guard in merge_order
)
assert "Blocked LaneStillPending" not in protocol
assert "pending (RunCommitted _)" not in protocol
def test_issue_fast_path_confirmation_is_persisted_and_resumable() -> None:
skill = skill_text("cs-issue")
report = skill_text("cs-issue", "references/report/protocol.md")
for phrase in (
"data IssuePath = PathUndecided | StandardPath | FastPathPending | FastPathApproved | FastPathRejected",
"issuePath : IssuePath",
"fastPathApproval approval == Pending = FastPathPending",
"issuePathField report == Just StandardPath = StandardPath",
"isNothing (issuePathField report) && reportStatus report == ArtifactConfirmed = StandardPath",
"s.reportStatus /= ArtifactConfirmed -> RoutedTo (IssueStage Report)",
"s.issuePath == FastPathApproved -> RoutedTo (IssueStage FastPath)",
"s.analysisStatus /= ArtifactConfirmed",
's.issuePath == PathUndecided -> NeedsHuman "confirmed report lacks path decision"',
"approval-report.md # 仅需 owner 决策时",
"s.fixCompletionApproval == ApprovalMissing",
"s.fixCompletionApproval == ApprovalApproved",
):
assert phrase in skill
for phrase in (
"advance Standard _ (Just ApproveCheckpoint) = PersistAndRoute StandardPath AnalyzeNext",
"advance Standard _ (Just RejectCheckpoint) = Blocked ReportRejected",
"ReviseAndCheckpoint feedback ConfirmReport",
"advance FastPath _ (Just ApproveCheckpoint) = PersistAndRoute FastPathApproved FixNext",
"advance FastPath _ (Just RejectCheckpoint) = PersistAndRoute FastPathRejected AnalyzeNext",
"ReviseAndCheckpoint feedback ConfirmFixPlan",
"PersistDraftAndCheckpoint ConfirmReport",
"PersistDraftAndCheckpoint ConfirmFixPlan",
"fast-track 不得绕过五问",
"记录 fast-path 已批准再进入 fix",
"issue_path: undecided # undecided | standard | fast-track",
):
assert phrase in report
assert "ConfirmFastPath" not in skill
guard_order = (
"s.hasFixNote && s.reviewStatus == Missing",
"not s.hasFixNote && s.reviewStatus /= Missing",
"s.reportStatus /= ArtifactConfirmed",
"s.issuePath == PathUndecided",
"s.issuePath == FastPathPending",
"s.issuePath == FastPathApproved && not",
"s.issuePath == FastPathApproved -> RoutedTo (IssueStage FastPath)",
"s.issuePath in [StandardPath, FastPathRejected] && s.analysisStatus /= ArtifactConfirmed",
)
assert [skill.index(guard) for guard in guard_order] == sorted(
skill.index(guard) for guard in guard_order
)
assert 's.issuePath == FastPathPending -> Blocked "pending fast-path approval lacks checkpoint state"' in skill
assert "ApprovalRevisionRequested Feedback" in skill
assert "ReviseFixOptions feedback" in skill_text("cs-issue", "references/analyze/protocol.md")
assert "fixCompletionApproval s is ApprovalRevisionRequested _" in skill_text("cs-issue", "references/fix/protocol.md")
def test_req_review_checkpoint_precedes_persistence() -> None:
text = skill_text("cs-req")
checkpoint = "HumanCheckpoint (newReviewCheckpoint s r)"
persist = "Drafted (persistReqAndRefreshIndex (clearPending s) draft)"
assert "data ReqInput" in text
assert "| ResumeScope Slug" in text
assert "| ResumeDraft DraftRef DraftDecision" in text
assert "pendingCheckpoint : Maybe ReqCheckpoint" in text
assert "Just (ConfirmScope r candidates) <- s.pendingCheckpoint, selected `elem` candidates" in text
assert "Just (ReviewDraft expected draft) <- s.pendingCheckpoint, ref == expected" in text
assert "NeedsHuman InvalidReqResume" in text
assert checkpoint in text
assert persist in text
assert text.index(checkpoint) < text.index(persist)
assert "writeReqThenReview" not in text
assert 's.capabilityLive == Unknown -> NeedsHuman "capability evidence missing"' in text
assert "HumanCheckpoint CapabilityUnproven" not in text
def test_docs_checkpoint_domain_matches_stage_protocols() -> None:
skill = skill_text("cs-docs")
tutorial = skill_text("cs-docs", "references/tutorial/protocol.md")
api = skill_text("cs-docs", "references/api/protocol.md")
reasons = {
"ReviewDraft",
"ReviewManifest",
"ConfirmOverwrite",
"ConfirmContractWording",
"ReviewEntry",
"ReviewSamples",
"ReviewAllDrafts",
}
assert all(reason in skill for reason in reasons)
assert 'targetAmbiguous state = NeedsHuman "which reader?"' in tutorial
assert "Just reason <- approvalGate state" in tutorial
assert "Just reason <- approvalGate s" in api
assert "ConfirmNewDoc" not in skill
assert "ConfirmReaderAndScope" not in skill
assert tutorial.index("targetAmbiguous state") < tutorial.index("Just reason <- approvalGate state")
def test_goal_owner_stop_has_resume_and_terminal_branches() -> None:
skill = skill_text("cs-goal")
reference = skill_text("cs-goal", "reference.md")
for phrase in (
"data OwnerStopState",
"| PendingStop CheckpointReason",
"| ResolvedStop GoalOwnerDecision",
"data GoalOwnerDecision = ResumeWith GoalDelta | ApproveLocalReviewFallback ApprovalRef | KeepBlocked",
"data GoalResume = ResolveGoalStop CheckpointReason GoalOwnerDecision",
"| Blocked GoalSummary",
"ResolvedStop (ResumeWith delta)",
"ResolvedStop KeepBlocked",
's.status == Blocked -> NeedsHuman "blocked goal lacks owner-stop state"',
):
assert phrase in skill
for phrase in (
"schema_version: 2",
"status: none # none | pending | resolved",
"status: pending",
"选择不继续时",
"读取 schema v1 时",
):
assert phrase in reference
terminal_order = (
"s.status == Complete && completionEvidenceReady s",
"s.ownerStop is PendingStop reason",
"ownerStopTriggered(s)",
"s.status == Active && completionEvidenceReady s",
"s.status == Active && acceptanceArtifactReady s",
"s.status == Active -> Iterating (nextMinimalAttempt s)",
)
assert [skill.index(guard) for guard in terminal_order] == sorted(
skill.index(guard) for guard in terminal_order
)
assert "s.status == Active && s.acceptancePassed -> Completed" not in skill
def test_audit_selected_finding_dispatches_in_the_current_run() -> None:
assert_doc_contains(
"cs-audit",
"SKILL.md",
"用户选中 finding 已构成确认",
"当前 run 加载 `cs-issue` 或 `cs-refactor`",
"原始 finding",
"selectedFinding",
"RoutedTo SkillName",
)
def test_feature_long_range_scenario_simulates_human_design_confirmation(tmp_path: Path) -> None:
assert_doc_contains(
"cs-feat",
"SKILL.md",
"design gate 停下来等用户确认",
"Goal lane",
"可见 Task agent goal driver 长程执行",
)
assert_doc_contains(
"cs-feat",
"references/goal/protocol.md",
"Goal 模式接管",
"CS_FEATURE_GOAL_COMPLETE",
"CS_FEATURE_GOAL_HANDOFF",
"transition Complete event",
"transition (Handoff reason) event",
)
repo = init_isolated_repo(tmp_path)
slug = "export-csv"
assert feature_next(repo, slug) == Action("load-reference", "cs-feat/references/design/protocol.md")
write_feature_design(repo, slug, status="draft", execution_lane="goal")
assert feature_next(repo, slug) == Action("load-reference", "cs-feat/references/design-review/protocol.md")
write_feature_design_review(repo, slug, status="passed")
assert feature_next(repo, slug) == Action("user-checkpoint", "feature-design-confirmation")
# Simulated human gate: user approved the reviewed design, so the workflow can continue.
replace_status(feature_dir(repo, slug) / f"{slug}-design.md", "approved")
assert feature_next(repo, slug) == Action("load-reference", "cs-feat/references/goal/protocol.md")
write_feature_goal_state(repo, slug, "implementation", "ready-to-dispatch")
assert feature_next(repo, slug) == Action("dispatch-goal-driver", "feature")
write_feature_goal_state(repo, slug, "implementation", "running", driver="host-agent")
assert feature_next(repo, slug) == Action("report-visible-driver", "host-agent-123")
def test_feature_quick_lane_is_default_for_clear_local_work_and_can_reclassify() -> None:
assert_doc_contains(
"cs-feat",
"SKILL.md",
"classifyExecutionLane",
"Quick",
"Standard",
"Goal",
"复用既有公开契约",
"目标验证入口",
"流程太重",
"重新分类",
"data QuickRunState",
"CS_FEATURE_QUICK_COMPLETE",
"CS_FEATURE_STANDARD_COMPLETE",
"先把 `execution_lane: quick`",
"已有 `goal-state.yaml` 时不得原地降级",
)
assert_doc_contains(
"cs-feat",
"references/fastforward/protocol.md",
"默认按任务事实自动选择",
"首次独立代码审查",
"不生成独立 QA / acceptance 报告",
)
def test_feature_spec_routes_roadmap_owned_children_to_epic() -> None:
assert_doc_contains(
"cs-feat",
"SKILL.md",
"roadmapOwner",
"hasRoadmapOwner(s)",
"parent `items.yaml`",
"精确目录 slug",
"features[].feature_dir",
"RoutedTo <return-to-cs-epic>",
)
def test_feature_review_restarts_only_for_material_changes() -> None:
assert_doc_contains(
"cs-feat",
"references/design-review/protocol.md",
"实质变化",
"focused closure",
"文字、编号、链接、格式",
)
assert_doc_contains(
"cs-code-review",
"SKILL.md",
"test/docs/type/metadata/nit-only",
"focused closure",
"行为、公开契约、安全、数据、并发或架构",
"完整独立复审",
)
def test_standard_implementation_proceeds_directly_to_code_review() -> None:
implementation = skill_text("cs-feat", "references/implementation/protocol.md")
assert "afterAllSteps Normal = RunCodeReview" in implementation
assert "modeForFix OwnerRequestedChanges = ReviewFixMode" in implementation
assert "modeForFix QAFix = QAFixMode" in implementation
assert "afterAllSteps ReviewFixMode = RunCodeReview" in implementation
assert "afterAllSteps QAFixMode = RunCodeReviewThenQA" in implementation
assert "afterAllSteps GoalMode = RunGatesThenCodeReview" in implementation
assert "ConfirmImplementation" not in implementation
@pytest.mark.parametrize(
("status", "route"),
[
("QAPassed", "RouteAcceptance"),
("QAFailed", "RouteQAFixThenReview"),
("QABlocked", "ResolveEnvironmentOrInputs"),
],
)
def test_behavioral_qa_keeps_existing_verdict_routes(status: str, route: str) -> None:
qa = skill_text("cs-feat", "references/qa/protocol.md")
line = next(line for line in qa.splitlines() if line.startswith(f"nextQA {status} "))
assert line.split() == ["nextQA", status, "=", route]
assert f'persistedQAStatus {status} = "{status[2:].lower()}"' in qa
def test_standard_inline_runs_the_same_behavior_contract_without_a_qa_artifact() -> None:
acceptance = skill_text("cs-feat", "references/acceptance/protocol.md")
behavioral = skill_text("cs-feat", "references/qa/behavioral-verification.md")
assert "references/qa/behavioral-verification.md" in acceptance
assert "不额外生成 QA 报告" in acceptance
assert "Standard accept-inline" in behavioral
assert "不创建 `{slug}-qa.md`" in behavioral
def test_acceptance_separates_evidence_causes_and_reaches_repeated_gap_handoff() -> None:
acceptance = skill_text("cs-feat", "references/acceptance/protocol.md")
ordered_guards = (
"coreEvidenceMissingDueToInput s",
"coreEvidenceMissingDueToEnvironment s",
"not (coreEvidencePassed s)",
"not (finalAuditPassed s) && sameFinalAuditGapCount s >= 3",
"not (allChecksPassed s)",
"| not (finalAuditPassed s) = RepairGap FinalAudit",
)
positions = [acceptance.index(guard) for guard in ordered_guards]
assert positions == sorted(positions)
assert "NeedsHuman CoreEvidenceInput" in acceptance
assert "NeedsHuman CoreEvidenceEnvironmentUnavailable" in acceptance
assert "RouteImplementation QAFix" in acceptance
assert "Handoff RepeatedFinalAuditGap" in acceptance
def test_review_focused_closure_never_masks_failed_or_pending_lanes() -> None:
review = skill_text("cs-code-review")
ordered_guards = (
"anyLaneFailed s",
"firstLaunchableLane s",
"firstPendingLane s",
"focusedClosureEligible s && hasBlocking s",
"focusedClosureEligible s -> FocusedClosure Passed",
)
positions = [review.index(guard) for guard in ordered_guards]
assert positions == sorted(positions)
assert "anyLaneFailed s -> ReviewWritten Blocked" in review
assert "s.priorIndependentReview" in review
assert "s.priorReview `elem` [Just Passed, Just ChangesRequested]" in review
assert "s.changeClass == ClosureOnly" in review
def test_refactor_human_validation_happens_after_the_step_is_applied() -> None:
protocol = skill_text("cs-refactor", "references/standard/protocol.md")
pending = "PendingApply` 必须先进入 `Run Apply`"
applied = "afterStep item HumanVerification StepApplied = PersistAndCheckpoint (HumanValidation item)"
verified = "afterStep _ HumanVerification VerificationPassed = Continue"
assert pending in protocol
assert applied in protocol
assert verified in protocol
assert protocol.index(applied) < protocol.index(verified)
@pytest.mark.parametrize(
("review_status", "expected"),
[
("changes-requested", Action("load-reference", "cs-feat/references/design/protocol.md")),
("blocked", Action("blocked", "feature-design-review-block")),
],
)
def test_feature_design_review_failure_preserves_failure_kind(
tmp_path: Path, review_status: str, expected: Action
) -> None:
repo = init_isolated_repo(tmp_path)
slug = "export-csv"
write_feature_design(repo, slug, status="draft")
write_feature_design_review(repo, slug, status=review_status)
assert feature_next(repo, slug) == expected
def test_epic_legacy_blocked_review_fails_closed_in_compact_model(tmp_path: Path) -> None:
repo = init_isolated_repo(tmp_path)
slug = "billing-system"
write_roadmap(repo, slug, status="draft")
write_roadmap_review(repo, slug, status="blocked")
assert epic_next(repo, slug) == Action("blocked", "roadmap-review-block")
@pytest.mark.parametrize(
("stage", "status", "expected"),
[
("implementation", "running", Action("load-reference", "cs-feat/references/implementation/protocol.md")),
("review", "ready", Action("load-skill", "cs-code-review")),
("review", "fixing", Action("load-reference", "cs-feat/references/implementation/protocol.md#review-fix")),
("qa", "ready", Action("load-reference", "cs-feat/references/qa/protocol.md")),
("qa", "fixing", Action("load-reference", "cs-feat/references/implementation/protocol.md#qa-fix")),
("acceptance", "ready", Action("load-reference", "cs-feat/references/acceptance/protocol.md")),
("complete", "passed", Action("complete", "CS_FEATURE_GOAL_COMPLETE")),
("handoff", "blocked", Action("user-checkpoint", "CS_FEATURE_GOAL_HANDOFF")),
],
)
def test_feature_goal_state_machine_routes_each_long_range_stage(tmp_path: Path, stage: str, status: str, expected: Action) -> None:
repo = init_isolated_repo(tmp_path)
slug = "export-csv"
write_feature_design(repo, slug, status="approved")
write_feature_design_review(repo, slug, status="passed")
write_feature_goal_state(repo, slug, stage, status)
assert feature_next(repo, slug) == expected
def test_epic_long_range_scenario_simulates_both_human_confirmations(tmp_path: Path) -> None:
assert_doc_contains(
"cs-epic",
"SKILL.md",
"roadmap review passed 但用户未确认",
"用户统一确认所有 design",
"可见 Task agent goal driver",
)
assert_doc_contains("cs-epic", "references/goal/protocol.md", "两次确认门禁", "CS_ROADMAP_GOAL_COMPLETE")
repo = init_isolated_repo(tmp_path)
slug = "billing-system"
assert epic_next(repo, slug) == Action("load-reference", "cs-epic/references/planning/protocol.md")
write_roadmap(repo, slug, status="draft")
assert epic_next(repo, slug) == Action("load-reference", "cs-epic/references/review/protocol.md")
write_roadmap_review(repo, slug, status="passed")
assert epic_next(repo, slug) == Action("user-checkpoint", "epic-roadmap-confirmation")
# Simulated human gate 1: roadmap is approved by the user.
replace_status(roadmap_dir(repo, slug) / f"{slug}-roadmap.md", "active")
assert epic_next(repo, slug) == Action("load-skill", "cs-feat design/design-review")
for child in ["api-seed", "ui-seed"]:
write_feature_design(repo, child, status="draft", roadmap=slug)
write_feature_design_review(repo, child, status="passed")
assert epic_next(repo, slug) == Action("user-checkpoint", "all-feature-designs-confirmation")
# Simulated human gate 2: all child designs are approved as a batch.
for child in ["api-seed", "ui-seed"]:
replace_status(feature_dir(repo, child) / f"{child}-design.md", "approved")
assert epic_next(repo, slug) == Action("load-reference", "cs-epic/references/goal/protocol.md")
write_epic_goal_state(repo, slug, "ready-to-dispatch")
assert epic_next(repo, slug) == Action("dispatch-goal-driver", "epic")
write_epic_goal_state(repo, slug, "running", driver="host-agent")
assert epic_next(repo, slug) == Action("report-visible-driver", "host-agent-roadmap-123")
def test_epic_child_design_batch_continues_after_one_child_review_passed(tmp_path: Path) -> None:
assert_doc_contains(
"cs-epic",
"SKILL.md",
"Child design batch loop",
"codestable-workflow-next.py epic",
"完成某一个 child 的 design + design-review `passed` 只是内部进度",
"不得 final answer",
"本轮必须继续调用 `cs-feat`",
"按 DAG 取下一个 design-ready 且缺 design、checklist",
)
# batch loop 纪律唯一权威在 SKILL.md;goal protocol 只保留 workflow-next hook 引用
assert_doc_contains(
"cs-epic",
"references/goal/protocol.md",
"codestable-workflow-next.py epic",
)
repo = init_isolated_repo(tmp_path)
slug = "billing-system"
write_roadmap(repo, slug, status="active")
write_roadmap_review(repo, slug, status="passed")
write_feature_design(repo, "api-seed", status="draft", roadmap=slug)
write_feature_design_review(repo, "api-seed", status="passed")
assert epic_next(repo, slug) == Action("load-skill", "cs-feat design/design-review")
def test_epic_goal_qa_blocked_hands_off_without_entering_review_fix() -> None:
feature_loop = skill_text("cs-epic", "references/goal/support/protocol-feature-loop.md")
assert "advance QA (Failed _) = Remediate Implementation Review" in feature_loop
assert "advance stage (Awaiting ref) = WaitFor stage ref" in feature_loop
assert "advance stage (NeedsHuman reason) = RequestHuman stage reason" in feature_loop
assert "advance stage (Blocked reason) = HandoffStage stage reason" in feature_loop
assert "advance QA _" not in feature_loop
def test_epic_goal_revalidates_commit_authorization_before_each_commit() -> None:
feature_loop = skill_text("cs-epic", "references/goal/support/protocol-feature-loop.md")
goal_protocol = skill_text("cs-epic", "references/goal/support/protocol.md")
final_audit = skill_text("cs-epic", "references/goal/support/protocol-audit.md")
for document in (feature_loop, goal_protocol):
assert "codestable-workflow-next.py epic --roadmap <roadmap-dir> --json" in document
assert "approval-report.md#goal-acceptance" in document
assert "approval-report.md#goal-commits" in document
assert "不得执行 `git commit`" in feature_loop
assert feature_loop.index("`current_feature_index` 加 1") < feature_loop.index(
"再次运行 `python3 <cs-onboard skill 目录>/tools/codestable-workflow-next.py epic"
) < feature_loop.index("scoped-commit 本 feature")
assert "只有所有状态更新之后工作树仍干净" in feature_loop
assert "goalAuthorizationsValid" in final_audit
assert "goal-acceptance" in final_audit and "goal-commits" in final_audit
def test_epic_goal_audit_hands_off_unapproved_h_only_evidence_immediately() -> None:
audit = skill_text("cs-epic", "references/goal/support/protocol-audit.md")
goal = skill_text("cs-epic", "references/goal/support/protocol.md")
gates = skill_text("cs-epic", "references/goal/support/protocol-gates.md")
assert "not (noUnapprovedHOnlyCoreCheck a) = AuditHandoff UnapprovedHOnlyCoreCheck" in audit
assert "sameAuditFailureCount a >= 3 = AuditHandoff RepeatedFailure" in audit
assert "AuditRepeatedFailure" not in audit
assert "unapprovedHOnlyCoreCheck s = Just UnapprovedHOnlyCoreCheck" in goal
assert "recover QA = FixQAThenReview" in gates
assert "| FixQA |" not in gates
def test_roadmap_item_enters_in_progress_when_design_starts() -> None:
planning = skill_text("cs-epic", "references/planning/reference.md")
shared = skill_text("cs-onboard", "references/shared-conventions.md")
assert "roadmap item 生命周期以项目 `.codestable/reference/shared-conventions.md`" in planning
assert "不复制 transition" in planning
assert "data ItemEvent" not in planning
assert "data ItemEvent = StartDesign | AcceptFeature | OwnerDrops Reason" in shared
assert "transition Planned StartDesign" in shared
assert "StartFeature" not in shared
def test_workflow_runtime_details_are_centralized_in_preflight() -> None:
assert_doc_contains(
"cs-onboard",
"SKILL.md",
"--mode refresh-runtime",
"可重复执行",
"不重新审计 / 迁移文档",
"selectOnboardPath RefreshRuntime Installed _ = Refresh",
".codestable/runtime-manifest.json",
"codestable-runtime-sync.py",
"不移动用户文件",
)
assert_doc_contains(
"cs-onboard",
"references/execution-conventions.md",
"Runtime 资产恢复",
"runtime capability",
".codestable/runtime-manifest.json",
"recoverRuntime RuntimeIncomplete = SyncRuntime",
"recoverRuntime ManagedPathsDirty = Stop ManagedRuntimeDirty",
"--check --json",
"去掉 `--check`",
"`workflow-next`",
"managed-paths-dirty",
"不自动覆盖",
".codestable/reference/agent-conventions.md",
)
assert_doc_contains(
"cs-feat",
"SKILL.md",
"codestable-workflow-next.py feature",
)
assert_doc_contains(
"cs-epic",
"SKILL.md",
"codestable-workflow-next.py epic",
"若 preflight 刚完成 runtime 同步,从仓库事实恢复 batch loop",
)
assert_doc_contains(
"cs-epic",
"references/goal/protocol.md",
"codestable-workflow-next.py epic",
)
def test_goal_driver_selection_requires_visibility_and_nested_reviewer() -> None:
assert_doc_contains(
"cs-onboard",
"references/agent-conventions.md",
"visibleHostDriver e && canSpawnReviewer e",
'PrintGoal "/goal"',
)
assert goal_driver_decision(host_agent=True, visible=True, nested_reviewer=True) == Action("dispatch", "host-agent")
assert goal_driver_decision(host_agent=True, visible=True, nested_reviewer=False) == Action("fallback", "fenced-/goal")
assert goal_driver_decision(host_agent=True, visible=False, nested_reviewer=True) == Action("fallback", "fenced-/goal")
assert goal_driver_decision() == Action("fallback", "fenced-/goal")
def test_task_agent_lifecycle_closes_consumed_agents_and_cleans_only_on_capacity_failure() -> None:
assert_doc_contains(
"cs-onboard",
"references/agent-conventions.md",
"## Task Agent 生命周期",
"data CreateRecovery = RetryCreate | CreateBlocked Reason",
"terminal s && resultConsumed s && not (permissionPending s)",
"recoverCreate CapacityExhausted",
"closeOldest mayClose >> RetryCreate",
"recoverCreate reason = CreateBlocked reason",
)
assert_doc_contains(
"cs-goal",
"SKILL.md",
"按 Task agent 生命周期关闭该验收 agent",
"先按 Task agent 生命周期处理容量失败重试",
)
assert_doc_contains(
"cs-goal",
"reference.md",
"Task agent id / run id、关闭结果或关闭失败 warning",
"Task agent 结果消费后",
)
assert_doc_contains(
"cs-code-review",
"references/independent-review/protocol.md",
"Task agent 生命周期关闭该 reviewer",
"只在失败后按最老已完成 agent 清理并重试一次",
)
assert_doc_contains("cs-feat", "references/qa/protocol.md", "Task agent 生命周期关闭该 runner")
assert_doc_contains("cs-feat", "references/acceptance/protocol.md", "auditor 输出被消费后按 Task agent 生命周期关闭")
assert_doc_contains("cs-epic", "references/goal/support/protocol-gates.md", "结果消费后按 Task agent 生命周期关闭")
def test_issue_scenario_progresses_through_main_entry_references(tmp_path: Path) -> None:
assert_doc_contains("cs-issue", "SKILL.md", "report、analyze、fix、review", "仓库事实")
repo = init_isolated_repo(tmp_path)
slug = "login-error"
assert issue_next(repo, slug) == Action("load-reference", "cs-issue/references/report/protocol.md")
write_issue_doc(repo, slug, "report")
assert issue_next(repo, slug) == Action("user-checkpoint", "issue-report-confirmation")
replace_status(issue_dir(repo, slug) / f"{slug}-report.md", "confirmed")
assert issue_next(repo, slug) == Action("load-reference", "cs-issue/references/analyze/protocol.md")
write_issue_doc(repo, slug, "analysis")
assert issue_next(repo, slug) == Action("user-checkpoint", "issue-fix-plan-confirmation")
replace_status(issue_dir(repo, slug) / f"{slug}-analysis.md", "confirmed")
assert issue_next(repo, slug) == Action("load-reference", "cs-issue/references/fix/protocol.md")
write_issue_doc(repo, slug, "fix-note")
assert issue_next(repo, slug) == Action("load-skill", "cs-code-review")
write_issue_doc(repo, slug, "review", status="changes-requested")
assert issue_next(repo, slug) == Action("load-reference", "cs-issue/references/fix/protocol.md#review-fix")
replace_status(issue_dir(repo, slug) / f"{slug}-review.md", "passed")
assert issue_next(repo, slug) == Action(
"load-reference", "cs-issue/references/fix/protocol.md#completion-checkpoint"
)
write(
issue_dir(repo, slug) / "approval-report.md",
"---\ndoc_type: approval-report\nstatus: pending\n---\n# Approval\n",
)
assert issue_next(repo, slug) == Action("user-checkpoint", "issue-fix-completion")
replace_status(issue_dir(repo, slug) / "approval-report.md", "approved")
assert issue_next(repo, slug) == Action("complete", "issue-reviewed")
def test_refactor_scenario_respects_scan_design_and_human_validation_gates(tmp_path: Path) -> None:
assert_doc_contains("cs-refactor", "SKILL.md", "scan → 用户勾选 → design → 用户确认 → apply → cs-code-review")
assert_doc_contains("cs-refactor", "references/standard/protocol.md", "用户勾选", "HUMAN 验证")
repo = init_isolated_repo(tmp_path)
slug = "split-helper"
assert refactor_next(repo, slug, mode="fastforward", small_scope=True) == Action(
"load-reference", "cs-refactor/references/fastforward/protocol.md"
)
assert refactor_next(repo, slug) == Action("load-reference", "cs-refactor/references/standard/protocol.md#scan")
write_refactor_doc(repo, slug, "scan")
assert refactor_next(repo, slug) == Action("user-checkpoint", "refactor-scan-selection")
write(refactor_dir(repo, slug) / f"{slug}-scan.md", "selected: true\n")
assert refactor_next(repo, slug) == Action("load-reference", "cs-refactor/references/standard/protocol.md#design")
write_refactor_doc(repo, slug, "refactor-design", status="draft")
write(refactor_dir(repo, slug) / f"{slug}-checklist.yaml", "steps:\n - id: visual\n verification: HUMAN\n")
assert refactor_next(repo, slug) == Action("user-checkpoint", "refactor-design-confirmation")
replace_status(refactor_dir(repo, slug) / f"{slug}-refactor-design.md", "approved")
assert refactor_next(repo, slug) == Action("load-reference", "cs-refactor/references/standard/protocol.md#apply")
write_refactor_doc(repo, slug, "apply-notes")
with (refactor_dir(repo, slug) / f"{slug}-apply-notes.md").open("a", encoding="utf-8") as handle:
handle.write("step_state: applied-awaiting-human\n")
assert refactor_next(repo, slug) == Action("user-checkpoint", "refactor-human-validation")
apply_notes = refactor_dir(repo, slug) / f"{slug}-apply-notes.md"
apply_notes.write_text(
apply_notes.read_text(encoding="utf-8").replace("applied-awaiting-human", "verified"),
encoding="utf-8",
)
assert refactor_next(repo, slug) == Action("load-skill", "cs-code-review")
def test_refactor_low_candidate_scan_does_not_enter_design() -> None:
protocol = skill_text("cs-refactor", "references/standard/protocol.md")
spec = protocol.split("```haskell", 1)[1].split("```", 1)[0]
scan_missing = "not (scanExists s) = Run Scan"
low_candidates = "candidateCount s < 3 && fastforwardEligible s"
selection_gate = "not (selectionConfirmed s) = Checkpoint ScanSelection"
assert scan_missing in spec
assert low_candidates in spec
assert selection_gate in spec
assert spec.index(scan_missing) < spec.index(low_candidates) < spec.index(selection_gate)
def test_docs_scenario_selects_docs_entry_or_neat_hygiene(tmp_path: Path) -> None:
assert_doc_contains("cs-docs", "SKILL.md", "不做全局同步", "转 `cs-docs-neat`")
assert_doc_contains("cs-docs-neat", "SKILL.md", "不是 `cs-docs`", "Phase 1:机械式枚举")
repo = init_isolated_repo(tmp_path)
assert docs_next(repo, "write api docs", mode="api") == Action("load-reference", "cs-docs/references/api/protocol.md")
assert docs_next(repo, "write a developer guide") == Action(
"load-reference", "cs-docs/references/tutorial/protocol.md"
)
write(
repo / "docs/dev/widget-guide.md",
"---\ndoc_type: dev-guide\nstatus: current\n---\n# Widget Guide\n",
)
assert docs_next(repo, "small edit to widget guide") == Action("focused-edit", "docs/dev/widget-guide.md")
assert docs_next(repo, "sync memory and README") == Action("load-skill", "cs-docs-neat")
def test_code_review_scenario_handles_feature_diff_and_ad_hoc_range(tmp_path: Path) -> None:
assert_doc_contains(
"cs-code-review",
"SKILL.md",
"ad-hoc range 审查允许工作区干净",
"横切代码审查 gate",
"references/independent-review/protocol.md",
)
repo = init_isolated_repo(tmp_path)
slug = "export-csv"
write_feature_design(repo, slug, status="approved")
write_feature_design_review(repo, slug, status="passed")
write(feature_dir(repo, slug) / f"{slug}-checklist.yaml", "steps:\n - id: step-1\n status: done\nchecks:\n - id: check-1\n status: pending\n")
write(repo / "src/app.py", "def hello():\n return 'hello export'\n")
assert code_review_next(repo, feature_slug=slug) == Action("run-review", ".codestable/features/2026-07-02-export-csv")
git(repo, "add", ".")
git(repo, "commit", "-m", "feature change")
assert code_review_next(repo, range_arg="HEAD~1..HEAD") == Action("ad-hoc-range-review", "HEAD~1..HEAD")
write(
feature_dir(repo, slug) / f"{slug}-review.md",
"---\ndoc_type: feature-review\nstatus: passed\nreviewer: subagent\n---\n# Review\n",
)
git(repo, "add", ".")
git(repo, "commit", "-m", "add review")
assert code_review_next(repo, feature_slug=slug) == Action("downstream", "cs-feat acceptance-inline")
@pytest.mark.parametrize("entry,expected", sorted(COMPATIBILITY_ENTRIES.items()))
def test_compatibility_entry_scenarios_delegate_without_rerun(entry: str, expected: tuple[str, str, str]) -> None:
main, key, value = expected
text = skill_text(entry)
assert f"加载 `{main}`" in text
assert f"{key}: {value}" in text
assert "不要要求用户重新调用" in text
assert "不维护独立流程规则" in text
assert "../" not in text
def test_solution_depth_prepass_is_authoritative_and_reachable() -> None:
# 权威源:方案深度 pre-pass 单一定义(防降本默认偏置:最小闭环/fake/正则),
# 硬约束 = 采用降级必须显式论证 + 转正条件。
assert_doc_contains(
"cs-onboard",
"references/solution-depth-conventions.md",
"方案深度 pre-pass",
"场景适配",
"最小闭环",
"fake",
"转正条件",
"不接受",
)
# 有 design 的选型阶段必须触达权威源(现状缺触达路径,指针不可删)。
assert_doc_contains("cs-feat", "references/design/protocol.md", "solution-depth-conventions", "方案深度")
assert_doc_contains("cs-epic", "references/planning/protocol.md", "solution-depth-conventions", "方案深度")
assert_doc_contains("cs-goal", "SKILL.md", "solution-depth-conventions", "方案深度 pre-pass")
# 快速路径(fastforward / fix)最易"够跑就行",同样必须触达(codex review imp-3)。
assert_doc_contains("cs-feat", "references/fastforward/protocol.md", "solution-depth-conventions")
assert_doc_contains("cs-issue", "references/fix/protocol.md", "solution-depth-conventions")
assert_doc_contains("cs-refactor", "references/fastforward/protocol.md", "solution-depth-conventions")
# shared-conventions 第 7 节结构反射检查与选型层降级互补,交叉引用不可断。
assert_doc_contains("cs-onboard", "references/shared-conventions.md", "solution-depth-conventions")
def test_readonly_task_agent_capability_is_provider_aware_and_reachable() -> None:
# 只读 mode 的唯一权威在共享 selector;阶段协议只组合 selector,不复制规则。
assert_doc_contains(
"cs-onboard",
"references/agent-conventions.md",
"data Isolation = Heterogeneous | Independent",
"data ReadOnlyControl = EnforcedReadOnly | VerifiedNoWrite",
"data AgentSelection",
"hostAgentCapabilities",
"readOnlyControlled",
"selectTaskAgent",
"reviewGate :: AgentSelection -> AgentRun",
)
for skill, path in (
("cs-feat", "references/design-review/protocol.md"),
("cs-epic", "references/review/protocol.md"),
("cs-code-review", "references/independent-review/protocol.md"),
):
assert_doc_contains(skill, path, "selectTaskAgent Review", "reviewGate")
assert "runReviewer" not in skill_text(skill, path)
# 其他只读 Task agent 启动点也须触达共享 mode 规则。
assert_doc_contains("cs-feat", "references/qa/protocol.md", "read-only 等价 mode")
assert_doc_contains("cs-feat", "references/acceptance/protocol.md", "read-only 等价 mode")
assert_doc_contains("cs-goal", "SKILL.md", "read-only 等价 mode")
def test_review_selector_preserves_guard_order_and_scope() -> None:
assert_doc_contains(
"cs-onboard",
"references/agent-conventions.md",
"Heterogeneous",
"Independent",
"hostAgentCapabilities",
"不依赖 backend 产品名或工具名",
"ExplicitConfigUnavailable",
)
selector = skill_text("cs-onboard", "references/agent-conventions.md")
selection_order = (
"Just agent <- bestFit",
"= Start agent config",
"SelectionBlocked ExplicitConfigUnavailable",
"SelectionNeedsOwnerApproval IndependentAgentUnavailable",
)
positions = [selector.index(fragment) for fragment in selection_order]
assert positions == sorted(positions)
review_gate_order = (
"reviewGate _ (Finished findings)",
"reviewGate _ (Active ref)",
"reviewGate _ (Failed _) (Just ApproveLocalOnly)",
"reviewGate _ (Failed reason) _",
"reviewGate (SelectionBlocked reason) NotStarted",
"reviewGate (SelectionNeedsOwnerApproval _) NotStarted (Just ApproveLocalOnly)",
"reviewGate (SelectionNeedsOwnerApproval reason) NotStarted",
"reviewGate (Start agent config) NotStarted",
)
review_gate_positions = [selector.index(fragment) for fragment in review_gate_order]
assert review_gate_positions == sorted(review_gate_positions)
assert "toReviewLane (NeedOwnerApproval reason) = Left reason" in selector
assert "data ReviewVerdict = Passed | ChangesRequested | ReviewBlocked Reason" in selector
assert_doc_contains(
"cs-code-review",
"references/independent-review/protocol.md",
"dirtyPaths scope `isSubsetOf` currentScope scope",
"OcrSkippedByUser",
"OcrReady (RunCommitted range)",
"OcrActive Text",
"MergeLaunch LaneName LaneCommand",
"failed (OcrFailed _)",
"finished (OcrFinished _)",
"verifiedFindings (IndependentFindings findings) laneB",
"mergeableDecision (MergeVerified findings)",
"`ApproveLocalOnly` 是 owner 的人类授权",
"环境变量是 runner 的机械 opt-in",
)
independent_review = skill_text("cs-code-review", "references/independent-review/protocol.md")
assert "otherwise = Blocked IndependentReviewRequired" not in independent_review
assert "reviewerField LocalReview" not in independent_review
def test_cs_skills_depend_on_host_agent_behavior_not_a_backend_tool_name() -> None:
selector = skill_text("cs-onboard", "references/agent-conventions.md")
assert "hostAgentCapabilities" in selector
assert "Heterogeneous" in selector
assert "Independent" in selector
assert "异构候选不可用不阻塞独立 review" in selector
for markdown in SKILLS.rglob("*.md"):
text = markdown.read_text(encoding="utf-8")
assert "cs_agent_" not in text, markdown
assert "CS Agent Facade" not in text, markdown
def test_runtime_reference_copies_match_templates() -> None:
# runtime 可保留 legacy-only 文件;每个 onboard 模板都必须存在同名逐字副本。
templates = SKILLS / "cs-onboard" / "references"
runtime = ROOT / ".codestable" / "reference"
for src in sorted(templates.rglob("*.md")):
relative = src.relative_to(templates)
copy = runtime / relative
assert copy.exists(), f"{relative}: 缺 runtime reference 副本"
assert src.read_text(encoding="utf-8") == copy.read_text(encoding="utf-8"), (
f"{relative}: 模板与项目副本不一致,需 sync"
)