import subprocess import textwrap from pathlib import Path import pytest from automation import workflow SUMMARY_TEMPLATE = """ # Summary — ## Decisions (ADR-style) - (none yet) ## Open Questions - (none yet) ## Awaiting Replies - (none yet) ## Action Items - (none yet) ## Votes (latest per participant) READY: 0 • CHANGES: 0 • REJECT: 0 - (no votes yet) ## Timeline (most recent first) - : ## Links - Related PRs: – - Commits: – - Design/Plan: ../design/design.md """ @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"