CascadingDev/tests/test_workflow.py

336 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import subprocess
import textwrap
from pathlib import Path
import pytest
from automation import workflow
SUMMARY_TEMPLATE = """
# Summary — <Stage Title>
<!-- SUMMARY:DECISIONS START -->
## Decisions (ADR-style)
- (none yet)
<!-- SUMMARY:DECISIONS END -->
<!-- SUMMARY:OPEN_QUESTIONS START -->
## Open Questions
- (none yet)
<!-- SUMMARY:OPEN_QUESTIONS END -->
<!-- SUMMARY:AWAITING START -->
## Awaiting Replies
- (none yet)
<!-- SUMMARY:AWAITING END -->
<!-- SUMMARY:ACTION_ITEMS START -->
## Action Items
- (none yet)
<!-- SUMMARY:ACTION_ITEMS END -->
<!-- SUMMARY:VOTES START -->
## Votes (latest per participant)
READY: 0 • CHANGES: 0 • REJECT: 0
- (no votes yet)
<!-- SUMMARY:VOTES END -->
<!-- SUMMARY:TIMELINE START -->
## Timeline (most recent first)
- <YYYY-MM-DD HH:MM> <name>: <one-liner>
<!-- SUMMARY:TIMELINE END -->
<!-- SUMMARY:LINKS START -->
## Links
- Related PRs:
- Commits:
- Design/Plan: ../design/design.md
<!-- SUMMARY:LINKS END -->
"""
@pytest.fixture()
def temp_repo(tmp_path, monkeypatch):
repo = tmp_path / "repo"
repo.mkdir()
run_git(repo, "init")
run_git(repo, "config", "user.email", "dev@example.com")
run_git(repo, "config", "user.name", "Dev")
monkeypatch.chdir(repo)
return repo
def run_git(cwd: Path, *args: str) -> None:
subprocess.run(
["git", *args],
cwd=cwd,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
def write_file(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(textwrap.dedent(content).strip() + "\n", encoding="utf-8")
def test_parse_votes_reads_index_snapshot(temp_repo):
repo = temp_repo
discussion = repo / "Docs/features/demo/discussions/example.discussion.md"
write_file(
discussion,
"""
## Thread
""",
)
run_git(repo, "add", ".")
run_git(repo, "commit", "-m", "seed")
# Stage a vote from Alice
write_file(
discussion,
"""
## Thread
- Alice: Looks good. VOTE: READY
""",
)
run_git(repo, "add", discussion.relative_to(repo).as_posix())
# Add an unstaged vote from Bob (should be ignored)
discussion.write_text(
textwrap.dedent(
"""
## Thread
- Alice: Looks good. VOTE: READY
- Bob: Still concerned. VOTE: REJECT
"""
).strip()
+ "\n",
encoding="utf-8",
)
votes = workflow.parse_votes(Path("Docs/features/demo/discussions/example.discussion.md"))
assert votes == {"Alice": "READY"}
def test_get_discussion_changes_returns_only_staged_lines(temp_repo):
repo = temp_repo
discussion = repo / "Docs/features/demo/discussions/sample.discussion.md"
write_file(
discussion,
"""
## Discussion
""",
)
run_git(repo, "add", ".")
run_git(repo, "commit", "-m", "base")
write_file(
discussion,
"""
## Discussion
- Alice: Proposal incoming. VOTE: READY
""",
)
run_git(repo, "add", discussion.relative_to(repo).as_posix())
# Unstaged change should be ignored
discussion.write_text(
textwrap.dedent(
"""
## Discussion
- Alice: Proposal incoming. VOTE: READY
- Bob: Needs changes. VOTE: CHANGES
"""
).strip()
+ "\n",
encoding="utf-8",
)
additions = workflow.get_discussion_changes(Path("Docs/features/demo/discussions/sample.discussion.md"))
assert "Alice" in additions
assert "Bob" not in additions
def test_get_discussion_changes_new_file_returns_full_content(temp_repo):
repo = temp_repo
discussion = repo / "Docs/features/new/discussions/brand-new.discussion.md"
write_file(
discussion,
"""
## Kickoff
- Maintainer: Bootstrapping. VOTE: READY
""",
)
run_git(repo, "add", discussion.relative_to(repo).as_posix())
additions = workflow.get_discussion_changes(Path("Docs/features/new/discussions/brand-new.discussion.md"))
assert "Bootstrapping" in additions
assert "Maintainer" in additions
def test_run_status_updates_summary_sections(temp_repo):
repo = temp_repo
discussion = repo / "Docs/features/demo/discussions/example.discussion.md"
summary = repo / "Docs/features/demo/discussions/example.discussion.sum.md"
write_file(discussion, """
## Discussion
""")
write_file(summary, SUMMARY_TEMPLATE)
run_git(repo, "add", ".")
run_git(repo, "commit", "-m", "seed")
write_file(discussion, """
## Discussion
- Alice: Kickoff. VOTE: READY
- Bob: Q: What is the rollout plan?
- Bob: TODO: Document rollout plan
- Carol: DONE: Documented rollout plan
- Alice: DECISION: Ship approach A
- Alice: Thanks team! @bob @carol
""")
run_git(repo, "add", discussion.relative_to(repo).as_posix())
workflow._run_status()
content = summary.read_text(encoding="utf-8")
assert "READY: 1 • CHANGES: 0 • REJECT: 0" in content
assert "- Alice: READY" in content
assert "## Open Questions" in content and "@Bob: What is the rollout plan" in content
assert "### TODO (unassigned):" in content and "Document rollout plan" in content
assert "### Completed:" in content and "Documented rollout plan" in content
assert "### Decision 1: Ship approach A" in content
assert "### @bob" in content
assert "@Alice: Kickoff. VOTE: READY" in content
staged = subprocess.run(
["git", "diff", "--cached", "--name-only"],
cwd=repo,
check=True,
capture_output=True,
text=True,
).stdout.split()
assert "Docs/features/demo/discussions/example.discussion.sum.md" in staged
def test_extract_structured_basic():
"""Test lightweight pattern matching for discussion markers."""
text = """
# Discussion Title
- Alice: Q: What about security considerations?
- Bob: TODO: Review OAuth libraries for security vulnerabilities
- Bob: @Alice I'll handle the security review
- Carol: DECISION: Use OAuth2 for third-party authentication
- Dave: DONE: Completed initial research on OAuth2 providers
- Eve: Question: Should we support social login providers?
- Frank: We should definitely support Google. What about GitHub?
- Grace: ACTION: Create comparison matrix for OAuth providers
- Grace: ASSIGNED: OAuth provider comparison (@Grace taking this)
"""
result = workflow.extract_structured_basic(text)
# Check questions
assert len(result["questions"]) == 3
question_texts = [q["question"] for q in result["questions"]]
assert "What about security considerations?" in question_texts
assert "Should we support social login providers?" in question_texts
assert "We should definitely support Google. What about GitHub" in question_texts
# Check participants
assert result["questions"][0]["participant"] == "Alice"
assert result["questions"][1]["participant"] == "Eve"
assert result["questions"][2]["participant"] == "Frank"
# Check action items
assert len(result["action_items"]) == 4
actions = result["action_items"]
# TODO items (Bob's TODO and Grace's ACTION both become TODO)
todo_items = [a for a in actions if a["status"] == "TODO"]
assert len(todo_items) == 2
bob_todo = next(a for a in todo_items if a["participant"] == "Bob")
assert "Review OAuth libraries" in bob_todo["action"]
grace_action = next(a for a in todo_items if "comparison matrix" in a["action"])
assert grace_action["participant"] == "Grace"
# DONE item
done = next(a for a in actions if a["status"] == "DONE")
assert "Completed initial research" in done["action"]
assert done["participant"] == "Dave"
assert done["completed_by"] == "Dave"
# ASSIGNED item
assigned = next(a for a in actions if a["status"] == "ASSIGNED")
assert "OAuth provider comparison" in assigned["action"]
assert assigned["participant"] == "Grace"
assert assigned["assignee"] == "Grace"
# Check decisions
assert len(result["decisions"]) == 1
decision = result["decisions"][0]
assert "Use OAuth2" in decision["decision"]
assert decision["participant"] == "Carol"
# Check mentions
assert len(result["mentions"]) == 2
mention_targets = [m["to"] for m in result["mentions"]]
assert "Alice" in mention_targets
assert "Grace" in mention_targets
# Check timeline
assert result["timeline"] is not None
assert result["timeline"]["participant"] == "Alice"
assert len(result["timeline"]["summary"]) <= 120
def test_extract_structured_basic_handles_edge_cases():
"""Test edge cases in pattern matching."""
text = """
- Alice: This is just a comment without markers
- Bob: TODO:
- Carol: DECISION:
- Dave: https://example.com/?param=value
- Eve: TODO: Valid action item here
"""
result = workflow.extract_structured_basic(text)
# Empty markers should be ignored
assert len(result["action_items"]) == 1
assert "Valid action item" in result["action_items"][0]["action"]
# Empty decision should be ignored
assert len(result["decisions"]) == 0
# URL with ? should not be treated as question
assert len(result["questions"]) == 0
# Timeline should capture first meaningful comment
assert result["timeline"]["participant"] == "Alice"
def test_extract_structured_basic_skips_headers():
"""Test that markdown headers are skipped."""
text = """
# Main Header
## Sub Header
- Alice: Q: Real question here?
"""
result = workflow.extract_structured_basic(text)
# Should have one question, headers ignored
assert len(result["questions"]) == 1
assert result["questions"][0]["question"] == "Real question here?"
# Timeline should use Alice, not the headers
assert result["timeline"]["participant"] == "Alice"