"""Tests for ForgeIntegration.""" from specify_cli.integrations import get_integration from specify_cli.integrations.manifest import IntegrationManifest class TestForgeIntegration: def test_forge_key_and_config(self): forge = get_integration("forge") assert forge is not None assert forge.key == "forge" assert forge.config["folder"] == ".forge/" assert forge.config["commands_subdir"] == "commands" assert forge.config["requires_cli"] is True assert forge.registrar_config["args"] == "{{parameters}}" assert forge.registrar_config["extension"] == ".md" assert forge.context_file == "AGENTS.md" def test_command_filename_md(self): forge = get_integration("forge") assert forge.command_filename("plan") == "speckit.plan.md" def test_setup_creates_md_files(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) created = forge.setup(tmp_path, m) assert len(created) > 0 # Separate command files from scripts command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"] assert len(command_files) > 0 for f in command_files: assert f.name.endswith(".md") def test_setup_installs_update_scripts(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) created = forge.setup(tmp_path, m) script_files = [f for f in created if "scripts" in f.parts] assert len(script_files) > 0 sh_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.sh" ps_script = tmp_path / ".specify" / "integrations" / "forge" / "scripts" / "update-context.ps1" assert sh_script in created assert ps_script in created assert sh_script.exists() assert ps_script.exists() def test_all_created_files_tracked_in_manifest(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) created = forge.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.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) created = forge.install(tmp_path, m) assert len(created) > 0 m.save() for f in created: assert f.exists() removed, skipped = forge.uninstall(tmp_path, m) assert len(removed) == len(created) assert skipped == [] def test_modified_file_survives_uninstall(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) created = forge.install(tmp_path, m) m.save() # Modify a command file (not a script) command_files = [f for f in created if f.parent == tmp_path / ".forge" / "commands"] modified_file = command_files[0] modified_file.write_text("user modified this", encoding="utf-8") removed, skipped = forge.uninstall(tmp_path, m) assert modified_file.exists() assert modified_file in skipped def test_directory_structure(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) forge.setup(tmp_path, m) commands_dir = tmp_path / ".forge" / "commands" assert commands_dir.is_dir() # Derive expected command names from the Forge command templates so the test # stays in sync if templates are added/removed. templates = forge.list_command_templates() expected_commands = {t.stem for t in templates} assert len(expected_commands) > 0, "No command templates found" # Check generated files match templates command_files = sorted(commands_dir.glob("speckit.*.md")) assert len(command_files) == len(expected_commands) actual_commands = {f.name.removeprefix("speckit.").removesuffix(".md") for f in command_files} assert actual_commands == expected_commands def test_templates_are_processed(self, tmp_path): from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) forge.setup(tmp_path, m) commands_dir = tmp_path / ".forge" / "commands" for cmd_file in commands_dir.glob("speckit.*.md"): content = cmd_file.read_text(encoding="utf-8") # Check standard replacements assert "{SCRIPT}" not in content, f"{cmd_file.name} has unprocessed {{SCRIPT}}" assert "__AGENT__" not in content, f"{cmd_file.name} has unprocessed __AGENT__" assert "{ARGS}" not in content, f"{cmd_file.name} has unprocessed {{ARGS}}" # Check Forge-specific: $ARGUMENTS should be replaced with {{parameters}} assert "$ARGUMENTS" not in content, f"{cmd_file.name} has unprocessed $ARGUMENTS" # Frontmatter sections should be stripped assert "\nscripts:\n" not in content assert "\nagent_scripts:\n" not in content def test_forge_specific_transformations(self, tmp_path): """Test Forge-specific processing: name injection and handoffs stripping.""" from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() m = IntegrationManifest("forge", tmp_path) forge.setup(tmp_path, m) commands_dir = tmp_path / ".forge" / "commands" for cmd_file in commands_dir.glob("speckit.*.md"): content = cmd_file.read_text(encoding="utf-8") # Check that name field is injected in frontmatter assert "\nname: " in content, f"{cmd_file.name} missing injected 'name' field" # Check that handoffs frontmatter key is stripped assert "\nhandoffs:" not in content, f"{cmd_file.name} has unstripped 'handoffs' key" def test_uses_parameters_placeholder(self, tmp_path): """Verify Forge replaces $ARGUMENTS with {{parameters}} in generated files.""" from specify_cli.integrations.forge import ForgeIntegration forge = ForgeIntegration() # The registrar_config should specify {{parameters}} assert forge.registrar_config["args"] == "{{parameters}}" # Generate files and verify $ARGUMENTS is replaced with {{parameters}} from specify_cli.integrations.manifest import IntegrationManifest m = IntegrationManifest("forge", tmp_path) forge.setup(tmp_path, m) commands_dir = tmp_path / ".forge" / "commands" # Check all generated command files for cmd_file in commands_dir.glob("speckit.*.md"): content = cmd_file.read_text(encoding="utf-8") # $ARGUMENTS should be replaced with {{parameters}} assert "$ARGUMENTS" not in content, ( f"{cmd_file.name} still contains $ARGUMENTS - it should be replaced with {{{{parameters}}}}" ) # At least some files should have {{parameters}} (those with user input sections) # We'll check the checklist file specifically as it has a User Input section # Verify checklist specifically has {{parameters}} in the User Input section checklist = commands_dir / "speckit.checklist.md" if checklist.exists(): content = checklist.read_text(encoding="utf-8") assert "{{parameters}}" in content, ( "checklist should contain {{parameters}} in User Input section" )