mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 21:07:01 +08:00
Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
"""命令工具的输出归档、超时、取消和交互回归,逐测试隔离终端 owner。"""
|
||||
"""
|
||||
命令工具的输出归档、超时、取消和交互回归,逐测试隔离终端 owner。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
@@ -22,13 +24,17 @@ pytestmark = [pytest.mark.asyncio, pytest.mark.usefixtures("terminal_scope")]
|
||||
|
||||
|
||||
def _python_command(code: str) -> str:
|
||||
"""生成当前虚拟环境解释器的 shell 命令,避免依赖系统 python 名称。"""
|
||||
"""
|
||||
生成当前虚拟环境解释器的 shell 命令,避免依赖系统 python 名称。
|
||||
"""
|
||||
args = [sys.executable, "-c", code]
|
||||
return subprocess.list2cmdline(args) if os.name == "nt" else shlex.join(args)
|
||||
|
||||
|
||||
def _archive_path(result: str) -> Path:
|
||||
"""从结构化回执取得归档路径,缺失归档不能被当作完整输出。"""
|
||||
"""
|
||||
从结构化回执取得归档路径,缺失归档不能被当作完整输出。
|
||||
"""
|
||||
path = json.loads(result)["output_file"]
|
||||
assert path
|
||||
return Path(path)
|
||||
@@ -36,13 +42,17 @@ def _archive_path(result: str) -> Path:
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def command_tool(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> AsyncIterator[ExecuteCommandTool]:
|
||||
"""每个测试独占 manager、并发信号量和归档文件,退出时完整回收。"""
|
||||
"""
|
||||
每个测试独占 manager、并发信号量和归档文件,退出时完整回收。
|
||||
"""
|
||||
manager = _TerminalSessionManager()
|
||||
archives = []
|
||||
original_temporary_file = command_module.NamedTemporaryFile
|
||||
|
||||
def temporary_file(*args: Any, **kwargs: Any) -> Any:
|
||||
"""仅改变归档存放位置,保留真实文件和生产归档写入逻辑。"""
|
||||
"""
|
||||
仅改变归档存放位置,保留真实文件和生产归档写入逻辑。
|
||||
"""
|
||||
kwargs["dir"] = tmp_path
|
||||
archive = original_temporary_file(*args, **kwargs)
|
||||
archives.append(archive)
|
||||
@@ -65,12 +75,16 @@ async def command_tool(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Async
|
||||
|
||||
|
||||
async def _start(tool: ExecuteCommandTool, command: str, *, use_pty: bool = False) -> dict[str, Any]:
|
||||
"""经工具入口启动真实会话,生命周期由测试独占的 manager 回收。"""
|
||||
"""
|
||||
经工具入口启动真实会话,生命周期由测试独占的 manager 回收。
|
||||
"""
|
||||
return json.loads(await tool.run(action="start", command=command, use_pty=use_pty))
|
||||
|
||||
|
||||
async def _write_input(tool: ExecuteCommandTool, session_id: str, text: str) -> None:
|
||||
"""确认 stdin 写入已被接收,避免把接口错误掩盖成后续读取超时。"""
|
||||
"""
|
||||
确认 stdin 写入已被接收,避免把接口错误掩盖成后续读取超时。
|
||||
"""
|
||||
payload = json.loads(await tool.run(action="write", session_id=session_id, input_text=text))
|
||||
assert payload.get("written_bytes") == len(text.encode("utf-8")), payload
|
||||
|
||||
@@ -78,7 +92,9 @@ async def _write_input(tool: ExecuteCommandTool, session_id: str, text: str) ->
|
||||
async def _wait_until_complete(
|
||||
tool: ExecuteCommandTool, session_id: str, since_seq: int = 0, since_offset: int = 0,
|
||||
) -> dict[str, Any]:
|
||||
"""按真实消费游标排空尾部,不把首批输出误当成进程和读取器均已结束。"""
|
||||
"""
|
||||
按真实消费游标排空尾部,不把首批输出误当成进程和读取器均已结束。
|
||||
"""
|
||||
output = []
|
||||
for _ in range(10):
|
||||
payload = json.loads(await tool.run(
|
||||
@@ -93,7 +109,9 @@ async def _wait_until_complete(
|
||||
|
||||
|
||||
async def test_large_output_is_truncated_before_returning_to_agent(command_tool: ExecuteCommandTool) -> None:
|
||||
"""大输出保留头尾预览,并将完整内容归档到真实文件。"""
|
||||
"""
|
||||
大输出保留头尾预览,并将完整内容归档到真实文件。
|
||||
"""
|
||||
command = _python_command("import sys; sys.stdout.write('HEAD-' + 'x' * 200000 + '-TAIL'); sys.stdout.flush()")
|
||||
result = await command_tool.run(action="run", command=command, timeout=60)
|
||||
path = _archive_path(result)
|
||||
@@ -109,7 +127,9 @@ async def test_large_output_is_truncated_before_returning_to_agent(command_tool:
|
||||
|
||||
|
||||
async def test_timeout_returns_partial_output_promptly(command_tool: ExecuteCommandTool) -> None:
|
||||
"""真实执行超时应及时返回终止前输出,保留原有四秒上限断言。"""
|
||||
"""
|
||||
真实执行超时应及时返回终止前输出,保留原有四秒上限断言。
|
||||
"""
|
||||
command = _python_command("import time; print('started', flush=True); time.sleep(5)")
|
||||
started_at = time.monotonic()
|
||||
result = await command_tool.run(action="run", command=command, timeout=1)
|
||||
@@ -121,13 +141,17 @@ async def test_timeout_returns_partial_output_promptly(command_tool: ExecuteComm
|
||||
async def test_cancelled_run_cleans_up_process(
|
||||
command_tool: ExecuteCommandTool, monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""真正的 exec 子进程创建后用 Event 取消,返回取消前必须清理该进程。"""
|
||||
"""
|
||||
真正的 exec 子进程创建后用 Event 取消,返回取消前必须清理该进程。
|
||||
"""
|
||||
original_create = asyncio.create_subprocess_exec
|
||||
created = asyncio.Event()
|
||||
processes = []
|
||||
|
||||
async def create_process(*args: Any, **kwargs: Any) -> asyncio.subprocess.Process:
|
||||
"""保留真实启动,仅在拿到进程对象后通知取消测试。"""
|
||||
"""
|
||||
保留真实启动,仅在拿到进程对象后通知取消测试。
|
||||
"""
|
||||
process = await original_create(*args, **kwargs)
|
||||
processes.append(process)
|
||||
created.set()
|
||||
@@ -151,7 +175,9 @@ async def test_cancelled_run_cleans_up_process(
|
||||
|
||||
|
||||
async def test_timeout_with_large_output_writes_partial_full_log_to_temp_file(command_tool: ExecuteCommandTool) -> None:
|
||||
"""超时大输出仍把终止前的完整内容归档,不能只留下截断预览。"""
|
||||
"""
|
||||
超时大输出仍把终止前的完整内容归档,不能只留下截断预览。
|
||||
"""
|
||||
command = _python_command("import sys, time; sys.stdout.write('x' * 60000); sys.stdout.flush(); time.sleep(5)")
|
||||
result = await command_tool.run(action="run", command=command, timeout=1)
|
||||
path = _archive_path(result)
|
||||
@@ -163,14 +189,18 @@ async def test_timeout_with_large_output_writes_partial_full_log_to_temp_file(co
|
||||
|
||||
|
||||
async def test_timeout_is_capped(command_tool: ExecuteCommandTool) -> None:
|
||||
"""超过上限的 timeout 限幅,同时真实短命令仍正常执行。"""
|
||||
"""
|
||||
超过上限的 timeout 限幅,同时真实短命令仍正常执行。
|
||||
"""
|
||||
result = await command_tool.run(action="run", command=_python_command("print('ok')"), timeout=9999)
|
||||
assert "timeout 参数超过上限" in result
|
||||
assert "ok" in result
|
||||
|
||||
|
||||
async def test_forbidden_command_is_rejected(command_tool: ExecuteCommandTool) -> None:
|
||||
"""根目录删除命令必须在启动前拒绝,并保留显式确认提示。"""
|
||||
"""
|
||||
根目录删除命令必须在启动前拒绝,并保留显式确认提示。
|
||||
"""
|
||||
result = await command_tool.run(action="run", command="echo ok && rm -rf /", timeout=60)
|
||||
payload = json.loads(result)
|
||||
assert payload["status"] == "error"
|
||||
@@ -178,7 +208,9 @@ async def test_forbidden_command_is_rejected(command_tool: ExecuteCommandTool) -
|
||||
|
||||
|
||||
async def test_dangerous_command_requires_explicit_confirmation(command_tool: ExecuteCommandTool) -> None:
|
||||
"""拒绝真实高危命令;确认分支仅运行打印文案的无害 Python。"""
|
||||
"""
|
||||
拒绝真实高危命令;确认分支仅运行打印文案的无害 Python。
|
||||
"""
|
||||
rejected = await command_tool.run(action="run", command="echo ok && shutdown now", timeout=1)
|
||||
allowed = await command_tool.run(
|
||||
action="run", command=_python_command("print('shutdown now confirmed')"), timeout=1, confirm_dangerous=True,
|
||||
@@ -190,7 +222,9 @@ async def test_dangerous_command_requires_explicit_confirmation(command_tool: Ex
|
||||
|
||||
|
||||
async def test_default_action_starts_session_promptly(command_tool: ExecuteCommandTool) -> None:
|
||||
"""省略 action 时快速返回运行中会话,保留原有 0.8 秒行为上限。"""
|
||||
"""
|
||||
省略 action 时快速返回运行中会话,保留原有 0.8 秒行为上限。
|
||||
"""
|
||||
command = _python_command("print('ready', flush=True); input()")
|
||||
started_at = time.monotonic()
|
||||
payload = json.loads(await command_tool.run(command=command, use_pty=False))
|
||||
@@ -200,7 +234,9 @@ async def test_default_action_starts_session_promptly(command_tool: ExecuteComma
|
||||
|
||||
|
||||
async def test_read_and_wait_get_incremental_output(command_tool: ExecuteCommandTool) -> None:
|
||||
"""READY 握手后再放行最后输出,验证同一工具的分段等待和消费位置。"""
|
||||
"""
|
||||
READY 握手后再放行最后输出,验证同一工具的分段等待和消费位置。
|
||||
"""
|
||||
initial = await _start(command_tool, _python_command("print('ready', flush=True); input(); print('done', flush=True)"))
|
||||
waiting = json.loads(await command_tool.run(
|
||||
action="wait", session_id=initial["session_id"], timeout_ms=200, since_seq=0,
|
||||
@@ -218,7 +254,9 @@ async def test_read_and_wait_get_incremental_output(command_tool: ExecuteCommand
|
||||
|
||||
|
||||
async def test_write_sends_input_to_running_process(command_tool: ExecuteCommandTool) -> None:
|
||||
"""write 向真实 stdin 发送交互输入,尾部结果可完整读取。"""
|
||||
"""
|
||||
write 向真实 stdin 发送交互输入,尾部结果可完整读取。
|
||||
"""
|
||||
initial = await _start(command_tool, _python_command("line = input('name: '); print('hello ' + line, flush=True)"))
|
||||
await _write_input(command_tool, initial["session_id"], "moviepilot\n")
|
||||
final = await _wait_until_complete(command_tool, initial["session_id"])
|
||||
@@ -227,7 +265,9 @@ async def test_write_sends_input_to_running_process(command_tool: ExecuteCommand
|
||||
|
||||
|
||||
async def test_kill_stops_long_running_process(command_tool: ExecuteCommandTool) -> None:
|
||||
"""读到启动标记后终止长命令,保持原有终态断言。"""
|
||||
"""
|
||||
读到启动标记后终止长命令,保持原有终态断言。
|
||||
"""
|
||||
initial = await _start(command_tool, _python_command("import time; print('started', flush=True); time.sleep(20)"))
|
||||
observed = json.loads(await command_tool.run(
|
||||
action="wait", session_id=initial["session_id"], timeout_ms=500, since_seq=0,
|
||||
|
||||
Reference in New Issue
Block a user