"""Tests for CopilotIntegration.""" import json import os from specify_cli.integrations import get_integration from specify_cli.integrations.manifest import IntegrationManifest class TestCopilotIntegration: def test_copilot_key_and_config(self): copilot = get_integration("copilot") assert copilot is not None assert copilot.key == "copilot" assert copilot.config["folder"] == ".github/" assert copilot.config["commands_subdir"] == "agents" assert copilot.registrar_config["extension"] == ".agent.md" assert copilot.context_file == ".github/copilot-instructions.md" def test_command_filename_agent_md(self): copilot = get_integration("copilot") assert copilot.command_filename("plan") == "speckit.plan.agent.md" def test_setup_creates_agent_md_files(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) assert len(created) > 0 agent_files = [f for f in created if ".agent." in f.name] assert len(agent_files) > 0 for f in agent_files: assert f.parent == tmp_path / ".github" / "agents" assert f.name.endswith(".agent.md") def test_setup_creates_companion_prompts(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) prompt_files = [f for f in created if f.parent.name == "prompts"] assert len(prompt_files) > 0 for f in prompt_files: assert f.name.endswith(".prompt.md") content = f.read_text(encoding="utf-8") assert content.startswith("---\nagent: speckit.") def test_agent_and_prompt_counts_match(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) agents = [f for f in created if ".agent.md" in f.name] prompts = [f for f in created if ".prompt.md" in f.name] assert len(agents) == len(prompts) def test_setup_creates_vscode_settings_new(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() assert copilot._vscode_settings_path() is not None m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) settings = tmp_path / ".vscode" / "settings.json" assert settings.exists() assert settings in created assert any("settings.json" in k for k in m.files) def test_setup_merges_existing_vscode_settings(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() vscode_dir = tmp_path / ".vscode" vscode_dir.mkdir(parents=True) existing = {"editor.fontSize": 14, "custom.setting": True} (vscode_dir / "settings.json").write_text(json.dumps(existing, indent=4), encoding="utf-8") m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) settings = tmp_path / ".vscode" / "settings.json" data = json.loads(settings.read_text(encoding="utf-8")) assert data["editor.fontSize"] == 14 assert data["custom.setting"] is True assert settings not in created assert not any("settings.json" in k for k in m.files) def test_all_created_files_tracked_in_manifest(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) created = copilot.setup(tmp_path, m) for f in created: rel = f.resolve().relative_to(tmp_path.resolve()).as_posix() assert rel in m.files, f"Created file {rel} not tracked in manifest" def test_install_uninstall_roundtrip(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) created = copilot.install(tmp_path, m) assert len(created) > 0 m.save() for f in created: assert f.exists() removed, skipped = copilot.uninstall(tmp_path, m) assert len(removed) == len(created) assert skipped == [] def test_modified_file_survives_uninstall(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) created = copilot.install(tmp_path, m) m.save() modified_file = created[0] modified_file.write_text("user modified this", encoding="utf-8") removed, skipped = copilot.uninstall(tmp_path, m) assert modified_file.exists() assert modified_file in skipped def test_directory_structure(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) copilot.setup(tmp_path, m) agents_dir = tmp_path / ".github" / "agents" assert agents_dir.is_dir() agent_files = sorted(agents_dir.glob("speckit.*.agent.md")) assert len(agent_files) == 9 expected_commands = { "analyze", "checklist", "clarify", "constitution", "implement", "plan", "specify", "tasks", "taskstoissues", } actual_commands = {f.name.removeprefix("speckit.").removesuffix(".agent.md") for f in agent_files} assert actual_commands == expected_commands def test_templates_are_processed(self, tmp_path): from specify_cli.integrations.copilot import CopilotIntegration copilot = CopilotIntegration() m = IntegrationManifest("copilot", tmp_path) copilot.setup(tmp_path, m) agents_dir = tmp_path / ".github" / "agents" for agent_file in agents_dir.glob("speckit.*.agent.md"): content = agent_file.read_text(encoding="utf-8") assert "{SCRIPT}" not in content, f"{agent_file.name} has unprocessed {{SCRIPT}}" assert "__AGENT__" not in content, f"{agent_file.name} has unprocessed __AGENT__" assert "{ARGS}" not in content, f"{agent_file.name} has unprocessed {{ARGS}}" assert "\nscripts:\n" not in content assert "\nagent_scripts:\n" not in content def test_complete_file_inventory_sh(self, tmp_path): """Every file produced by specify init --integration copilot --script sh.""" from typer.testing import CliRunner from specify_cli import app project = tmp_path / "inventory-sh" project.mkdir() old_cwd = os.getcwd() try: os.chdir(project) result = CliRunner().invoke(app, [ "init", "--here", "--integration", "copilot", "--script", "sh", "--no-git", ], catch_exceptions=False) finally: os.chdir(old_cwd) assert result.exit_code == 0 actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()) expected = sorted([ ".github/agents/speckit.analyze.agent.md", ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", ".github/agents/speckit.implement.agent.md", ".github/agents/speckit.plan.agent.md", ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", ".github/prompts/speckit.implement.prompt.md", ".github/prompts/speckit.plan.prompt.md", ".github/prompts/speckit.specify.prompt.md", ".github/prompts/speckit.tasks.prompt.md", ".github/prompts/speckit.taskstoissues.prompt.md", ".vscode/settings.json", ".specify/integration.json", ".specify/init-options.json", ".specify/integrations/copilot.manifest.json", ".specify/integrations/speckit.manifest.json", ".specify/integrations/copilot/scripts/update-context.ps1", ".specify/integrations/copilot/scripts/update-context.sh", ".specify/scripts/bash/check-prerequisites.sh", ".specify/scripts/bash/common.sh", ".specify/scripts/bash/create-new-feature.sh", ".specify/scripts/bash/setup-plan.sh", ".specify/scripts/bash/update-agent-context.sh", ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", ".specify/memory/constitution.md", ".specify/workflows/speckit/workflow.yml", ".specify/workflows/workflow-registry.json", ]) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" ) def test_complete_file_inventory_ps(self, tmp_path): """Every file produced by specify init --integration copilot --script ps.""" from typer.testing import CliRunner from specify_cli import app project = tmp_path / "inventory-ps" project.mkdir() old_cwd = os.getcwd() try: os.chdir(project) result = CliRunner().invoke(app, [ "init", "--here", "--integration", "copilot", "--script", "ps", "--no-git", ], catch_exceptions=False) finally: os.chdir(old_cwd) assert result.exit_code == 0 actual = sorted(p.relative_to(project).as_posix() for p in project.rglob("*") if p.is_file()) expected = sorted([ ".github/agents/speckit.analyze.agent.md", ".github/agents/speckit.checklist.agent.md", ".github/agents/speckit.clarify.agent.md", ".github/agents/speckit.constitution.agent.md", ".github/agents/speckit.implement.agent.md", ".github/agents/speckit.plan.agent.md", ".github/agents/speckit.specify.agent.md", ".github/agents/speckit.tasks.agent.md", ".github/agents/speckit.taskstoissues.agent.md", ".github/prompts/speckit.analyze.prompt.md", ".github/prompts/speckit.checklist.prompt.md", ".github/prompts/speckit.clarify.prompt.md", ".github/prompts/speckit.constitution.prompt.md", ".github/prompts/speckit.implement.prompt.md", ".github/prompts/speckit.plan.prompt.md", ".github/prompts/speckit.specify.prompt.md", ".github/prompts/speckit.tasks.prompt.md", ".github/prompts/speckit.taskstoissues.prompt.md", ".vscode/settings.json", ".specify/integration.json", ".specify/init-options.json", ".specify/integrations/copilot.manifest.json", ".specify/integrations/speckit.manifest.json", ".specify/integrations/copilot/scripts/update-context.ps1", ".specify/integrations/copilot/scripts/update-context.sh", ".specify/scripts/powershell/check-prerequisites.ps1", ".specify/scripts/powershell/common.ps1", ".specify/scripts/powershell/create-new-feature.ps1", ".specify/scripts/powershell/setup-plan.ps1", ".specify/scripts/powershell/update-agent-context.ps1", ".specify/templates/agent-file-template.md", ".specify/templates/checklist-template.md", ".specify/templates/constitution-template.md", ".specify/templates/plan-template.md", ".specify/templates/spec-template.md", ".specify/templates/tasks-template.md", ".specify/memory/constitution.md", ".specify/workflows/speckit/workflow.yml", ".specify/workflows/workflow-registry.json", ]) assert actual == expected, ( f"Missing: {sorted(set(expected) - set(actual))}\n" f"Extra: {sorted(set(actual) - set(expected))}" )