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
37 changes: 37 additions & 0 deletions tests/tools/test_terminal_compound_background.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@ def test_multiple_rewrites_in_one_script(self):
assert rewrite(cmd) == "A && { B & }\nfalse || { C & }"


class TestTrailingCommandStaysValid:
"""A command following the backgrounded job on the SAME line must stay
syntactically valid: ``A && B & C`` rewritten to ``A && { B & } C`` is a
bash syntax error (a brace group needs a separator before the next
command). The rewrite must splice in a ``;``.
"""

def test_trailing_command_same_line(self):
assert rewrite("A && B & C") == "A && { B & }; C"

def test_trailing_command_realistic(self):
cmd = "build && python app.py & echo started"
assert rewrite(cmd) == "build && { python app.py & }; echo started"

def test_trailing_command_no_space_after_amp(self):
assert rewrite("A && B &C") == "A && { B & }; C"

def test_rewritten_output_is_valid_bash(self):
# The actual defect: the pre-fix output failed `bash -n`.
import subprocess

for cmd in (
"echo a && sleep 100 & echo done",
"A && B & C",
"build && run & tail -f log",
):
out = rewrite(cmd)
proc = subprocess.run(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please skip this bash-dependent assertion when shutil.which("bash") is unavailable. Existing tests use that guard, and without it this regression test fails on native Windows or minimal environments before it can assert rewrite behavior.

["bash", "-n", "-c", out], capture_output=True, text=True
)
assert proc.returncode == 0, f"{out!r} failed bash -n: {proc.stderr}"

def test_trailing_command_idempotent(self):
once = rewrite("A && B & C")
assert rewrite(once) == once


class TestPreserved:
"""Commands that DON'T have the bug MUST pass through unchanged."""

Expand Down
15 changes: 14 additions & 1 deletion tools/terminal_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,20 @@ def _rewrite_compound_background(command: str) -> str:
suffix = result[amp_pos + 1 :]
# `{` needs a trailing space in bash; the closing `}` needs to be
# preceded by `;` or `&` β€” we're providing `&` from the backgrounding.
result = prefix + "{ " + middle + "& }" + suffix
#
# A brace group must also be *followed* by a separator before any
# further command on the same line: `A && B & C` would otherwise
# become `A && { B & } C`, which bash rejects with
# "syntax error near unexpected token". When a command directly
# follows the backgrounded job (`& C`), splice in a `;` so the
# rewrite stays valid. Tails that already begin with a separator
# β€” a newline-delimited next statement, or a `&&`/`||`/`|` chain β€”
# need nothing.
tail = suffix.lstrip(" \t")
if tail and tail[0] not in (";", "&", "|", "\n", ")"):
result = prefix + "{ " + middle + "& }; " + tail
else:
result = prefix + "{ " + middle + "& }" + suffix

return result

Expand Down