Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Add explicit UTF-8 encoding to prompt file read/write operations to prevent `UnicodeDecodeError` on non-UTF-8 default locales (e.g., Windows CP950) (#604)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changelog entries are expected to end with the PR number (e.g., '(#648)') rather than the linked issue number. Please update this line to reference the PR ID, and optionally mention the issue in the text (e.g., 'Fixes #604') if desired.

Copilot generated this review using guidance from repository custom instructions.
- Propagate headers and environment variables through OpenCode MCP adapter with defensive copies to prevent mutation (#622)
### Changed

Expand Down
6 changes: 3 additions & 3 deletions src/apm_cli/core/script_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ def _auto_compile_prompts(
compiled_prompt_files.append(prompt_file)

# Read the compiled content
with open(compiled_path, "r") as f:
with open(compiled_path, "r", encoding="utf-8") as f:
compiled_content = f.read().strip()

# Check if this is a runtime command (copilot, codex, llm) before transformation
Expand Down Expand Up @@ -916,7 +916,7 @@ def compile(self, prompt_file: str, params: Dict[str, str]) -> str:
# Now ensure compiled directory exists
self.compiled_dir.mkdir(parents=True, exist_ok=True)

with open(prompt_path, "r") as f:
with open(prompt_path, "r", encoding="utf-8") as f:
content = f.read()

# Parse frontmatter and content
Expand All @@ -939,7 +939,7 @@ def compile(self, prompt_file: str, params: Dict[str, str]) -> str:
output_path = self.compiled_dir / output_name

# Write compiled content
with open(output_path, "w") as f:
with open(output_path, "w", encoding="utf-8") as f:
f.write(compiled_content)

return str(output_path)
Expand Down
55 changes: 55 additions & 0 deletions tests/unit/test_script_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,61 @@ def test_compile_file_not_found(self, mock_exists):
with pytest.raises(FileNotFoundError, match="Prompt file 'nonexistent.prompt.md' not found"):
self.compiler.compile("nonexistent.prompt.md", {})

def test_compile_utf8_content_with_cjk_characters(self):
"""Test that prompt files with non-ASCII characters compile correctly.

Regression test for #604: UnicodeDecodeError on Windows CP950
when .prompt.md contains CJK or other non-ASCII characters.
"""
tmp_dir = tempfile.mkdtemp()
try:
prompt_dir = Path(tmp_dir)
Comment on lines +334 to +336
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests manually manage a temp directory via tempfile.mkdtemp()/shutil.rmtree(). The rest of this file already uses tempfile.TemporaryDirectory() context managers; switching these new tests to TemporaryDirectory() would reduce cleanup risk (e.g., if rmtree fails on Windows due to open handles) and match existing conventions.

Copilot uses AI. Check for mistakes.
prompt_path = prompt_dir / "i18n.prompt.md"
cjk_content = (
"---\n"
"description: 国際化テスト\n"
"---\n"
"\n"
"你好世界!こんにちは ${input:name}!\n"
"Ünïcödé résumé naïve café"
)
Comment on lines +338 to +345
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test introduces non-ASCII characters directly in a Python source file (CJK and accented Latin in string literals). Repo encoding rules require all source files to remain printable ASCII; please represent these characters via Unicode escape sequences (e.g., \uXXXX/\UXXXXXXXX) while still writing UTF-8 bytes to the prompt file, so the test coverage remains but the source stays ASCII-only.

Copilot generated this review using guidance from repository custom instructions.
prompt_path.write_text(cjk_content, encoding="utf-8")

compiler = PromptCompiler()
compiler.compiled_dir = prompt_dir / ".compiled"

result_path = compiler.compile(
str(prompt_path), {"name": "ユーザー"}
)

compiled = Path(result_path).read_text(encoding="utf-8")
assert "你好世界!こんにちは ユーザー!" in compiled
assert "Ünïcödé résumé naïve café" in compiled
# Frontmatter must be stripped
assert "---" not in compiled
finally:
shutil.rmtree(tmp_dir)

def test_compile_utf8_content_without_frontmatter(self):
"""Test non-ASCII prompt without frontmatter compiles correctly."""
tmp_dir = tempfile.mkdtemp()
try:
prompt_dir = Path(tmp_dir)
prompt_path = prompt_dir / "simple_cjk.prompt.md"
prompt_path.write_text(
"Привет ${input:who}! 🚀", encoding="utf-8"
)
Comment on lines +369 to +371
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test includes Cyrillic text and an emoji in the Python source string literal. The repo encoding rules require Python source files to be printable ASCII only; please rewrite the literal using Unicode escape sequences (e.g., \u041f\u0440... and \U0001F680) to keep the source ASCII while still exercising UTF-8 file I/O.

Copilot generated this review using guidance from repository custom instructions.

compiler = PromptCompiler()
compiler.compiled_dir = prompt_dir / ".compiled"

result_path = compiler.compile(str(prompt_path), {"who": "мир"})

compiled = Path(result_path).read_text(encoding="utf-8")
assert compiled == "Привет мир! 🚀"
finally:
shutil.rmtree(tmp_dir)


class TestPromptCompilerDependencyDiscovery:
"""Test PromptCompiler dependency discovery functionality."""
Expand Down
Loading