mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-31 04:45:23 +08:00
183 lines
6.7 KiB
Python
183 lines
6.7 KiB
Python
"""Continuation feature parity with owner-bound, expiring new-format records."""
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from langbot_plugin.api.entities.builtin.runner import InteractionSubmission
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
from tests.test_traditional_runner_protocol_fixes import (
|
|
_collect_async,
|
|
_ctx,
|
|
_FakePluginStorage,
|
|
_load_runner_module,
|
|
_type,
|
|
)
|
|
|
|
if os.environ.get("HTTP_RUNNER_SOURCE_ROOT"):
|
|
_load_runner_module.__globals__["ROOT"] = Path(os.environ["HTTP_RUNNER_SOURCE_ROOT"])
|
|
|
|
|
|
def test_dify_interaction_resume_submits_mapped_values_and_clears_continuation() -> None:
|
|
module = _load_runner_module("dify-agent")
|
|
runner = object.__new__(module.DefaultRunner)
|
|
storage = _FakePluginStorage()
|
|
runner.get_run_api = lambda ctx: storage
|
|
interaction_id = "dify-test"
|
|
storage_key = module._interaction_storage_key(interaction_id)
|
|
storage.values[storage_key] = json.dumps(
|
|
{
|
|
"version": 1,
|
|
"interaction_id": interaction_id,
|
|
"form_token": "form-private",
|
|
"workflow_run_id": "workflow-private",
|
|
"user": "user_1",
|
|
"field_map": {"field_1": "priority"},
|
|
"action_map": {"action_1": "approve-private"},
|
|
"default_inputs": {"comment": "default"},
|
|
}
|
|
).encode()
|
|
captured: dict[str, Any] = {}
|
|
|
|
class FakeClient:
|
|
async def workflow_submit(self, **kwargs):
|
|
captured.update(kwargs)
|
|
yield {
|
|
"event": "workflow_finished",
|
|
"data": {"error": None, "outputs": {"summary": "Approved"}},
|
|
}
|
|
|
|
submission = InteractionSubmission(
|
|
interaction_id=interaction_id,
|
|
action_id="action_1",
|
|
values={"field_1": "high"},
|
|
)
|
|
asyncio.run(
|
|
runner._store_interaction_continuation(
|
|
_ctx(), json.loads(storage.values[module._interaction_storage_key(interaction_id)])
|
|
)
|
|
)
|
|
results = asyncio.run(_collect_async(runner._resume_workflow(_ctx(), FakeClient(), submission, False)))
|
|
|
|
assert captured == {
|
|
"form_token": "form-private",
|
|
"workflow_run_id": "workflow-private",
|
|
"inputs": {"comment": "default", "priority": "high"},
|
|
"user": "user_1",
|
|
"action": "approve-private",
|
|
}
|
|
assert [_type(result) for result in results] == ["message.delta", "run.completed"]
|
|
assert storage_key in storage.deleted
|
|
assert storage_key not in storage.values
|
|
|
|
|
|
def test_dify_field_submission_advances_to_action_without_calling_provider() -> None:
|
|
module = _load_runner_module("dify-agent")
|
|
runner = object.__new__(module.DefaultRunner)
|
|
storage = _FakePluginStorage()
|
|
runner.get_run_api = lambda ctx: storage
|
|
interaction_id = "dify-field"
|
|
storage.values[module._interaction_storage_key(interaction_id)] = json.dumps(
|
|
{
|
|
"version": 1,
|
|
"interaction_id": interaction_id,
|
|
"form_token": "form-private",
|
|
"workflow_run_id": "workflow-private",
|
|
"user": "user_1",
|
|
"field_map": {"field_1": "priority"},
|
|
"action_map": {"action_1": "approve-private"},
|
|
"default_inputs": {},
|
|
"title": "Manual review",
|
|
"description": "Review it",
|
|
"fallback_text": "Review it",
|
|
"phase": "field",
|
|
"current_fields": [
|
|
{
|
|
"id": "field_1",
|
|
"label": "Priority",
|
|
"type": "select",
|
|
"required": True,
|
|
"options": [{"value": "high", "label": "High"}],
|
|
}
|
|
],
|
|
"remaining_fields": [],
|
|
"interaction_actions": [{"id": "action_1", "label": "Approve", "style": "primary"}],
|
|
}
|
|
).encode()
|
|
|
|
class ProviderMustNotRun:
|
|
def workflow_submit(self, **kwargs):
|
|
raise AssertionError("provider must not resume before the action step")
|
|
|
|
submission = InteractionSubmission(interaction_id=interaction_id, values={"field_1": "high"})
|
|
asyncio.run(
|
|
runner._store_interaction_continuation(
|
|
_ctx(), json.loads(storage.values[module._interaction_storage_key(interaction_id)])
|
|
)
|
|
)
|
|
results = asyncio.run(_collect_async(runner._resume_workflow(_ctx(), ProviderMustNotRun(), submission, False)))
|
|
|
|
assert [_type(result) for result in results] == ["action.requested"]
|
|
request = results[0].data["payload"]
|
|
assert request["fields"] == []
|
|
assert request["actions"][0]["label"] == "Approve"
|
|
continuation = json.loads(storage.values[module._interaction_storage_key(request["interaction_id"])])
|
|
assert continuation["default_inputs"] == {"priority": "high"}
|
|
|
|
|
|
def test_dify_interaction_resume_can_pause_again() -> None:
|
|
module = _load_runner_module("dify-agent")
|
|
runner = object.__new__(module.DefaultRunner)
|
|
storage = _FakePluginStorage()
|
|
runner.get_run_api = lambda ctx: storage
|
|
interaction_id = "dify-first"
|
|
old_key = module._interaction_storage_key(interaction_id)
|
|
storage.values[old_key] = json.dumps(
|
|
{
|
|
"version": 1,
|
|
"interaction_id": interaction_id,
|
|
"form_token": "form-1",
|
|
"workflow_run_id": "workflow-1",
|
|
"user": "user_1",
|
|
"field_map": {},
|
|
"action_map": {"continue": ""},
|
|
"default_inputs": {},
|
|
}
|
|
).encode()
|
|
|
|
class FakeClient:
|
|
async def workflow_submit(self, **kwargs):
|
|
yield {
|
|
"event": "workflow_paused",
|
|
"data": {
|
|
"workflow_run_id": "workflow-2",
|
|
"reasons": [
|
|
{
|
|
"TYPE": "human_input_required",
|
|
"form_token": "form-2",
|
|
"node_title": "Second review",
|
|
"inputs": [],
|
|
"actions": [{"id": "done", "title": "Done"}],
|
|
}
|
|
],
|
|
},
|
|
}
|
|
|
|
submission = InteractionSubmission(interaction_id=interaction_id, action_id="continue")
|
|
asyncio.run(
|
|
runner._store_interaction_continuation(
|
|
_ctx(), json.loads(storage.values[module._interaction_storage_key(interaction_id)])
|
|
)
|
|
)
|
|
results = asyncio.run(_collect_async(runner._resume_workflow(_ctx(), FakeClient(), submission, False)))
|
|
|
|
assert [_type(result) for result in results] == ["action.requested"]
|
|
new_interaction_id = results[0].data["payload"]["interaction_id"]
|
|
assert new_interaction_id != interaction_id
|
|
assert old_key in storage.deleted
|
|
assert module._interaction_storage_key(new_interaction_id) in storage.values
|