项目文件夹

文件
2026-07-13 13:12:00 +08:00

249 行
9.0 KiB
Python

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
"""End-to-end coverage for the REPL "!" shell passthrough.
The unit suite (``tests/repl/test_bang_command.py``) covers the extracted
helpers — the clip/context builders and the ``_run_bang_command`` runner. What
it cannot reach is the wiring inside ``run_repl.on_input``: the ``!`` / ``!!`` /
bare-``!`` dispatch and the buffer that folds a command's output into the *next*
agent turn. Those only exist as a closure over the live REPL, so they are
exercised here by driving the real ``omnigent run`` REPL under a PTY and
inspecting what the model actually received via the mock LLM server.
**What breaks if these fail:**
- ``!<cmd>`` stops running in the shell / stops rendering its output + footer.
- A bare ``!<cmd>`` wrongly costs a model turn (or silently drops its output
instead of folding it into the next turn's ``llm_text``).
- The bare ``!`` usage hint or the ``!!`` literal-escape routing regresses.
"""
from __future__ import annotations
import json
from pathlib import Path
import httpx
from tests.e2e.omnigent._pexpect_harness import (
await_turn_complete,
clean_exit,
spawn_omnigent_run,
strip_ansi,
submit_prompt,
)
from tests.e2e.omnigent._repl_test_helpers import drain_for
from tests.e2e.omnigent.conftest import configure_mock_llm, reset_mock_llm
_MODEL = "mock-model"
_HARNESS = "openai-agents"
# Visible turn-synchronization markers — same ones the green smoke / model /
# effort e2e tests use. ``working`` is the streaming activity line; `` `` is
# the input prompt the REPL re-renders when a turn settles.
_RUNNING_MARKER = r"working"
_COMPLETION_MARKER = r" "
_SPAWN_TIMEOUT = 60.0
_BOOT_TIMEOUT = 30.0
_RUNNING_TIMEOUT = 20.0
_COMPLETION_TIMEOUT = 60.0
_BANG_TIMEOUT = 20.0
_EXIT_TIMEOUT = 15.0
def _captured_requests(mock_llm_server_url: str) -> list[dict]:
resp = httpx.get(f"{mock_llm_server_url}/mock/requests", timeout=5.0)
resp.raise_for_status()
return resp.json()["requests"]
def test_repl_bang_runs_and_folds_into_next_turn(
omnigent_python: Path,
omnigent_repo_root: Path,
mock_credentials_env: dict[str, str],
mock_llm_server_url: str,
) -> None:
"""
``!echo`` runs locally, renders its output, costs no model turn, and its
output is folded into the following prompt's request to the model.
"""
reset_mock_llm(mock_llm_server_url)
configure_mock_llm(mock_llm_server_url, [{"text": "Understood."}])
yaml_path = omnigent_repo_root / "tests" / "resources" / "examples" / "hello_world.yaml"
child = spawn_omnigent_run(
omnigent_python=omnigent_python,
yaml_path=yaml_path,
model=_MODEL,
harness=_HARNESS,
env=mock_credentials_env,
cwd=omnigent_repo_root,
timeout=_SPAWN_TIMEOUT,
)
try:
child.expect(_COMPLETION_MARKER, timeout=_BOOT_TIMEOUT)
# A "!" command runs in the shell and prints its own exit footer; it
# does NOT enter a model turn, so synchronize on the footer, not on
# "working".
submit_prompt(child, "!echo BANG_SENTINEL_XYZ")
child.expect(r"exit 0", timeout=_BANG_TIMEOUT)
bang_render = strip_ansi(child.before or "") + strip_ansi(drain_for(child, 2.0))
assert "BANG_SENTINEL_XYZ" in bang_render, (
"bang command stdout not rendered on screen:\n" + bang_render[-1500:]
)
# The bang alone must not have hit the model.
assert _captured_requests(mock_llm_server_url) == [], (
"a bare '!' command wrongly triggered a model request"
)
# Now a normal prompt: the buffered bang output must be folded into it.
submit_prompt(child, "what did that print?")
await_turn_complete(
child,
running_timeout=_RUNNING_TIMEOUT,
completion_timeout=_COMPLETION_TIMEOUT,
running_marker=_RUNNING_MARKER,
completion_pattern=_COMPLETION_MARKER,
)
clean_exit(child, timeout=_EXIT_TIMEOUT)
finally:
if not child.closed:
child.close(force=True)
requests = _captured_requests(mock_llm_server_url)
assert requests, "the follow-up prompt never reached the model"
blob = json.dumps(requests)
assert "BANG_SENTINEL_XYZ" in blob, (
"bang stdout was not folded into the next turn's model request:\n" + blob[-2000:]
)
assert "I ran a shell command" in blob, (
"the model-facing bang context header is missing from the request"
)
assert "what did that print?" in blob, (
"the user's follow-up prompt is missing from the model request"
)
def test_repl_bang_bare_shows_hint_and_double_bang_escapes(
omnigent_python: Path,
omnigent_repo_root: Path,
mock_credentials_env: dict[str, str],
mock_llm_server_url: str,
) -> None:
"""
A bare ``!`` prints a usage hint and costs no model turn; ``!!text`` drops
one ``!`` and is sent to the model as the literal prompt ``!text`` (it is
NOT executed as a shell command).
"""
reset_mock_llm(mock_llm_server_url)
configure_mock_llm(mock_llm_server_url, [{"text": "Ok."}])
yaml_path = omnigent_repo_root / "tests" / "resources" / "examples" / "hello_world.yaml"
child = spawn_omnigent_run(
omnigent_python=omnigent_python,
yaml_path=yaml_path,
model=_MODEL,
harness=_HARNESS,
env=mock_credentials_env,
cwd=omnigent_repo_root,
timeout=_SPAWN_TIMEOUT,
)
try:
child.expect(_COMPLETION_MARKER, timeout=_BOOT_TIMEOUT)
submit_prompt(child, "!")
child.expect(r"runs a shell command", timeout=_BANG_TIMEOUT)
assert _captured_requests(mock_llm_server_url) == [], (
"a bare '!' usage hint wrongly triggered a model request"
)
# "!!" escapes: one "!" is stripped and the rest is an ordinary prompt.
submit_prompt(child, "!!literal-bang-prompt")
turn = await_turn_complete(
child,
running_timeout=_RUNNING_TIMEOUT,
completion_timeout=_COMPLETION_TIMEOUT,
running_marker=_RUNNING_MARKER,
completion_pattern=_COMPLETION_MARKER,
)
clean_exit(child, timeout=_EXIT_TIMEOUT)
finally:
if not child.closed:
child.close(force=True)
requests = _captured_requests(mock_llm_server_url)
assert requests, "the '!!'-escaped prompt never reached the model"
blob = json.dumps(requests)
# Exactly one leading "!" survives the escape.
assert "!literal-bang-prompt" in blob, (
"'!!' did not send a literal single-'!' prompt to the model:\n" + blob[-2000:]
)
# The escaped prompt must NOT have been run as a shell command.
assert "I ran a shell command" not in blob, (
"'!!' prompt was wrongly executed as a shell command"
)
assert "exit 0" not in turn.stripped, (
"'!!' prompt rendered a shell exit footer — it was executed, not escaped"
)
def test_repl_bang_buffer_dropped_on_new_conversation(
omnigent_python: Path,
omnigent_repo_root: Path,
mock_credentials_env: dict[str, str],
mock_llm_server_url: str,
) -> None:
"""
Buffered ``!`` output is discarded when a new conversation starts (``/clear``)
— it belonged to the prior conversation and must not leak into the fresh
conversation's first turn.
"""
reset_mock_llm(mock_llm_server_url)
configure_mock_llm(mock_llm_server_url, [{"text": "Ok."}])
yaml_path = omnigent_repo_root / "tests" / "resources" / "examples" / "hello_world.yaml"
child = spawn_omnigent_run(
omnigent_python=omnigent_python,
yaml_path=yaml_path,
model=_MODEL,
harness=_HARNESS,
env=mock_credentials_env,
cwd=omnigent_repo_root,
timeout=_SPAWN_TIMEOUT,
)
try:
child.expect(_COMPLETION_MARKER, timeout=_BOOT_TIMEOUT)
submit_prompt(child, "!echo LEAK_SENTINEL_QQQ")
child.expect(r"exit 0", timeout=_BANG_TIMEOUT)
# /clear starts a new conversation — it must drop the buffered output.
# Re-sync on the freshly re-rendered prompt before sending the next
# line, otherwise it races the /clear redraw and never submits.
submit_prompt(child, "/clear")
child.expect(_COMPLETION_MARKER, timeout=_BOOT_TIMEOUT)
drain_for(child, 1.5)
submit_prompt(child, "hello fresh conversation")
await_turn_complete(
child,
running_timeout=_RUNNING_TIMEOUT,
completion_timeout=_COMPLETION_TIMEOUT,
running_marker=_RUNNING_MARKER,
completion_pattern=_COMPLETION_MARKER,
)
clean_exit(child, timeout=_EXIT_TIMEOUT)
finally:
if not child.closed:
child.close(force=True)
requests = _captured_requests(mock_llm_server_url)
assert requests, "the post-/clear prompt never reached the model"
blob = json.dumps(requests)
assert "LEAK_SENTINEL_QQQ" not in blob, (
"buffered bang output leaked into the new conversation after /clear:\n" + blob[-2000:]
)
assert "hello fresh conversation" in blob, (
"the post-/clear prompt is missing from the model request"
)