Skip to content
Merged
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
18 changes: 17 additions & 1 deletion bin/ask
Original file line number Diff line number Diff line change
Expand Up @@ -657,12 +657,28 @@ def main(argv: list[str]) -> int:
script_file = log_dir / f"ask-{provider}-{task_id}.ps1"
status_file_win = str(status_file).replace('"', '`"')
log_file_win = str(log_file).replace('"', '`"')

# Pass CCB_RUN_DIR so background script can find askd state file
ccb_run_dir = os.environ.get("CCB_RUN_DIR", "")
run_dir_line = f'$env:CCB_RUN_DIR = "{ccb_run_dir}"\n' if ccb_run_dir else ""

# Collect CCB_EMAIL_* env vars for email caller
email_env_lines = ""
if caller == "email":
for key in ("CCB_EMAIL_REQ_ID", "CCB_EMAIL_MSG_ID", "CCB_EMAIL_FROM"):
val = os.environ.get(key, "")
if val:
email_env_lines += f'$env:{key} = "{val}"\n'

script_content = f'''$ErrorActionPreference = "SilentlyContinue"
$OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$env:PYTHONIOENCODING = "utf-8"
$env:CCB_REQ_ID = "{task_id}"
$env:CCB_CALLER = "{caller}"
$env:CCB_WORK_DIR = "{os.getcwd()}"
$statusFile = "{status_file_win}"
{run_dir_line}{email_env_lines}$statusFile = "{status_file_win}"
$logFile = "{log_file_win}"
function Write-CcbStatus([string]$line) {{
Add-Content -Path $statusFile -Value ("{{0}} {{1}}" -f (Get-Date -Format "yyyy-MM-ddTHH:mm:sszzz"), $line) -Encoding UTF8
Expand Down
116 changes: 116 additions & 0 deletions test/test_windows_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Tests for Windows compatibility fixes in bin/ask (issue #127)."""
from __future__ import annotations

from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[1]
ASK_SCRIPT = REPO_ROOT / "bin" / "ask"


def _read_ask_source() -> str:
return ASK_SCRIPT.read_text(encoding="utf-8")


def _extract_windows_block(source: str) -> str:
"""Extract the main Windows (os.name == 'nt') block that generates the PowerShell script.

There are multiple ``os.name == "nt"`` checks in the source. The one we
care about is the large block inside ``main()`` that writes the .ps1
script. We identify it by looking for the block that contains
'PowerShell' or 'script_content' so we skip the small helper guard in
``_maybe_start_unified_daemon``.
"""
lines = source.splitlines()
blocks: list[list[str]] = []
in_block = False
block_lines: list[str] = []
indent_level: int | None = None

for line in lines:
if ('os.name == "nt"' in line or "os.name == 'nt'" in line) and not in_block:
in_block = True
indent_level = len(line) - len(line.lstrip())
block_lines = [line]
continue
if in_block:
if line.strip() == "" or len(line) - len(line.lstrip()) > indent_level:
block_lines.append(line)
elif line.strip().startswith("else:"):
blocks.append(block_lines)
in_block = False
block_lines = []
else:
block_lines.append(line)

if in_block and block_lines:
blocks.append(block_lines)

# Return the block that contains the PowerShell script generation
for block in blocks:
text = "\n".join(block)
if "script_content" in text or "PowerShell" in text or ".ps1" in text:
return text

# Fallback: return all blocks concatenated
return "\n".join(line for block in blocks for line in block)


class TestWindowsPowerShellScript:
"""Verify the Windows PowerShell script template includes required settings."""

def setup_method(self):
self.source = _read_ask_source()
self.win_block = _extract_windows_block(self.source)

def test_ccb_run_dir_in_windows_block(self):
"""CCB_RUN_DIR must be passed to PowerShell script (issue #127)."""
assert "CCB_RUN_DIR" in self.win_block, (
"Windows block must include CCB_RUN_DIR env var"
)

def test_utf8_output_encoding(self):
"""$OutputEncoding must be set for proper pipe encoding."""
assert "$OutputEncoding" in self.win_block

def test_utf8_input_encoding(self):
"""Console InputEncoding must be set for Chinese chars."""
assert "InputEncoding" in self.win_block

def test_pythonioencoding(self):
"""PYTHONIOENCODING must be set for Python subprocess UTF-8."""
assert "PYTHONIOENCODING" in self.win_block

def test_console_output_encoding(self):
"""Console OutputEncoding must still be present."""
assert "[Console]::OutputEncoding" in self.win_block

def test_email_env_vars_in_windows_block(self):
"""CCB_EMAIL_* env vars must be handled in Windows block."""
assert "CCB_EMAIL" in self.win_block or "email_env_lines" in self.win_block

def test_unix_block_still_has_run_dir(self):
"""Unix block must still include CCB_RUN_DIR (no regression)."""
# Find the else/Unix block
assert 'export CCB_RUN_DIR' in self.source


class TestWindowsUnixParity:
"""Verify Windows and Unix script generation have feature parity."""

def setup_method(self):
self.source = _read_ask_source()

def test_both_blocks_set_req_id(self):
"""Both Windows and Unix set CCB_REQ_ID."""
assert '$env:CCB_REQ_ID' in self.source # Windows
assert 'export CCB_REQ_ID' in self.source # Unix

def test_both_blocks_set_caller(self):
"""Both Windows and Unix set CCB_CALLER."""
assert '$env:CCB_CALLER' in self.source # Windows
assert 'export CCB_CALLER' in self.source # Unix

def test_both_blocks_set_work_dir(self):
"""Both Windows and Unix set CCB_WORK_DIR."""
assert '$env:CCB_WORK_DIR' in self.source # Windows
assert 'export CCB_WORK_DIR' in self.source # Unix
Loading