feat: OpenMesh 基础平台与 MD/PDF 转换技能
- 后端: coworker 智能体框架, WS API, 文件上传, 附件处理 - 前端: Open WebUI, 文件全量走 upload API (含 MD/TXT/JSON 等文本类) - 技能: md-to-office (pandoc + wkhtmltopdf) - 修复: 上传文件路径丢失, Agent 搜索浪费, 输出文件跑到 uploads/ - 打包: PyInstaller one-dir, 预打包 pandoc/wkhtmltopdf/chromium
This commit is contained in:
124
tests/test_durable_resume.py
Normal file
124
tests/test_durable_resume.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""Durable resume: a prompt pending when the process 'restarts' is answered later and the turn
|
||||
continues — rebuilt from the persisted thread, with no live await."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from coworker.providers import (
|
||||
AssistantTurn,
|
||||
ModelCapabilities,
|
||||
ProviderClient,
|
||||
ToolCall,
|
||||
)
|
||||
from coworker.server.manager import SessionManager
|
||||
|
||||
|
||||
class ScriptedProvider(ProviderClient):
|
||||
def __init__(self, turns):
|
||||
self._turns = list(turns)
|
||||
|
||||
def complete(self, *, model, messages, tools=None, **settings):
|
||||
return self._turns.pop(0)
|
||||
|
||||
def capabilities(self, model):
|
||||
return ModelCapabilities()
|
||||
|
||||
|
||||
def _tool(name, args, call_id):
|
||||
return AssistantTurn(tool_calls=[ToolCall(id=call_id, name=name, arguments=args)])
|
||||
|
||||
|
||||
def _text(text):
|
||||
return AssistantTurn(text=text, finish_reason="stop")
|
||||
|
||||
|
||||
async def _run_until_pending(mgr, sid, engine):
|
||||
async def first():
|
||||
async for _ in engine.run("go"):
|
||||
pass
|
||||
|
||||
task = asyncio.create_task(first())
|
||||
pend = []
|
||||
for _ in range(100):
|
||||
await asyncio.sleep(0.02)
|
||||
pend = mgr.inbox.pending(sid)
|
||||
if pend:
|
||||
break
|
||||
assert pend, "prompt never became a pending Inbox item"
|
||||
# simulate a restart: cancel the suspended turn + drop the live engine
|
||||
task.cancel()
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
mgr._engines.pop(sid, None)
|
||||
mgr.mark_idle(sid)
|
||||
return pend[0]
|
||||
|
||||
|
||||
def _final_assistant_texts(mgr, sid):
|
||||
rec = mgr.session_store.load(sid)
|
||||
return [
|
||||
m.get("content")
|
||||
for m in rec.messages
|
||||
if m.get("role") == "assistant" and m.get("content")
|
||||
]
|
||||
|
||||
|
||||
def test_durable_resume_question(tmp_path):
|
||||
mgr = SessionManager(
|
||||
workspace=tmp_path,
|
||||
provider=ScriptedProvider(
|
||||
[
|
||||
_tool(
|
||||
"ask_user",
|
||||
{
|
||||
"question": "Which region?",
|
||||
"options": ["us-east-1", "us-west-2"],
|
||||
},
|
||||
"call_q",
|
||||
),
|
||||
_text("You chose us-west-2."),
|
||||
]
|
||||
),
|
||||
)
|
||||
sid = "dur-q"
|
||||
|
||||
async def scenario():
|
||||
engine = mgr.get_engine(sid, agent="cowork", workspace=str(tmp_path))
|
||||
item = await _run_until_pending(mgr, sid, engine)
|
||||
assert item.kind == "question" and item.tool_call_id == "call_q"
|
||||
await mgr.resolve_inbox(item.id, "us-west-2") # restart-style resume
|
||||
|
||||
asyncio.run(scenario())
|
||||
assert any("us-west-2" in (t or "") for t in _final_assistant_texts(mgr, sid))
|
||||
assert mgr.inbox.pending(sid) == [] # nothing left pending
|
||||
|
||||
|
||||
def test_durable_resume_approval_executes_tool(tmp_path):
|
||||
# The model wants a write (needs approval); on durable resume "allow" must RE-EXECUTE the tool.
|
||||
target = tmp_path / "scratch_marker.txt"
|
||||
mgr = SessionManager(
|
||||
workspace=tmp_path,
|
||||
provider=ScriptedProvider(
|
||||
[
|
||||
_tool("write_file", {"path": str(target), "content": "ok"}, "call_w"),
|
||||
_text("Done — file written."),
|
||||
]
|
||||
),
|
||||
)
|
||||
sid = "dur-a"
|
||||
|
||||
async def scenario():
|
||||
engine = mgr.get_engine(sid, agent="cowork", workspace=str(tmp_path))
|
||||
item = await _run_until_pending(mgr, sid, engine)
|
||||
assert item.kind == "approval" and item.tool_call_id == "call_w"
|
||||
assert not target.exists() # not executed before approval
|
||||
await mgr.resolve_inbox(
|
||||
item.id, "allow"
|
||||
) # restart-style resume → re-execute the tool
|
||||
|
||||
asyncio.run(scenario())
|
||||
assert (
|
||||
target.exists() and target.read_text() == "ok"
|
||||
) # the approved write actually ran
|
||||
assert any("Done" in (t or "") for t in _final_assistant_texts(mgr, sid))
|
||||
Reference in New Issue
Block a user