Replace the Superpowers plan pipeline with grill-with-docs, specs, local tickets, and ticket-native execution. BREAKING CHANGE: Remove the legacy Plan CLI, prompt templates, and Superpowers skills.
2485 lines
88 KiB
Python
2485 lines
88 KiB
Python
import importlib.util
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
SCRIPT = ROOT / "scripts" / "main_loop.py"
|
|
|
|
_MAIN_LOOP_SPEC = importlib.util.spec_from_file_location(
|
|
"playbook_main_loop", ROOT / "scripts" / "main_loop.py"
|
|
)
|
|
assert _MAIN_LOOP_SPEC and _MAIN_LOOP_SPEC.loader
|
|
MAIN_LOOP = importlib.util.module_from_spec(_MAIN_LOOP_SPEC)
|
|
sys.modules[_MAIN_LOOP_SPEC.name] = MAIN_LOOP
|
|
_MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
|
|
|
|
|
|
def run_cli(*args: str, cwd: Path) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
[sys.executable, str(SCRIPT), *args],
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def start_cli(*args: str, cwd: Path) -> subprocess.Popen[str]:
|
|
return subprocess.Popen(
|
|
[sys.executable, str(SCRIPT), *args],
|
|
cwd=cwd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def run_git(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
return subprocess.run(
|
|
["git", *args],
|
|
cwd=root,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
def parse_assignments(output: str) -> dict[str, str]:
|
|
return dict(line.split("=", 1) for line in output.splitlines() if "=" in line)
|
|
|
|
|
|
def verification_evidence(commit: str, detail: str) -> str:
|
|
return f"commit={commit}; result=pass; {detail}"
|
|
|
|
|
|
def review_evidence(commit: str, base: str) -> str:
|
|
return f"commit={commit}; base={base}; standards=pass; spec=pass"
|
|
|
|
|
|
def init_repo(root: Path) -> None:
|
|
initialized = run_git(root, "init", "-b", "main")
|
|
if initialized.returncode != 0:
|
|
initialized = run_git(root, "init")
|
|
if initialized.returncode == 0:
|
|
run_git(root, "checkout", "-b", "main")
|
|
if initialized.returncode != 0:
|
|
raise AssertionError(initialized.stderr)
|
|
run_git(root, "config", "user.name", "Test User")
|
|
run_git(root, "config", "user.email", "test@example.invalid")
|
|
(root / ".gitignore").write_text(".scratch/\n", encoding="utf-8", newline="\n")
|
|
(root / "app.txt").write_text("base\n", encoding="utf-8", newline="\n")
|
|
run_git(root, "add", ".gitignore", "app.txt")
|
|
committed = run_git(root, "commit", "-m", "base")
|
|
if committed.returncode != 0:
|
|
raise AssertionError(committed.stderr)
|
|
|
|
|
|
def write_ticket(
|
|
issues: Path,
|
|
number: str,
|
|
slug: str,
|
|
title: str,
|
|
blocked_by: str = "None - can start immediately.",
|
|
status: str = "ready-for-agent",
|
|
) -> Path:
|
|
path = issues / f"{number}-{slug}.md"
|
|
path.write_text(
|
|
"\n".join(
|
|
[
|
|
f"# {number} - {title}",
|
|
"",
|
|
f"**What to build:** Deliver {title}.",
|
|
"",
|
|
f"**Blocked by:** {blocked_by}",
|
|
"",
|
|
f"**Status:** {status}",
|
|
"",
|
|
"- [ ] The behavior is observable.",
|
|
"",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
return path
|
|
|
|
|
|
def write_feature(root: Path, slug: str) -> Path:
|
|
feature = root / ".scratch" / slug
|
|
issues = feature / "issues"
|
|
issues.mkdir(parents=True)
|
|
(feature / "spec.md").write_text(
|
|
f"# {slug} spec\n", encoding="utf-8", newline="\n"
|
|
)
|
|
return issues
|
|
|
|
|
|
class MainLoopCliTests(unittest.TestCase):
|
|
def test_entrypoint_exposes_only_ticket_native_commands(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
result = run_cli("--help", cwd=Path(tmp_dir))
|
|
|
|
self.assertEqual(result.returncode, 0, msg=result.stderr)
|
|
for command in (
|
|
"enqueue",
|
|
"status",
|
|
"claim",
|
|
"finish",
|
|
"heartbeat",
|
|
"reclaim",
|
|
"integrate",
|
|
):
|
|
self.assertIn(command, result.stdout)
|
|
|
|
def test_enqueue_writes_a_prettier_stable_queue(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(
|
|
(root / ".scratch" / "queue.md").read_text(encoding="utf-8"),
|
|
"# Feature Queue\n\n"
|
|
"<!-- main-loop:queue:start -->\n\n"
|
|
"- `alpha`\n\n"
|
|
"<!-- main-loop:queue:end -->\n",
|
|
)
|
|
|
|
def test_cli_rejects_a_caller_supplied_clock(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
|
|
result = run_cli(
|
|
"status",
|
|
"--state-root",
|
|
".scratch",
|
|
"--now",
|
|
"2099-01-01T00:00:00Z",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("unrecognized arguments: --now", result.stderr)
|
|
|
|
def test_in_place_claim_resumes_with_uncommitted_work(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
original = parse_assignments(claim.stdout)
|
|
(root / "unfinished.txt").write_text(
|
|
"preserve me\n", encoding="utf-8", newline="\n"
|
|
)
|
|
|
|
resumed = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(resumed.returncode, 0, msg=resumed.stderr)
|
|
context = parse_assignments(resumed.stdout)
|
|
self.assertEqual(context["WORKSPACE"], original["WORKSPACE"])
|
|
self.assertEqual(context["BRANCH"], original["BRANCH"])
|
|
self.assertEqual(
|
|
(root / "unfinished.txt").read_text(encoding="utf-8"),
|
|
"preserve me\n",
|
|
)
|
|
|
|
def test_released_in_place_ticket_preserves_uncommitted_work(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
original = parse_assignments(claim.stdout)
|
|
(root / "unfinished.txt").write_text(
|
|
"preserve me\n", encoding="utf-8", newline="\n"
|
|
)
|
|
released = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"released",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(released.returncode, 0, msg=released.stderr)
|
|
|
|
reclaimed = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-2",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(reclaimed.returncode, 0, msg=reclaimed.stderr)
|
|
context = parse_assignments(reclaimed.stdout)
|
|
self.assertEqual(context["WORKSPACE"], original["WORKSPACE"])
|
|
self.assertEqual(context["BRANCH"], original["BRANCH"])
|
|
self.assertEqual(
|
|
(root / "unfinished.txt").read_text(encoding="utf-8"),
|
|
"preserve me\n",
|
|
)
|
|
|
|
def test_worktree_claim_refuses_to_resume_on_the_wrong_branch(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
context = parse_assignments(claim.stdout)
|
|
workspace = Path(context["WORKSPACE"])
|
|
wrong_branch = run_git(workspace, "checkout", "-b", "wrong-branch")
|
|
self.assertEqual(wrong_branch.returncode, 0, msg=wrong_branch.stderr)
|
|
|
|
resumed = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(resumed.returncode, 2)
|
|
self.assertIn("claim workspace is not on", resumed.stderr)
|
|
|
|
def test_blocked_feature_allows_later_development_but_integration_stays_ordered(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
alpha_issues = write_feature(root, "alpha")
|
|
beta_issues = write_feature(root, "beta")
|
|
write_ticket(alpha_issues, "01", "alpha", "Alpha")
|
|
write_ticket(beta_issues, "01", "beta", "Beta")
|
|
for feature in ("alpha", "beta"):
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
feature,
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
alpha_claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"alpha-owner",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
busy = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"waiting-owner",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
blocked = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"alpha-owner",
|
|
"--result",
|
|
"blocked",
|
|
"--reason",
|
|
"waiting for decision",
|
|
cwd=root,
|
|
)
|
|
beta_claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"beta-owner",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(alpha_claim.returncode, 0, msg=alpha_claim.stderr)
|
|
self.assertEqual(busy.stdout.strip(), "BUSY")
|
|
self.assertEqual(blocked.returncode, 0, msg=blocked.stderr)
|
|
self.assertEqual(beta_claim.returncode, 0, msg=beta_claim.stderr)
|
|
self.assertEqual(parse_assignments(beta_claim.stdout)["FEATURE"], "beta")
|
|
|
|
released = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"alpha-owner",
|
|
"--result",
|
|
"released",
|
|
cwd=root,
|
|
)
|
|
resumed_alpha = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"alpha-owner-2",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(released.returncode, 0, msg=released.stderr)
|
|
self.assertEqual(resumed_alpha.returncode, 0, msg=resumed_alpha.stderr)
|
|
self.assertEqual(parse_assignments(resumed_alpha.stdout)["FEATURE"], "alpha")
|
|
|
|
contexts = {
|
|
"alpha": parse_assignments(resumed_alpha.stdout),
|
|
"beta": parse_assignments(beta_claim.stdout),
|
|
}
|
|
owners = {"alpha": "alpha-owner-2", "beta": "beta-owner"}
|
|
for feature in ("alpha", "beta"):
|
|
workspace = Path(contexts[feature]["WORKSPACE"])
|
|
(workspace / f"{feature}.txt").write_text(
|
|
f"{feature}\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(workspace, "add", f"{feature}.txt")
|
|
committed = run_git(workspace, "commit", "-m", f"implement {feature}")
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
commit = run_git(workspace, "rev-parse", "HEAD").stdout.strip()
|
|
finished = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
feature,
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
owners[feature],
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
commit,
|
|
"--feature-head",
|
|
contexts[feature]["BASE"],
|
|
"--review-base",
|
|
contexts[feature]["BASE"],
|
|
"--verified",
|
|
verification_evidence(commit, f"{feature} ticket tests"),
|
|
"--reviewed",
|
|
review_evidence(commit, contexts[feature]["BASE"]),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(finished.returncode, 0, msg=finished.stderr)
|
|
|
|
barrier = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"new-owner",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
beta_head = run_git(
|
|
root, "rev-parse", "feature/beta"
|
|
).stdout.strip()
|
|
beta_out_of_order = run_cli(
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"beta",
|
|
"--feature-head",
|
|
beta_head,
|
|
"--verified",
|
|
verification_evidence(beta_head, "beta feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(beta_head, "beta main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(
|
|
beta_head,
|
|
run_git(root, "rev-parse", "main").stdout.strip(),
|
|
),
|
|
cwd=root,
|
|
)
|
|
self.assertIn("INTEGRATION_REQUIRED=alpha", barrier.stdout)
|
|
self.assertEqual(beta_out_of_order.returncode, 2)
|
|
self.assertIn("earlier feature alpha", beta_out_of_order.stderr)
|
|
|
|
alpha_head = run_git(root, "rev-parse", "feature/alpha").stdout.strip()
|
|
main_before_alpha = run_git(root, "rev-parse", "main").stdout.strip()
|
|
alpha_integrated = run_cli(
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--feature-head",
|
|
alpha_head,
|
|
"--verified",
|
|
verification_evidence(alpha_head, "alpha feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(alpha_head, "alpha main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(alpha_head, main_before_alpha),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(alpha_integrated.returncode, 0, msg=alpha_integrated.stderr)
|
|
|
|
beta_head = run_git(root, "rev-parse", "feature/beta").stdout.strip()
|
|
main_after_alpha = run_git(root, "rev-parse", "main").stdout.strip()
|
|
beta_needs_sync = run_cli(
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"beta",
|
|
"--feature-head",
|
|
beta_head,
|
|
"--verified",
|
|
verification_evidence(beta_head, "beta feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(beta_head, "beta main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(beta_head, main_after_alpha),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(beta_needs_sync.returncode, 0, msg=beta_needs_sync.stderr)
|
|
self.assertIn("RETRY: feature needs main sync", beta_needs_sync.stdout)
|
|
|
|
beta_integration = root / ".scratch" / "worktrees" / "_integration" / "beta"
|
|
synced = run_git(beta_integration, "merge", "--no-edit", "main")
|
|
self.assertEqual(synced.returncode, 0, msg=synced.stderr)
|
|
synced_beta_head = run_git(root, "rev-parse", "feature/beta").stdout.strip()
|
|
beta_integrated = run_cli(
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"beta",
|
|
"--feature-head",
|
|
synced_beta_head,
|
|
"--verified",
|
|
verification_evidence(
|
|
synced_beta_head, "beta feature tests after sync"
|
|
),
|
|
"--main-verified",
|
|
verification_evidence(
|
|
synced_beta_head, "beta main candidate tests after sync"
|
|
),
|
|
"--reviewed",
|
|
review_evidence(synced_beta_head, main_after_alpha),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(beta_integrated.returncode, 0, msg=beta_integrated.stderr)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
self.assertIn("FEATURE=alpha STATE=integrated", status.stdout)
|
|
self.assertIn("FEATURE=beta STATE=integrated", status.stdout)
|
|
|
|
def test_integration_block_allows_later_development_until_released(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
alpha_issues = write_feature(root, "alpha")
|
|
beta_issues = write_feature(root, "beta")
|
|
write_ticket(
|
|
alpha_issues,
|
|
"01",
|
|
"alpha",
|
|
"Alpha",
|
|
status="resolved",
|
|
)
|
|
write_ticket(beta_issues, "01", "beta", "Beta")
|
|
for feature in ("alpha", "beta"):
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
feature,
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
blocked = run_cli(
|
|
"block-feature",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
"--reason",
|
|
"main sync conflict",
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
beta_claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"beta-owner",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
released = run_cli(
|
|
"release-feature",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
barrier = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"next-owner",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(blocked.returncode, 0, msg=blocked.stderr)
|
|
self.assertIn("FEATURE_BLOCKED=alpha", blocked.stdout)
|
|
self.assertIn("FEATURE=alpha STATE=blocked", status.stdout)
|
|
self.assertIn("FEATURE_BLOCKED=alpha REASON=main sync conflict", status.stdout)
|
|
self.assertEqual(beta_claim.returncode, 0, msg=beta_claim.stderr)
|
|
self.assertEqual(parse_assignments(beta_claim.stdout)["FEATURE"], "beta")
|
|
self.assertEqual(released.returncode, 0, msg=released.stderr)
|
|
self.assertIn("FEATURE_RELEASED=alpha", released.stdout)
|
|
self.assertEqual(barrier.stdout.strip(), "INTEGRATION_REQUIRED=alpha")
|
|
|
|
def test_reclaim_does_not_allow_the_caller_to_shorten_the_lease(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1-20260810-a1b2",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
|
|
reclaim = run_cli(
|
|
"reclaim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-2-20260810-c3d4",
|
|
"--stale-after-seconds",
|
|
"0",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
self.assertEqual(reclaim.returncode, 2)
|
|
self.assertIn("unrecognized arguments: --stale-after-seconds 0", reclaim.stderr)
|
|
|
|
def test_heartbeat_stale_reclaim_release_blocked_and_skipped(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
write_ticket(issues, "02", "second", "Second", "01 - First.")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
original = parse_assignments(claim.stdout)
|
|
|
|
heartbeat = run_cli(
|
|
"heartbeat",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
cwd=root,
|
|
)
|
|
wrong_owner = run_cli(
|
|
"heartbeat",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-2",
|
|
cwd=root,
|
|
)
|
|
feature = MAIN_LOOP.load_feature(root / ".scratch", "alpha")
|
|
ticket = feature.tickets["01"]
|
|
metadata = dict(ticket.metadata)
|
|
metadata["heartbeat_at"] = "2000-01-01T00:00:00Z"
|
|
MAIN_LOOP.update_ticket_state(ticket, metadata=metadata)
|
|
status = run_cli(
|
|
"status",
|
|
"--state-root",
|
|
".scratch",
|
|
cwd=root,
|
|
)
|
|
reclaim = run_cli(
|
|
"reclaim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-2",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(heartbeat.returncode, 0, msg=heartbeat.stderr)
|
|
self.assertEqual(wrong_owner.returncode, 2)
|
|
self.assertIn("owned by another session", wrong_owner.stderr)
|
|
self.assertEqual(status.returncode, 0, msg=status.stderr)
|
|
self.assertIn("CLAIM=alpha/01", status.stdout)
|
|
self.assertIn("STALE=yes", status.stdout)
|
|
self.assertEqual(reclaim.returncode, 0, msg=reclaim.stderr)
|
|
reclaimed = parse_assignments(reclaim.stdout)
|
|
self.assertEqual(reclaimed["WORKSPACE"], original["WORKSPACE"])
|
|
self.assertEqual(reclaimed["BRANCH"], original["BRANCH"])
|
|
|
|
blocked = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-2",
|
|
"--result",
|
|
"blocked",
|
|
"--reason",
|
|
"environment unavailable",
|
|
cwd=root,
|
|
)
|
|
blocked_status = run_cli(
|
|
"status", "--state-root", ".scratch", cwd=root
|
|
)
|
|
released = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-2",
|
|
"--result",
|
|
"released",
|
|
cwd=root,
|
|
)
|
|
resumed = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-3",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
skipped = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-3",
|
|
"--result",
|
|
"skipped",
|
|
"--reason",
|
|
"explicit product decision",
|
|
cwd=root,
|
|
)
|
|
final_status = run_cli(
|
|
"status", "--state-root", ".scratch", cwd=root
|
|
)
|
|
|
|
self.assertEqual(blocked.returncode, 0, msg=blocked.stderr)
|
|
self.assertIn("FEATURE=alpha STATE=blocked", blocked_status.stdout)
|
|
self.assertIn("REASON=environment unavailable", blocked_status.stdout)
|
|
self.assertEqual(released.returncode, 0, msg=released.stderr)
|
|
self.assertEqual(resumed.returncode, 0, msg=resumed.stderr)
|
|
self.assertEqual(
|
|
parse_assignments(resumed.stdout)["WORKSPACE"],
|
|
original["WORKSPACE"],
|
|
)
|
|
self.assertEqual(skipped.returncode, 0, msg=skipped.stderr)
|
|
self.assertIn("FEATURE=alpha STATE=active PARTIAL=yes", final_status.stdout)
|
|
self.assertIn("FRONTIER=02", final_status.stdout)
|
|
|
|
def test_parallel_processes_claim_distinct_frontier_tickets(self):
|
|
if os.name == "nt":
|
|
self.assertIsNotNone(MAIN_LOOP.msvcrt)
|
|
else:
|
|
self.assertIsNotNone(MAIN_LOOP.fcntl)
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
for number in ("01", "02", "03", "04"):
|
|
write_ticket(issues, number, f"ticket-{number}", f"Ticket {number}")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
processes = [
|
|
start_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
f"session-{index}",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
for index in range(4)
|
|
]
|
|
results = []
|
|
for process in processes:
|
|
stdout, stderr = process.communicate(timeout=30)
|
|
results.append((process.returncode, stdout, stderr))
|
|
|
|
for returncode, _stdout, stderr in results:
|
|
self.assertEqual(returncode, 0, msg=stderr)
|
|
contexts = [parse_assignments(stdout) for _, stdout, _ in results]
|
|
self.assertEqual(
|
|
{context["TICKET"] for context in contexts},
|
|
{"01", "02", "03", "04"},
|
|
)
|
|
self.assertEqual(
|
|
len({context["WORKSPACE"] for context in contexts}),
|
|
4,
|
|
)
|
|
|
|
def test_in_place_claim_is_busy_while_a_worktree_claim_is_active(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
write_ticket(issues, "02", "second", "Second")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
worktree_claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
|
|
in_place_claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-2",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(worktree_claim.returncode, 0, msg=worktree_claim.stderr)
|
|
self.assertEqual(in_place_claim.returncode, 0, msg=in_place_claim.stderr)
|
|
self.assertEqual(in_place_claim.stdout.strip(), "BUSY")
|
|
self.assertEqual(status.stdout.count("CLAIM=alpha/"), 1)
|
|
|
|
def test_parallel_worktree_claims_retry_when_feature_head_advances(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
write_ticket(issues, "02", "second", "Second")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
first = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
second = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-2",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(first.returncode, 0, msg=first.stderr)
|
|
self.assertEqual(second.returncode, 0, msg=second.stderr)
|
|
first_context = parse_assignments(first.stdout)
|
|
second_context = parse_assignments(second.stdout)
|
|
self.assertEqual(first_context["TICKET"], "01")
|
|
self.assertEqual(second_context["TICKET"], "02")
|
|
self.assertNotEqual(first_context["WORKSPACE"], second_context["WORKSPACE"])
|
|
first_workspace = Path(first_context["WORKSPACE"])
|
|
second_workspace = Path(second_context["WORKSPACE"])
|
|
self.assertTrue(first_workspace.is_dir())
|
|
self.assertTrue(second_workspace.is_dir())
|
|
|
|
(first_workspace / "first.txt").write_text(
|
|
"first\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(first_workspace, "add", "first.txt")
|
|
committed = run_git(first_workspace, "commit", "-m", "implement first")
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
first_commit = run_git(first_workspace, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
(second_workspace / "second.txt").write_text(
|
|
"second\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(second_workspace, "add", "second.txt")
|
|
committed = run_git(second_workspace, "commit", "-m", "implement second")
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
second_commit = run_git(second_workspace, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
first_finish = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
first_commit,
|
|
"--feature-head",
|
|
first_context["BASE"],
|
|
"--review-base",
|
|
first_context["BASE"],
|
|
"--verified",
|
|
verification_evidence(first_commit, "tests"),
|
|
"--reviewed",
|
|
review_evidence(first_commit, first_context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
stale_finish = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"02",
|
|
"--owner",
|
|
"session-2",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
second_commit,
|
|
"--feature-head",
|
|
second_context["BASE"],
|
|
"--review-base",
|
|
second_context["BASE"],
|
|
"--verified",
|
|
verification_evidence(second_commit, "tests"),
|
|
"--reviewed",
|
|
review_evidence(second_commit, second_context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(first_finish.returncode, 0, msg=first_finish.stderr)
|
|
self.assertEqual(stale_finish.returncode, 0, msg=stale_finish.stderr)
|
|
self.assertIn("RETRY: feature advanced", stale_finish.stdout)
|
|
advanced_head = run_git(root, "rev-parse", "feature/alpha").stdout.strip()
|
|
shortcut_finish = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"02",
|
|
"--owner",
|
|
"session-2",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
second_commit,
|
|
"--feature-head",
|
|
advanced_head,
|
|
"--review-base",
|
|
advanced_head,
|
|
"--verified",
|
|
verification_evidence(second_commit, "stale tests"),
|
|
"--reviewed",
|
|
review_evidence(second_commit, advanced_head),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(shortcut_finish.returncode, 2)
|
|
self.assertIn(
|
|
"ticket branch must include the verified feature head",
|
|
shortcut_finish.stderr,
|
|
)
|
|
synced = run_git(second_workspace, "merge", "--no-edit", "feature/alpha")
|
|
self.assertEqual(synced.returncode, 0, msg=synced.stderr)
|
|
synced_commit = run_git(second_workspace, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
stale_review = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"02",
|
|
"--owner",
|
|
"session-2",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
synced_commit,
|
|
"--feature-head",
|
|
advanced_head,
|
|
"--review-base",
|
|
second_context["BASE"],
|
|
"--verified",
|
|
verification_evidence(synced_commit, "tests after sync"),
|
|
"--reviewed",
|
|
review_evidence(synced_commit, second_context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(stale_review.returncode, 2)
|
|
self.assertIn(
|
|
"review base must equal the verified feature head",
|
|
stale_review.stderr,
|
|
)
|
|
|
|
second_finish = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"02",
|
|
"--owner",
|
|
"session-2",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
synced_commit,
|
|
"--feature-head",
|
|
advanced_head,
|
|
"--review-base",
|
|
advanced_head,
|
|
"--verified",
|
|
verification_evidence(synced_commit, "tests after sync"),
|
|
"--reviewed",
|
|
review_evidence(synced_commit, advanced_head),
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(second_finish.returncode, 0, msg=second_finish.stderr)
|
|
self.assertIn("RESOLVED=alpha/02", second_finish.stdout)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
self.assertIn("FEATURE=alpha STATE=ready-to-integrate", status.stdout)
|
|
|
|
def test_finish_recovers_the_actual_ticket_merge_and_review_base(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"worktree",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
context = parse_assignments(claim.stdout)
|
|
ticket_workspace = Path(context["WORKSPACE"])
|
|
integration_workspace = (
|
|
root / ".scratch" / "worktrees" / "_integration" / "alpha"
|
|
)
|
|
|
|
(ticket_workspace / "feature.txt").write_text(
|
|
"delivered\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(ticket_workspace, "add", "feature.txt")
|
|
committed = run_git(
|
|
ticket_workspace, "commit", "-m", "deliver ticket"
|
|
)
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
ticket_head = run_git(
|
|
ticket_workspace, "rev-parse", "HEAD"
|
|
).stdout.strip()
|
|
|
|
merged = run_git(
|
|
integration_workspace,
|
|
"merge",
|
|
"--no-ff",
|
|
"-m",
|
|
"integrate before ticket state write",
|
|
context["BRANCH"],
|
|
)
|
|
self.assertEqual(merged.returncode, 0, msg=merged.stderr)
|
|
ticket_merge = run_git(
|
|
integration_workspace, "rev-parse", "HEAD"
|
|
).stdout.strip()
|
|
(integration_workspace / "later.txt").write_text(
|
|
"later feature work\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(integration_workspace, "add", "later.txt")
|
|
advanced = run_git(
|
|
integration_workspace, "commit", "-m", "advance feature"
|
|
)
|
|
self.assertEqual(advanced.returncode, 0, msg=advanced.stderr)
|
|
|
|
common_args = (
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
ticket_head,
|
|
)
|
|
wrong_base = run_cli(
|
|
*common_args,
|
|
"--feature-head",
|
|
ticket_head,
|
|
"--review-base",
|
|
ticket_head,
|
|
"--verified",
|
|
verification_evidence(ticket_head, "ticket tests"),
|
|
"--reviewed",
|
|
review_evidence(ticket_head, ticket_head),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(wrong_base.returncode, 2)
|
|
self.assertIn(
|
|
"verified feature head must equal the ticket integration base",
|
|
wrong_base.stderr,
|
|
)
|
|
|
|
recovered = run_cli(
|
|
*common_args,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
context["BASE"],
|
|
"--verified",
|
|
verification_evidence(ticket_head, "ticket tests"),
|
|
"--reviewed",
|
|
review_evidence(ticket_head, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(recovered.returncode, 0, msg=recovered.stderr)
|
|
self.assertIn(f"INTEGRATION_COMMIT={ticket_merge}", recovered.stdout)
|
|
|
|
def test_serial_in_place_claim_resume_and_finish(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
write_ticket(issues, "02", "second", "Second", "01 - First.")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
resumed = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
busy = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-2",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
context = parse_assignments(claim.stdout)
|
|
self.assertEqual(context["FEATURE"], "alpha")
|
|
self.assertEqual(context["TICKET"], "01")
|
|
self.assertEqual(context["BRANCH"], "ticket/alpha/01-first")
|
|
self.assertEqual(
|
|
Path(context["STATE_ROOT"]),
|
|
(root / ".scratch").resolve(),
|
|
)
|
|
self.assertEqual(
|
|
run_git(root, "branch", "--show-current").stdout.strip(),
|
|
"ticket/alpha/01-first",
|
|
)
|
|
self.assertEqual(resumed.returncode, 0, msg=resumed.stderr)
|
|
self.assertEqual(parse_assignments(resumed.stdout), context)
|
|
self.assertEqual(busy.returncode, 0, msg=busy.stderr)
|
|
self.assertEqual(busy.stdout.strip(), "BUSY")
|
|
|
|
(root / "app.txt").write_text(
|
|
"base\nimplemented\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "app.txt")
|
|
implementation = run_git(root, "commit", "-m", "implement first")
|
|
self.assertEqual(implementation.returncode, 0, msg=implementation.stderr)
|
|
implementation_commit = run_git(root, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
finish = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
implementation_commit,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
context["BASE"],
|
|
"--verified",
|
|
verification_evidence(
|
|
implementation_commit, "python -m unittest"
|
|
),
|
|
"--reviewed",
|
|
review_evidence(implementation_commit, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
status = run_cli(
|
|
"status", "--state-root", ".scratch", cwd=root
|
|
)
|
|
|
|
self.assertEqual(finish.returncode, 0, msg=finish.stderr)
|
|
self.assertIn("RESOLVED=alpha/01", finish.stdout)
|
|
self.assertIn("INTEGRATION_COMMIT=", finish.stdout)
|
|
self.assertEqual(
|
|
run_git(root, "branch", "--show-current").stdout.strip(),
|
|
"feature/alpha",
|
|
)
|
|
self.assertIn("FEATURE=alpha STATE=active PARTIAL=no", status.stdout)
|
|
self.assertIn("FRONTIER=02", status.stdout)
|
|
|
|
def test_enqueue_and_status_report_all_frontier_tickets(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
write_ticket(issues, "02", "second", "Second")
|
|
write_ticket(
|
|
issues,
|
|
"03",
|
|
"combine",
|
|
"Combine",
|
|
"01 - First; 02 - Second.",
|
|
)
|
|
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
status = run_cli(
|
|
"status", "--state-root", ".scratch", cwd=root
|
|
)
|
|
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(enqueue.stdout.strip(), "ENQUEUED=alpha")
|
|
self.assertEqual(status.returncode, 0, msg=status.stderr)
|
|
self.assertIn("FEATURE=alpha STATE=queued PARTIAL=no", status.stdout)
|
|
self.assertIn("FRONTIER=01,02", status.stdout)
|
|
queue = (root / ".scratch" / "queue.md").read_text(encoding="utf-8")
|
|
self.assertIn("- `alpha`", queue)
|
|
|
|
def test_status_reports_a_claimed_ticket_integration_error(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "alpha")
|
|
ticket_path = write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
ticket = MAIN_LOOP.parse_ticket(ticket_path)
|
|
MAIN_LOOP.update_ticket_state(
|
|
ticket,
|
|
status="claimed",
|
|
metadata={
|
|
"claimed_by": "session-1",
|
|
"heartbeat_at": "2099-01-01T00:00:00Z",
|
|
"isolation": "worktree",
|
|
"workspace": "ticket-workspace",
|
|
"last_error": "merge failed: conflict",
|
|
},
|
|
)
|
|
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(status.returncode, 0, msg=status.stderr)
|
|
self.assertIn(
|
|
"TICKET_ERROR=alpha/01 REASON=merge failed: conflict",
|
|
status.stdout,
|
|
)
|
|
|
|
def test_enqueue_rejects_missing_blocker_before_writing_queue(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "broken")
|
|
write_ticket(issues, "01", "broken", "Broken", "99 - Missing.")
|
|
|
|
result = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"broken",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("unknown blocker 99", result.stderr)
|
|
self.assertFalse((root / ".scratch" / "queue.md").exists())
|
|
|
|
def test_enqueue_rejects_partially_parsed_blockers(self):
|
|
cases = (
|
|
"None; 99 - Hidden.",
|
|
"01 - First; garbage",
|
|
)
|
|
for blocked_by in cases:
|
|
with self.subTest(blocked_by=blocked_by), tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "broken")
|
|
write_ticket(issues, "01", "first", "First")
|
|
write_ticket(
|
|
issues,
|
|
"02",
|
|
"second",
|
|
"Second",
|
|
blocked_by,
|
|
)
|
|
|
|
result = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"broken",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("malformed Blocked by field", result.stderr)
|
|
self.assertFalse((root / ".scratch" / "queue.md").exists())
|
|
|
|
def test_enqueue_rejects_cycles_and_invalid_status_without_writing_queue(self):
|
|
cases = ("cycle", "invalid-status")
|
|
for case in cases:
|
|
with self.subTest(case=case), tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "broken")
|
|
if case == "cycle":
|
|
write_ticket(issues, "01", "first", "First", "02 - Second.")
|
|
write_ticket(issues, "02", "second", "Second", "01 - First.")
|
|
expected = "ticket dependency cycle"
|
|
else:
|
|
write_ticket(
|
|
issues,
|
|
"01",
|
|
"first",
|
|
"First",
|
|
status="done",
|
|
)
|
|
expected = "invalid status done"
|
|
|
|
result = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"broken",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn(expected, result.stderr)
|
|
self.assertFalse((root / ".scratch" / "queue.md").exists())
|
|
|
|
def test_enqueue_rejects_duplicate_ticket_machine_blocks(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "broken")
|
|
ticket = write_ticket(issues, "01", "first", "First")
|
|
original = ticket.read_text(encoding="utf-8")
|
|
block = (
|
|
"<!-- main-loop:ticket-state:start -->\n"
|
|
"{}\n"
|
|
"<!-- main-loop:ticket-state:end -->\n"
|
|
)
|
|
ticket.write_text(
|
|
f"{original}\n{block}\n{block}",
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
|
|
result = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"broken",
|
|
cwd=root,
|
|
)
|
|
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIn("multiple ticket state blocks", result.stderr)
|
|
self.assertFalse((root / ".scratch" / "queue.md").exists())
|
|
|
|
def test_status_rejects_duplicate_queue_machine_blocks(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
queue = root / ".scratch" / "queue.md"
|
|
queue.write_text(
|
|
queue.read_text(encoding="utf-8")
|
|
+ "\n<!-- main-loop:queue:start -->\n"
|
|
+ "- `alpha`\n"
|
|
+ "<!-- main-loop:queue:end -->\n",
|
|
encoding="utf-8",
|
|
newline="\n",
|
|
)
|
|
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(status.returncode, 2)
|
|
self.assertIn("queue.md has an invalid managed block", status.stderr)
|
|
|
|
def test_claim_records_environment_preparation_failures_as_blocked(self):
|
|
cases = (
|
|
("dirty", "in-place", "in-place checkout is dirty"),
|
|
("detached", "in-place", "in-place claim requires an attached HEAD"),
|
|
("worktree-path", "worktree", "worktree path is not empty"),
|
|
)
|
|
for case, isolation, expected in cases:
|
|
with self.subTest(case=case), tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
if case == "dirty":
|
|
(root / "app.txt").write_text(
|
|
"dirty\n", encoding="utf-8", newline="\n"
|
|
)
|
|
elif case == "detached":
|
|
detached = run_git(root, "checkout", "--detach", "HEAD")
|
|
self.assertEqual(detached.returncode, 0, msg=detached.stderr)
|
|
else:
|
|
occupied = (
|
|
root
|
|
/ ".scratch"
|
|
/ "worktrees"
|
|
/ "_integration"
|
|
/ "alpha"
|
|
)
|
|
occupied.mkdir(parents=True)
|
|
(occupied / "keep.txt").write_text(
|
|
"occupied\n", encoding="utf-8", newline="\n"
|
|
)
|
|
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
isolation,
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(claim.returncode, 2)
|
|
self.assertIn(expected, claim.stderr)
|
|
self.assertEqual(status.returncode, 0, msg=status.stderr)
|
|
self.assertIn("FEATURE=alpha STATE=blocked", status.stdout)
|
|
self.assertIn("BLOCKED=alpha/01", status.stdout)
|
|
self.assertIn(expected, status.stdout)
|
|
|
|
released = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"released",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(released.returncode, 0, msg=released.stderr)
|
|
|
|
def test_finish_binds_review_to_ticket_head_and_verified_feature_head(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
context = parse_assignments(claim.stdout)
|
|
|
|
(root / "reviewed.txt").write_text(
|
|
"reviewed\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "reviewed.txt")
|
|
first_commit = run_git(root, "commit", "-m", "reviewed implementation")
|
|
self.assertEqual(first_commit.returncode, 0, msg=first_commit.stderr)
|
|
reviewed_commit = run_git(root, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
(root / "unreviewed.txt").write_text(
|
|
"unreviewed\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "unreviewed.txt")
|
|
second_commit = run_git(root, "commit", "-m", "unreviewed follow-up")
|
|
self.assertEqual(second_commit.returncode, 0, msg=second_commit.stderr)
|
|
ticket_head = run_git(root, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
stale_commit_review = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
ticket_head,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
context["BASE"],
|
|
"--verified",
|
|
verification_evidence(ticket_head, "ticket tests"),
|
|
"--reviewed",
|
|
review_evidence(reviewed_commit, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
wrong_review_base = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
ticket_head,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
reviewed_commit,
|
|
"--verified",
|
|
verification_evidence(ticket_head, "ticket tests"),
|
|
"--reviewed",
|
|
review_evidence(ticket_head, reviewed_commit),
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(stale_commit_review.returncode, 2)
|
|
self.assertIn(
|
|
"review commit must equal ticket branch HEAD",
|
|
stale_commit_review.stderr,
|
|
)
|
|
self.assertEqual(wrong_review_base.returncode, 2)
|
|
self.assertIn(
|
|
"review base must equal the verified feature head",
|
|
wrong_review_base.stderr,
|
|
)
|
|
self.assertIn("CLAIM=alpha/01", status.stdout)
|
|
|
|
def test_finish_rejects_unbound_verification_evidence(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
context = parse_assignments(claim.stdout)
|
|
(root / "feature.txt").write_text(
|
|
"delivered\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "feature.txt")
|
|
committed = run_git(root, "commit", "-m", "deliver alpha")
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
implementation_commit = run_git(root, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
finish = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
implementation_commit,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
context["BASE"],
|
|
"--verified",
|
|
"result=pass; ticket tests",
|
|
"--reviewed",
|
|
review_evidence(implementation_commit, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
conflicting = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
implementation_commit,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
context["BASE"],
|
|
"--verified",
|
|
(
|
|
f"commit={implementation_commit}; result=fail; "
|
|
"result=pass; ticket tests"
|
|
),
|
|
"--reviewed",
|
|
review_evidence(implementation_commit, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(finish.returncode, 2)
|
|
self.assertIn("verification evidence must include commit", finish.stderr)
|
|
self.assertEqual(conflicting.returncode, 2)
|
|
self.assertIn(
|
|
"verification evidence must include exactly one result",
|
|
conflicting.stderr,
|
|
)
|
|
self.assertIn("CLAIM=alpha/01", status.stdout)
|
|
|
|
def test_partial_integration_requires_explicit_authorization(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "optional", "Optional")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
skipped = run_cli(
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"skipped",
|
|
"--reason",
|
|
"explicit product decision",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
self.assertEqual(skipped.returncode, 0, msg=skipped.stderr)
|
|
feature_head = run_git(root, "rev-parse", "feature/alpha").stdout.strip()
|
|
main_head = run_git(root, "rev-parse", "main").stdout.strip()
|
|
common_args = (
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--feature-head",
|
|
feature_head,
|
|
"--verified",
|
|
verification_evidence(feature_head, "feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(feature_head, main_head),
|
|
)
|
|
|
|
rejected = run_cli(*common_args, cwd=root)
|
|
accepted = run_cli(*common_args, "--allow-partial", cwd=root)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(rejected.returncode, 2)
|
|
self.assertIn("pass --allow-partial explicitly", rejected.stderr)
|
|
self.assertEqual(accepted.returncode, 0, msg=accepted.stderr)
|
|
self.assertIn("INTEGRATED=alpha", accepted.stdout)
|
|
self.assertIn("FEATURE=alpha STATE=integrated PARTIAL=yes", status.stdout)
|
|
|
|
def test_integrate_recovers_when_feature_is_already_on_main(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(
|
|
issues,
|
|
"01",
|
|
"delivered",
|
|
"Delivered",
|
|
status="resolved",
|
|
)
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
|
|
created = run_git(root, "checkout", "-b", "feature/alpha")
|
|
self.assertEqual(created.returncode, 0, msg=created.stderr)
|
|
(root / "feature.txt").write_text(
|
|
"delivered\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "feature.txt")
|
|
committed = run_git(root, "commit", "-m", "deliver alpha")
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
feature_head = run_git(root, "rev-parse", "HEAD").stdout.strip()
|
|
|
|
checkout_main = run_git(root, "checkout", "main")
|
|
self.assertEqual(checkout_main.returncode, 0, msg=checkout_main.stderr)
|
|
review_base = run_git(root, "rev-parse", "main").stdout.strip()
|
|
merged = run_git(
|
|
root,
|
|
"merge",
|
|
"--no-ff",
|
|
"-m",
|
|
"integrate before state write",
|
|
"feature/alpha",
|
|
)
|
|
self.assertEqual(merged.returncode, 0, msg=merged.stderr)
|
|
integration_commit = run_git(
|
|
root, "rev-parse", "main"
|
|
).stdout.strip()
|
|
(root / "later.txt").write_text(
|
|
"later main work\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "later.txt")
|
|
advanced = run_git(root, "commit", "-m", "advance main")
|
|
self.assertEqual(advanced.returncode, 0, msg=advanced.stderr)
|
|
main_head = run_git(root, "rev-parse", "main").stdout.strip()
|
|
|
|
recovered = run_cli(
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--feature-head",
|
|
feature_head,
|
|
"--verified",
|
|
verification_evidence(feature_head, "feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(feature_head, review_base),
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(recovered.returncode, 0, msg=recovered.stderr)
|
|
self.assertIn("INTEGRATED=alpha", recovered.stdout)
|
|
self.assertIn(
|
|
f"MAIN_INTEGRATION_COMMIT={integration_commit}",
|
|
recovered.stdout,
|
|
)
|
|
self.assertEqual(run_git(root, "rev-parse", "main").stdout.strip(), main_head)
|
|
self.assertIn("FEATURE=alpha STATE=integrated", status.stdout)
|
|
|
|
def test_integrate_retries_cleanup_after_integration_state_is_written(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(
|
|
issues,
|
|
"01",
|
|
"delivered",
|
|
"Delivered",
|
|
status="resolved",
|
|
)
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
state_root = root / ".scratch"
|
|
feature = MAIN_LOOP.load_feature(state_root, "alpha")
|
|
main_head = run_git(root, "rev-parse", "main").stdout.strip()
|
|
MAIN_LOOP.write_feature_state(
|
|
feature,
|
|
{
|
|
"feature_branch": "feature/alpha",
|
|
"integration_commit": main_head,
|
|
},
|
|
)
|
|
|
|
with mock.patch.object(
|
|
MAIN_LOOP,
|
|
"cleanup_integrated_feature_worktrees",
|
|
return_value=["cleanup retried"],
|
|
) as cleanup:
|
|
recovered = MAIN_LOOP.integrate_feature(
|
|
state_root,
|
|
root,
|
|
"alpha",
|
|
main_head,
|
|
verification_evidence(main_head, "feature tests"),
|
|
verification_evidence(main_head, "main candidate tests"),
|
|
review_evidence(main_head, main_head),
|
|
"main",
|
|
False,
|
|
)
|
|
|
|
cleanup.assert_called_once()
|
|
self.assertIn("INTEGRATED=alpha", recovered)
|
|
self.assertIn("WARNING=cleanup retried", recovered)
|
|
|
|
def test_atomic_replace_failure_preserves_state_and_next_claim_is_unique(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
ticket_path = write_ticket(issues, "01", "first", "First")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
state_root = root / ".scratch"
|
|
queue_path = state_root / "queue.md"
|
|
queue_before = queue_path.read_text(encoding="utf-8")
|
|
ticket_before = ticket_path.read_text(encoding="utf-8")
|
|
|
|
with mock.patch.object(
|
|
MAIN_LOOP.os,
|
|
"replace",
|
|
side_effect=OSError("injected replace failure"),
|
|
):
|
|
with self.assertRaisesRegex(OSError, "injected replace failure"):
|
|
MAIN_LOOP.atomic_write_text(queue_path, "broken\n")
|
|
self.assertEqual(queue_path.read_text(encoding="utf-8"), queue_before)
|
|
self.assertEqual(MAIN_LOOP.load_queue(queue_path), ["alpha"])
|
|
|
|
feature = MAIN_LOOP.load_feature(state_root, "alpha")
|
|
ticket = feature.tickets["01"]
|
|
with mock.patch.object(
|
|
MAIN_LOOP.os,
|
|
"replace",
|
|
side_effect=OSError("injected replace failure"),
|
|
):
|
|
with self.assertRaisesRegex(OSError, "injected replace failure"):
|
|
MAIN_LOOP.update_ticket_state(
|
|
ticket,
|
|
status="claimed",
|
|
metadata={"claimed_by": "failed-owner"},
|
|
)
|
|
self.assertEqual(ticket_path.read_text(encoding="utf-8"), ticket_before)
|
|
self.assertEqual(
|
|
MAIN_LOOP.parse_ticket(ticket_path).status,
|
|
"ready-for-agent",
|
|
)
|
|
self.assertEqual(list(state_root.rglob("*.tmp")), [])
|
|
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
self.assertEqual(status.stdout.count("CLAIM=alpha/01"), 1)
|
|
self.assertIn("OWNER=session-1", status.stdout)
|
|
|
|
def test_full_serial_workflow_enforces_all_evidence_gates(self):
|
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
root = Path(tmp_dir)
|
|
init_repo(root)
|
|
issues = write_feature(root, "alpha")
|
|
write_ticket(issues, "01", "deliver", "Deliver")
|
|
enqueue = run_cli(
|
|
"enqueue",
|
|
"--state-root",
|
|
".scratch",
|
|
"--feature",
|
|
"alpha",
|
|
cwd=root,
|
|
)
|
|
claim = run_cli(
|
|
"claim",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--owner",
|
|
"session-1",
|
|
"--isolation",
|
|
"in-place",
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(enqueue.returncode, 0, msg=enqueue.stderr)
|
|
self.assertEqual(claim.returncode, 0, msg=claim.stderr)
|
|
context = parse_assignments(claim.stdout)
|
|
(root / "feature.txt").write_text(
|
|
"delivered\n", encoding="utf-8", newline="\n"
|
|
)
|
|
run_git(root, "add", "feature.txt")
|
|
committed = run_git(root, "commit", "-m", "deliver alpha")
|
|
self.assertEqual(committed.returncode, 0, msg=committed.stderr)
|
|
implementation_commit = run_git(root, "rev-parse", "HEAD").stdout.strip()
|
|
finish_args = (
|
|
"finish",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--ticket",
|
|
"01",
|
|
"--owner",
|
|
"session-1",
|
|
"--result",
|
|
"resolved",
|
|
"--implementation-commit",
|
|
implementation_commit,
|
|
"--feature-head",
|
|
context["BASE"],
|
|
"--review-base",
|
|
context["BASE"],
|
|
)
|
|
|
|
missing_ticket_verification = run_cli(
|
|
*finish_args,
|
|
"--reviewed",
|
|
review_evidence(implementation_commit, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
incomplete_ticket_review = run_cli(
|
|
*finish_args,
|
|
"--verified",
|
|
verification_evidence(implementation_commit, "ticket tests"),
|
|
"--reviewed",
|
|
(
|
|
f"commit={implementation_commit}; base={context['BASE']}; "
|
|
"standards=pass"
|
|
),
|
|
cwd=root,
|
|
)
|
|
resolved = run_cli(
|
|
*finish_args,
|
|
"--verified",
|
|
verification_evidence(implementation_commit, "ticket tests"),
|
|
"--reviewed",
|
|
review_evidence(implementation_commit, context["BASE"]),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(missing_ticket_verification.returncode, 2)
|
|
self.assertIn(
|
|
"verification evidence is required",
|
|
missing_ticket_verification.stderr,
|
|
)
|
|
self.assertEqual(incomplete_ticket_review.returncode, 2)
|
|
self.assertIn("spec=pass", incomplete_ticket_review.stderr)
|
|
self.assertEqual(resolved.returncode, 0, msg=resolved.stderr)
|
|
|
|
feature_head = run_git(root, "rev-parse", "feature/alpha").stdout.strip()
|
|
main_before = run_git(root, "rev-parse", "main").stdout.strip()
|
|
integrate_args = (
|
|
"integrate",
|
|
"--state-root",
|
|
".scratch",
|
|
"--repo-root",
|
|
".",
|
|
"--feature",
|
|
"alpha",
|
|
"--feature-head",
|
|
feature_head,
|
|
)
|
|
missing_feature_verification = run_cli(
|
|
*integrate_args,
|
|
"--verified",
|
|
"",
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(feature_head, main_before),
|
|
cwd=root,
|
|
)
|
|
missing_main_verification = run_cli(
|
|
*integrate_args,
|
|
"--verified",
|
|
verification_evidence(feature_head, "feature tests"),
|
|
"--main-verified",
|
|
"",
|
|
"--reviewed",
|
|
review_evidence(feature_head, main_before),
|
|
cwd=root,
|
|
)
|
|
incomplete_final_review = run_cli(
|
|
*integrate_args,
|
|
"--verified",
|
|
verification_evidence(feature_head, "feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
f"commit={feature_head}; base={main_before}; standards=pass",
|
|
cwd=root,
|
|
)
|
|
unbound_feature_verification = run_cli(
|
|
*integrate_args,
|
|
"--verified",
|
|
"result=pass; feature tests",
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(feature_head, main_before),
|
|
cwd=root,
|
|
)
|
|
wrong_final_review_base = run_cli(
|
|
*integrate_args,
|
|
"--verified",
|
|
verification_evidence(feature_head, "feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(feature_head, feature_head),
|
|
cwd=root,
|
|
)
|
|
self.assertEqual(missing_feature_verification.returncode, 2)
|
|
self.assertIn(
|
|
"feature verification evidence is required",
|
|
missing_feature_verification.stderr,
|
|
)
|
|
self.assertEqual(missing_main_verification.returncode, 2)
|
|
self.assertIn(
|
|
"main candidate verification evidence is required",
|
|
missing_main_verification.stderr,
|
|
)
|
|
self.assertEqual(incomplete_final_review.returncode, 2)
|
|
self.assertIn("spec=pass", incomplete_final_review.stderr)
|
|
self.assertEqual(unbound_feature_verification.returncode, 2)
|
|
self.assertIn(
|
|
"feature verification evidence must include commit",
|
|
unbound_feature_verification.stderr,
|
|
)
|
|
self.assertEqual(wrong_final_review_base.returncode, 2)
|
|
self.assertIn(
|
|
"review base must equal the latest main HEAD",
|
|
wrong_final_review_base.stderr,
|
|
)
|
|
self.assertEqual(
|
|
run_git(root, "rev-parse", "main").stdout.strip(),
|
|
main_before,
|
|
)
|
|
|
|
integrated = run_cli(
|
|
*integrate_args,
|
|
"--verified",
|
|
verification_evidence(feature_head, "feature tests"),
|
|
"--main-verified",
|
|
verification_evidence(feature_head, "main candidate tests"),
|
|
"--reviewed",
|
|
review_evidence(feature_head, main_before),
|
|
cwd=root,
|
|
)
|
|
status = run_cli("status", "--state-root", ".scratch", cwd=root)
|
|
|
|
self.assertEqual(integrated.returncode, 0, msg=integrated.stderr)
|
|
self.assertIn("INTEGRATED=alpha", integrated.stdout)
|
|
self.assertIn("FEATURE=alpha STATE=integrated PARTIAL=no", status.stdout)
|
|
self.assertTrue((root / "feature.txt").is_file())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|