fix: preserve task stop intent and guard remote execution boundaries

This commit is contained in:
Xin Wang
2026-10-23 19:53:17 +08:00
parent 8ac75a1873
commit 89c3bad58e
16 changed files with 199 additions and 20 deletions
+24
View File
@@ -106,6 +106,30 @@ class TaskServiceTest(unittest.IsolatedAsyncioTestCase):
await self.wait_status(second["id"], "cancelled")
self.assertEqual(SimulatedExecutor.calls, 1)
async def test_restart_retries_pending_stop_instead_of_resuming_execution(self):
SimulatedExecutor.confirms_stop = False
task = await self.submit()
await self.wait_status(task["id"], "running")
await self.service.cancel(task["id"])
async with asyncio.timeout(2):
while self.service.runtime.executor.active_jobs:
await asyncio.sleep(.005)
await self.service.close()
SimulatedExecutor.confirms_stop = True
await self.service.start()
await self.wait_status(task["id"], "cancelled")
self.assertEqual(SimulatedExecutor.calls, 1)
async def test_unconfirmed_remote_stop_blocks_the_next_submission(self):
SimulatedExecutor.confirms_stop = False
first = await self.submit("call_first")
await self.wait_status(first["id"], "running")
second = await self.submit("call_second")
await self.service.cancel(first["id"])
await self.wait_status(second["id"], "failed")
self.assertEqual(SimulatedExecutor.calls, 1)
self.assertFalse((await self.service.get(second["id"])).remote_handle)
async def test_restart_observes_existing_remote_handle(self):
task = await self.submit()
await self.wait_status(task["id"], "running")
+10
View File
@@ -5,6 +5,16 @@ from services.background_tasks.opencode import OpenCodeExecutor
class OpenCodeTaskTest(unittest.IsolatedAsyncioTestCase):
async def test_persisted_result_keeps_long_assistant_text(self):
text = "重要前提:仅完成部分修改。" + "结果" * 10000 + "仍有失败。"
async def respond(request):
if request.url.path.endswith("/message"):
return httpx.Response(200, json=[{"info": {"role": "assistant", "parentID": "msg_test", "time": {"completed": 1}}, "parts": [{"type": "text", "text": text}]}])
return httpx.Response(200, json={})
executor = OpenCodeExecutor({"url": "https://remote.test", "directory": "/demo"}, {}, transport=httpx.MockTransport(respond))
state = await executor.snapshot({"session_id": "ses_test", "message_id": "msg_test"})
self.assertEqual(state["result"], text)
async def test_correlated_completion_ignores_initial_idle_and_other_sessions(self):
handle = {"session_id": "ses_test", "message_id": "msg_test"}
submitted = False
@@ -37,3 +37,16 @@ class PromptTaskBridgeTest(unittest.IsolatedAsyncioTestCase):
await bridge.functions(tool, None)[0][1](SimpleNamespace(result_callback=callback))
service.submit.assert_not_awaited()
self.assertEqual(callback.await_args.args[0]["status"], "error")
async def test_test_runtime_never_controls_an_existing_real_task(self):
runtime = SimpleNamespace(session_id="conv_1", transport=None, tool_executor_factory=object())
service = SimpleNamespace(get=AsyncMock(), cancel=AsyncMock(), send_input=AsyncMock())
bridge = PromptTaskBridge(AssistantConfig(), runtime, ToolExecutor(DynamicVariableStore({})), service)
tool = RuntimeTool(id="tool_task", name="coding", function_name="coding_task", type="task", definition={"type": "task", "config": {}})
for schema, handler in bridge.functions(tool, None)[1:]:
callback = AsyncMock()
await handler(SimpleNamespace(function_name=schema.name, arguments={"task_id": "real_task", "message": "yes"}, result_callback=callback))
self.assertEqual(callback.await_args.args[0]["status"], "error")
service.get.assert_not_awaited()
service.cancel.assert_not_awaited()
service.send_input.assert_not_awaited()
+7 -1
View File
@@ -4,7 +4,7 @@ import unittest
from types import SimpleNamespace
from fastapi import HTTPException
from routes.assistants import _validate_startup_actions
from routes.assistants import _validate_startup_actions, _validate_tool_bindings
from models import RuntimeToolBinding
from schemas import AssistantUpsert
from services.config_resolver import (
@@ -81,6 +81,12 @@ class FakeToolCatalogSession:
class StartupActionValidationTests(unittest.IsolatedAsyncioTestCase):
async def test_task_tools_are_rejected_in_literal_startup_actions(self):
tool = SimpleNamespace(id="tool_task", name="后台任务", function_name="coding_task", type="task", status="active", definition={"config": {}})
body = AssistantUpsert(name="任务启动动作", startup={"actions": [{"id": "coding", "toolBinding": {"toolId": tool.id, "argumentBindings": {"instruction": {"source": "literal", "value": "编辑仓库"}}}}]})
with self.assertRaisesRegex(HTTPException, "不支持启动 Action"):
await _validate_tool_bindings(FakeToolCatalogSession([tool]), body)
def test_runtime_pool_includes_bound_and_startup_only_tools(self):
self.assertEqual(
_runtime_tool_ids(