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
3 changes: 2 additions & 1 deletion scripts/python/check_prerequisites.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@


def _json_line(payload: object) -> str:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
# ASCII escapes keep the JSON intact on a legacy Windows stdout code page.
return json.dumps(payload, ensure_ascii=True, separators=(",", ":")) + "\n"


HELP_TEXT = """Usage: check_prerequisites.py [OPTIONS]
Expand Down
3 changes: 2 additions & 1 deletion scripts/python/resolve_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def main(argv: list[str] | None = None) -> int:
"TEMPLATE_NAME": args.template_name,
"TEMPLATE_CONTENT": content,
},
ensure_ascii=False,
# ASCII escapes keep the JSON intact on a legacy Windows stdout code page.
ensure_ascii=True,
separators=(",", ":"),
)
)
Expand Down
3 changes: 2 additions & 1 deletion scripts/python/setup_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@


def _json_line(payload: object) -> str:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
# ASCII escapes keep the JSON intact on a legacy Windows stdout code page.
return json.dumps(payload, ensure_ascii=True, separators=(",", ":")) + "\n"


def _help_text(argv0: str) -> str:
Expand Down
3 changes: 2 additions & 1 deletion scripts/python/setup_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@


def _json_line(payload: object) -> str:
return json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n"
# ASCII escapes keep the JSON intact on a legacy Windows stdout code page.
return json.dumps(payload, ensure_ascii=True, separators=(",", ":")) + "\n"


def _help_text(argv0: str) -> str:
Expand Down
20 changes: 19 additions & 1 deletion tests/test_check_prerequisites_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import pytest

from tests.conftest import requires_bash
from tests.parity_helpers import install_composition_stack
from tests.parity_helpers import install_composition_stack, run

PROJECT_ROOT = Path(__file__).resolve().parent.parent
COMMON_SH = PROJECT_ROOT / "scripts" / "bash" / "common.sh"
Expand Down Expand Up @@ -122,6 +122,24 @@ def _json_stdout(result: subprocess.CompletedProcess[str]) -> object:
return json.loads(result.stdout)


def test_python_json_preserves_non_ascii_feature_on_legacy_stdout(
prereq_repo: Path,
) -> None:
feature = prereq_repo / "specs/001-功能"
feature.mkdir(parents=True)
(feature / "plan.md").write_bytes(b"# Plan\r\n")
_write_feature_json(prereq_repo, "specs/001-功能")
env = _clean_env()
env["PYTHONIOENCODING"] = "cp1252"

result = run(_py_cmd(prereq_repo, "--json"), prereq_repo, env)

assert result.returncode == 0, result.stderr
payload = _json_stdout(result)
assert Path(payload["FEATURE_DIR"]) == feature
assert payload["AVAILABLE_DOCS"] == []


def _normalize_status_text(text: str) -> str:
return (
text.replace(" ✓ ", " [OK] ")
Expand Down
19 changes: 19 additions & 0 deletions tests/test_resolve_template_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@
TEMPLATE = "constitution-template"


def test_python_json_preserves_constitution_on_legacy_stdout(tmp_path: Path) -> None:
repo = make_repo(tmp_path)
install_scripts(repo, SCRIPT)
template = repo / ".specify/templates/overrides/constitution-template.md"
template.parent.mkdir(parents=True)
expected = "# Конституція\n\nПравила: 功能 → звіт.\n"
template.write_bytes(expected.encode("utf-8"))
env = clean_env()
env["PYTHONIOENCODING"] = "cp1252"

result = run(py_cmd(repo, SCRIPT, TEMPLATE, "--json"), repo, env)

assert result.returncode == 0, result.stderr
assert json_stdout(result) == {
"TEMPLATE_NAME": TEMPLATE, "TEMPLATE_CONTENT": expected,
}
assert template.read_bytes() == expected.encode("utf-8")


def _setup_repo(tmp_path: Path) -> tuple[Path, str]:
repo = make_repo(tmp_path)
install_scripts(repo, SCRIPT)
Expand Down
19 changes: 19 additions & 0 deletions tests/test_setup_plan_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,25 @@ def repo(tmp_path: Path) -> Path:
return _setup_repo(tmp_path)


def test_python_json_preserves_non_ascii_feature_on_legacy_stdout(repo: Path) -> None:
feature = repo / "specs/001-功能"
feature.mkdir()
write_feature_json(repo, "specs/001-功能")
env = clean_env()
env["PYTHONIOENCODING"] = "cp1252"

result = run(py_cmd(repo, SCRIPT, "--json"), repo, env)

assert result.returncode == 0, result.stderr
payload = json_stdout(result)
assert payload["BRANCH"] == "001-功能"
assert Path(payload["FEATURE_DIR"]) == feature
assert Path(payload["FEATURE_SPEC"]) == feature / "spec.md"
assert Path(payload["IMPL_PLAN"]) == feature / "plan.md"
template = repo / ".specify" / "templates" / "plan-template.md"
assert (feature / "plan.md").read_bytes() == template.read_bytes()


@pytest.mark.parametrize("port", PORTS)
@pytest.mark.parametrize("json_mode", [True, False], ids=["json", "text"])
@pytest.mark.parametrize("template_state", ["missing", "empty", "empty_override"])
Expand Down
39 changes: 39 additions & 0 deletions tests/test_setup_tasks_python_parity.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,45 @@
SCRIPT = "setup-tasks"


def test_installed_python_json_preserves_stock_tasks_template(
tmp_path: Path,
) -> None:
"""The tasks command's actual script must emit JSON for the stock template."""
from typer.testing import CliRunner

from specify_cli import app

project = tmp_path / "project"
initialized = CliRunner().invoke(
app,
[
"init", str(project), "--integration", "codex", "--script", "py",
"--ignore-agent-tools", "--non-interactive",
],
)
assert initialized.exit_code == 0, initialized.output
created = run(
py_cmd(project, "create-new-feature", "--json", "Add user authentication"),
project,
)
assert created.returncode == 0, created.stderr
plan = run(py_cmd(project, "setup-plan", "--json"), project)
assert plan.returncode == 0, plan.stderr
template = project / ".specify/templates/tasks-template.md"
expected = template.read_bytes().decode("utf-8")
assert "🎯" in expected
env = clean_env()
env["PYTHONIOENCODING"] = "cp1252"

result = run(py_cmd(project, SCRIPT, "--json"), project, env)

assert result.returncode == 0, result.stderr
payload = json_stdout(result)
assert payload["TASKS_TEMPLATE_CONTENT"] == expected
assert Path(payload["TASKS_TEMPLATE"]) == template
assert payload["AVAILABLE_DOCS"] == []


def _setup_repo(tmp_path: Path) -> Path:
repo = make_repo(tmp_path)
install_scripts(repo, SCRIPT)
Expand Down
Loading