| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- """文件后端在"缺 session 身份的 Runtime"下必须能自救。
- 背景(真实故障):摘要压缩中间件在 `awrap_model_call` 里需要后端,它按框架自己的写法
- 构造一个 runtime:
- ```python
- config = cast("RunnableConfig", getattr(runtime, "config", {}))
- tool_runtime = ToolRuntime(..., config=config, ...)
- ```
- 而 `ModelRequest.runtime` 的类型是 `langgraph.runtime.Runtime`,**它没有 `config` 字段**,
- 于是 config 恒为 `{}`。若工厂只在"属性不存在"时回退运行上下文,这里就会直接抛
- `ValueError: 文件后端缺少服务端会话身份`——表现为长会话(或读了长文档的子智能体)
- 一触发压缩就整轮失败,而不是压缩生效。
- """
- from __future__ import annotations
- from typing import Any
- import pytest
- from agents import workspace
- from agents.workspace import backend_for_runtime, session_root
- @pytest.fixture(autouse=True)
- def isolate_root(tmp_path, monkeypatch):
- """把沙箱根挪到临时目录,避免测试往仓库 data/workspaces 写文件。"""
- monkeypatch.setattr(workspace, "_SANDBOX_BASE", tmp_path, raising=False)
- monkeypatch.setattr(workspace.config, "ROOT", tmp_path, raising=False)
- return tmp_path
- class _LanggraphRuntime:
- """最小复刻 langgraph.runtime.Runtime:**故意没有 config 属性**。"""
- def __init__(self) -> None:
- self.context = None
- self.store = None
- self.stream_writer = lambda _: None
- def _call_factory_with_empty_config(seen: dict[str, Any]) -> None:
- """在 langgraph 运行上下文里,用框架那种 config={} 的 ToolRuntime 调工厂。"""
- from langchain.tools import ToolRuntime
- runtime = ToolRuntime(
- state={}, context=None, stream_writer=lambda _: None, store=None,
- config={}, tool_call_id=None, tools=[],
- )
- backend = backend_for_runtime(runtime)
- seen["routes"] = sorted(backend.routes)
- seen["has_default"] = backend.default is not None
- def test_backend_falls_back_to_run_context_when_runtime_config_is_empty():
- """config 为空字典时必须回退到运行上下文里的会话身份,而不是直接失败。"""
- from langgraph.graph import END, START, StateGraph
- seen: dict[str, Any] = {}
- def node(_state):
- _call_factory_with_empty_config(seen)
- return {}
- graph = StateGraph(dict)
- graph.add_node("n", node)
- graph.add_edge(START, "n")
- graph.add_edge("n", END)
- graph.compile().invoke(
- {}, {"configurable": {"workspace_user": "alice", "workspace_session": "s-1"}})
- assert "/skills/" in seen["routes"] and seen["has_default"]
- # 落到了正确的会话沙箱(而不是别处或临时目录)
- assert (session_root("alice", "s-1") / "workspace").is_dir()
- def test_backend_still_fails_loudly_without_any_session_identity():
- """两个来源都没有身份时仍要失败——绝不能悄悄写到别的会话目录。"""
- from langgraph.graph import END, START, StateGraph
- def node(_state):
- backend_for_runtime(_LanggraphRuntime())
- return {}
- graph = StateGraph(dict)
- graph.add_node("n", node)
- graph.add_edge(START, "n")
- graph.add_edge("n", END)
- with pytest.raises(ValueError, match="缺少服务端会话身份"):
- graph.compile().invoke({}, {"configurable": {"unrelated": "x"}})
- def test_backend_accepts_runtime_whose_config_carries_identity():
- """文件工具的路径(ToolRuntime 带完整 config)照旧工作。"""
- from langchain.tools import ToolRuntime
- runtime = ToolRuntime(
- state={}, context=None, stream_writer=lambda _: None, store=None, tools=[],
- config={"configurable": {"workspace_user": "bob", "workspace_session": "s-2"}},
- tool_call_id="call-1",
- )
- backend = backend_for_runtime(runtime)
- assert backend.default is not None
- assert (session_root("bob", "s-2") / "workspace").is_dir()
- def test_backend_prefers_runtime_config_over_run_context():
- """两者都有身份时以 runtime.config 为准(显式传入的更权威)。"""
- from langchain.tools import ToolRuntime
- runtime = ToolRuntime(
- state={}, context=None, stream_writer=lambda _: None, store=None, tools=[],
- config={"configurable": {"workspace_user": "from-config", "workspace_session": "s"}},
- tool_call_id="call-1",
- )
- from langgraph.graph import END, START, StateGraph
- def node(_state):
- backend_for_runtime(runtime)
- return {}
- graph = StateGraph(dict)
- graph.add_node("n", node)
- graph.add_edge(START, "n")
- graph.add_edge("n", END)
- graph.compile().invoke(
- {}, {"configurable": {"workspace_user": "from-context", "workspace_session": "s"}})
- assert (session_root("from-config", "s") / "workspace").is_dir()
|