Skip to content

Commit 27c7db3

Browse files
committed
e2e task added
1 parent 73e7a3d commit 27c7db3

4 files changed

Lines changed: 197 additions & 6 deletions

File tree

src/server/core/acontext_core/llm/complete/mock_sdk.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ async def mock_complete(
2929
Logic:
3030
- If prompt contains "Simple Hello" -> Return "Hello World"
3131
- If prompt contains "CALL_TOOL_DISK_LIST" -> Return structured tool call JSON for disk.list
32+
- If prompt contains "SESSION_TITLE_E2E" -> Create one deterministic task, then stop
3233
- Otherwise return a generic response
3334
"""
3435
# Safe handling of mutable default arguments
@@ -45,7 +46,9 @@ async def mock_complete(
4546
if system_prompt:
4647
full_text += str(system_prompt)
4748
for msg in history_messages:
48-
if hasattr(msg, 'content') and msg.content:
49+
if isinstance(msg, dict) and msg.get("content"):
50+
full_text += str(msg["content"])
51+
elif hasattr(msg, "content") and msg.content:
4952
full_text += str(msg.content)
5053

5154
LOG.info(f"Mock LLM processing: prompt_id={prompt_id}, text_length={len(full_text)}")
@@ -66,6 +69,25 @@ async def mock_complete(
6669
)
6770
)
6871
]
72+
elif "SESSION_TITLE_E2E" in full_text:
73+
if "Task 1 created" in full_text:
74+
content = "Session title task captured"
75+
tool_calls = None
76+
else:
77+
content = None
78+
tool_calls = [
79+
LLMToolCall(
80+
id="call_mock_insert_task",
81+
type="function",
82+
function=LLMFunction(
83+
name="insert_task",
84+
arguments={
85+
"after_task_order": 0,
86+
"task_description": "Mock session title task",
87+
},
88+
),
89+
)
90+
]
6991
else:
7092
content = "This is a mock response for testing purposes."
7193
tool_calls = None
@@ -88,4 +110,4 @@ async def mock_complete(
88110
raw_response=MockRawResponse(mock=True, content=content, tool_calls=tool_calls), # Required field
89111
content=content,
90112
tool_calls=tool_calls,
91-
)
113+
)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest
2+
3+
from acontext_core.llm.complete.mock_sdk import mock_complete
4+
5+
6+
class _ObjMessage:
7+
def __init__(self, content: str):
8+
self.content = content
9+
10+
11+
@pytest.mark.asyncio
12+
async def test_session_title_trigger_works_with_dict_history_messages():
13+
response = await mock_complete(
14+
history_messages=[{"role": "user", "content": "SESSION_TITLE_E2E create task"}],
15+
)
16+
17+
assert response.tool_calls is not None
18+
assert len(response.tool_calls) == 1
19+
assert response.tool_calls[0].function.name == "insert_task"
20+
21+
22+
@pytest.mark.asyncio
23+
async def test_simple_hello_still_works_with_object_history_messages():
24+
response = await mock_complete(history_messages=[_ObjMessage("Simple Hello")])
25+
26+
assert response.content == "Hello World"

src/server/docker-compose.test.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ services:
7676
MQ_URL: amqp://acontext:helloworld@rabbitmq:5672/
7777
REDIS_URL: redis://:helloworld@redis:6379
7878
S3_ENDPOINT: http://seaweedfs:9000
79-
LLM_SDK: mock
80-
LLM_SIMPLE_MODEL: mock-model
81-
LLM_API_KEY: fake-key
79+
LLM_SDK: ${LLM_SDK:-mock}
80+
LLM_SIMPLE_MODEL: ${LLM_SIMPLE_MODEL:-mock-model}
81+
LLM_API_KEY: ${LLM_API_KEY:-fake-key}
82+
LLM_BASE_URL: ${LLM_BASE_URL:-}
8283
OTEL_ENABLED: "false"
8384
LOGGING_LEVEL: DEBUG
8485
depends_on:
@@ -143,13 +144,15 @@ services:
143144
CORE_URL: http://core:8000
144145
DB_URL: postgresql://acontext:helloworld@pg:5432/acontext_test
145146
TEST_TOKEN: test-token
147+
POLL_MAX_ITERATIONS: ${POLL_MAX_ITERATIONS:-30}
148+
POLL_INTERVAL_SECONDS: ${POLL_INTERVAL_SECONDS:-2}
146149
volumes:
147150
- ./tests:/app/tests
148151
- ./pytest.ini:/app/pytest.ini
149152
working_dir: /app
150153
depends_on:
151154
api: { condition: service_healthy }
152-
command: [ "sh", "-c", "pip install httpx asyncpg pydantic pytest pytest-asyncio==0.21.* && pytest tests/e2e/test_simple.py -v --asyncio-mode=auto" ]
155+
command: [ "sh", "-c", "pip install httpx asyncpg pydantic pytest pytest-asyncio==0.21.* && pytest ${PYTEST_TARGET:-tests/e2e/test_simple.py} -v --asyncio-mode=auto" ]
153156

154157
networks:
155158
default:
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import asyncio
2+
import asyncpg
3+
import hashlib
4+
import hmac
5+
import httpx
6+
import json
7+
import logging
8+
import os
9+
import pytest
10+
import uuid
11+
12+
13+
logging.basicConfig(level=logging.INFO)
14+
logger = logging.getLogger(__name__)
15+
16+
API_URL = os.getenv("API_URL", "http://api:8029")
17+
CORE_URL = os.getenv("CORE_URL", "http://core:8000")
18+
DB_URL = os.getenv("DB_URL", "postgresql://acontext:helloworld@pg:5432/acontext_test")
19+
TEST_TOKEN_PREFIX = os.getenv("TEST_TOKEN_PREFIX", "sk-ac-")
20+
PEPPER = os.getenv("AUTH_PEPPER", "test-pepper")
21+
POLL_MAX_ITERATIONS = int(os.getenv("POLL_MAX_ITERATIONS", "60"))
22+
POLL_INTERVAL_SECONDS = int(os.getenv("POLL_INTERVAL_SECONDS", "2"))
23+
24+
25+
def generate_hmac(secret: str, pepper: str) -> str:
26+
h = hmac.new(pepper.encode(), secret.encode(), hashlib.sha256)
27+
return h.hexdigest()
28+
29+
30+
async def create_test_project(conn):
31+
project_id = uuid.uuid4()
32+
secret = str(uuid.uuid4())
33+
bearer_token = f"{TEST_TOKEN_PREFIX}{secret}"
34+
token_hmac = generate_hmac(secret, PEPPER)
35+
configs = {"project_session_message_buffer_max_turns": 1}
36+
await conn.execute(
37+
"INSERT INTO projects (id, secret_key_hmac, secret_key_hash_phc, configs) VALUES ($1, $2, $3, $4)",
38+
project_id, token_hmac, "dummy-phc", json.dumps(configs)
39+
)
40+
return project_id, {"Authorization": f"Bearer {bearer_token}"}
41+
42+
43+
async def cleanup_test_project(conn, project_id: uuid.UUID) -> None:
44+
await conn.execute(
45+
"DELETE FROM messages WHERE session_id IN (SELECT id FROM sessions WHERE project_id = $1)",
46+
project_id,
47+
)
48+
await conn.execute("DELETE FROM tasks WHERE project_id = $1", project_id)
49+
await conn.execute("DELETE FROM sessions WHERE project_id = $1", project_id)
50+
await conn.execute("DELETE FROM projects WHERE id = $1", project_id)
51+
52+
53+
async def wait_for_services() -> None:
54+
async with httpx.AsyncClient() as client:
55+
for _ in range(POLL_MAX_ITERATIONS):
56+
try:
57+
if (
58+
(await client.get(f"{API_URL}/health", timeout=2.0)).status_code == 200
59+
and (await client.get(f"{CORE_URL}/health", timeout=2.0)).status_code == 200
60+
):
61+
return
62+
except (httpx.RequestError, httpx.TimeoutException):
63+
pass
64+
await asyncio.sleep(POLL_INTERVAL_SECONDS)
65+
raise TimeoutError("Services did not become healthy")
66+
67+
68+
async def poll_message_status(conn, message_id: str) -> str:
69+
for _ in range(POLL_MAX_ITERATIONS):
70+
status = await conn.fetchval(
71+
"SELECT session_task_process_status FROM messages WHERE id = $1",
72+
uuid.UUID(message_id),
73+
)
74+
if status in ("success", "failed", "disable_tracking", "limit_exceed"):
75+
return status
76+
await asyncio.sleep(POLL_INTERVAL_SECONDS)
77+
raise TimeoutError("Message processing timed out")
78+
79+
80+
async def poll_first_task_and_title(conn, session_id: str):
81+
for _ in range(POLL_MAX_ITERATIONS):
82+
row = await conn.fetchrow(
83+
"""
84+
SELECT s.display_title, t.data->>'task_description' AS task_description
85+
FROM sessions s
86+
LEFT JOIN tasks t
87+
ON t.session_id = s.id
88+
AND t.is_planning = false
89+
WHERE s.id = $1
90+
ORDER BY t."order" ASC
91+
LIMIT 1
92+
""",
93+
uuid.UUID(session_id),
94+
)
95+
if row and row["display_title"] and row["task_description"]:
96+
return row["display_title"], row["task_description"]
97+
await asyncio.sleep(POLL_INTERVAL_SECONDS)
98+
raise TimeoutError("Task/title sync timed out")
99+
100+
101+
@pytest.mark.asyncio
102+
async def test_session_title_follows_first_task_description_with_mock():
103+
await wait_for_services()
104+
conn = await asyncpg.connect(DB_URL)
105+
project_id, headers = await create_test_project(conn)
106+
try:
107+
async with httpx.AsyncClient() as client:
108+
session_resp = await client.post(f"{API_URL}/api/v1/session", json={}, headers=headers)
109+
assert session_resp.status_code in (200, 201), session_resp.text
110+
session_id = session_resp.json()["data"]["id"]
111+
112+
msg_resp = await client.post(
113+
f"{API_URL}/api/v1/session/{session_id}/messages",
114+
json={
115+
"format": "acontext",
116+
"blob": {
117+
"role": "user",
118+
"parts": [
119+
{
120+
"type": "text",
121+
"text": "SESSION_TITLE_E2E please create one task for this request",
122+
}
123+
],
124+
},
125+
},
126+
headers=headers,
127+
)
128+
assert msg_resp.status_code in (200, 201), msg_resp.text
129+
message_id = msg_resp.json()["data"]["id"]
130+
131+
status = await poll_message_status(conn, message_id)
132+
assert status == "success", status
133+
134+
display_title, task_description = await poll_first_task_and_title(conn, session_id)
135+
assert task_description == "Mock session title task"
136+
assert display_title == task_description
137+
logger.info("display_title=%s", display_title)
138+
finally:
139+
await cleanup_test_project(conn, project_id)
140+
await conn.close()

0 commit comments

Comments
 (0)