diff --git a/src/specify_cli/integrations/base.py b/src/specify_cli/integrations/base.py index 4c71b165e5..a3d8a42aa2 100644 --- a/src/specify_cli/integrations/base.py +++ b/src/specify_cli/integrations/base.py @@ -482,7 +482,7 @@ def upsert_context_section( ) if ctx_path.exists(): - content = ctx_path.read_text(encoding="utf-8") + content = ctx_path.read_text(encoding="utf-8-sig") start_idx = content.find(self.CONTEXT_MARKER_START) end_idx = content.find( self.CONTEXT_MARKER_END, @@ -547,7 +547,7 @@ def remove_context_section(self, project_root: Path) -> bool: if not ctx_path.exists(): return False - content = ctx_path.read_text(encoding="utf-8") + content = ctx_path.read_text(encoding="utf-8-sig") start_idx = content.find(self.CONTEXT_MARKER_START) end_idx = content.find( self.CONTEXT_MARKER_END, diff --git a/tests/integrations/test_integration_claude.py b/tests/integrations/test_integration_claude.py index 153983dcf4..72e73bb02b 100644 --- a/tests/integrations/test_integration_claude.py +++ b/tests/integrations/test_integration_claude.py @@ -1,5 +1,6 @@ """Tests for ClaudeIntegration.""" +import codecs import json import os from unittest.mock import patch @@ -74,6 +75,46 @@ def test_setup_upserts_context_section(self, tmp_path): assert "" in content assert "read the current plan" in content + def test_upsert_context_section_strips_bom(self, tmp_path): + """Existing context file with UTF-8 BOM must be cleaned up on upsert.""" + integration = get_integration("claude") + ctx_path = tmp_path / integration.context_file + + # Write a file that starts with a UTF-8 BOM (as the old PowerShell script did) + bom = codecs.BOM_UTF8 + ctx_path.write_bytes(bom + b"# CLAUDE.md\n\nSome existing content.\n") + + integration.upsert_context_section(tmp_path) + + result = ctx_path.read_bytes() + assert not result.startswith(bom), "BOM must be stripped after upsert" + content = result.decode("utf-8") + assert "" in content + assert "Some existing content." in content + + def test_remove_context_section_strips_bom(self, tmp_path): + """remove_context_section must clean BOM from context file on Windows-authored files.""" + integration = get_integration("claude") + ctx_path = tmp_path / integration.context_file + + marker_content = ( + "# CLAUDE.md\n\n" + "\n" + "For additional context about technologies to be used, project structure,\n" + "shell commands, and other important information, read the current plan\n" + "\n" + ) + ctx_path.write_bytes(codecs.BOM_UTF8 + marker_content.encode("utf-8")) + + result = integration.remove_context_section(tmp_path) + + assert result is True + assert ctx_path.exists(), "File should exist (non-empty content remains)" + remaining = ctx_path.read_bytes() + assert not remaining.startswith(codecs.BOM_UTF8), "BOM must be stripped after remove" + assert b"