fix: add UTF-8 encoding configuration for Windows across all entry points
Fixes Unicode encoding errors on Windows by adding encoding configuration to all entry-point scripts and modules that output Unicode characters. Changes: 1. Added Windows UTF-8 encoding configuration to all entry-point scripts in apps/backend/ that can be run directly or output Unicode content 2. Added encoding="utf-8", errors="replace" to subprocess calls in tests 3. Added encoding configuration to test conftest.py for subprocess safety This follows the same pattern from run.py to handle Unicode characters on Windows platforms where the default console encoding is 'charmap'. Fixes test failures: - tests/spec/test_validate_spec.py::TestMainCli::test_main_with_all_checkpoints_default - tests/test_commit_message.py::TestEdgeCases::test_unicode_in_description Files modified: - Entry points: commit_message.py, context/main.py, memory/main.py, etc. - Runners: ai_analyzer_runner.py, ideation_runner.py, insights_runner.py, etc. - Tests: test_validate_spec.py, test_commit_message.py, conftest.py
This commit is contained in:
@@ -8,9 +8,38 @@ This script verifies that:
|
||||
3. Backwards compatibility is maintained
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
@@ -29,9 +29,39 @@ All actual implementation is in focused submodules for better maintainability.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Import from the new modular structure
|
||||
from .analyzers import (
|
||||
ProjectAnalyzer,
|
||||
|
||||
@@ -12,11 +12,40 @@ Usage:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
from cli.utils import find_specs_dir
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ Features:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
@@ -20,6 +21,36 @@ import sys
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
# This is needed because this module may be imported in contexts that log
|
||||
# Unicode content, and the logging system uses stdout/stderr.
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
@@ -26,9 +26,39 @@ The context builder will:
|
||||
4. Output focused context for AI agents
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
from context import (
|
||||
ContextBuilder,
|
||||
FileMatch,
|
||||
|
||||
@@ -48,6 +48,37 @@ Usage Examples:
|
||||
pass
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Re-export all public functions from the memory package
|
||||
from memory import (
|
||||
append_gotcha,
|
||||
|
||||
@@ -12,9 +12,38 @@ Usage:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
from .file_timeline import FileTimelineTracker
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Output:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
@@ -22,6 +23,34 @@ import urllib.error
|
||||
import urllib.request
|
||||
from typing import Any
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
DEFAULT_OLLAMA_URL = "http://localhost:11434"
|
||||
|
||||
# Minimum Ollama version required for newer embedding models (qwen3-embedding, etc.)
|
||||
|
||||
@@ -18,9 +18,39 @@ Usage:
|
||||
python auto-claude/planner.py --spec-dir auto-claude/specs/001-feature/
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
from implementation_plan import ImplementationPlan
|
||||
from planner_lib.context import ContextLoader
|
||||
from planner_lib.generators import get_plan_generator
|
||||
|
||||
@@ -10,10 +10,39 @@ Usage:
|
||||
python prediction.py auto-claude/specs/001-feature/
|
||||
"""
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
from prediction import generate_subtask_checklist
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ Output:
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -25,6 +26,34 @@ import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
|
||||
# Apply LadybugDB monkeypatch BEFORE any graphiti imports
|
||||
def apply_monkeypatch():
|
||||
|
||||
@@ -36,9 +36,38 @@ Usage:
|
||||
python auto-claude/review.py --spec-dir auto-claude/specs/001-feature
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Re-export all public APIs from the review package
|
||||
from review import (
|
||||
ReviewState,
|
||||
|
||||
@@ -2,11 +2,41 @@
|
||||
Main orchestrator for AI-powered project analysis.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
from .analyzers import AnalyzerFactory
|
||||
from .cache_manager import CacheManager
|
||||
from .claude_client import CLAUDE_SDK_AVAILABLE, ClaudeAnalysisClient
|
||||
|
||||
@@ -17,9 +17,39 @@ Example:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""CLI entry point."""
|
||||
|
||||
@@ -20,9 +20,38 @@ Usage:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Add auto-claude to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
@@ -8,10 +8,39 @@ about a codebase. It can also suggest tasks based on the conversation.
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Add auto-claude to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
@@ -15,9 +15,38 @@ Usage:
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import io
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Add auto-claude to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
@@ -23,6 +23,37 @@ The security system has three layers:
|
||||
See project_analyzer.py for the detection logic.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
# Configure safe encoding on Windows to handle Unicode characters in output
|
||||
if sys.platform == "win32":
|
||||
for _stream_name in ("stdout", "stderr"):
|
||||
_stream = getattr(sys, _stream_name)
|
||||
# Method 1: Try reconfigure (works for TTY)
|
||||
if hasattr(_stream, "reconfigure"):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
continue
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Method 2: Wrap with TextIOWrapper for piped output
|
||||
try:
|
||||
if hasattr(_stream, "buffer"):
|
||||
_new_stream = io.TextIOWrapper(
|
||||
_stream.buffer,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
line_buffering=True,
|
||||
)
|
||||
setattr(sys, _stream_name, _new_stream)
|
||||
except (AttributeError, io.UnsupportedOperation, OSError):
|
||||
pass
|
||||
# Clean up temporary variables
|
||||
del _stream_name, _stream
|
||||
if "_new_stream" in dir():
|
||||
del _new_stream
|
||||
|
||||
# Import everything from the security module to maintain backward compatibility
|
||||
from security import * # noqa: F401, F403
|
||||
|
||||
|
||||
@@ -47,8 +47,19 @@ def pytest_configure(config):
|
||||
This hook ensures that the real ui module is imported before test collection starts.
|
||||
This prevents test_spec_pipeline's mocking from affecting the ui module during
|
||||
collection of ui tests.
|
||||
|
||||
Also configures UTF-8 encoding for stdout/stderr to handle Unicode on Windows.
|
||||
"""
|
||||
import importlib
|
||||
import io
|
||||
|
||||
# Configure UTF-8 encoding for stdout/stderr on all platforms
|
||||
# This is critical for Windows tests that use Unicode characters
|
||||
# Without this, pytest's output capture fails with UnicodeEncodeError on Windows
|
||||
if sys.stdout.encoding.lower() not in ('utf-8', 'utf_8', 'utf8'):
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
if sys.stderr.encoding.lower() not in ('utf-8', 'utf_8', 'utf8'):
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
# Import the real ui module and its submodules BEFORE test collection
|
||||
# This ensures they exist in sys.modules before any test can mock them
|
||||
|
||||
@@ -109,6 +109,8 @@ class TestUnstageGitignoredFiles:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert "test.log" in result_before.stdout
|
||||
|
||||
@@ -123,6 +125,8 @@ class TestUnstageGitignoredFiles:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
# We expect unstaging if git check-ignore reports it
|
||||
# (depends on .gitignore configuration)
|
||||
@@ -152,6 +156,8 @@ class TestUnstageGitignoredFiles:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert ".auto-claude" not in result.stdout
|
||||
|
||||
@@ -180,6 +186,8 @@ class TestUnstageGitignoredFiles:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert ".auto-claude" not in result.stdout
|
||||
|
||||
@@ -222,6 +230,8 @@ class TestUnstageGitignoredFiles:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert "normal.txt" in result_before.stdout
|
||||
|
||||
@@ -235,6 +245,8 @@ class TestUnstageGitignoredFiles:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert "normal.txt" in result.stdout # Normal file should remain staged
|
||||
assert ".auto-claude" not in result.stdout # .auto-claude should always be unstaged
|
||||
@@ -266,6 +278,8 @@ class TestCreateWorktree:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert str(result.path) in worktree_list.stdout
|
||||
|
||||
@@ -611,6 +625,8 @@ class TestMergeWorktree:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert "normal.txt" in result.stdout # Normal file should be staged
|
||||
# test.log may or may not be staged depending on git check-ignore behavior
|
||||
@@ -642,6 +658,8 @@ class TestMergeWorktree:
|
||||
cwd=git_project,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert "normal.txt" in result.stdout
|
||||
assert ".auto-claude" not in result.stdout
|
||||
|
||||
@@ -618,6 +618,8 @@ class TestCLIActualExecution:
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
cwd=str(backend_path),
|
||||
)
|
||||
|
||||
|
||||
@@ -722,6 +722,8 @@ class TestMainModuleCliExecution:
|
||||
[sys.executable, str(main_path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
# Should show usage message
|
||||
|
||||
@@ -27,6 +27,8 @@ def _run_validate_spec(spec_dir: str, *args: str) -> subprocess.CompletedProcess
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
cwd=str(SCRIPT_PATH.parent),
|
||||
env=env,
|
||||
)
|
||||
@@ -43,6 +45,8 @@ class TestMainCli:
|
||||
[sys.executable, str(SCRIPT_PATH)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
cwd=str(SCRIPT_PATH.parent),
|
||||
env=env,
|
||||
)
|
||||
|
||||
@@ -83,7 +83,9 @@ def get_latest_commit(project_dir: Path) -> str:
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
return result.stdout.strip() if result.returncode == 0 else ""
|
||||
|
||||
|
||||
@@ -68,6 +68,8 @@ def _check_glab_detection() -> bool:
|
||||
[glab_path, "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
|
||||
@@ -123,6 +123,8 @@ def temp_git_repo():
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
commit_sha = result.stdout.strip()
|
||||
|
||||
@@ -134,6 +136,8 @@ def temp_git_repo():
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert status_result.stdout.strip() == "", f"Git repo not clean: {status_result.stdout}"
|
||||
|
||||
|
||||
@@ -250,7 +250,9 @@ def test_good_commit_tracking():
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace"
|
||||
)
|
||||
commit_hash = result.stdout.strip()
|
||||
|
||||
@@ -272,7 +274,9 @@ def test_good_commit_tracking():
|
||||
["git", "rev-parse", "HEAD"],
|
||||
cwd=project_dir,
|
||||
capture_output=True,
|
||||
text=True
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace"
|
||||
)
|
||||
commit_hash2 = result.stdout.strip()
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class TestSubprocessTimeout:
|
||||
["echo", "hello"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=5,
|
||||
)
|
||||
|
||||
@@ -578,6 +580,8 @@ class TestTimeoutErrorHandling:
|
||||
["sleep", "10"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=0.1,
|
||||
)
|
||||
output = result.stdout
|
||||
|
||||
@@ -702,6 +702,8 @@ class TestRebaseSpecBranch:
|
||||
cwd=temp_git_repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
).stdout.strip()
|
||||
|
||||
# Rebase the spec branch
|
||||
@@ -715,6 +717,8 @@ class TestRebaseSpecBranch:
|
||||
cwd=temp_git_repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
).stdout.strip()
|
||||
|
||||
# Commits should be different (rebase changed the commit hash)
|
||||
@@ -726,6 +730,8 @@ class TestRebaseSpecBranch:
|
||||
cwd=temp_git_repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
).stdout
|
||||
assert "Main commit" in log, "Spec branch should have main commit after rebase"
|
||||
|
||||
@@ -797,6 +803,8 @@ class TestRebaseSpecBranch:
|
||||
cwd=temp_git_repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert current_branch.stdout.strip() == "main", "Should still be on main branch"
|
||||
|
||||
@@ -812,6 +820,8 @@ class TestRebaseSpecBranch:
|
||||
cwd=temp_git_repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
assert status_result.stdout.strip() == "", "Git status should be clean"
|
||||
|
||||
@@ -983,6 +993,8 @@ class TestRebaseErrorHandling:
|
||||
cwd=temp_git_repo,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
)
|
||||
commit_hash = commit_result.stdout.strip()
|
||||
subprocess.run(
|
||||
|
||||
@@ -142,7 +142,7 @@ class TestWorktreeRemoval:
|
||||
# Verify branch is deleted
|
||||
result = subprocess.run(
|
||||
["git", "branch", "--list", branch_name],
|
||||
cwd=temp_git_repo, capture_output=True, text=True
|
||||
cwd=temp_git_repo, capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
assert branch_name not in result.stdout
|
||||
|
||||
@@ -287,7 +287,7 @@ class TestWorktreeCommitAndMerge:
|
||||
# Verify branch was deleted
|
||||
branch_list_result = subprocess.run(
|
||||
["git", "branch", "--list", branch_name],
|
||||
cwd=temp_git_repo, capture_output=True, text=True
|
||||
cwd=temp_git_repo, capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
assert branch_name not in branch_list_result.stdout, f"Branch {branch_name} should be deleted"
|
||||
|
||||
@@ -342,7 +342,7 @@ class TestWorktreeCommitAndMerge:
|
||||
# Verify git status shows no unmerged/conflict status codes
|
||||
git_status = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
cwd=temp_git_repo, capture_output=True, text=True
|
||||
cwd=temp_git_repo, capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
# Should have no output (clean working directory)
|
||||
assert git_status.returncode == 0
|
||||
@@ -399,7 +399,7 @@ class TestWorktreeCommitAndMerge:
|
||||
# Verify git status shows no staged/unstaged changes
|
||||
git_status = subprocess.run(
|
||||
["git", "status", "--porcelain"],
|
||||
cwd=temp_git_repo, capture_output=True, text=True
|
||||
cwd=temp_git_repo, capture_output=True, text=True, encoding="utf-8", errors="replace"
|
||||
)
|
||||
assert git_status.returncode == 0
|
||||
assert not git_status.stdout.strip(), f"Expected clean status, got: {git_status.stdout}"
|
||||
|
||||
Reference in New Issue
Block a user