Files
wehub-resource-sync 0418dc5cf9
CI / Python tests (3.10) (push) Has been cancelled
CI / Python tests (3.11) (push) Has been cancelled
CI / Python quality (push) Has been cancelled
CI / Frontend typecheck (push) Has been cancelled
Autopilot Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:45:00 +08:00

216 lines
7.0 KiB
Python

import asyncio
from pathlib import Path
import pytest
from openharness.tools.base import ToolExecutionContext
from openharness.tools.bash_tool import BashTool, BashToolInput
import openharness.tools.bash_tool as bash_tool_module
class _FakeStdout:
def __init__(self, chunks: list[bytes], *, sleep_forever: bool = False):
self._chunks = list(chunks)
self._sleep_forever = sleep_forever
self._process = None
def attach(self, process) -> None:
self._process = process
async def read(self, _size: int = -1):
if self._chunks:
if _size == -1:
chunks = self._chunks[:]
self._chunks.clear()
return b"".join(chunks)
total = bytearray()
while self._chunks and (len(total) < _size):
next_chunk = self._chunks[0]
remaining = _size - len(total)
if len(next_chunk) <= remaining:
total.extend(self._chunks.pop(0))
continue
total.extend(next_chunk[:remaining])
self._chunks[0] = next_chunk[remaining:]
break
return bytes(total)
if self._process is not None and self._process.returncode is not None:
return b""
if self._sleep_forever:
await asyncio.sleep(0.05)
if self._process is not None and self._process.returncode is not None:
return b""
return b""
class _FakeProcess:
def __init__(self, *, stdout=None, returncode=None):
self.stdout = stdout
self.returncode = returncode
self.terminated = False
self.killed = False
if hasattr(self.stdout, "attach"):
self.stdout.attach(self)
async def wait(self):
if self.returncode is None:
await asyncio.sleep(60)
return self.returncode
def terminate(self):
self.terminated = True
self.returncode = -15
def kill(self):
self.killed = True
self.returncode = -9
class _NeverClosingStdout:
async def read(self, _size: int = -1):
await asyncio.sleep(60)
return b""
@pytest.mark.asyncio
async def test_bash_tool_preflight_short_circuits_interactive_scaffold_even_with_timeout_fixture(monkeypatch, tmp_path: Path):
process = _FakeProcess(
stdout=_FakeStdout(
[
b"Creating a new Next.js app in /tmp/coolblog.\n",
b"Would you like to use Turbopack? \n",
],
sleep_forever=True,
)
)
async def fake_create_shell_subprocess(*args, **kwargs):
return process
monkeypatch.setitem(BashTool.execute.__globals__, "create_shell_subprocess", fake_create_shell_subprocess)
result = await BashTool().execute(
BashToolInput(
command='npx create-next-app@latest coolblog --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"',
timeout_seconds=1,
),
ToolExecutionContext(cwd=tmp_path),
)
assert result.is_error is True
assert "This command appears to require interactive input before it can continue." in result.output
assert result.metadata["interactive_required"] is True
@pytest.mark.asyncio
async def test_bash_tool_preflights_interactive_scaffold_commands(tmp_path: Path):
result = await BashTool().execute(
BashToolInput(
command='npx create-next-app@latest coolblog --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"',
timeout_seconds=1,
),
ToolExecutionContext(cwd=tmp_path),
)
assert result.is_error is True
assert result.metadata["interactive_required"] is True
assert "cannot answer installer/scaffold prompts live" in result.output
assert "non-interactive flags" in result.output
@pytest.mark.asyncio
async def test_bash_tool_timeout_returns_partial_output_for_real_command(tmp_path: Path):
result = await BashTool().execute(
BashToolInput(
command=(
"python -u -c \"print('Creating a new Next.js app in /tmp/coolblog.'); "
"print('Would you like to use Turbopack?'); "
"import time; time.sleep(5)\""
),
timeout_seconds=1,
),
ToolExecutionContext(cwd=tmp_path),
)
assert result.is_error is True
assert "Command timed out after 1 seconds." in result.output
assert "Partial output:" in result.output
assert "Creating a new Next.js app in /tmp/coolblog." in result.output
assert "Would you like to use Turbopack?" in result.output
assert "This command appears to require interactive input." in result.output
assert result.metadata["timed_out"] is True
@pytest.mark.asyncio
async def test_bash_tool_collects_combined_output(monkeypatch, tmp_path: Path):
process = _FakeProcess(
stdout=_FakeStdout([b"line one\n", b"line two\n", b""]),
returncode=0,
)
async def fake_create_shell_subprocess(*args, **kwargs):
return process
monkeypatch.setitem(BashTool.execute.__globals__, "create_shell_subprocess", fake_create_shell_subprocess)
result = await BashTool().execute(
BashToolInput(command="printf 'line one\\nline two\\n'"),
ToolExecutionContext(cwd=tmp_path),
)
assert result.is_error is False
assert result.output == "line one\nline two"
assert result.metadata["returncode"] == 0
@pytest.mark.asyncio
async def test_bash_tool_uses_devnull_stdin_for_non_interactive_shell(monkeypatch, tmp_path: Path):
process = _FakeProcess(
stdout=_FakeStdout([b"ok\n", b""]),
returncode=0,
)
seen_kwargs: dict[str, object] = {}
async def fake_create_shell_subprocess(*args, **kwargs):
del args
seen_kwargs.update(kwargs)
return process
monkeypatch.setitem(BashTool.execute.__globals__, "create_shell_subprocess", fake_create_shell_subprocess)
result = await BashTool().execute(
BashToolInput(command="echo ok"),
ToolExecutionContext(cwd=tmp_path),
)
assert result.is_error is False
assert seen_kwargs["stdin"] == asyncio.subprocess.DEVNULL
assert seen_kwargs["prefer_pty"] is True
@pytest.mark.asyncio
async def test_bash_tool_timeout_does_not_hang_when_stdout_stays_open(monkeypatch, tmp_path: Path):
process = _FakeProcess(stdout=_NeverClosingStdout())
async def fake_create_shell_subprocess(*args, **kwargs):
return process
monkeypatch.setattr("openharness.tools.bash_tool.create_shell_subprocess", fake_create_shell_subprocess)
monkeypatch.setattr(
bash_tool_module,
"_READ_REMAINING_OUTPUT_TIMEOUT_SECONDS",
0.05,
raising=False,
)
result = await asyncio.wait_for(
BashTool().execute(
BashToolInput(command="sleep 10", timeout_seconds=1),
ToolExecutionContext(cwd=tmp_path),
),
timeout=2.0,
)
assert result.is_error is True
assert result.metadata["timed_out"] is True