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

@@ -987,3 +987,305 @@ class TestExtensionCatalog:
assert not catalog.cache_file.exists()
assert not catalog.cache_metadata_file.exists()
# ===== Handoffs Field Tests (ADO Sync) =====
class TestHandoffsField:
"""Test parsing and handling of handoffs field in command frontmatter (ADO sync feature)."""
def test_parse_frontmatter_with_handoffs(self):
"""Test parsing frontmatter containing handoffs field."""
content = """---
description: "Test command with handoffs"
handoffs:
- label: Sync to Azure DevOps
agent: speckit.adosync
prompt: Sync user stories to Azure DevOps
send: true
- label: Create Tasks
agent: speckit.tasks
prompt: Break down into tasks
send: false
---
# Command content
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert "handoffs" in frontmatter
assert len(frontmatter["handoffs"]) == 2
# Verify first handoff
assert frontmatter["handoffs"][0]["label"] == "Sync to Azure DevOps"
assert frontmatter["handoffs"][0]["agent"] == "speckit.adosync"
assert frontmatter["handoffs"][0]["prompt"] == "Sync user stories to Azure DevOps"
assert frontmatter["handoffs"][0]["send"] is True
# Verify second handoff
assert frontmatter["handoffs"][1]["label"] == "Create Tasks"
assert frontmatter["handoffs"][1]["agent"] == "speckit.tasks"
assert frontmatter["handoffs"][1]["send"] is False
def test_parse_frontmatter_with_multiline_handoff_prompt(self):
"""Test parsing handoffs with multiline prompts."""
content = """---
description: "Test command"
handoffs:
- label: Sync Tasks to ADO
agent: speckit.adosync
prompt: |
Read the tasks.md file and show me all the tasks.
Ask me which tasks I want to sync (I can say "all", specific numbers like "1,2,3", or ranges like "1-10").
Then create Task work items in Azure DevOps.
send: true
---
# Command
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert "handoffs" in frontmatter
assert len(frontmatter["handoffs"]) == 1
assert "Read the tasks.md file" in frontmatter["handoffs"][0]["prompt"]
assert "Ask me which tasks" in frontmatter["handoffs"][0]["prompt"]
def test_parse_frontmatter_with_handoffs_missing_optional_fields(self):
"""Test parsing handoffs with only required fields."""
content = """---
description: "Minimal handoff"
handoffs:
- label: Next Step
agent: speckit.plan
---
# Command
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert "handoffs" in frontmatter
assert len(frontmatter["handoffs"]) == 1
assert frontmatter["handoffs"][0]["label"] == "Next Step"
assert frontmatter["handoffs"][0]["agent"] == "speckit.plan"
assert "prompt" not in frontmatter["handoffs"][0]
assert "send" not in frontmatter["handoffs"][0]
def test_handoffs_field_preserved_in_rendered_markdown(self):
"""Test that handoffs field is preserved when rendering commands."""
frontmatter = {
"description": "Create specification",
"handoffs": [
{
"label": "Sync to Azure DevOps",
"agent": "speckit.adosync",
"prompt": "Sync user stories from the spec.md",
"send": True
}
]
}
body = "# Specify Command\n\n$ARGUMENTS"
registrar = CommandRegistrar()
rendered = registrar._render_markdown_command(frontmatter, body, "test-ext")
# Verify handoffs is in the frontmatter
assert "handoffs:" in rendered
assert "agent: speckit.adosync" in rendered
assert "Sync user stories from the spec.md" in rendered
assert "send: true" in rendered
def test_handoffs_field_preserved_in_rendered_toml(self):
"""Test that handoffs field is NOT included in TOML format (unsupported)."""
frontmatter = {
"description": "Create specification",
"handoffs": [
{
"label": "Sync to ADO",
"agent": "speckit.adosync",
"send": True
}
]
}
body = "# Command\n\n{{args}}"
registrar = CommandRegistrar()
rendered = registrar._render_toml_command(frontmatter, body, "test-ext")
# TOML format only extracts description, not complex structures like handoffs
assert 'description = "Create specification"' in rendered
# Handoffs should not appear in TOML (it only supports simple fields)
assert "handoffs" not in rendered
def test_register_command_with_handoffs_to_claude(self, temp_dir, project_dir):
"""Test registering command with handoffs field for Claude."""
import yaml
# Create extension with handoffs in command
ext_dir = temp_dir / "ext-handoffs"
ext_dir.mkdir()
manifest_data = {
"schema_version": "1.0",
"extension": {
"id": "ext-handoffs",
"name": "Extension with Handoffs",
"version": "1.0.0",
"description": "Test handoffs",
},
"requires": {"speckit_version": ">=0.1.0"},
"provides": {
"commands": [
{
"name": "speckit.handoffs.specify",
"file": "commands/specify.md",
}
]
},
}
with open(ext_dir / "extension.yml", 'w') as f:
yaml.dump(manifest_data, f)
(ext_dir / "commands").mkdir()
cmd_content = """---
description: Create spec with handoffs
handoffs:
- label: Sync to ADO
agent: speckit.adosync
prompt: Sync to Azure DevOps
send: true
---
# Specify
$ARGUMENTS
"""
(ext_dir / "commands" / "specify.md").write_text(cmd_content)
# Register command
claude_dir = project_dir / ".claude" / "commands"
claude_dir.mkdir(parents=True)
manifest = ExtensionManifest(ext_dir / "extension.yml")
registrar = CommandRegistrar()
registered = registrar.register_commands_for_claude(manifest, ext_dir, project_dir)
# Verify registration
assert len(registered) == 1
cmd_file = claude_dir / "speckit.handoffs.specify.md"
assert cmd_file.exists()
# Verify handoffs field is preserved
content = cmd_file.read_text()
assert "handoffs:" in content
assert "agent: speckit.adosync" in content
assert "Sync to Azure DevOps" in content
def test_handoffs_agent_field_format_validation(self):
"""Test that agent field in handoffs uses correct format."""
content = """---
description: "Test"
handoffs:
- label: Invalid Agent Format
agent: invalid-agent-name
---
# Command
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
# Should parse successfully (validation happens elsewhere)
assert "handoffs" in frontmatter
assert frontmatter["handoffs"][0]["agent"] == "invalid-agent-name"
def test_multiple_handoffs_with_same_agent(self):
"""Test command with multiple handoffs referencing the same agent."""
content = """---
description: "Multiple handoffs"
handoffs:
- label: Sync User Stories
agent: speckit.adosync
prompt: Sync user stories
send: true
- label: Sync Tasks
agent: speckit.adosync
prompt: Sync tasks with -FromTasks flag
send: false
---
# Command
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert len(frontmatter["handoffs"]) == 2
assert frontmatter["handoffs"][0]["agent"] == "speckit.adosync"
assert frontmatter["handoffs"][1]["agent"] == "speckit.adosync"
assert frontmatter["handoffs"][0]["label"] != frontmatter["handoffs"][1]["label"]
def test_handoffs_with_interpolation_placeholders(self):
"""Test handoffs with prompt containing variable placeholders."""
content = """---
description: "Command with variable interpolation"
handoffs:
- label: Sync to ADO
agent: speckit.adosync
prompt: |
Sync user stories from the spec.md we just created.
The spec file path is: {spec_file_path}
Please:
1. Show me the list of user stories found
2. Ask which ones I want to sync (or suggest 'all')
3. Create the work items in Azure DevOps
send: true
---
# Command
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert "handoffs" in frontmatter
assert "{spec_file_path}" in frontmatter["handoffs"][0]["prompt"]
def test_empty_handoffs_array(self):
"""Test command with empty handoffs array."""
content = """---
description: "No handoffs"
handoffs: []
---
# Command
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
assert "handoffs" in frontmatter
assert len(frontmatter["handoffs"]) == 0
def test_handoffs_field_not_present(self):
"""Test command without handoffs field (backwards compatibility)."""
content = """---
description: "Legacy command without handoffs"
---
# Command
$ARGUMENTS
"""
registrar = CommandRegistrar()
frontmatter, body = registrar.parse_frontmatter(content)
# Should not have handoffs field
assert "handoffs" not in frontmatter