chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agents import RunConfig, Runner, function_tool
|
||||
from agents.items import RunItem, ToolCallOutputItem
|
||||
from agents.run_state import RunState
|
||||
from agents.sandbox import SandboxAgent, SandboxRunConfig
|
||||
from agents.sandbox.sandboxes.unix_local import UnixLocalSandboxClient
|
||||
from agents.sandbox.session import CallbackSink, Instrumentation, SandboxSessionEvent
|
||||
from tests.sandbox.integration_tests._helpers import (
|
||||
SandboxFileCapability,
|
||||
SandboxLifecycleProbeCapability,
|
||||
build_manifest_with_all_entry_types,
|
||||
create_local_sources,
|
||||
install_mock_external_tools,
|
||||
)
|
||||
from tests.sandbox.integration_tests.test_model import TestModel
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_preserves_unix_local_lifecycle_state_across_pause_and_resume(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
install_mock_external_tools(monkeypatch, tmp_path)
|
||||
source_root = create_local_sources(tmp_path)
|
||||
manifest = build_manifest_with_all_entry_types(
|
||||
workspace_root=Path("/workspace"),
|
||||
source_root=source_root,
|
||||
)
|
||||
events: list[SandboxSessionEvent] = []
|
||||
client = UnixLocalSandboxClient(
|
||||
instrumentation=Instrumentation(
|
||||
sinks=[CallbackSink(lambda event, _session: events.append(event), mode="sync")]
|
||||
)
|
||||
)
|
||||
model = TestModel()
|
||||
model.queue_function_call(
|
||||
"assert_manifest_materialized",
|
||||
{},
|
||||
call_id="call_manifest_materialized",
|
||||
)
|
||||
model.queue_function_call(
|
||||
"write_file",
|
||||
{"path": "runtime_note.txt", "content": "runtime note v1\n"},
|
||||
call_id="call_write_runtime_note",
|
||||
)
|
||||
model.queue_function_call(
|
||||
"apply_lifecycle_patch",
|
||||
{},
|
||||
call_id="call_apply_lifecycle_patch",
|
||||
)
|
||||
model.queue_function_call(
|
||||
"assert_workspace_escape_blocked",
|
||||
{},
|
||||
call_id="call_assert_workspace_escape_blocked",
|
||||
)
|
||||
model.queue_function_call(
|
||||
"extract_lifecycle_archive",
|
||||
{},
|
||||
call_id="call_extract_lifecycle_archive",
|
||||
)
|
||||
model.queue_function_call(
|
||||
"start_lifecycle_pty",
|
||||
{},
|
||||
call_id="call_start_lifecycle_pty",
|
||||
)
|
||||
model.queue_function_call("approval_tool", {}, call_id="call_approval")
|
||||
|
||||
@function_tool(name_override="approval_tool", needs_approval=True)
|
||||
def approval_tool() -> str:
|
||||
return "approved"
|
||||
|
||||
agent = SandboxAgent(
|
||||
name="sandbox",
|
||||
model=model,
|
||||
instructions="Use the sandbox lifecycle tools.",
|
||||
default_manifest=manifest,
|
||||
tools=[approval_tool],
|
||||
capabilities=[SandboxFileCapability(), SandboxLifecycleProbeCapability()],
|
||||
)
|
||||
|
||||
first_run = await Runner.run(
|
||||
agent,
|
||||
"verify the UnixLocal sandbox lifecycle and wait for approval",
|
||||
run_config=RunConfig(sandbox=SandboxRunConfig(client=client)),
|
||||
)
|
||||
|
||||
assert _tool_outputs(first_run.new_items, agent=agent) == [
|
||||
"manifest materialized",
|
||||
"wrote runtime_note.txt",
|
||||
"lifecycle patch applied",
|
||||
"workspace escape blocked",
|
||||
"archive extracted",
|
||||
"pty started and echoed stdin",
|
||||
]
|
||||
assert len(first_run.interruptions) == 1
|
||||
state = first_run.to_state()
|
||||
assert state._sandbox is not None
|
||||
assert state._sandbox["backend_id"] == "unix_local"
|
||||
assert state._sandbox["current_agent_name"] == "sandbox"
|
||||
session_state = state._sandbox["session_state"]
|
||||
assert isinstance(session_state, dict)
|
||||
snapshot = session_state["snapshot"]
|
||||
assert isinstance(snapshot, dict)
|
||||
assert snapshot["type"] == "local"
|
||||
assert session_state["workspace_root_owned"] is True
|
||||
assert session_state["workspace_root_ready"] is True
|
||||
workspace_root = _session_state_manifest_root(session_state)
|
||||
assert not workspace_root.exists()
|
||||
assert _successful_event_count(events, op="stop") == 1
|
||||
assert _successful_event_count(events, op="shutdown") == 1
|
||||
|
||||
resumed_model = TestModel()
|
||||
resumed_model.queue_function_call(
|
||||
"assert_restored_lifecycle_state",
|
||||
{},
|
||||
call_id="call_assert_restored_lifecycle_state",
|
||||
)
|
||||
resumed_model.queue_function_call(
|
||||
"read_file",
|
||||
{"path": "runtime_note.txt"},
|
||||
call_id="call_read_runtime_note",
|
||||
)
|
||||
resumed_model.queue_final_output("done")
|
||||
resumed_agent = SandboxAgent(
|
||||
name="sandbox",
|
||||
model=resumed_model,
|
||||
instructions="Use the sandbox lifecycle tools.",
|
||||
default_manifest=manifest,
|
||||
tools=[approval_tool],
|
||||
capabilities=[SandboxFileCapability(), SandboxLifecycleProbeCapability()],
|
||||
)
|
||||
|
||||
restored_state = await RunState.from_json(resumed_agent, state.to_json())
|
||||
restored_interruptions = restored_state.get_interruptions()
|
||||
assert len(restored_interruptions) == 1
|
||||
restored_state.approve(restored_interruptions[0])
|
||||
|
||||
resumed = await Runner.run(
|
||||
resumed_agent,
|
||||
restored_state,
|
||||
run_config=RunConfig(sandbox=SandboxRunConfig(client=client)),
|
||||
)
|
||||
|
||||
assert resumed.final_output == "done"
|
||||
assert not workspace_root.exists()
|
||||
assert _successful_event_count(events, op="stop") == 2
|
||||
assert _successful_event_count(events, op="shutdown") == 2
|
||||
assert _tool_outputs(resumed.new_items, agent=resumed_agent)[-3:] == [
|
||||
"approved",
|
||||
"restored lifecycle state verified",
|
||||
"runtime note v1\n",
|
||||
]
|
||||
|
||||
|
||||
def _session_state_manifest_root(session_state: dict[str, object]) -> Path:
|
||||
manifest = session_state["manifest"]
|
||||
assert isinstance(manifest, dict)
|
||||
root = manifest["root"]
|
||||
assert isinstance(root, str)
|
||||
return Path(root)
|
||||
|
||||
|
||||
def _successful_event_count(events: list[SandboxSessionEvent], *, op: str) -> int:
|
||||
return sum(
|
||||
1
|
||||
for event in events
|
||||
if event.op == op and event.phase == "finish" and getattr(event, "ok", False) is True
|
||||
)
|
||||
|
||||
|
||||
def _tool_outputs(items: Sequence[RunItem], *, agent: SandboxAgent) -> list[str]:
|
||||
outputs: list[str] = []
|
||||
for item in items:
|
||||
if isinstance(item, ToolCallOutputItem) and item.agent is agent:
|
||||
assert isinstance(item.output, str)
|
||||
outputs.append(item.output)
|
||||
return outputs
|
||||
Reference in New Issue
Block a user