Files
LangBot/plugins/Runner/http-certification-tests/test_dify_continuation_parity.py

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