213 lines
7.0 KiB
Python
213 lines
7.0 KiB
Python
from yuxi.agents.backends.sandbox import (
|
|
VIRTUAL_PATH_PREFIX,
|
|
ensure_thread_dirs,
|
|
sandbox_outputs_dir,
|
|
sandbox_uploads_dir,
|
|
)
|
|
from yuxi.agents.buildin.chatbot.state import merge_subagent_runs
|
|
from yuxi.agents.state import merge_artifacts
|
|
from yuxi.agents.toolkits.buildin.tools import _normalize_presented_artifact_path
|
|
from yuxi.services.chat_service import extract_agent_state
|
|
from yuxi.utils.paths import CONVERSATION_HISTORY_DIR_NAME, LARGE_TOOL_RESULTS_DIR_NAME
|
|
|
|
|
|
def _runtime_with_thread(thread_id: str, uid: str = "user-1"):
|
|
context = type("RuntimeContext", (), {"thread_id": thread_id, "uid": uid})()
|
|
return type("RuntimeStub", (), {"context": context})()
|
|
|
|
|
|
def test_merge_artifacts_deduplicates_and_preserves_order():
|
|
assert merge_artifacts(
|
|
["/home/gem/user-data/outputs/a.md"],
|
|
["/home/gem/user-data/outputs/a.md", "/home/gem/user-data/outputs/b.md"],
|
|
) == [
|
|
"/home/gem/user-data/outputs/a.md",
|
|
"/home/gem/user-data/outputs/b.md",
|
|
]
|
|
|
|
|
|
def test_merge_subagent_runs_does_not_merge_entries_without_run_id():
|
|
assert merge_subagent_runs(
|
|
[{"id": "run-1", "status": "completed"}],
|
|
[
|
|
{"id": "run-1", "status": "failed", "error": "boom"},
|
|
{"id": "run-2", "status": "completed"},
|
|
],
|
|
) == [
|
|
{"id": "run-1", "status": "completed"},
|
|
{"id": "run-1", "status": "failed", "error": "boom"},
|
|
{"id": "run-2", "status": "completed"},
|
|
]
|
|
|
|
|
|
def test_merge_subagent_runs_updates_existing_run_by_run_id():
|
|
assert merge_subagent_runs(
|
|
[
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-1",
|
|
"child_thread_id": "child-thread",
|
|
"status": "running",
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-1",
|
|
"child_thread_id": "child-thread",
|
|
"status": "completed",
|
|
}
|
|
],
|
|
) == [
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-1",
|
|
"child_thread_id": "child-thread",
|
|
"status": "completed",
|
|
}
|
|
]
|
|
|
|
|
|
def test_merge_subagent_runs_keeps_continuation_run_history():
|
|
assert merge_subagent_runs(
|
|
[
|
|
{
|
|
"id": "tool-old",
|
|
"run_id": "agent-run-old",
|
|
"child_thread_id": "child-thread",
|
|
"status": "completed",
|
|
"completed_at": "2026-06-20T01:00:00Z",
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"id": "tool-new",
|
|
"run_id": "agent-run-new",
|
|
"child_thread_id": "child-thread",
|
|
"status": "pending",
|
|
}
|
|
],
|
|
) == [
|
|
{
|
|
"id": "tool-old",
|
|
"run_id": "agent-run-old",
|
|
"child_thread_id": "child-thread",
|
|
"status": "completed",
|
|
"completed_at": "2026-06-20T01:00:00Z",
|
|
},
|
|
{
|
|
"id": "tool-new",
|
|
"run_id": "agent-run-new",
|
|
"child_thread_id": "child-thread",
|
|
"status": "pending",
|
|
},
|
|
]
|
|
|
|
|
|
def test_merge_subagent_runs_does_not_merge_different_run_ids_by_state_id():
|
|
assert merge_subagent_runs(
|
|
[
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-old",
|
|
"child_thread_id": "child-thread",
|
|
"status": "completed",
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-new",
|
|
"child_thread_id": "child-thread",
|
|
"status": "pending",
|
|
}
|
|
],
|
|
) == [
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-old",
|
|
"child_thread_id": "child-thread",
|
|
"status": "completed",
|
|
},
|
|
{
|
|
"id": "tool-1",
|
|
"run_id": "agent-run-new",
|
|
"child_thread_id": "child-thread",
|
|
"status": "pending",
|
|
},
|
|
]
|
|
|
|
|
|
def test_normalize_presented_artifact_path_accepts_host_path():
|
|
thread_id = "artifacts-host-path"
|
|
ensure_thread_dirs(thread_id, "user-1")
|
|
output_file = sandbox_outputs_dir(thread_id) / "report.md"
|
|
output_file.write_text("# demo", encoding="utf-8")
|
|
|
|
normalized = _normalize_presented_artifact_path(str(output_file), _runtime_with_thread(thread_id))
|
|
|
|
assert normalized == f"{VIRTUAL_PATH_PREFIX}/outputs/report.md"
|
|
|
|
|
|
def test_normalize_presented_artifact_path_accepts_virtual_path():
|
|
thread_id = "artifacts-virtual-path"
|
|
ensure_thread_dirs(thread_id, "user-1")
|
|
output_file = sandbox_outputs_dir(thread_id) / "summary.txt"
|
|
output_file.write_text("demo", encoding="utf-8")
|
|
|
|
normalized = _normalize_presented_artifact_path(
|
|
f"{VIRTUAL_PATH_PREFIX}/outputs/summary.txt",
|
|
_runtime_with_thread(thread_id),
|
|
)
|
|
|
|
assert normalized == f"{VIRTUAL_PATH_PREFIX}/outputs/summary.txt"
|
|
|
|
|
|
def test_normalize_presented_artifact_path_rejects_non_outputs_path():
|
|
thread_id = "artifacts-reject-path"
|
|
ensure_thread_dirs(thread_id, "user-1")
|
|
upload_file = sandbox_uploads_dir(thread_id) / "note.txt"
|
|
upload_file.write_text("demo", encoding="utf-8")
|
|
|
|
try:
|
|
_normalize_presented_artifact_path(str(upload_file), _runtime_with_thread(thread_id))
|
|
except ValueError as exc:
|
|
assert f"{VIRTUAL_PATH_PREFIX}/outputs/" in str(exc)
|
|
else:
|
|
raise AssertionError("expected ValueError for non-outputs file")
|
|
|
|
|
|
def test_normalize_presented_artifact_path_rejects_internal_output_files():
|
|
thread_id = "artifacts-reject-internal"
|
|
ensure_thread_dirs(thread_id, "user-1")
|
|
|
|
for dir_name in [LARGE_TOOL_RESULTS_DIR_NAME, CONVERSATION_HISTORY_DIR_NAME, "large_tool_history"]:
|
|
output_file = sandbox_outputs_dir(thread_id) / dir_name / "stage.txt"
|
|
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
output_file.write_text("internal", encoding="utf-8")
|
|
|
|
try:
|
|
_normalize_presented_artifact_path(str(output_file), _runtime_with_thread(thread_id))
|
|
except ValueError as exc:
|
|
assert "工具调用阶段文件" in str(exc)
|
|
else:
|
|
raise AssertionError(f"expected ValueError for internal output file under {dir_name}")
|
|
|
|
|
|
def test_extract_agent_state_includes_artifacts():
|
|
state = extract_agent_state(
|
|
{
|
|
"todos": [{"content": "done", "status": "completed"}],
|
|
"files": {"/tmp/demo.txt": {"content": ["x"]}},
|
|
"artifacts": ["/home/gem/user-data/outputs/demo.txt"],
|
|
"subagent_runs": [{"id": "tool-1", "status": "completed"}],
|
|
"token_usage": {"llm_input_tokens": 42},
|
|
}
|
|
)
|
|
|
|
assert state["todos"] == [{"content": "done", "status": "completed"}]
|
|
assert state["files"] == {"/tmp/demo.txt": {"content": ["x"]}}
|
|
assert state["artifacts"] == ["/home/gem/user-data/outputs/demo.txt"]
|
|
assert state["subagent_runs"] == [{"id": "tool-1", "status": "completed"}]
|
|
assert state["token_usage"] == {"llm_input_tokens": 42}
|