Files
wehub-resource-sync 177604c7d1
Verify and Test / build (3.10) (push) Waiting to run
Lint / build (3.10) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:42:59 +08:00

765 lines
23 KiB
Python

# Copyright 2026 Emcie Co Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
import httpx
from fastapi import status, HTTPException
from lagom import Container
from pytest import mark, raises
from parlant.core.journeys import JourneyStore
from parlant.core.guidelines import GuidelineStore
from parlant.core.tags import Tag, TagStore
from parlant.core.common import ItemNotFoundError
async def test_that_a_journey_can_be_created(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
payload = {
"title": "Customer Onboarding",
"description": "Guide new customers through onboarding steps",
"triggers": ["Customer asks for onboarding help"],
}
response = await async_client.post("/journeys", json=payload)
assert response.status_code == status.HTTP_201_CREATED
journey = response.json()
assert journey["title"] == payload["title"]
assert journey["description"] == payload["description"]
assert journey["tags"] == []
assert len(journey["triggers"]) == 1
guideline = await guideline_store.read_guideline(guideline_id=journey["triggers"][0])
assert guideline.id == journey["triggers"][0]
guideline_after_update = await guideline_store.read_guideline(guideline.id)
assert guideline_after_update.tags == [Tag.for_journey_id(journey["id"]).id]
async def test_that_a_journey_can_be_created_with_multiple_triggers(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
payload = {
"title": "Customer Onboarding",
"description": "Guide new customers through onboarding steps",
"triggers": ["Customer asks for onboarding help", "Customer wants to signup"],
}
response = await async_client.post("/journeys", json=payload)
assert response.status_code == status.HTTP_201_CREATED
journey = response.json()
assert journey["title"] == payload["title"]
assert journey["description"] == payload["description"]
assert journey["tags"] == []
assert len(journey["triggers"]) == 2
first_guideline = await guideline_store.read_guideline(guideline_id=journey["triggers"][0])
second_guideline = await guideline_store.read_guideline(guideline_id=journey["triggers"][1])
assert first_guideline.id == journey["triggers"][0]
assert second_guideline.id == journey["triggers"][1]
async def test_that_a_journey_can_be_created_with_tags(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
tag1 = await tag_store.create_tag("tag1")
tag2 = await tag_store.create_tag("tag2")
response = await async_client.post(
"/journeys",
json={
"title": "Product Support",
"description": "Assist customers with product issues",
"triggers": ["Customer reports an issue"],
"tags": [tag1.id, tag2.id],
},
)
assert response.status_code == status.HTTP_201_CREATED
journey_dto = (
(await async_client.get(f"/journeys/{response.json()['id']}")).raise_for_status().json()
)
assert journey_dto["title"] == "Product Support"
assert set(journey_dto["tags"]) == {tag1.id, tag2.id}
async def test_that_a_journey_can_be_created_with_custom_id(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
"""Test that a journey can be created with a custom ID."""
guideline_store = container[GuidelineStore]
custom_id = "custom-journey-id-123"
payload = {
"id": custom_id,
"title": "Custom ID Journey",
"description": "Journey with a custom identifier",
"triggers": ["Custom ID condition"],
}
response = await async_client.post("/journeys", json=payload)
assert response.status_code == status.HTTP_201_CREATED
journey = response.json()
# Verify that the custom ID was used
assert journey["id"] == custom_id
assert journey["title"] == payload["title"]
assert journey["description"] == payload["description"]
assert journey["tags"] == []
assert len(journey["triggers"]) == 1
guideline = await guideline_store.read_guideline(guideline_id=journey["triggers"][0])
assert guideline.id == journey["triggers"][0]
async def test_that_creating_journey_with_duplicate_id_fails(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
"""Test that creating a journey with a duplicate ID fails appropriately."""
payload = {
"id": "duplicate-id-test",
"title": "First Journey",
"description": "First journey with this ID",
"triggers": ["First condition"],
}
# Create first journey
response1 = await async_client.post("/journeys", json=payload)
assert response1.status_code == status.HTTP_201_CREATED
# Try to create second journey with same ID
payload["title"] = "Second Journey"
payload["description"] = "This should fail due to duplicate ID"
with raises(HTTPException) as exc_info:
_ = await async_client.post("/journeys", json=payload)
# Should fail due to duplicate ID
assert exc_info.value.detail == "Journey with id 'duplicate-id-test' already exists"
async def test_that_journeys_can_be_listed(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
_ = (
(
await async_client.post(
"/journeys",
json={
"title": "Customer Onboarding",
"description": "Guide new customers",
"triggers": ["Customer asks for onboarding help"],
},
)
)
.raise_for_status()
.json()
)
journeys = (await async_client.get("/journeys")).raise_for_status().json()
assert len(journeys) == 1
first_journey = journeys[0]
assert first_journey["title"] == "Customer Onboarding"
assert len(first_journey["triggers"]) == 1
guideline = await guideline_store.read_guideline(guideline_id=first_journey["triggers"][0])
assert guideline.id == first_journey["triggers"][0]
async def test_that_a_journey_can_be_read(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
journey = (
(
await async_client.post(
"/journeys",
json={
"title": "Customer Onboarding",
"description": "Guide new customers",
"triggers": ["Customer asks for onboarding help"],
},
)
)
.raise_for_status()
.json()
)
journey_dto = (await async_client.get(f"/journeys/{journey['id']}")).raise_for_status().json()
assert journey_dto["title"] == "Customer Onboarding"
assert journey_dto["description"] == "Guide new customers"
assert len(journey_dto["triggers"]) == 1
guideline = await guideline_store.read_guideline(guideline_id=journey_dto["triggers"][0])
assert guideline.id == journey_dto["triggers"][0]
@mark.parametrize(
"update_payload, expected_title, expected_description, expected_condition",
[
(
{"title": "New Title"},
"New Title",
"Guide new customers",
"Customer asks for onboarding help",
),
(
{"description": "Updated description"},
"Customer Onboarding",
"Updated description",
"Customer asks for onboarding help",
),
],
)
async def test_that_a_journey_can_be_updated(
async_client: httpx.AsyncClient,
container: Container,
update_payload: dict[str, Any],
expected_title: str,
expected_description: str,
expected_condition: str,
) -> None:
journey = (
(
await async_client.post(
"/journeys",
json={
"title": "Customer Onboarding",
"description": "Guide new customers",
"triggers": ["Customer asks for onboarding help"],
},
)
)
.raise_for_status()
.json()
)
response = await async_client.patch(f"/journeys/{journey['id']}", json=update_payload)
response.raise_for_status()
updated_journey = response.json()
assert updated_journey["title"] == expected_title
assert updated_journey["description"] == expected_description
async def test_that_tags_can_be_added_to_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
tag1 = await tag_store.create_tag("tag1")
tag2 = await tag_store.create_tag("tag2")
tag3 = await tag_store.create_tag("tag3")
journey = (
(
await async_client.post(
"/journeys",
json={
"title": "Customer Onboarding",
"description": "Guide new customers",
"triggers": ["Customer asks for onboarding help"],
"tags": [tag1.id],
},
)
)
.raise_for_status()
.json()
)
update_payload = {"tags": {"add": [tag2.id, tag3.id]}}
response = await async_client.patch(f"/journeys/{journey['id']}", json=update_payload)
response.raise_for_status()
updated_journey = response.json()
assert tag1.id in updated_journey["tags"]
assert tag2.id in updated_journey["tags"]
assert tag3.id in updated_journey["tags"]
async def test_that_tags_can_be_removed_from_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
journey_store = container[JourneyStore]
tag2 = await tag_store.create_tag("tag2")
tag3 = await tag_store.create_tag("tag3")
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[],
tags=[tag2.id, tag3.id],
)
update_payload = {"tags": {"remove": [tag2.id]}}
_ = (
await async_client.patch(f"/journeys/{journey.id}", json=update_payload)
).raise_for_status()
journey_after_second_update = (
(await async_client.get(f"/journeys/{journey.id}")).raise_for_status().json()
)
assert tag2.id not in journey_after_second_update["tags"]
assert tag3.id in journey_after_second_update["tags"]
async def test_that_a_journey_can_be_deleted(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
journey_store = container[JourneyStore]
guideline_store = container[GuidelineStore]
guideline = await guideline_store.create_guideline(
condition="Customer asks for onboarding help",
action=None,
)
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[guideline.id],
)
delete_response = await async_client.delete(f"/journeys/{journey.id}")
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
with raises(ItemNotFoundError):
await journey_store.read_journey(journey.id)
async def test_that_a_guideline_is_deleted_when_it_is_removed_from_all_journeys(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
journey_store = container[JourneyStore]
guideline_store = container[GuidelineStore]
guideline = await guideline_store.create_guideline(
condition="Customer asks for onboarding help",
action=None,
)
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[guideline.id],
)
delete_response = await async_client.delete(f"/journeys/{journey.id}")
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
with raises(ItemNotFoundError):
await guideline_store.read_guideline(guideline.id)
async def test_that_a_guideline_is_not_deleted_when_it_is_used_in_multiple_journeys(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
journey_store = container[JourneyStore]
guideline = await guideline_store.create_guideline(
condition="Customer asks for onboarding help",
action=None,
)
journey_to_delete = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[guideline.id],
)
journey_to_keep = await journey_store.create_journey(
title="Customer Signup",
description="Guide new customers to signup",
triggers=[guideline.id],
)
await guideline_store.upsert_tag(
guideline_id=guideline.id, tag_id=Tag.for_journey_id(journey_to_delete.id).id
)
await guideline_store.upsert_tag(
guideline_id=guideline.id, tag_id=Tag.for_journey_id(journey_to_keep.id).id
)
delete_response = await async_client.delete(f"/journeys/{journey_to_delete.id}")
assert delete_response.status_code == status.HTTP_204_NO_CONTENT
guideline_after_update = await guideline_store.read_guideline(guideline.id)
assert guideline_after_update.tags == [Tag.for_journey_id(journey_to_keep.id).id]
async def test_that_a_tag_can_be_added_to_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
journey_store = container[JourneyStore]
tag = await tag_store.create_tag("new_tag")
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[],
)
response = await async_client.patch(
f"/journeys/{journey.id}",
json={"tags": {"add": [tag.id]}},
)
response.raise_for_status()
updated_journey = response.json()
assert tag.id in updated_journey["tags"]
async def test_that_a_tag_can_be_removed_from_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
journey_store = container[JourneyStore]
tag = await tag_store.create_tag("removable_tag")
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[],
tags=[tag.id],
)
response = await async_client.patch(
f"/journeys/{journey.id}",
json={"tags": {"remove": [tag.id]}},
)
response.raise_for_status()
updated_journey = response.json()
assert tag.id not in updated_journey["tags"]
async def test_that_triggers_can_be_added_to_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
journey_store = container[JourneyStore]
guideline = await guideline_store.create_guideline(
condition="New Condition",
action=None,
)
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[],
)
response = await async_client.patch(
f"/journeys/{journey.id}",
json={"triggers": {"add": [guideline.id]}},
)
response.raise_for_status()
updated_journey = response.json()
assert guideline.id in updated_journey["triggers"]
guideline_after_update = await guideline_store.read_guideline(guideline.id)
assert guideline_after_update.tags == [Tag.for_journey_id(journey.id).id]
async def test_that_triggers_can_be_removed_from_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
journey_store = container[JourneyStore]
guideline = await guideline_store.create_guideline(
condition="Removable Condition",
action=None,
)
journey_to_delete = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[guideline.id],
)
journey_to_keep = await journey_store.create_journey(
title="Customer Signup",
description="Guide new customers to signup",
triggers=[guideline.id],
)
await guideline_store.upsert_tag(
guideline_id=guideline.id, tag_id=Tag.for_journey_id(journey_to_keep.id).id
)
await guideline_store.upsert_tag(
guideline_id=guideline.id, tag_id=Tag.for_journey_id(journey_to_delete.id).id
)
response = await async_client.patch(
f"/journeys/{journey_to_delete.id}",
json={"triggers": {"remove": [guideline.id]}},
)
response.raise_for_status()
updated_journey = response.json()
assert guideline.id not in updated_journey["triggers"]
guideline_after_update = await guideline_store.read_guideline(guideline.id)
assert guideline_after_update.tags == [Tag.for_journey_id(journey_to_keep.id).id]
async def test_that_a_guideline_is_deleted_when_triggers_are_removed_from_all_journeys(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
guideline_store = container[GuidelineStore]
journey_store = container[JourneyStore]
guideline = await guideline_store.create_guideline(
condition="Removable Condition",
action=None,
)
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[guideline.id],
)
await journey_store.create_journey(
title="Customer Signup",
description="Guide new customers to signup",
triggers=[guideline.id],
)
await guideline_store.upsert_tag(
guideline_id=guideline.id, tag_id=Tag.for_journey_id(journey.id).id
)
response = await async_client.patch(
f"/journeys/{journey.id}",
json={"triggers": {"remove": [guideline.id]}},
)
response.raise_for_status()
with raises(ItemNotFoundError):
await guideline_store.read_guideline(guideline.id)
async def test_that_journeys_can_be_filtered_by_tag(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
tag_store = container[TagStore]
journey_store = container[JourneyStore]
tag = await tag_store.create_tag("tag1")
journey = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[],
tags=[tag.id],
)
_ = await journey_store.create_journey(
title="Customer Onboarding",
description="Guide new customers",
triggers=[],
)
response = await async_client.get(f"/journeys?tag_id={tag.id}")
response.raise_for_status()
journeys = response.json()
assert len(journeys) == 1
assert journeys[0]["id"] == journey.id
async def test_that_journey_composition_mode_can_be_set_and_updated(
async_client: httpx.AsyncClient,
) -> None:
# Create journey with CANNED_COMPOSITED mode
response = await async_client.post(
"/journeys",
json={
"title": "Customer Onboarding",
"description": "Guide new customers through onboarding",
"triggers": ["User asks about onboarding"],
"composition_mode": "composited_canned",
},
)
assert response.status_code == status.HTTP_201_CREATED
journey = response.json()
journey_id = journey["id"]
# Check that the composition mode is set correctly after creation
assert journey["composition_mode"] == "composited_canned"
# Retrieve journey and verify composition mode
response = await async_client.get(f"/journeys/{journey_id}")
assert response.status_code == status.HTTP_200_OK
journey = response.json()
assert journey["composition_mode"] == "composited_canned"
# Update journey to CANNED_STRICT mode
response = await async_client.patch(
f"/journeys/{journey_id}",
json={
"composition_mode": "strict_canned",
},
)
assert response.status_code == status.HTTP_200_OK
journey = response.json()
# Check that the composition mode is updated correctly
assert journey["composition_mode"] == "strict_canned"
# Retrieve journey again and verify composition mode
response = await async_client.get(f"/journeys/{journey_id}")
assert response.status_code == status.HTTP_200_OK
journey = response.json()
assert journey["composition_mode"] == "strict_canned"
###############################################################################
## Labels Tests
###############################################################################
async def test_that_a_journey_can_be_created_with_labels(
async_client: httpx.AsyncClient,
) -> None:
response = await async_client.post(
"/journeys",
json={
"title": "Labeled Journey",
"description": "A journey with labels",
"triggers": ["Customer asks about something"],
"labels": ["premium", "support"],
},
)
assert response.status_code == status.HTTP_201_CREATED
journey = response.json()
assert journey["title"] == "Labeled Journey"
assert set(journey["labels"]) == {"premium", "support"}
async def test_that_a_journey_is_created_with_empty_labels_by_default(
async_client: httpx.AsyncClient,
) -> None:
response = await async_client.post(
"/journeys",
json={
"title": "Journey without labels",
"description": "A journey",
"triggers": ["Customer asks about something"],
},
)
assert response.status_code == status.HTTP_201_CREATED
journey = response.json()
assert journey["labels"] == []
async def test_that_labels_can_be_added_to_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
journey_store = container[JourneyStore]
journey = await journey_store.create_journey(
title="Test Journey",
description="A test journey",
triggers=[],
labels={"initial"},
)
response = await async_client.patch(
f"/journeys/{journey.id}",
json={"labels": {"upsert": ["new_label", "another_label"]}},
)
assert response.status_code == status.HTTP_200_OK
updated_journey = response.json()
assert set(updated_journey["labels"]) == {"initial", "new_label", "another_label"}
async def test_that_labels_can_be_removed_from_a_journey(
async_client: httpx.AsyncClient,
container: Container,
) -> None:
journey_store = container[JourneyStore]
journey = await journey_store.create_journey(
title="Test Journey",
description="A test journey",
triggers=[],
labels={"label1", "label2", "label3"},
)
response = await async_client.patch(
f"/journeys/{journey.id}",
json={"labels": {"remove": ["label2"]}},
)
assert response.status_code == status.HTTP_200_OK
updated_journey = response.json()
assert set(updated_journey["labels"]) == {"label1", "label3"}