mirror of
https://github.com/github/spec-kit.git
synced 2026-07-03 12:28:06 +08:00
fix(integrations): strip UTF-8 BOM when reading agent context files (#2283)
* fix(integrations): strip UTF-8 BOM when reading agent context files * test(integrations): add BOM regression tests for context file read/write * test(workflows): mock shutil.which in tests that assume CLI is absent * test(integrations): remove unused manifest variable in BOM test
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""Tests for ClaudeIntegration."""
|
||||
|
||||
import codecs
|
||||
import json
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
@@ -74,6 +75,46 @@ class TestClaudeIntegration:
|
||||
assert "<!-- SPECKIT END -->" 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 "<!-- SPECKIT START -->" 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"
|
||||
"<!-- SPECKIT START -->\n"
|
||||
"For additional context about technologies to be used, project structure,\n"
|
||||
"shell commands, and other important information, read the current plan\n"
|
||||
"<!-- SPECKIT END -->\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"<!-- SPECKIT" not in remaining
|
||||
assert b"# CLAUDE.md" in remaining
|
||||
|
||||
def test_ai_flag_auto_promotes_and_enables_skills(self, tmp_path):
|
||||
from typer.testing import CliRunner
|
||||
from specify_cli import app
|
||||
|
||||
@@ -400,6 +400,7 @@ class TestCommandStep:
|
||||
"""Test the command step type."""
|
||||
|
||||
def test_execute_basic(self):
|
||||
from unittest.mock import patch
|
||||
from specify_cli.workflows.steps.command import CommandStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
@@ -413,7 +414,8 @@ class TestCommandStep:
|
||||
"command": "speckit.specify",
|
||||
"input": {"args": "{{ inputs.name }}"},
|
||||
}
|
||||
result = step.execute(config, ctx)
|
||||
with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
|
||||
result = step.execute(config, ctx)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert result.output["command"] == "speckit.specify"
|
||||
assert result.output["integration"] == "claude"
|
||||
@@ -474,6 +476,7 @@ class TestCommandStep:
|
||||
|
||||
def test_dispatch_not_attempted_without_cli(self):
|
||||
"""When the CLI tool is not installed, step should fail."""
|
||||
from unittest.mock import patch
|
||||
from specify_cli.workflows.steps.command import CommandStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
@@ -488,7 +491,8 @@ class TestCommandStep:
|
||||
"command": "speckit.specify",
|
||||
"input": {"args": "{{ inputs.name }}"},
|
||||
}
|
||||
result = step.execute(config, ctx)
|
||||
with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
|
||||
result = step.execute(config, ctx)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert result.output["dispatched"] is False
|
||||
assert result.error is not None
|
||||
@@ -566,6 +570,7 @@ class TestPromptStep:
|
||||
"""Test the prompt step type."""
|
||||
|
||||
def test_execute_basic(self):
|
||||
from unittest.mock import patch
|
||||
from specify_cli.workflows.steps.prompt import PromptStep
|
||||
from specify_cli.workflows.base import StepContext, StepStatus
|
||||
|
||||
@@ -579,7 +584,8 @@ class TestPromptStep:
|
||||
"type": "prompt",
|
||||
"prompt": "Review {{ inputs.file }} for security issues",
|
||||
}
|
||||
result = step.execute(config, ctx)
|
||||
with patch("specify_cli.workflows.steps.prompt.shutil.which", return_value=None):
|
||||
result = step.execute(config, ctx)
|
||||
assert result.status == StepStatus.FAILED
|
||||
assert result.output["prompt"] == "Review auth.py for security issues"
|
||||
assert result.output["integration"] == "claude"
|
||||
@@ -1311,6 +1317,7 @@ class TestWorkflowEngine:
|
||||
engine.load_workflow("nonexistent")
|
||||
|
||||
def test_execute_simple_workflow(self, project_dir):
|
||||
from unittest.mock import patch
|
||||
from specify_cli.workflows.engine import WorkflowEngine, WorkflowDefinition
|
||||
from specify_cli.workflows.base import RunStatus
|
||||
|
||||
@@ -1333,7 +1340,8 @@ steps:
|
||||
"""
|
||||
definition = WorkflowDefinition.from_string(yaml_str)
|
||||
engine = WorkflowEngine(project_dir)
|
||||
state = engine.execute(definition, {"name": "login"})
|
||||
with patch("specify_cli.workflows.steps.command.shutil.which", return_value=None):
|
||||
state = engine.execute(definition, {"name": "login"})
|
||||
|
||||
assert state.status == RunStatus.FAILED
|
||||
assert "step-one" in state.step_results
|
||||
|
||||
Reference in New Issue
Block a user