Skip to content
Open
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
20 changes: 18 additions & 2 deletions src/specify_cli/workflows/overlays/_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,15 @@ def workflow_overlay_add(
existed_before = target_path.exists()
staged = _stage_workflow_file(target_path.parent)
try:
staged.write_bytes(yaml.safe_dump(data, sort_keys=False).encode("utf-8"))
# ``allow_unicode=True`` matches every other YAML writer in the
# repo. Without it every non-ASCII character in a hand-authored
# overlay is rewritten as a ``\uXXXX`` escape, so merely toggling
# an overlay makes the user's own file unreadable.
staged.write_bytes(
yaml.safe_dump(data, sort_keys=False, allow_unicode=True).encode(
"utf-8"
)
)
backup = _commit_workflow_file(staged, target_path, existed_before)
except BaseException:
_safe_discard_staged_workflow_file(
Expand Down Expand Up @@ -267,7 +275,15 @@ def _update_overlay_field(
existed_before = path.exists()
staged = _stage_workflow_file(path.parent)
try:
staged.write_bytes(yaml.safe_dump(data, sort_keys=False).encode("utf-8"))
# ``allow_unicode=True`` matches every other YAML writer in the
# repo. Without it every non-ASCII character in a hand-authored
# overlay is rewritten as a ``\uXXXX`` escape, so merely toggling
# an overlay makes the user's own file unreadable.
staged.write_bytes(
yaml.safe_dump(data, sort_keys=False, allow_unicode=True).encode(
"utf-8"
)
)
backup = _commit_workflow_file(staged, path, existed_before)
except BaseException:
_safe_discard_staged_workflow_file(staged, path.parent, existed_before)
Expand Down
109 changes: 109 additions & 0 deletions tests/workflows/test_overlay_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,115 @@ def test_overlay_add_rejects_non_positive_priority(self, project_dir, monkeypatc
assert result.exit_code == 1
assert "must be >= 1" in result.output

def test_overlay_add_keeps_non_ascii_text_readable(
self, project_dir, monkeypatch
):
"""``overlay add`` must not escape non-ASCII text in the written file.

Overlay files are documented as hand-authored, so writing them back
with ``\\uXXXX`` escapes makes the user's own file unreadable.
"""
monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir)
_write_workflow(
project_dir,
"wf",
{
"schema_version": "1.0",
"workflow": {"id": "wf", "name": "WF", "version": "1.0.0"},
"steps": [{"id": "a", "type": "command", "command": "echo"}],
},
)
message = "Revisar el plan — ¿aprobar? 日本語"
overlay_file = project_dir / "overlay.yml"
overlay_file.write_text(
yaml.safe_dump(
{
"id": "ov1",
"extends": "wf",
"priority": 10,
"edits": [
{
"operation": "replace",
"anchor": "a",
"step": {
"id": "a",
"type": "gate",
"message": message,
"options": ["approve"],
},
}
],
},
allow_unicode=True,
),
encoding="utf-8",
)

result = runner.invoke(app, ["workflow", "overlay", "add", str(overlay_file)])
assert result.exit_code == 0, result.output

installed = (
project_dir / ".specify" / "workflows" / "overlays" / "wf" / "ov1.yml"
)
text = installed.read_text(encoding="utf-8")
assert message in text, text
assert "\\u" not in text and "\\x" not in text, text
# The value must still round-trip identically.
data = yaml.safe_load(text)
assert data["edits"][0]["step"]["message"] == message

def test_overlay_set_priority_keeps_non_ascii_text_readable(
self, project_dir, monkeypatch
):
"""Toggling an overlay must not mangle non-ASCII text already in it."""
monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir)
_write_workflow(
project_dir,
"wf",
{
"schema_version": "1.0",
"workflow": {"id": "wf", "name": "WF", "version": "1.0.0"},
"steps": [{"id": "a", "type": "command", "command": "echo"}],
},
)
message = "Revisar el plan — ¿aprobar? 日本語"
_write_overlay(
project_dir,
"wf",
"ov1",
{
"id": "ov1",
"extends": "wf",
"priority": 10,
"edits": [
{
"operation": "replace",
"anchor": "a",
"step": {
"id": "a",
"type": "gate",
"message": message,
"options": ["approve"],
},
}
],
},
)

result = runner.invoke(
app, ["workflow", "overlay", "set-priority", "wf", "ov1", "20"]
)
assert result.exit_code == 0, result.output

text = (
project_dir / ".specify" / "workflows" / "overlays" / "wf" / "ov1.yml"
).read_text(encoding="utf-8")
assert message in text, text
assert "\\u" not in text and "\\x" not in text, text
data = yaml.safe_load(text)
assert data["priority"] == 20
assert data["edits"][0]["step"]["message"] == message

def test_overlay_set_priority(self, project_dir, monkeypatch):
monkeypatch.setattr("specify_cli._require_specify_project", lambda: project_dir)
_write_workflow(
Expand Down