|
1 | 1 | """Tests for OpencodeIntegration.""" |
2 | 2 |
|
| 3 | +import os |
| 4 | + |
| 5 | +import yaml |
| 6 | + |
3 | 7 | import warnings |
4 | 8 |
|
5 | 9 | from specify_cli.agents import CommandRegistrar |
@@ -198,3 +202,178 @@ def test_setup_writes_to_canonical_dir(self, tmp_path): |
198 | 202 | assert canonical.is_dir() |
199 | 203 | assert not legacy.exists() |
200 | 204 | assert any(canonical.glob("speckit.*.md")) |
| 205 | + |
| 206 | + |
| 207 | +class TestOpencodeSkillsMode: |
| 208 | + KEY = "opencode" |
| 209 | + |
| 210 | + def test_skills_option_declared(self): |
| 211 | + integration = get_integration(self.KEY) |
| 212 | + opts = integration.options() |
| 213 | + names = [o.name for o in opts] |
| 214 | + assert "--skills" in names |
| 215 | + skills_opt = next(o for o in opts if o.name == "--skills") |
| 216 | + assert skills_opt.is_flag is True |
| 217 | + assert skills_opt.default is False |
| 218 | + |
| 219 | + def test_skills_mode_creates_skill_md_files(self, tmp_path): |
| 220 | + integration = get_integration(self.KEY) |
| 221 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 222 | + created = integration.setup(tmp_path, manifest, parsed_options={"skills": True}, script_type="sh") |
| 223 | + |
| 224 | + skill_files = [p for p in created if p.name == "SKILL.md"] |
| 225 | + assert skill_files |
| 226 | + |
| 227 | + skills_dir = tmp_path / ".opencode" / "skills" |
| 228 | + assert skills_dir.is_dir() |
| 229 | + |
| 230 | + specify_skill = skills_dir / "speckit-specify" / "SKILL.md" |
| 231 | + assert specify_skill.exists() |
| 232 | + |
| 233 | + def test_skills_mode_does_not_create_md_command_files(self, tmp_path): |
| 234 | + integration = get_integration(self.KEY) |
| 235 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 236 | + integration.setup(tmp_path, manifest, parsed_options={"skills": True}, script_type="sh") |
| 237 | + |
| 238 | + command_dir = tmp_path / ".opencode" / "commands" |
| 239 | + md_files = list(command_dir.glob("*.md")) if command_dir.exists() else [] |
| 240 | + assert md_files == [] |
| 241 | + |
| 242 | + def test_skills_mode_frontmatter(self, tmp_path): |
| 243 | + integration = get_integration(self.KEY) |
| 244 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 245 | + integration.setup(tmp_path, manifest, parsed_options={"skills": True}, script_type="sh") |
| 246 | + |
| 247 | + skill_path = tmp_path / ".opencode" / "skills" / "speckit-plan" / "SKILL.md" |
| 248 | + assert skill_path.exists() |
| 249 | + |
| 250 | + content = skill_path.read_text(encoding="utf-8") |
| 251 | + parts = content.split("---", 2) |
| 252 | + parsed = yaml.safe_load(parts[1]) |
| 253 | + |
| 254 | + assert parsed["name"] == "speckit-plan" |
| 255 | + assert "description" in parsed |
| 256 | + assert "compatibility" in parsed |
| 257 | + assert parsed["metadata"]["author"] == "github-spec-kit" |
| 258 | + |
| 259 | + def test_default_mode_unchanged(self, tmp_path): |
| 260 | + integration = get_integration(self.KEY) |
| 261 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 262 | + integration.setup(tmp_path, manifest, script_type="sh") |
| 263 | + |
| 264 | + command_dir = tmp_path / ".opencode" / "commands" |
| 265 | + assert command_dir.is_dir() |
| 266 | + md_files = list(command_dir.glob("speckit.*.md")) |
| 267 | + assert md_files |
| 268 | + |
| 269 | + def test_effective_invoke_separator_skills_mode(self): |
| 270 | + integration = get_integration(self.KEY) |
| 271 | + assert integration.effective_invoke_separator({"skills": True}) == "-" |
| 272 | + |
| 273 | + def test_effective_invoke_separator_default_mode(self): |
| 274 | + integration = get_integration(self.KEY) |
| 275 | + assert integration.effective_invoke_separator({}) == "." |
| 276 | + |
| 277 | + def test_skills_mode_flag_set_on_instance(self, tmp_path): |
| 278 | + integration = get_integration(self.KEY) |
| 279 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 280 | + integration.setup(tmp_path, manifest, parsed_options={"skills": True}, script_type="sh") |
| 281 | + assert integration._skills_mode is True |
| 282 | + |
| 283 | + def test_skills_mode_resets_on_default_setup(self, tmp_path): |
| 284 | + integration = get_integration(self.KEY) |
| 285 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 286 | + integration.setup(tmp_path, manifest, parsed_options={"skills": True}, script_type="sh") |
| 287 | + assert integration._skills_mode is True |
| 288 | + |
| 289 | + manifest2 = IntegrationManifest(self.KEY, tmp_path) |
| 290 | + integration.setup(tmp_path, manifest2, script_type="sh") |
| 291 | + assert integration._skills_mode is False |
| 292 | + |
| 293 | + def test_init_cli_with_skills_option(self, tmp_path): |
| 294 | + from typer.testing import CliRunner |
| 295 | + from specify_cli import app |
| 296 | + |
| 297 | + project = tmp_path / "opencode-skills" |
| 298 | + project.mkdir() |
| 299 | + old_cwd = os.getcwd() |
| 300 | + try: |
| 301 | + os.chdir(project) |
| 302 | + result = CliRunner().invoke(app, [ |
| 303 | + "init", "--here", "--integration", "opencode", |
| 304 | + "--integration-options", "--skills", |
| 305 | + "--script", "sh", "--no-git", "--ignore-agent-tools", |
| 306 | + ], catch_exceptions=False) |
| 307 | + finally: |
| 308 | + os.chdir(old_cwd) |
| 309 | + |
| 310 | + assert result.exit_code == 0, f"init failed: {result.output}" |
| 311 | + skills_dir = project / ".opencode" / "skills" |
| 312 | + assert skills_dir.is_dir(), "Skills directory was not created" |
| 313 | + plan_skill = skills_dir / "speckit-plan" / "SKILL.md" |
| 314 | + assert plan_skill.exists(), "speckit-plan/SKILL.md not found" |
| 315 | + |
| 316 | + import json |
| 317 | + init_opts = json.loads((project / ".specify" / "init-options.json").read_text()) |
| 318 | + assert init_opts.get("ai_skills") is True |
| 319 | + |
| 320 | + commands_dir = project / ".opencode" / "commands" |
| 321 | + if commands_dir.exists(): |
| 322 | + assert not list(commands_dir.glob("*.md")) |
| 323 | + |
| 324 | + def test_build_command_invocation_skills_mode(self, tmp_path): |
| 325 | + integration = get_integration(self.KEY) |
| 326 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 327 | + integration.setup(tmp_path, manifest, parsed_options={"skills": True}, script_type="sh") |
| 328 | + |
| 329 | + assert integration.build_command_invocation("speckit.plan", "add OAuth") == "/speckit-plan add OAuth" |
| 330 | + assert integration.build_command_invocation("speckit.specify", "") == "/speckit-specify" |
| 331 | + |
| 332 | + def test_build_command_invocation_default_mode(self, tmp_path): |
| 333 | + integration = get_integration(self.KEY) |
| 334 | + manifest = IntegrationManifest(self.KEY, tmp_path) |
| 335 | + integration.setup(tmp_path, manifest, script_type="sh") |
| 336 | + assert integration.build_command_invocation("speckit.plan", "add OAuth") == "/speckit.plan add OAuth" |
| 337 | + |
| 338 | + def test_dispatch_command_resets_skills_mode_for_non_skills_project(self, tmp_path): |
| 339 | + from unittest import mock |
| 340 | + |
| 341 | + integration = get_integration(self.KEY) |
| 342 | + # Manually set _skills_mode to True to simulate a prior dispatch |
| 343 | + integration._skills_mode = True |
| 344 | + |
| 345 | + # Create a project_root with no skills layout |
| 346 | + project = tmp_path / "regular-project" |
| 347 | + project.mkdir() |
| 348 | + (project / ".opencode").mkdir() |
| 349 | + |
| 350 | + # Mock subprocess.run to prevent actual CLI invocation |
| 351 | + with mock.patch("subprocess.run"): |
| 352 | + integration.dispatch_command( |
| 353 | + "plan", "test args", project_root=project |
| 354 | + ) |
| 355 | + |
| 356 | + # Should have reset _skills_mode to False since no skills dir exists |
| 357 | + assert integration._skills_mode is False |
| 358 | + |
| 359 | + def test_dispatch_command_detects_skills_project(self, tmp_path): |
| 360 | + from unittest import mock |
| 361 | + |
| 362 | + integration = get_integration(self.KEY) |
| 363 | + # Start with _skills_mode = False |
| 364 | + integration._skills_mode = False |
| 365 | + |
| 366 | + # Create a skills-mode project layout |
| 367 | + project = tmp_path / "skills-project" |
| 368 | + skills_dir = project / ".opencode" / "skills" / "speckit-plan" |
| 369 | + skills_dir.mkdir(parents=True) |
| 370 | + (skills_dir / "SKILL.md").write_text("# skill", encoding="utf-8") |
| 371 | + |
| 372 | + # Mock subprocess.run to prevent actual CLI invocation |
| 373 | + with mock.patch("subprocess.run"): |
| 374 | + integration.dispatch_command( |
| 375 | + "plan", "test args", project_root=project |
| 376 | + ) |
| 377 | + |
| 378 | + # Should have detected and set _skills_mode to True |
| 379 | + assert integration._skills_mode is True |
0 commit comments