mirror of
https://github.com/codestable/CodeStable.git
synced 2026-09-19 09:03:09 +08:00
2752 lines
102 KiB
Python
2752 lines
102 KiB
Python
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
|
|
TOOLS_DIR = Path(__file__).resolve().parents[1] / "plugins/codestable/skills/cs-onboard/tools"
|
|
sys.path.insert(0, str(TOOLS_DIR))
|
|
|
|
|
|
def load_tool():
|
|
spec = importlib.util.spec_from_file_location("codestable_workflow_next", TOOLS_DIR / "codestable-workflow-next.py")
|
|
assert spec and spec.loader
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
workflow_next = load_tool()
|
|
|
|
|
|
def write(path: Path, text: str) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(text, encoding="utf-8")
|
|
|
|
|
|
def init_repo(tmp_path: Path) -> Path:
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / ".codestable").mkdir()
|
|
return repo
|
|
|
|
|
|
def write_roadmap(repo: Path, status: str = "active") -> Path:
|
|
roadmap = repo / ".codestable/roadmap/billing-system"
|
|
write(
|
|
roadmap / "billing-system-roadmap.md",
|
|
f"---\ndoc_type: roadmap\nslug: billing-system\nstatus: {status}\n---\n# Roadmap\n",
|
|
)
|
|
write(roadmap / "billing-system-roadmap-review.md", "---\ndoc_type: roadmap-review\nstatus: passed\n---\n# Review\n")
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
" status: planned\n"
|
|
" feature: null\n"
|
|
" - slug: ui-seed\n"
|
|
" status: planned\n"
|
|
" feature: null\n",
|
|
)
|
|
return roadmap
|
|
|
|
|
|
def write_feature(
|
|
repo: Path,
|
|
slug: str,
|
|
*,
|
|
design_status: str = "draft",
|
|
review_status: str = "passed",
|
|
review_state: str | None = None,
|
|
review_reason: str = "",
|
|
reviewer_id: str = "",
|
|
execution_lane: str | None = None,
|
|
execution_lane_reason: str | None = None,
|
|
include_roadmap: bool = False,
|
|
) -> Path:
|
|
feature = repo / ".codestable/features" / f"2026-07-02-{slug}"
|
|
roadmap_fields = f"roadmap: billing-system\nroadmap_item: {slug}\n" if include_roadmap else ""
|
|
lane_field = f"execution_lane: {execution_lane}\n" if execution_lane else ""
|
|
lane_reason_field = f"execution_lane_reason: {execution_lane_reason}\n" if execution_lane_reason else ""
|
|
review_state_fields = ""
|
|
if review_state is not None:
|
|
review_state_fields = (
|
|
f"review_state: {review_state}\n"
|
|
f'review_reason: "{review_reason}"\n'
|
|
f'reviewer_id: "{reviewer_id}"\n'
|
|
)
|
|
write(
|
|
feature / f"{slug}-design.md",
|
|
f"---\ndoc_type: feature-design\nfeature: 2026-07-02-{slug}\n"
|
|
f"{roadmap_fields}{lane_field}{lane_reason_field}"
|
|
f"status: {design_status}\n---\n# Design\n",
|
|
)
|
|
write(feature / f"{slug}-checklist.yaml", "steps:\n - id: step-1\n status: pending\n")
|
|
write(
|
|
feature / f"{slug}-design-review.md",
|
|
f"---\ndoc_type: feature-design-review\nstatus: {review_status}\n"
|
|
f"{review_state_fields}---\n# Review\n",
|
|
)
|
|
return feature
|
|
|
|
|
|
def write_goal_state(
|
|
directory: Path,
|
|
*,
|
|
status: str,
|
|
stage: str | None = None,
|
|
driver_kind: str = "none",
|
|
driver_id: str = "",
|
|
handoff_reason: str = "",
|
|
handoff_next: str = "",
|
|
acceptance_authorization: str | None = "approved",
|
|
acceptance_authorization_ref: str = "approval-report.md#goal-acceptance",
|
|
approval_decision_status: str | None = "approved",
|
|
commit_authorization: str | None = "approved",
|
|
commit_authorization_ref: str = "approval-report.md#goal-commits",
|
|
commit_decision_status: str | None = "approved",
|
|
) -> None:
|
|
lines = [f"status: {status}"]
|
|
if stage is not None:
|
|
lines.append(f"stage: {stage}")
|
|
lines.extend(
|
|
[
|
|
f"driver_kind: {driver_kind}",
|
|
f'driver_id: "{driver_id}"',
|
|
f'handoff_reason: "{handoff_reason}"',
|
|
f'handoff_next: "{handoff_next}"',
|
|
]
|
|
)
|
|
if acceptance_authorization is not None:
|
|
lines.extend(
|
|
[
|
|
f"acceptance_authorization: {acceptance_authorization}",
|
|
f'acceptance_authorization_ref: "{acceptance_authorization_ref}"',
|
|
]
|
|
)
|
|
if commit_authorization is not None:
|
|
lines.extend(
|
|
[
|
|
f"commit_authorization: {commit_authorization}",
|
|
f'commit_authorization_ref: "{commit_authorization_ref}"',
|
|
]
|
|
)
|
|
write(directory / "goal-state.yaml", "\n".join(lines) + "\n")
|
|
approval_rows: list[str] = []
|
|
if acceptance_authorization == "approved" and approval_decision_status is not None:
|
|
approval_rows.append(f" goal-acceptance: {approval_decision_status}")
|
|
if commit_authorization == "approved" and commit_decision_status is not None:
|
|
approval_rows.append(f" goal-commits: {commit_decision_status}")
|
|
if approval_rows:
|
|
write(
|
|
directory / "approval-report.md",
|
|
"---\ndoc_type: approval-report\nstatus: approved\napprovals:\n"
|
|
+ "\n".join(approval_rows)
|
|
+ "\n---\n# Approval\n",
|
|
)
|
|
|
|
|
|
def write_code_review(
|
|
feature: Path,
|
|
slug: str,
|
|
*,
|
|
status: str = "passed",
|
|
reviewer: str | None = "subagent",
|
|
doc_type: str = "feature-review",
|
|
) -> None:
|
|
reviewer_field = f"reviewer: {reviewer}\n" if reviewer is not None else ""
|
|
write(
|
|
feature / f"{slug}-review.md",
|
|
f"---\ndoc_type: {doc_type}\nstatus: {status}\n{reviewer_field}---\n# Review\n",
|
|
)
|
|
|
|
|
|
def write_ff_note(feature: Path, slug: str) -> None:
|
|
write(
|
|
feature / f"{slug}-ff-note.md",
|
|
f"---\ndoc_type: feature-ff-note\nfeature: {slug}\ndate: 2026-07-13\n---\n# Fastforward Note\n",
|
|
)
|
|
|
|
|
|
def write_roadmap_goal_state(
|
|
roadmap: Path,
|
|
*,
|
|
feature_slug: str = "api-seed",
|
|
status: str = "ready-to-dispatch",
|
|
driver_kind: str = "host-agent",
|
|
driver_id: str = "epic-run-123",
|
|
acceptance_authorization: str | None = "approved",
|
|
approval_decision_status: str | None = "approved",
|
|
commit_authorization: str | None = "approved",
|
|
commit_decision_status: str | None = "approved",
|
|
execution_confirmation_id: str | None = None,
|
|
approval_group_status: str | None = None,
|
|
approval_group_confirmation_id: str = "",
|
|
) -> None:
|
|
lines = [
|
|
"roadmap: billing-system",
|
|
f"status: {status}",
|
|
f"driver_kind: {driver_kind}",
|
|
f'driver_id: "{driver_id}"',
|
|
]
|
|
if execution_confirmation_id is not None:
|
|
lines.append(f'execution_confirmation_id: "{execution_confirmation_id}"')
|
|
if acceptance_authorization is not None:
|
|
lines.extend(
|
|
[
|
|
f"acceptance_authorization: {acceptance_authorization}",
|
|
'acceptance_authorization_ref: "approval-report.md#goal-acceptance"',
|
|
]
|
|
)
|
|
if commit_authorization is not None:
|
|
lines.extend(
|
|
[
|
|
f"commit_authorization: {commit_authorization}",
|
|
'commit_authorization_ref: "approval-report.md#goal-commits"',
|
|
]
|
|
)
|
|
lines.extend(
|
|
[
|
|
"current_feature_index: 0",
|
|
"features:",
|
|
f" - slug: {feature_slug}",
|
|
f" roadmap_item: {feature_slug}",
|
|
f" feature_dir: .codestable/features/2026-07-02-{feature_slug}",
|
|
" status: implementing",
|
|
]
|
|
)
|
|
write(roadmap / "goal-state.yaml", "\n".join(lines) + "\n")
|
|
approval_rows = []
|
|
if approval_decision_status is not None:
|
|
approval_rows.append(f" goal-acceptance: {approval_decision_status}")
|
|
if commit_decision_status is not None:
|
|
approval_rows.append(f" goal-commits: {commit_decision_status}")
|
|
if approval_rows or approval_group_status is not None:
|
|
approvals = (
|
|
"approvals: {}\n"
|
|
if not approval_rows
|
|
else "approvals:\n" + "\n".join(approval_rows) + "\n"
|
|
)
|
|
approval_group = ""
|
|
if approval_group_status is not None:
|
|
approval_group = (
|
|
"approval_groups:\n"
|
|
" goal-execution:\n"
|
|
f" status: {approval_group_status}\n"
|
|
f' confirmation_id: "{approval_group_confirmation_id}"\n'
|
|
" decisions:\n"
|
|
" - goal-acceptance\n"
|
|
" - goal-commits\n"
|
|
)
|
|
write(
|
|
roadmap / "approval-report.md",
|
|
"---\ndoc_type: approval-report\nstatus: approved\n"
|
|
+ approvals
|
|
+ approval_group
|
|
+ "---\n# Approval\n",
|
|
)
|
|
|
|
|
|
def write_reverse_owner_state(
|
|
repo: Path,
|
|
roadmap_slug: str,
|
|
*,
|
|
state_roadmap: str | None = None,
|
|
rows: list[tuple[str | None, str]],
|
|
) -> Path:
|
|
lines = [
|
|
f"roadmap: {state_roadmap or roadmap_slug}",
|
|
"status: ready-to-dispatch",
|
|
"features:",
|
|
]
|
|
for item, feature_dir in rows:
|
|
lines.append(f" - slug: {item}" if item else " - status: implementing")
|
|
if item:
|
|
lines.append(f" roadmap_item: {item}")
|
|
lines.append(f" feature_dir: {feature_dir}")
|
|
if item:
|
|
lines.append(" status: implementing")
|
|
goal_state = repo / ".codestable/roadmap" / roadmap_slug / "goal-state.yaml"
|
|
write(goal_state, "\n".join(lines) + "\n")
|
|
return goal_state
|
|
|
|
|
|
def run_cli_json(repo: Path, workflow: str, path: Path) -> tuple[subprocess.CompletedProcess[str], dict[str, object]]:
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
(TOOLS_DIR / "codestable-workflow-next.py").as_posix(),
|
|
workflow,
|
|
f"--{workflow if workflow == 'feature' else 'roadmap'}",
|
|
path.as_posix(),
|
|
"--json",
|
|
],
|
|
cwd=repo,
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env={"PYTHONDONTWRITEBYTECODE": "1"},
|
|
timeout=10,
|
|
)
|
|
return completed, json.loads(completed.stdout)
|
|
|
|
|
|
def test_epic_continues_when_only_first_child_design_review_passed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", include_roadmap=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["ok"] is True
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat design/design-review"
|
|
assert result["must_continue"] is True
|
|
assert result["final_answer_allowed"] is False
|
|
assert result["evidence"]["next_item"]["item"] == "ui-seed"
|
|
|
|
|
|
def test_epic_child_batch_design_admits_dependent_after_predecessor_review(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
" status: in-progress\n"
|
|
" feature: null\n"
|
|
" - slug: ui-seed\n"
|
|
" status: planned\n"
|
|
" depends_on: [api-seed]\n"
|
|
" feature: null\n",
|
|
)
|
|
write_feature(repo, "api-seed", include_roadmap=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat design/design-review"
|
|
assert result["evidence"]["next_item"]["item"] == "ui-seed"
|
|
assert result["evidence"]["design_admission"] == {
|
|
"dependencies": [
|
|
{
|
|
"item": "api-seed",
|
|
"item_status": "in-progress",
|
|
"design_review_status": "passed",
|
|
"design_ready": True,
|
|
"ready_by": "design-review",
|
|
"reason": None,
|
|
}
|
|
],
|
|
"ready": True,
|
|
"review_state": "missing",
|
|
"review_reason": None,
|
|
}
|
|
|
|
|
|
def test_epic_child_batch_selects_unblocked_predecessor_from_unsorted_dag(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: ui-seed\n"
|
|
" status: planned\n"
|
|
" depends_on: [api-seed]\n"
|
|
" feature: null\n"
|
|
" - slug: api-seed\n"
|
|
" status: planned\n"
|
|
" feature: null\n",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["evidence"]["next_item"]["item"] == "api-seed"
|
|
|
|
|
|
def test_epic_child_batch_blocks_cyclic_design_dependencies(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
" status: planned\n"
|
|
" depends_on: [ui-seed]\n"
|
|
" feature: null\n"
|
|
" - slug: ui-seed\n"
|
|
" status: planned\n"
|
|
" depends_on: [api-seed]\n"
|
|
" feature: null\n",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-roadmap-items"
|
|
assert result["evidence"]["cycle_items"] == ["api-seed", "ui-seed"]
|
|
|
|
|
|
def test_epic_blocks_cycle_even_when_all_design_reviews_passed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: api-seed\n status: in-progress\n depends_on: [ui-seed]\n"
|
|
" - slug: ui-seed\n status: in-progress\n depends_on: [api-seed]\n",
|
|
)
|
|
write_feature(repo, "api-seed", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", include_roadmap=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-roadmap-items"
|
|
assert "cycle" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("depends_on", "reason"),
|
|
[
|
|
("[missing-seed]", "missing item missing-seed"),
|
|
('"api-seed"', "must be a list"),
|
|
("[api-seed, api-seed]", "must not contain duplicate"),
|
|
],
|
|
)
|
|
def test_epic_rejects_invalid_dependency_graph(
|
|
tmp_path: Path,
|
|
depends_on: str,
|
|
reason: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: api-seed\n status: planned\n"
|
|
f" - slug: ui-seed\n status: planned\n depends_on: {depends_on}\n",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-roadmap-items"
|
|
assert reason in result["reason"]
|
|
|
|
|
|
def test_epic_rejects_duplicate_item_slug(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: api-seed\n status: planned\n"
|
|
" - slug: api-seed\n status: planned\n",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-roadmap-items"
|
|
assert "duplicate roadmap item slug" in result["reason"]
|
|
|
|
|
|
def test_epic_rejects_whitespace_padded_item_slug_without_traceback(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
'roadmap: billing-system\nitems:\n - slug: " api-seed "\n status: planned\n',
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["evidence"]["next_item"]["item"] == " api-seed "
|
|
|
|
|
|
def test_epic_dependency_ambiguous_feature_lookup_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: ui-seed\n status: planned\n depends_on: [api-seed]\n"
|
|
" - slug: api-seed\n status: in-progress\n",
|
|
)
|
|
(repo / ".codestable/features/2026-07-01-api-seed").mkdir(parents=True)
|
|
(repo / ".codestable/features/2026-07-02-api-seed").mkdir(parents=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-roadmap-items"
|
|
assert "multiple feature directories" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize("dependency_status", ["done", "dropped"])
|
|
def test_epic_design_admits_terminal_dependency_state(tmp_path: Path, dependency_status: str) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
f" - slug: api-seed\n status: {dependency_status}\n"
|
|
" - slug: ui-seed\n status: planned\n depends_on: [api-seed]\n",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["evidence"]["next_item"]["item"] == "ui-seed"
|
|
assert result["evidence"]["design_admission"]["dependencies"][0]["ready_by"] == dependency_status
|
|
|
|
|
|
def test_epic_blocks_dropped_dependency_before_batch_confirmation(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: api-seed\n status: dropped\n"
|
|
" - slug: ui-seed\n status: in-progress\n depends_on: [api-seed]\n",
|
|
)
|
|
write_feature(repo, "ui-seed", include_roadmap=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "resolve-dropped-implementation-dependencies"
|
|
assert result["evidence"]["dependency_blockers"] == [
|
|
{"item": "ui-seed", "dropped_dependencies": ["api-seed"]}
|
|
]
|
|
|
|
|
|
def test_feature_implementation_gate_requires_all_dependencies_done(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: api-seed\n status: in-progress\n"
|
|
" - slug: ui-seed\n status: in-progress\n depends_on: [api-seed]\n"
|
|
" feature: .codestable/features/2026-07-02-ui-seed\n",
|
|
)
|
|
feature = write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
|
|
blocked = workflow_next.feature_next(feature, False, require_implementation_ready=True)
|
|
assert blocked["status"] == "blocked"
|
|
assert blocked["next_action"] == "complete-roadmap-dependencies-before-implementation"
|
|
assert blocked["evidence"]["implementation_dependencies"][0]["status"] == "in-progress"
|
|
|
|
items = roadmap / "billing-system-items.yaml"
|
|
write(items, items.read_text(encoding="utf-8").replace("status: in-progress", "status: done", 1))
|
|
ready = workflow_next.feature_next(feature, False, require_implementation_ready=True)
|
|
assert ready["ok"] is True
|
|
assert ready["evidence"]["implementation_ready"] is True
|
|
|
|
completed = subprocess.run(
|
|
[
|
|
sys.executable,
|
|
(TOOLS_DIR / "codestable-workflow-next.py").as_posix(),
|
|
"feature",
|
|
"--feature",
|
|
feature.as_posix(),
|
|
"--require-implementation-ready",
|
|
"--json",
|
|
],
|
|
cwd=repo,
|
|
check=False,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env={"PYTHONDONTWRITEBYTECODE": "1"},
|
|
timeout=10,
|
|
)
|
|
assert completed.returncode == 0
|
|
assert json.loads(completed.stdout)["evidence"]["implementation_ready"] is True
|
|
|
|
|
|
def test_epic_user_gate_only_after_all_child_design_reviews_passed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", include_roadmap=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "all-feature-designs-confirmation"
|
|
assert result["must_continue"] is False
|
|
assert result["final_answer_allowed"] is True
|
|
assert {item["item"] for item in result["evidence"]["unapproved_items"]} == {"api-seed", "ui-seed"}
|
|
|
|
|
|
def test_epic_goal_package_after_batch_approval(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "goal_package"
|
|
assert result["next_action"] == "cs-epic goal-package"
|
|
assert result["must_continue"] is True
|
|
assert result["final_answer_allowed"] is False
|
|
|
|
|
|
def test_feature_epic_child_batch_returns_to_epic_loop(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
write_roadmap(repo)
|
|
feature = write_feature(repo, "api-seed", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=True)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic-batch-loop"
|
|
assert result["must_continue"] is True
|
|
assert result["final_answer_allowed"] is False
|
|
assert result["evidence"]["roadmap_item"] == "api-seed"
|
|
assert result["evidence"]["epic_command"] == (
|
|
f"python3 {(TOOLS_DIR / 'codestable-workflow-next.py').as_posix()} "
|
|
"epic --roadmap .codestable/roadmap/billing-system --json"
|
|
)
|
|
|
|
|
|
def test_feature_single_mode_stops_at_design_confirmation(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "feature-design-confirmation"
|
|
assert result["must_continue"] is False
|
|
assert result["final_answer_allowed"] is True
|
|
|
|
|
|
def test_feature_legacy_epic_child_with_roadmap_goal_state_returns_to_epic(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_roadmap_goal_state(roadmap)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic"
|
|
assert result["evidence"]["roadmap_item"] == "api-seed"
|
|
assert result["evidence"]["roadmap_goal_state"].endswith("billing-system/goal-state.yaml")
|
|
|
|
|
|
@pytest.mark.parametrize("artifacts_complete", [False, True])
|
|
def test_feature_legacy_epic_child_without_roadmap_frontmatter_is_reverse_owned(
|
|
tmp_path: Path,
|
|
artifacts_complete: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_roadmap_goal_state(roadmap)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
if artifacts_complete:
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic"
|
|
assert result["evidence"]["execution_lane"] == "goal"
|
|
assert result["evidence"]["execution_lane_source"] == "roadmap-goal-state"
|
|
assert result["next_action"] != "CS_FEATURE_STANDARD_COMPLETE"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("design_status", "artifacts_complete"),
|
|
[("draft", False), ("approved", True)],
|
|
)
|
|
def test_feature_pre_goal_package_metadata_less_child_with_items_pointer_returns_to_epic(
|
|
tmp_path: Path,
|
|
design_status: str,
|
|
artifacts_complete: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
" status: planned\n"
|
|
" feature: .codestable/features/2026-07-02-api-seed\n",
|
|
)
|
|
feature = write_feature(repo, "api-seed", design_status=design_status, include_roadmap=False)
|
|
if artifacts_complete:
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
epic_result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic"
|
|
assert result["evidence"]["execution_lane"] == "goal"
|
|
assert result["evidence"]["execution_lane_source"] == "roadmap-items"
|
|
assert result["evidence"]["roadmap_items"].endswith("billing-system-items.yaml")
|
|
assert result["next_action"] not in {"feature-design-confirmation", "CS_FEATURE_STANDARD_COMPLETE"}
|
|
assert epic_result["next_action"] == (
|
|
"all-feature-designs-confirmation" if design_status == "draft" else "cs-epic goal-package"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("design_status", "artifacts_complete"),
|
|
[("draft", False), ("approved", True)],
|
|
)
|
|
def test_feature_pre_goal_package_metadata_less_child_with_items_glob_returns_to_epic(
|
|
tmp_path: Path,
|
|
design_status: str,
|
|
artifacts_complete: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
write_roadmap(repo)
|
|
feature = write_feature(repo, "api-seed", design_status=design_status, include_roadmap=False)
|
|
if artifacts_complete:
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic"
|
|
assert result["evidence"]["execution_lane_source"] == "roadmap-items"
|
|
assert result["next_action"] not in {"feature-design-confirmation", "CS_FEATURE_STANDARD_COMPLETE"}
|
|
|
|
|
|
@pytest.mark.parametrize("same_items_file", [False, True])
|
|
def test_reverse_items_owner_multiple_claims_fail_closed(
|
|
tmp_path: Path,
|
|
same_items_file: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
feature_dir = ".codestable/features/2026-07-02-api-seed"
|
|
first = repo / ".codestable/roadmap/first-roadmap/first-roadmap-items.yaml"
|
|
if same_items_file:
|
|
write(
|
|
first,
|
|
"roadmap: first-roadmap\nitems:\n"
|
|
f" - slug: api-seed\n feature: {feature_dir}\n"
|
|
f" - slug: api-seed-copy\n feature: {feature_dir}\n",
|
|
)
|
|
expected_paths = [first.relative_to(repo).as_posix()]
|
|
else:
|
|
second = repo / ".codestable/roadmap/second-roadmap/second-roadmap-items.yaml"
|
|
write(first, f"roadmap: first-roadmap\nitems:\n - slug: api-seed\n feature: {feature_dir}\n")
|
|
write(second, f"roadmap: second-roadmap\nitems:\n - slug: api-seed\n feature: {feature_dir}\n")
|
|
expected_paths = [first.relative_to(repo).as_posix(), second.relative_to(repo).as_posix()]
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "multiple roadmap items claim" in result["reason"]
|
|
assert all(path in result["blocking"][0] for path in expected_paths)
|
|
|
|
|
|
@pytest.mark.parametrize("contents", ["items: [unterminated\n", "- api-seed\n"])
|
|
def test_reverse_items_owner_invalid_artifact_fails_closed(
|
|
tmp_path: Path,
|
|
contents: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
items_path = repo / ".codestable/roadmap/broken-roadmap/broken-roadmap-items.yaml"
|
|
write(items_path, contents)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected_path = items_path.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert expected_path in result["blocking"][0]
|
|
assert expected_path in result["evidence"]["roadmap_owner_error"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"contents",
|
|
[
|
|
"roadmap: broken-roadmap\nitems:\n slug: api-seed\n feature: null\n",
|
|
"roadmap: broken-roadmap\nitems:\n - slug: api-seed\n feature: []\n",
|
|
],
|
|
)
|
|
def test_reverse_items_owner_invalid_yaml_shape_returns_structured_block(
|
|
tmp_path: Path,
|
|
contents: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
items_path = repo / ".codestable/roadmap/broken-roadmap/broken-roadmap-items.yaml"
|
|
write(items_path, contents)
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
|
|
expected_path = items_path.relative_to(repo).as_posix()
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert expected_path in payload["blocking"][0]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"contents",
|
|
[
|
|
"roadmap: billing-system\nitems:\n slug: api-seed\n feature: null\n",
|
|
"roadmap: billing-system\nitems:\n - slug: api-seed\n feature: []\n",
|
|
],
|
|
)
|
|
def test_epic_invalid_items_yaml_shape_returns_structured_block(
|
|
tmp_path: Path,
|
|
contents: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
items_path = roadmap / "billing-system-items.yaml"
|
|
write(items_path, contents)
|
|
|
|
completed, payload = run_cli_json(repo, "epic", roadmap)
|
|
|
|
expected_path = items_path.relative_to(repo).as_posix()
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert payload["next_action"] == "fix-epic-artifact-yaml"
|
|
assert expected_path in payload["blocking"][0]
|
|
|
|
|
|
@pytest.mark.parametrize("quick", [False, True])
|
|
def test_reverse_items_owner_ignores_unrelated_valid_items(tmp_path: Path, quick: bool) -> None:
|
|
repo = init_repo(tmp_path)
|
|
if quick:
|
|
feature = repo / ".codestable/features/2026-07-13-small-export"
|
|
write_ff_note(feature, "small-export")
|
|
else:
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(
|
|
repo / ".codestable/roadmap/other-roadmap/other-roadmap-items.yaml",
|
|
"roadmap: other-roadmap\n"
|
|
"items:\n"
|
|
" - slug: other-feature\n"
|
|
" feature: .codestable/features/2026-07-02-other-feature\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == ("cs-code-review" if quick else "cs-feat implementation")
|
|
assert result["evidence"]["execution_lane"] == ("quick" if quick else "standard")
|
|
|
|
|
|
@pytest.mark.parametrize("quick", [False, True])
|
|
def test_reverse_items_owner_ignores_slug_suffix_collision(tmp_path: Path, quick: bool) -> None:
|
|
repo = init_repo(tmp_path)
|
|
if quick:
|
|
item_slug = "export"
|
|
feature = repo / ".codestable/features/2026-07-13-small-export"
|
|
write_ff_note(feature, "small-export")
|
|
write_code_review(feature, "small-export")
|
|
else:
|
|
item_slug = "auth"
|
|
feature = write_feature(repo, "user-auth", design_status="approved", include_roadmap=False)
|
|
write(
|
|
repo / ".codestable/roadmap/platform/platform-items.yaml",
|
|
"roadmap: platform\n"
|
|
"items:\n"
|
|
f" - slug: {item_slug}\n"
|
|
" feature: null\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == ("complete" if quick else "continue")
|
|
assert result["next_action"] == ("CS_FEATURE_QUICK_COMPLETE" if quick else "cs-feat implementation")
|
|
assert result["evidence"]["execution_lane"] == ("quick" if quick else "standard")
|
|
|
|
|
|
def test_reverse_items_owner_multiple_exact_slug_directories_fail_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(repo / ".codestable/features/2026-07-01-api-seed/placeholder.txt", "")
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n - slug: api-seed\n feature: null\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
epic_result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "multiple feature directories match roadmap item api-seed" in result["reason"]
|
|
assert epic_result["status"] == "blocked"
|
|
assert epic_result["next_action"] == "fix-roadmap-items"
|
|
assert "multiple feature directories match roadmap item api-seed" in epic_result["reason"]
|
|
|
|
|
|
def test_reverse_items_owner_does_not_fallback_from_explicit_missing_pointer(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
" feature: .codestable/features/2026-07-02-missing-feature\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
epic_result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat implementation"
|
|
assert result["evidence"]["execution_lane"] == "standard"
|
|
assert epic_result["status"] == "continue"
|
|
assert epic_result["next_action"] == "cs-feat design/design-review"
|
|
assert epic_result["evidence"]["next_item"]["feature_dir"] is None
|
|
|
|
|
|
def test_reverse_items_owner_invalid_pointer_returns_structured_block(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
items_path = repo / ".codestable/roadmap/platform/platform-items.yaml"
|
|
write(
|
|
items_path,
|
|
"roadmap: platform\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
' feature: ".codestable/features/2026\\0-07-02-api-seed"\n',
|
|
)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
|
|
expected_path = items_path.relative_to(repo).as_posix()
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert expected_path in payload["blocking"][0]
|
|
assert payload["next_action"] != "CS_FEATURE_STANDARD_COMPLETE"
|
|
|
|
|
|
def test_reverse_owner_ignores_unrelated_valid_goal_state(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write_reverse_owner_state(
|
|
repo,
|
|
"other-roadmap",
|
|
rows=[("other-feature", ".codestable/features/2026-07-02-other-feature")],
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat implementation"
|
|
assert result["evidence"]["execution_lane"] == "standard"
|
|
|
|
|
|
@pytest.mark.parametrize("quick", [False, True])
|
|
def test_reverse_owner_ignores_unrelated_identity_mismatch(tmp_path: Path, quick: bool) -> None:
|
|
repo = init_repo(tmp_path)
|
|
if quick:
|
|
feature = repo / ".codestable/features/2026-07-13-small-export"
|
|
write_ff_note(feature, "small-export")
|
|
else:
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write_reverse_owner_state(
|
|
repo,
|
|
"renamed-roadmap",
|
|
state_roadmap="old-roadmap",
|
|
rows=[("other-feature", ".codestable/features/2026-07-02-other-feature")],
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == ("cs-code-review" if quick else "cs-feat implementation")
|
|
|
|
|
|
def test_reverse_owner_identity_mismatch_claiming_feature_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
goal_state = write_reverse_owner_state(
|
|
repo,
|
|
"renamed-roadmap",
|
|
state_roadmap="old-roadmap",
|
|
rows=[("api-seed", ".codestable/features/2026-07-02-api-seed")],
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected_path = goal_state.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert expected_path in result["blocking"][0]
|
|
assert expected_path in result["evidence"]["roadmap_owner_error"]
|
|
|
|
|
|
def test_reverse_owner_unparseable_goal_state_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
goal_state = repo / ".codestable/roadmap/broken-roadmap/goal-state.yaml"
|
|
write(goal_state, "roadmap: broken-roadmap\nfeatures: [unterminated\n")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected_path = goal_state.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert expected_path in result["blocking"][0]
|
|
assert expected_path in result["evidence"]["roadmap_owner_error"]
|
|
|
|
|
|
@pytest.mark.parametrize("same_state", [False, True])
|
|
def test_reverse_owner_multiple_claims_fail_closed(tmp_path: Path, same_state: bool) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
feature_dir = ".codestable/features/2026-07-02-api-seed"
|
|
first = write_reverse_owner_state(repo, "first-roadmap", rows=[("api-seed", feature_dir)])
|
|
if same_state:
|
|
write_reverse_owner_state(
|
|
repo,
|
|
"first-roadmap",
|
|
rows=[("api-seed", feature_dir), ("api-seed-copy", feature_dir)],
|
|
)
|
|
expected_paths = [first.relative_to(repo).as_posix()]
|
|
else:
|
|
second = write_reverse_owner_state(repo, "second-roadmap", rows=[("api-seed", feature_dir)])
|
|
expected_paths = [first.relative_to(repo).as_posix(), second.relative_to(repo).as_posix()]
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert "multiple roadmap goal-states claim" in result["reason"]
|
|
assert all(path in result["blocking"][0] for path in expected_paths)
|
|
|
|
|
|
def test_reverse_owner_matching_row_without_item_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
goal_state = write_reverse_owner_state(
|
|
repo,
|
|
"broken-roadmap",
|
|
rows=[(None, ".codestable/features/2026-07-02-api-seed")],
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected_path = goal_state.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert expected_path in result["blocking"][0]
|
|
assert expected_path in result["evidence"]["roadmap_owner_error"]
|
|
|
|
|
|
@pytest.mark.parametrize("forward", [False, True])
|
|
def test_goal_state_invalid_feature_dir_returns_structured_block(
|
|
tmp_path: Path,
|
|
forward: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
goal_state = roadmap / "goal-state.yaml"
|
|
write(
|
|
goal_state,
|
|
"roadmap: billing-system\n"
|
|
"status: ready-to-dispatch\n"
|
|
"features:\n"
|
|
" - slug: api-seed\n"
|
|
" roadmap_item: api-seed\n"
|
|
' feature_dir: ".codestable/features/2026\\0-07-02-api-seed"\n'
|
|
" status: implementing\n",
|
|
)
|
|
feature = write_feature(
|
|
repo,
|
|
"api-seed",
|
|
design_status="approved",
|
|
include_roadmap=forward,
|
|
)
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
|
|
expected_path = goal_state.relative_to(repo).as_posix()
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert expected_path in payload["blocking"][0]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"features_yaml",
|
|
[
|
|
"features:\n slug: api-seed\n feature_dir: .codestable/features/2026-07-02-api-seed\n",
|
|
"features:\n - api-seed\n",
|
|
],
|
|
)
|
|
def test_reverse_goal_owner_invalid_features_shape_fails_closed(
|
|
tmp_path: Path,
|
|
features_yaml: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
goal_state = roadmap / "goal-state.yaml"
|
|
write(goal_state, "roadmap: billing-system\nstatus: ready-to-dispatch\n" + features_yaml)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
|
|
expected_path = goal_state.relative_to(repo).as_posix()
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert expected_path in payload["blocking"][0]
|
|
assert payload["next_action"] != "CS_FEATURE_STANDARD_COMPLETE"
|
|
|
|
|
|
def test_forward_metadata_missing_state_detects_external_reverse_claim(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
external = write_reverse_owner_state(
|
|
repo,
|
|
"external-roadmap",
|
|
rows=[("api-seed", ".codestable/features/2026-07-02-api-seed")],
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected = [
|
|
".codestable/roadmap/billing-system/goal-state.yaml",
|
|
external.relative_to(repo).as_posix(),
|
|
]
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert result["next_action"] != "CS_FEATURE_STANDARD_COMPLETE"
|
|
assert all(path in result["blocking"][0] for path in expected)
|
|
|
|
|
|
def test_forward_owner_detects_second_external_claim(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_roadmap_goal_state(roadmap)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
external = write_reverse_owner_state(
|
|
repo,
|
|
"external-roadmap",
|
|
rows=[("api-seed", ".codestable/features/2026-07-02-api-seed")],
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert external.relative_to(repo).as_posix() in result["blocking"][0]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("design_status", "artifacts_complete"),
|
|
[("draft", False), ("approved", True)],
|
|
)
|
|
def test_feature_pre_goal_package_roadmap_child_returns_to_epic(
|
|
tmp_path: Path,
|
|
design_status: str,
|
|
artifacts_complete: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
write_roadmap(repo)
|
|
feature = write_feature(repo, "api-seed", design_status=design_status, include_roadmap=True)
|
|
if artifacts_complete:
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic"
|
|
assert result["evidence"]["roadmap_owner_source"] == "roadmap-items"
|
|
assert result["evidence"]["roadmap_items"].endswith("billing-system-items.yaml")
|
|
assert result["next_action"] not in {"feature-design-confirmation", "CS_FEATURE_STANDARD_COMPLETE"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("metadata_field", "metadata_value"),
|
|
[("roadmap", "billing-system"), ("roadmap_item", "api-seed")],
|
|
)
|
|
def test_feature_incomplete_roadmap_metadata_fails_closed(
|
|
tmp_path: Path,
|
|
metadata_field: str,
|
|
metadata_value: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(
|
|
feature / "api-seed-design.md",
|
|
"---\ndoc_type: feature-design\nfeature: 2026-07-02-api-seed\n"
|
|
f"{metadata_field}: {metadata_value}\nstatus: approved\n---\n# Design\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-roadmap-metadata"
|
|
assert "roadmap and roadmap_item together" in result["reason"]
|
|
|
|
|
|
def test_feature_invalid_roadmap_slug_returns_structured_block(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=False)
|
|
write(
|
|
feature / "api-seed-design.md",
|
|
"---\n"
|
|
"doc_type: feature-design\n"
|
|
"feature: 2026-07-02-api-seed\n"
|
|
'roadmap: "billing\\0x"\n'
|
|
"roadmap_item: api-seed\n"
|
|
"status: approved\n"
|
|
"---\n# Design\n",
|
|
)
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert payload["next_action"] == "inspect-epic-goal-state"
|
|
assert "invalid roadmap owner path" in payload["reason"]
|
|
|
|
|
|
def test_feature_pre_goal_package_roadmap_without_matching_item_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\nitems:\n - slug: ui-seed\n status: planned\n",
|
|
)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
items_path = ".codestable/roadmap/billing-system/billing-system-items.yaml"
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert items_path in result["blocking"][0]
|
|
assert "do not uniquely own" in result["reason"]
|
|
|
|
|
|
def test_feature_pre_goal_package_roadmap_item_pointer_must_match_feature(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-items.yaml",
|
|
"roadmap: billing-system\n"
|
|
"items:\n"
|
|
" - slug: api-seed\n"
|
|
" status: planned\n"
|
|
" feature: .codestable/features/2026-07-02-other-feature\n",
|
|
)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
items_path = ".codestable/roadmap/billing-system/billing-system-items.yaml"
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "feature pointer does not match" in result["reason"]
|
|
assert items_path in result["blocking"][0]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"contents",
|
|
[
|
|
"roadmap: billing-system\nitems:\n slug: api-seed\n feature: null\n",
|
|
"roadmap: billing-system\nitems:\n - slug: api-seed\n feature: []\n",
|
|
],
|
|
)
|
|
def test_forward_items_owner_invalid_yaml_shape_returns_structured_block(
|
|
tmp_path: Path,
|
|
contents: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
items_path = roadmap / "billing-system-items.yaml"
|
|
write(items_path, contents)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
|
|
expected_path = items_path.relative_to(repo).as_posix()
|
|
assert completed.returncode == 1
|
|
assert completed.stderr == ""
|
|
assert payload["status"] == "blocked"
|
|
assert payload["next_action"] == "inspect-epic-goal-state"
|
|
assert expected_path in payload["blocking"][0]
|
|
|
|
|
|
@pytest.mark.parametrize("same_items_file", [False, True])
|
|
def test_forward_items_owner_detects_second_items_claim(
|
|
tmp_path: Path,
|
|
same_items_file: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
feature_dir = ".codestable/features/2026-07-02-api-seed"
|
|
primary_items = roadmap / "billing-system-items.yaml"
|
|
if same_items_file:
|
|
write(
|
|
primary_items,
|
|
"roadmap: billing-system\nitems:\n"
|
|
f" - slug: api-seed\n feature: {feature_dir}\n"
|
|
f" - slug: other-item\n feature: {feature_dir}\n",
|
|
)
|
|
expected_paths = [primary_items.relative_to(repo).as_posix()]
|
|
else:
|
|
secondary_items = repo / ".codestable/roadmap/secondary/secondary-items.yaml"
|
|
write(
|
|
primary_items,
|
|
f"roadmap: billing-system\nitems:\n - slug: api-seed\n feature: {feature_dir}\n",
|
|
)
|
|
write(
|
|
secondary_items,
|
|
f"roadmap: secondary\nitems:\n - slug: other-item\n feature: {feature_dir}\n",
|
|
)
|
|
expected_paths = [
|
|
primary_items.relative_to(repo).as_posix(),
|
|
secondary_items.relative_to(repo).as_posix(),
|
|
]
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "multiple roadmap items claim" in result["reason"]
|
|
assert all(path in result["blocking"][0] for path in expected_paths)
|
|
|
|
|
|
@pytest.mark.parametrize("same_items_file", [False, True])
|
|
def test_forward_goal_owner_detects_second_items_claim(
|
|
tmp_path: Path,
|
|
same_items_file: bool,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_roadmap_goal_state(roadmap)
|
|
feature_dir = ".codestable/features/2026-07-02-api-seed"
|
|
primary_items = roadmap / "billing-system-items.yaml"
|
|
primary = (
|
|
"roadmap: billing-system\nitems:\n"
|
|
" - slug: api-seed\n feature: null\n"
|
|
)
|
|
if same_items_file:
|
|
write(primary_items, primary + f" - slug: other-item\n feature: {feature_dir}\n")
|
|
expected_paths = [primary_items.relative_to(repo).as_posix()]
|
|
else:
|
|
secondary_items = repo / ".codestable/roadmap/secondary/secondary-items.yaml"
|
|
write(primary_items, primary)
|
|
write(
|
|
secondary_items,
|
|
f"roadmap: secondary\nitems:\n - slug: other-item\n feature: {feature_dir}\n",
|
|
)
|
|
expected_paths = [
|
|
primary_items.relative_to(repo).as_posix(),
|
|
secondary_items.relative_to(repo).as_posix(),
|
|
]
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "multiple roadmap items claim" in result["reason"]
|
|
assert all(path in result["blocking"][0] for path in expected_paths)
|
|
|
|
|
|
def test_forward_goal_owner_detects_second_claim_in_same_goal_state(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
feature_dir = ".codestable/features/2026-07-02-api-seed"
|
|
goal_state = roadmap / "goal-state.yaml"
|
|
write(
|
|
goal_state,
|
|
"roadmap: billing-system\n"
|
|
"status: ready-to-dispatch\n"
|
|
"features:\n"
|
|
" - slug: api-seed\n"
|
|
" roadmap_item: api-seed\n"
|
|
f" feature_dir: {feature_dir}\n"
|
|
" status: implementing\n"
|
|
" - slug: duplicate-claim\n"
|
|
" roadmap_item: duplicate-claim\n"
|
|
f" feature_dir: {feature_dir}\n"
|
|
" status: pending\n",
|
|
)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected_path = goal_state.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "multiple roadmap goal-states claim" in result["reason"]
|
|
assert expected_path in result["blocking"][0]
|
|
|
|
|
|
def test_feature_epic_child_batch_never_completes_as_standard(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
write_roadmap(repo)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=True)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic-batch-loop"
|
|
assert result["evidence"]["execution_lane"] == "goal"
|
|
assert result["evidence"]["execution_lane_source"] == "epic-child-batch"
|
|
assert result["next_action"] != "CS_FEATURE_STANDARD_COMPLETE"
|
|
|
|
|
|
def test_feature_draft_owned_by_roadmap_goal_returns_to_epic(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_roadmap_goal_state(roadmap)
|
|
feature = write_feature(
|
|
repo,
|
|
"api-seed",
|
|
design_status="draft",
|
|
review_status="passed",
|
|
include_roadmap=True,
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "return-to-cs-epic"
|
|
assert result["next_action"] != "feature-design-confirmation"
|
|
assert result["evidence"]["execution_lane_source"] == "roadmap-goal-state"
|
|
|
|
|
|
def test_feature_parent_goal_state_with_mismatched_owner_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_roadmap_goal_state(roadmap, feature_slug="ui-seed")
|
|
feature = write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "inspect-epic-goal-state"
|
|
assert "standard" not in result["next_action"]
|
|
|
|
|
|
def test_feature_quick_ff_note_recovers_review_then_completion(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = repo / ".codestable/features/2026-07-13-small-export"
|
|
write_ff_note(feature, "small-export")
|
|
|
|
review = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert review["status"] == "continue"
|
|
assert review["next_action"] == "cs-code-review"
|
|
assert review["evidence"]["execution_lane"] == "quick"
|
|
|
|
write_code_review(feature, "small-export")
|
|
complete = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert complete["status"] == "complete"
|
|
assert complete["next_action"] == "CS_FEATURE_QUICK_COMPLETE"
|
|
|
|
|
|
def test_feature_quick_ff_note_routes_review_fix(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = repo / ".codestable/features/2026-07-13-small-export"
|
|
write_ff_note(feature, "small-export")
|
|
write_code_review(feature, "small-export", status="changes-requested")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat --mode quick"
|
|
assert "review-fix" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("artifact", "status"),
|
|
[("qa", "failed"), ("qa", "blocked"), ("acceptance", "failed"), ("acceptance", "blocked")],
|
|
)
|
|
def test_feature_quick_rejects_existing_failed_quality_evidence(
|
|
tmp_path: Path,
|
|
artifact: str,
|
|
status: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(
|
|
repo,
|
|
"api-seed",
|
|
design_status="approved",
|
|
execution_lane="quick",
|
|
execution_lane_reason="owner-requested-after-risk-recheck",
|
|
)
|
|
write_ff_note(feature, "api-seed")
|
|
write_code_review(feature, "api-seed")
|
|
doc_type = "feature-qa" if artifact == "qa" else "feature-acceptance"
|
|
write(
|
|
feature / f"api-seed-{artifact}.md",
|
|
f"---\ndoc_type: {doc_type}\nstatus: {status}\n---\n# Evidence\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
artifact_path = f".codestable/features/2026-07-02-api-seed/api-seed-{artifact}.md"
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "resolve-quick-quality-conflict"
|
|
assert artifact_path in result["blocking"][0]
|
|
assert result["next_action"] != "CS_FEATURE_QUICK_COMPLETE"
|
|
|
|
|
|
def test_feature_quick_allows_existing_passed_quality_evidence(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(
|
|
repo,
|
|
"api-seed",
|
|
design_status="approved",
|
|
execution_lane="quick",
|
|
execution_lane_reason="owner-requested-after-risk-recheck",
|
|
)
|
|
write_ff_note(feature, "api-seed")
|
|
write_code_review(feature, "api-seed")
|
|
write(feature / "api-seed-qa.md", "---\ndoc_type: feature-qa\nstatus: passed\n---\n# QA\n")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "complete"
|
|
assert result["next_action"] == "CS_FEATURE_QUICK_COMPLETE"
|
|
|
|
|
|
def test_feature_reclassified_quick_design_resumes_fastforward(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(
|
|
repo,
|
|
"api-seed",
|
|
design_status="approved",
|
|
execution_lane="quick",
|
|
execution_lane_reason="owner-requested-after-risk-recheck",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat --mode quick"
|
|
assert result["evidence"]["execution_lane_source"] == "design"
|
|
|
|
|
|
def test_feature_reclassified_quick_design_requires_persisted_reason(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="quick")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-execution-lane"
|
|
assert "execution_lane_reason" in result["reason"]
|
|
|
|
|
|
def test_feature_ff_note_conflicting_with_recorded_standard_lane_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write_ff_note(feature, "api-seed")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "resolve-feature-execution-lane-conflict"
|
|
|
|
|
|
def test_feature_goal_state_overrides_reclassified_quick_lane(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="quick")
|
|
write_ff_note(feature, "api-seed")
|
|
write_goal_state(feature, stage="implementation", status="ready-to-dispatch")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "dispatch_goal"
|
|
assert result["evidence"]["execution_lane"] == "goal"
|
|
assert result["evidence"]["execution_lane_source"] == "feature-goal-state"
|
|
|
|
|
|
def test_feature_goal_state_requires_explicit_acceptance_authorization(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="goal")
|
|
write_goal_state(
|
|
feature,
|
|
stage="implementation",
|
|
status="ready-to-dispatch",
|
|
acceptance_authorization=None,
|
|
)
|
|
|
|
missing = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert missing["status"] == "user_gate"
|
|
assert missing["next_action"] == "authorize-feature-goal-acceptance"
|
|
|
|
write_goal_state(
|
|
feature,
|
|
stage="implementation",
|
|
status="ready-to-dispatch",
|
|
acceptance_authorization="rejected",
|
|
)
|
|
rejected = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert rejected["status"] == "handoff"
|
|
assert rejected["next_action"] == "CS_FEATURE_GOAL_HANDOFF"
|
|
|
|
write_goal_state(
|
|
feature,
|
|
stage="complete",
|
|
status="passed",
|
|
acceptance_authorization=None,
|
|
)
|
|
legacy_complete = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert legacy_complete["status"] == "user_gate"
|
|
assert legacy_complete["next_action"] == "authorize-feature-goal-acceptance"
|
|
|
|
write_goal_state(
|
|
feature,
|
|
stage="implementation",
|
|
status="running",
|
|
driver_kind="host-agent",
|
|
driver_id="feature-run-123",
|
|
acceptance_authorization=None,
|
|
)
|
|
unauthorized_driver = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert unauthorized_driver["status"] == "user_gate"
|
|
assert unauthorized_driver["next_action"] == "authorize-feature-goal-acceptance"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("reference", "approval_status", "reason"),
|
|
[
|
|
("does-not-exist.md#goal-acceptance", "approved", "unit approval-report.md"),
|
|
("approval-report.md#goal-acceptance", None, "goal-acceptance is not approved"),
|
|
("approval-report.md#goal-acceptance", "pending", "is not approved"),
|
|
],
|
|
)
|
|
def test_feature_goal_state_validates_acceptance_approval_artifact(
|
|
tmp_path: Path,
|
|
reference: str,
|
|
approval_status: str | None,
|
|
reason: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="goal")
|
|
write_goal_state(
|
|
feature,
|
|
stage="implementation",
|
|
status="ready-to-dispatch",
|
|
acceptance_authorization_ref=reference,
|
|
approval_decision_status=approval_status,
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-feature-goal-acceptance"
|
|
assert reason in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("authorization", "expected_status", "expected_action"),
|
|
[
|
|
(None, "user_gate", "authorize-epic-goal-execution"),
|
|
("rejected", "handoff", "CS_ROADMAP_GOAL_HANDOFF"),
|
|
],
|
|
)
|
|
def test_epic_goal_state_requires_explicit_acceptance_authorization(
|
|
tmp_path: Path,
|
|
authorization: str | None,
|
|
expected_status: str,
|
|
expected_action: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(roadmap, acceptance_authorization=authorization)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == expected_status
|
|
assert result["next_action"] == expected_action
|
|
|
|
|
|
def test_epic_pending_package_template_stops_at_single_execution_gate(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
status="awaiting-authorization",
|
|
driver_kind="none",
|
|
driver_id="",
|
|
acceptance_authorization="pending",
|
|
approval_decision_status="pending",
|
|
commit_authorization="pending",
|
|
commit_decision_status="pending",
|
|
execution_confirmation_id="",
|
|
approval_group_status="pending",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert "approval group goal-execution is not approved" in result["reason"]
|
|
assert "acceptance_authorization is not approved" in result["reason"]
|
|
assert "commit_authorization is not approved" in result["reason"]
|
|
assert result["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
assert result["evidence"]["commit_authorization_ref"] == "approval-report.md#goal-commits"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("acceptance", "commit", "state_confirmation_id", "status", "reason"),
|
|
[
|
|
("pending", "pending", "", "awaiting-authorization", "not synchronized"),
|
|
(
|
|
"approved",
|
|
"pending",
|
|
"goal-confirm-1",
|
|
"ready-to-dispatch",
|
|
"commit_authorization",
|
|
),
|
|
("approved", "approved", "", "ready-to-dispatch", "not synchronized"),
|
|
("approved", "approved", "other-confirmation", "ready-to-dispatch", "not synchronized"),
|
|
("approved", "approved", "goal-confirm-1", "awaiting-authorization", "still awaits"),
|
|
("rejected", "approved", "goal-confirm-1", "ready-to-dispatch", "projection is rejected"),
|
|
],
|
|
)
|
|
def test_epic_durable_execution_confirmation_repairs_partial_goal_state_without_user_gate(
|
|
tmp_path: Path,
|
|
acceptance: str,
|
|
commit: str,
|
|
state_confirmation_id: str,
|
|
status: str,
|
|
reason: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
status=status,
|
|
driver_kind="none",
|
|
driver_id="",
|
|
acceptance_authorization=acceptance,
|
|
commit_authorization=commit,
|
|
execution_confirmation_id=state_confirmation_id,
|
|
approval_group_status="approved",
|
|
approval_group_confirmation_id="goal-confirm-1",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "repair-epic-goal-execution-authorization"
|
|
assert reason in result["reason"]
|
|
assert result["must_continue"] is True
|
|
assert result["final_answer_allowed"] is False
|
|
assert result["evidence"]["execution_confirmation_id"] == "goal-confirm-1"
|
|
assert result["evidence"]["acceptance_authorization"] == "approved"
|
|
assert result["evidence"]["commit_authorization"] == "approved"
|
|
|
|
|
|
def test_epic_repaired_execution_confirmation_dispatches_without_another_user_gate(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
driver_kind="none",
|
|
driver_id="",
|
|
execution_confirmation_id="goal-confirm-1",
|
|
approval_group_status="approved",
|
|
approval_group_confirmation_id="goal-confirm-1",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "dispatch_goal"
|
|
assert result["next_action"] == "dispatch-epic-goal-driver-or-print-goal"
|
|
assert result["evidence"]["execution_confirmation_id"] == "goal-confirm-1"
|
|
|
|
|
|
def test_epic_pending_execution_group_cannot_be_bypassed_by_state_first_updates(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
execution_confirmation_id="goal-confirm-1",
|
|
approval_group_status="pending",
|
|
approval_group_confirmation_id="goal-confirm-1",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert "approval group goal-execution is not approved" in result["reason"]
|
|
|
|
|
|
def test_epic_rejected_execution_group_handoffs(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
execution_confirmation_id="goal-confirm-1",
|
|
approval_group_status="rejected",
|
|
approval_group_confirmation_id="goal-confirm-1",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "handoff"
|
|
assert result["next_action"] == "CS_ROADMAP_GOAL_HANDOFF"
|
|
|
|
|
|
def test_epic_legacy_approved_authorizations_dispatch_with_warning(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(roadmap, driver_kind="none", driver_id="")
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "dispatch_goal"
|
|
assert "legacy epic goal approvals" in result["warnings"][-1]
|
|
|
|
|
|
def test_epic_new_state_without_execution_group_cannot_use_legacy_bypass(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
driver_kind="none",
|
|
driver_id="",
|
|
execution_confirmation_id="",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
|
|
|
|
def test_epic_approved_execution_group_without_confirmation_id_does_not_repair(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
driver_kind="none",
|
|
driver_id="",
|
|
execution_confirmation_id="",
|
|
approval_group_status="approved",
|
|
approval_group_confirmation_id="",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert result["next_action"] != "repair-epic-goal-execution-authorization"
|
|
assert "approval group goal-execution has no confirmation_id" in result["reason"]
|
|
|
|
|
|
def test_epic_external_approval_symlink_fails_closed_instead_of_repair_loop(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(
|
|
roadmap,
|
|
driver_kind="none",
|
|
driver_id="",
|
|
execution_confirmation_id="goal-confirm-1",
|
|
approval_group_status="approved",
|
|
approval_group_confirmation_id="goal-confirm-1",
|
|
)
|
|
approval = roadmap / "approval-report.md"
|
|
external = tmp_path / "external-approval-report.md"
|
|
external.write_text(approval.read_text(encoding="utf-8"), encoding="utf-8")
|
|
approval.unlink()
|
|
approval.symlink_to(external)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert result["next_action"] != "repair-epic-goal-execution-authorization"
|
|
assert "approval-report.md escapes the workflow unit" in result["reason"]
|
|
|
|
|
|
def test_epic_legacy_complete_goal_state_still_requires_acceptance_authorization(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(roadmap, status="complete", acceptance_authorization=None)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
|
|
|
|
def test_epic_goal_state_rejects_unverifiable_acceptance_approval_ref(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(
|
|
roadmap,
|
|
status="ready-to-dispatch",
|
|
acceptance_authorization_ref="missing.md#goal-acceptance",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert "unit approval-report.md" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("authorization", "expected_status", "expected_action"),
|
|
[
|
|
(None, "user_gate", "authorize-epic-goal-execution"),
|
|
("Approved", "user_gate", "authorize-epic-goal-execution"),
|
|
("rejected", "handoff", "CS_ROADMAP_GOAL_HANDOFF"),
|
|
],
|
|
)
|
|
def test_epic_goal_state_requires_independent_commit_authorization(
|
|
tmp_path: Path,
|
|
authorization: str | None,
|
|
expected_status: str,
|
|
expected_action: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_roadmap_goal_state(roadmap, commit_authorization=authorization)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == expected_status
|
|
assert result["next_action"] == expected_action
|
|
|
|
|
|
def test_epic_complete_state_still_requires_commit_authorization(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(roadmap, status="complete", commit_authorization=None)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
|
|
|
|
def test_epic_goal_state_rejects_unapproved_commit_decision(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(
|
|
roadmap,
|
|
status="ready-to-dispatch",
|
|
commit_decision_status="pending",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert "goal-commits is not approved" in result["reason"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("reference", "reason"),
|
|
[
|
|
("approval-report.md#goal-acceptance", "must be approval-report.md#goal-commits"),
|
|
("../approval-report.md#goal-commits", "escapes the workflow unit"),
|
|
],
|
|
)
|
|
def test_epic_goal_state_rejects_unverifiable_commit_approval_ref(
|
|
tmp_path: Path,
|
|
reference: str,
|
|
reason: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(
|
|
roadmap,
|
|
status="ready-to-dispatch",
|
|
commit_authorization_ref=reference,
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "user_gate"
|
|
assert result["next_action"] == "authorize-epic-goal-execution"
|
|
assert reason in result["reason"]
|
|
|
|
|
|
def test_epic_goal_state_treats_named_commit_rejection_as_handoff(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(
|
|
roadmap,
|
|
status="ready-to-dispatch",
|
|
commit_decision_status="rejected",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "handoff"
|
|
assert result["next_action"] == "CS_ROADMAP_GOAL_HANDOFF"
|
|
|
|
|
|
def test_epic_goal_rejection_precedes_another_missing_authorization(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(
|
|
roadmap,
|
|
status="ready-to-dispatch",
|
|
acceptance_authorization=None,
|
|
commit_authorization="rejected",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "handoff"
|
|
assert result["next_action"] == "CS_ROADMAP_GOAL_HANDOFF"
|
|
|
|
|
|
def test_feature_standard_lane_recovers_without_goal_package_or_standalone_qa(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
|
|
implementation = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert implementation["status"] == "continue"
|
|
assert implementation["next_action"] == "cs-feat implementation"
|
|
assert implementation["evidence"]["execution_lane"] == "standard"
|
|
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
review = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert review["status"] == "continue"
|
|
assert review["next_action"] == "cs-code-review"
|
|
|
|
write_code_review(feature, "api-seed")
|
|
acceptance = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert acceptance["status"] == "continue"
|
|
assert acceptance["next_action"] == "cs-feat --stage accept"
|
|
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: feature-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
complete = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert complete["status"] == "complete"
|
|
assert complete["next_action"] == "CS_FEATURE_STANDARD_COMPLETE"
|
|
|
|
|
|
def test_feature_standard_lane_routes_review_fix_without_goal_state(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write(
|
|
feature / "api-seed-review.md",
|
|
"---\ndoc_type: feature-review\nstatus: changes-requested\n---\n# Review\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat implementation review-fix"
|
|
assert result["evidence"]["execution_lane"] == "standard"
|
|
|
|
|
|
@pytest.mark.parametrize("qa_status", ["failed", "blocked"])
|
|
def test_feature_standard_lane_routes_failed_qa_to_qa_fix(tmp_path: Path, qa_status: str) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-qa.md",
|
|
f"---\ndoc_type: feature-qa\nstatus: {qa_status}\n---\n# QA\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat --stage impl qa-fix"
|
|
|
|
|
|
def test_feature_standard_lane_resumes_nonterminal_qa(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(feature / "api-seed-qa.md", "---\ndoc_type: feature-qa\nstatus: pending\n---\n# QA\n")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat --stage qa"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("doc_type", "reviewer"),
|
|
[
|
|
("feature-review", None),
|
|
("feature-review", "self"),
|
|
("feature-review", "ocr"),
|
|
("wrong-review", "subagent"),
|
|
],
|
|
)
|
|
def test_feature_standard_lane_rejects_untrusted_passed_review(
|
|
tmp_path: Path,
|
|
doc_type: str,
|
|
reviewer: str | None,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed", doc_type=doc_type, reviewer=reviewer)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-code-review-evidence"
|
|
|
|
|
|
def test_feature_standard_lane_rejects_reviewer_marker_outside_frontmatter(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write(
|
|
feature / "api-seed-review.md",
|
|
"---\ndoc_type: feature-review\nstatus: passed\nreviewer: self\n---\n"
|
|
"# Review\n\nreviewer: subagent\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-code-review-evidence"
|
|
|
|
|
|
def test_feature_standard_lane_honors_explicit_self_review_fallback(
|
|
tmp_path: Path,
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed", reviewer="self")
|
|
monkeypatch.setenv("CODESTABLE_ALLOW_SELF_REVIEW_FALLBACK", "1")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat --stage accept"
|
|
|
|
|
|
def test_feature_execution_lane_is_normalized(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane='" Standard "')
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["evidence"]["execution_lane"] == "standard"
|
|
|
|
|
|
def test_feature_standard_lane_rejects_wrong_acceptance_doc_type(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write(feature / "api-seed-checklist.yaml", "steps:\n - id: step-1\n status: done\n")
|
|
write_code_review(feature, "api-seed")
|
|
write(
|
|
feature / "api-seed-acceptance.md",
|
|
"---\ndoc_type: wrong-acceptance\nstatus: passed\n---\n# Acceptance\n",
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-acceptance-evidence"
|
|
|
|
|
|
def test_feature_standard_lane_parses_checklist_once(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
calls = 0
|
|
original = workflow_next.all_checklist_steps_done
|
|
|
|
def counted(checklist: Path | None) -> bool:
|
|
nonlocal calls
|
|
calls += 1
|
|
return original(checklist)
|
|
|
|
monkeypatch.setattr(workflow_next, "all_checklist_steps_done", counted)
|
|
workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert calls == 1
|
|
|
|
|
|
def test_feature_goal_lane_still_requires_goal_package(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="goal")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "goal_package"
|
|
assert result["next_action"] == "cs-feat goal-package"
|
|
assert result["evidence"]["execution_lane"] == "goal"
|
|
|
|
|
|
def test_feature_existing_goal_state_overrides_recorded_standard_lane(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
write_goal_state(feature, stage="implementation", status="ready-to-dispatch")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "dispatch_goal"
|
|
assert result["evidence"]["execution_lane"] == "goal"
|
|
|
|
|
|
def test_feature_unknown_execution_lane_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", execution_lane="turbo")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-execution-lane"
|
|
|
|
|
|
def test_feature_review_changes_return_to_design(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "changes-requested", review_status="changes-requested")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "continue"
|
|
assert result["next_action"] == "cs-feat design"
|
|
assert result["reason"] == "design-review requested changes"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("review_state", "review_reason", "reviewer_id", "expected_status", "expected_action"),
|
|
[
|
|
("awaiting-reviewer", "", "feature-review-123", "awaiting", "resume-feature-design-reviewer"),
|
|
(
|
|
"needs-owner-approval",
|
|
"independent reviewer unavailable",
|
|
"",
|
|
"user_gate",
|
|
"approve-feature-design-review-fallback",
|
|
),
|
|
(
|
|
"reviewer-failed",
|
|
"reviewer process failed",
|
|
"",
|
|
"blocked",
|
|
"retry-feature-design-reviewer",
|
|
),
|
|
(
|
|
"blocked",
|
|
"design evidence is invalid",
|
|
"",
|
|
"blocked",
|
|
"resolve-feature-design-review-block",
|
|
),
|
|
],
|
|
)
|
|
def test_feature_design_review_state_preserves_recovery_semantics(
|
|
tmp_path: Path,
|
|
review_state: str,
|
|
review_reason: str,
|
|
reviewer_id: str,
|
|
expected_status: str,
|
|
expected_action: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(
|
|
repo,
|
|
review_state,
|
|
review_status="blocked",
|
|
review_state=review_state,
|
|
review_reason=review_reason,
|
|
reviewer_id=reviewer_id,
|
|
)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert workflow_next.feature_design_review_state(
|
|
feature / f"{review_state}-design-review.md"
|
|
)[0] == review_state
|
|
assert result["status"] == expected_status
|
|
assert result["next_action"] == expected_action
|
|
assert result["evidence"]["design_review_state"] == review_state
|
|
|
|
|
|
def test_feature_legacy_blocked_design_review_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "legacy-blocked", review_status="blocked")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "classify-feature-design-review-block"
|
|
assert result["evidence"]["design_review_state"] == "legacy-blocked"
|
|
|
|
|
|
def test_feature_approved_design_requires_passed_review_before_goal_state(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
|
|
for review_status in ("missing", "changes-requested"):
|
|
slug = f"invalid-{review_status}"
|
|
feature = write_feature(
|
|
repo,
|
|
slug,
|
|
design_status="approved",
|
|
review_status="passed" if review_status == "missing" else review_status,
|
|
)
|
|
if review_status == "missing":
|
|
(feature / f"{slug}-design-review.md").unlink()
|
|
write_goal_state(feature, stage="complete", status="passed")
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-design-review-state"
|
|
assert result["final_answer_allowed"] is True
|
|
assert result["blocking"] == [f"approved design has design-review state: {review_status}"]
|
|
|
|
|
|
def test_feature_goal_runtime_distinguishes_dispatch_and_active_driver(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved")
|
|
write_goal_state(feature, stage="implementation", status="ready-to-dispatch")
|
|
|
|
dispatch = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert dispatch["status"] == "dispatch_goal"
|
|
assert dispatch["next_action"] == "dispatch-feature-goal-driver-or-print-goal"
|
|
assert dispatch["final_answer_allowed"] is False
|
|
assert dispatch["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
|
|
write_goal_state(
|
|
feature,
|
|
stage="implementation",
|
|
status="running",
|
|
driver_kind="host-agent",
|
|
driver_id="feature-run-123",
|
|
)
|
|
active = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert active["status"] == "awaiting"
|
|
assert active["evidence"]["driver_id"] == "feature-run-123"
|
|
assert active["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
|
|
|
|
def test_feature_terminal_goal_states_override_stale_driver(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved")
|
|
write_goal_state(
|
|
feature,
|
|
stage="complete",
|
|
status="passed",
|
|
driver_kind="host-agent",
|
|
driver_id="feature-run-123",
|
|
)
|
|
|
|
complete = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert complete["status"] == "complete"
|
|
assert complete["next_action"] == "CS_FEATURE_GOAL_COMPLETE"
|
|
assert complete["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
|
|
write_goal_state(
|
|
feature,
|
|
stage="handoff",
|
|
status="blocked",
|
|
driver_kind="host-agent",
|
|
driver_id="feature-run-123",
|
|
handoff_reason="missing production credential",
|
|
handoff_next="provide credential",
|
|
acceptance_authorization=None,
|
|
)
|
|
handoff = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
assert handoff["status"] == "handoff"
|
|
assert handoff["next_action"] == "CS_FEATURE_GOAL_HANDOFF"
|
|
assert handoff["reason"] == "missing production credential"
|
|
assert handoff["evidence"]["handoff_next"] == "provide credential"
|
|
|
|
|
|
def test_epic_goal_runtime_distinguishes_dispatch_and_active_driver(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(roadmap, status="ready-to-dispatch")
|
|
|
|
dispatch = workflow_next.epic_next(roadmap)
|
|
assert dispatch["status"] == "dispatch_goal"
|
|
assert dispatch["next_action"] == "dispatch-epic-goal-driver-or-print-goal"
|
|
assert dispatch["final_answer_allowed"] is False
|
|
assert dispatch["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
assert dispatch["evidence"]["commit_authorization_ref"] == "approval-report.md#goal-commits"
|
|
|
|
write_goal_state(
|
|
roadmap,
|
|
status="ready-to-dispatch",
|
|
driver_kind="host-agent",
|
|
driver_id="epic-run-123",
|
|
)
|
|
active = workflow_next.epic_next(roadmap)
|
|
assert active["status"] == "awaiting"
|
|
assert active["evidence"]["driver_id"] == "epic-run-123"
|
|
assert active["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
assert active["evidence"]["commit_authorization_ref"] == "approval-report.md#goal-commits"
|
|
|
|
|
|
def test_epic_active_roadmap_requires_review_artifact_before_goal_state(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
(roadmap / "billing-system-roadmap-review.md").unlink()
|
|
write_goal_state(roadmap, status="complete")
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-roadmap-review-state"
|
|
assert result["final_answer_allowed"] is True
|
|
assert result["blocking"] == ["active roadmap has no roadmap review artifact"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("review_state", "status", "review_reason", "reviewer_id", "expected_status", "expected_action"),
|
|
[
|
|
("passed", "passed", "", "", "continue", "cs-feat design/design-review"),
|
|
(
|
|
"changes-requested",
|
|
"changes-requested",
|
|
"",
|
|
"",
|
|
"continue",
|
|
"cs-epic planning/update then review",
|
|
),
|
|
("awaiting-reviewer", "blocked", "", "review-run-123", "awaiting", "wait-roadmap-reviewer"),
|
|
(
|
|
"needs-owner-approval",
|
|
"blocked",
|
|
"independent reviewer unavailable",
|
|
"",
|
|
"user_gate",
|
|
"resolve-roadmap-review-approval",
|
|
),
|
|
(
|
|
"reviewer-failed",
|
|
"blocked",
|
|
"reviewer crashed",
|
|
"",
|
|
"blocked",
|
|
"retry-roadmap-reviewer",
|
|
),
|
|
(
|
|
"blocked",
|
|
"blocked",
|
|
"explicit reviewer config unavailable",
|
|
"",
|
|
"blocked",
|
|
"resolve-roadmap-review-block",
|
|
),
|
|
],
|
|
)
|
|
def test_epic_roadmap_review_state_routes_without_collapsing_failure_modes(
|
|
tmp_path: Path,
|
|
review_state: str,
|
|
status: str,
|
|
review_reason: str,
|
|
reviewer_id: str,
|
|
expected_status: str,
|
|
expected_action: str,
|
|
) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-roadmap-review.md",
|
|
"---\n"
|
|
"doc_type: roadmap-review\n"
|
|
f"status: {status}\n"
|
|
f"review_state: {review_state}\n"
|
|
f'review_reason: "{review_reason}"\n'
|
|
f'reviewer_id: "{reviewer_id}"\n'
|
|
"---\n# Review\n",
|
|
)
|
|
|
|
review_file = roadmap / "billing-system-roadmap-review.md"
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == expected_status
|
|
assert result["next_action"] == expected_action
|
|
assert workflow_next.roadmap_review_state(review_file)[0] == review_state
|
|
if review_state != "passed":
|
|
assert result["evidence"]["roadmap_review_state"] == review_state
|
|
|
|
|
|
def test_epic_legacy_blocked_review_fails_closed(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write(
|
|
roadmap / "billing-system-roadmap-review.md",
|
|
"---\ndoc_type: roadmap-review\nstatus: blocked\n---\n# Legacy Review\n",
|
|
)
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "rerun-cs-epic-review-to-migrate-state"
|
|
assert result["evidence"]["roadmap_review_state"] == "legacy-blocked"
|
|
|
|
|
|
def test_epic_terminal_goal_states_override_stale_driver(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
write_goal_state(
|
|
roadmap,
|
|
status="complete",
|
|
driver_kind="host-agent",
|
|
driver_id="epic-run-123",
|
|
)
|
|
|
|
complete = workflow_next.epic_next(roadmap)
|
|
assert complete["status"] == "complete"
|
|
assert complete["next_action"] == "CS_ROADMAP_GOAL_COMPLETE"
|
|
assert complete["evidence"]["acceptance_authorization_ref"] == "approval-report.md#goal-acceptance"
|
|
assert complete["evidence"]["commit_authorization_ref"] == "approval-report.md#goal-commits"
|
|
|
|
write_goal_state(
|
|
roadmap,
|
|
status="handoff",
|
|
driver_kind="host-agent",
|
|
driver_id="epic-run-123",
|
|
handoff_reason="migration environment unavailable",
|
|
handoff_next="provision migration environment",
|
|
)
|
|
handoff = workflow_next.epic_next(roadmap)
|
|
assert handoff["status"] == "handoff"
|
|
assert handoff["next_action"] == "CS_ROADMAP_GOAL_HANDOFF"
|
|
assert handoff["reason"] == "migration environment unavailable"
|
|
assert handoff["evidence"]["handoff_next"] == "provision migration environment"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"artifact",
|
|
["design", "checklist", "review", "qa", "acceptance", "ff-note", "goal-state"],
|
|
)
|
|
def test_feature_corrupt_artifact_returns_structured_json_block(tmp_path: Path, artifact: str) -> None:
|
|
repo = init_repo(tmp_path)
|
|
feature = write_feature(repo, "api-seed", design_status="approved", execution_lane="standard")
|
|
corrupt = "---\ndoc_type: [unterminated\n---\n# Corrupt\n"
|
|
if artifact == "design":
|
|
target = feature / "api-seed-design.md"
|
|
write(target, corrupt)
|
|
elif artifact == "checklist":
|
|
target = feature / "api-seed-checklist.yaml"
|
|
write(target, "steps: [unterminated\n")
|
|
elif artifact == "goal-state":
|
|
target = feature / "goal-state.yaml"
|
|
write(target, "stage: [unterminated\n")
|
|
else:
|
|
target = feature / f"api-seed-{artifact}.md"
|
|
write(target, corrupt)
|
|
|
|
result = workflow_next.feature_next(feature, epic_child_batch=False)
|
|
|
|
expected_path = target.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-feature-artifact-yaml"
|
|
assert expected_path in result["blocking"][0]
|
|
assert expected_path in result["evidence"]["invalid_artifact"]
|
|
|
|
completed, payload = run_cli_json(repo, "feature", feature)
|
|
assert completed.returncode == 1
|
|
assert "Traceback" not in completed.stderr
|
|
assert payload["status"] == "blocked"
|
|
assert expected_path in str(payload["reason"])
|
|
|
|
|
|
def test_epic_corrupt_goal_state_returns_structured_json_block(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", design_status="approved", include_roadmap=True)
|
|
write_feature(repo, "ui-seed", design_status="approved", include_roadmap=True)
|
|
target = roadmap / "goal-state.yaml"
|
|
write(target, "status: [unterminated\n")
|
|
|
|
result = workflow_next.epic_next(roadmap)
|
|
|
|
expected_path = target.relative_to(repo).as_posix()
|
|
assert result["status"] == "blocked"
|
|
assert result["next_action"] == "fix-epic-artifact-yaml"
|
|
assert expected_path in result["blocking"][0]
|
|
|
|
completed, payload = run_cli_json(repo, "epic", roadmap)
|
|
assert completed.returncode == 1
|
|
assert "Traceback" not in completed.stderr
|
|
assert payload["status"] == "blocked"
|
|
assert expected_path in str(payload["reason"])
|
|
|
|
|
|
def test_cli_accepts_json_before_or_after_subcommand(tmp_path: Path) -> None:
|
|
repo = init_repo(tmp_path)
|
|
roadmap = write_roadmap(repo)
|
|
write_feature(repo, "api-seed", include_roadmap=True)
|
|
|
|
for command in (
|
|
[
|
|
sys.executable,
|
|
(TOOLS_DIR / "codestable-workflow-next.py").as_posix(),
|
|
"--json",
|
|
"epic",
|
|
"--roadmap",
|
|
roadmap.as_posix(),
|
|
],
|
|
[
|
|
sys.executable,
|
|
(TOOLS_DIR / "codestable-workflow-next.py").as_posix(),
|
|
"epic",
|
|
"--roadmap",
|
|
roadmap.as_posix(),
|
|
"--json",
|
|
],
|
|
):
|
|
completed = subprocess.run(
|
|
command,
|
|
cwd=repo,
|
|
check=True,
|
|
text=True,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
env={"PYTHONDONTWRITEBYTECODE": "1"},
|
|
)
|
|
|
|
result = json.loads(completed.stdout)
|
|
assert result["status"] == "continue"
|
|
assert result["final_answer_allowed"] is False
|