Add Azure DevOps work item synchronization with handoffs system

This commit is contained in:
pragya247
2026-03-03 00:27:38 +05:30
parent b55d00beed
commit 39ac7e48d6
8 changed files with 2133 additions and 1 deletions

View File

@@ -632,6 +632,272 @@ class TestCliValidation:
assert "agent skills" in plain.lower()
class TestHandoffsFieldInSkills:
"""Test handling of handoffs field in command templates for AI skills (ADO sync feature)."""
def test_skill_generation_with_handoffs_in_template(self, project_dir):
"""Skills should generate successfully from templates containing handoffs field."""
# Create template with handoffs
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "specify.md").write_text(
"---\n"
"description: Create specification\n"
"handoffs:\n"
" - label: Sync to Azure DevOps\n"
" agent: speckit.adosync\n"
" prompt: Sync user stories to ADO\n"
" send: true\n"
" - label: Build Plan\n"
" agent: speckit.plan\n"
" send: false\n"
"---\n"
"\n"
"# Specify Command\n"
"\n"
"Create specs.\n",
encoding="utf-8",
)
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
assert skill_file.exists()
content = skill_file.read_text()
# Verify skill has valid structure
assert "name: speckit-specify" in content
assert "description:" in content
# Body content should be preserved
assert "Create specs." in content
def test_skill_generation_with_multiline_handoffs_prompt(self, project_dir):
"""Skills should generate successfully from templates with multiline handoffs prompts."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "plan.md").write_text(
"---\n"
"description: Create plan\n"
"handoffs:\n"
" - label: Sync Tasks\n"
" agent: speckit.adosync\n"
" prompt: |\n"
" Read the tasks.md file and show me all the tasks.\n"
" Ask me which tasks I want to sync.\n"
" Then create Task work items in Azure DevOps.\n"
" send: true\n"
"---\n"
"\n"
"# Plan\n"
"\n"
"Plan body.\n",
encoding="utf-8",
)
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-plan" / "SKILL.md"
content = skill_file.read_text()
# Verify skill was generated successfully
assert "name: speckit-plan" in content
assert "Plan body." in content
def test_handoffs_field_parseable_in_generated_skill(self, project_dir):
"""Generated SKILL.md should have valid parseable YAML regardless of source frontmatter."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "tasks.md").write_text(
"---\n"
"description: Generate tasks\n"
"handoffs:\n"
" - label: Sync to ADO\n"
" agent: speckit.adosync\n"
" prompt: Sync tasks to Azure DevOps\n"
"---\n"
"\n"
"# Tasks\n"
"\n"
"Task content.\n",
encoding="utf-8",
)
install_ai_skills(project_dir, "claude")
skill_file = project_dir / ".claude" / "skills" / "speckit-tasks" / "SKILL.md"
content = skill_file.read_text()
# Extract and parse frontmatter to verify it's valid YAML
parts = content.split("---", 2)
assert len(parts) >= 3
parsed = yaml.safe_load(parts[1])
# The generated SKILL.md should have agentskills.io compliant frontmatter
assert isinstance(parsed, dict)
assert "name" in parsed
assert parsed["name"] == "speckit-tasks"
assert "description" in parsed
assert "compatibility" in parsed
# Body should be preserved
assert "Task content." in content
def test_templates_with_handoffs_and_scripts_fields(self, project_dir):
"""Skills should generate from templates with multiple complex fields like handoffs and scripts."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "specify.md").write_text(
"---\n"
"description: Spec command\n"
"handoffs:\n"
" - label: Sync to ADO\n"
" agent: speckit.adosync\n"
" prompt: |\n"
" Sync user stories from spec.md.\n"
" The spec file path is: {spec_file_path}\n"
"scripts:\n"
" sh: scripts/bash/create-new-feature.sh\n"
" ps: scripts/powershell/create-new-feature.ps1\n"
"---\n"
"\n"
"# Specify\n"
"\n"
"Command body.\n",
encoding="utf-8",
)
install_ai_skills(project_dir, "claude")
skill_file = project_dir / ".claude" / "skills" / "speckit-specify" / "SKILL.md"
content = skill_file.read_text()
# Skill should be generated successfully
assert "name: speckit-specify" in content
assert "Command body." in content
def test_multiple_handoffs_dont_break_skill_generation(self, project_dir):
"""Templates with multiple handoffs should generate skills without errors."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "plan.md").write_text(
"---\n"
"description: Plan command\n"
"handoffs:\n"
" - label: Sync User Stories\n"
" agent: speckit.adosync\n"
" prompt: Sync user stories\n"
" send: true\n"
" - label: Sync Tasks\n"
" agent: speckit.adosync\n"
" prompt: Sync tasks with -FromTasks\n"
" send: false\n"
" - label: Create Checklist\n"
" agent: speckit.checklist\n"
" send: true\n"
"---\n"
"\n"
"# Plan\n"
"\n"
"Planning content.\n",
encoding="utf-8",
)
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-plan" / "SKILL.md"
content = skill_file.read_text()
# Skill should be generated with valid structure
assert "name: speckit-plan" in content
assert "Planning content." in content
def test_handoffs_field_optional_in_skills(self, project_dir):
"""Commands without handoffs should still generate valid skills."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "legacy.md").write_text(
"---\n"
"description: Legacy command without handoffs\n"
"---\n"
"\n"
"# Legacy Command\n",
encoding="utf-8",
)
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-legacy" / "SKILL.md"
assert skill_file.exists()
content = skill_file.read_text()
# Should have valid structure without handoffs
assert "name: speckit-legacy" in content
assert "Legacy command without handoffs" in content
def test_empty_handoffs_array_in_skills(self, project_dir):
"""Commands with empty handoffs array should generate valid skills."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "test.md").write_text(
"---\n"
"description: Test command\n"
"handoffs: []\n"
"---\n"
"\n"
"# Test\n",
encoding="utf-8",
)
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-test" / "SKILL.md"
content = skill_file.read_text()
# Should handle empty handoffs gracefully
assert "name: speckit-test" in content
def test_adosync_command_generates_skill(self, project_dir):
"""The adosync command itself should generate a valid skill."""
cmds_dir = project_dir / ".claude" / "commands"
cmds_dir.mkdir(parents=True)
(cmds_dir / "adosync.md").write_text(
"---\n"
"description: Sync selected user stories or tasks to Azure DevOps\n"
"scripts:\n"
" sh: scripts/bash/create-ado-workitems.sh\n"
" ps: scripts/powershell/create-ado-workitems.ps1\n"
"---\n"
"\n"
"# ADO Sync Command\n"
"\n"
"Sync to Azure DevOps.\n",
encoding="utf-8",
)
result = install_ai_skills(project_dir, "claude")
assert result is True
skill_file = project_dir / ".claude" / "skills" / "speckit-adosync" / "SKILL.md"
assert skill_file.exists()
content = skill_file.read_text()
assert "name: speckit-adosync" in content
assert "Azure DevOps" in content
class TestParameterOrderingIssue:
"""Test fix for GitHub issue #1641: parameter ordering issues."""