Stabilize streaming runtime completion
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from fastapi.testclient import TestClient
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
|
||||
from duck_core.model_client import ModelResponse
|
||||
|
||||
@@ -55,6 +56,79 @@ def test_stream_chat_endpoint_emits_sse_reasoning_and_content(tmp_path, monkeypa
|
||||
assert "answer" in body
|
||||
|
||||
|
||||
def test_stream_chat_runs_memory_policy_and_reflection_after_completion(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
||||
|
||||
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
|
||||
if role == "action":
|
||||
content = {
|
||||
"kind": "action_directive",
|
||||
"intent": "answer directly",
|
||||
"risk_level": "none",
|
||||
"actions": [],
|
||||
}
|
||||
elif role == "memory_policy":
|
||||
content = {
|
||||
"should_store": True,
|
||||
"memory_type": "preference",
|
||||
"summary": "User wants streamed chats to retain memory.",
|
||||
"importance": 0.8,
|
||||
"scope": "workspace",
|
||||
"metadata": {},
|
||||
}
|
||||
elif role == "critic":
|
||||
return ModelResponse(
|
||||
role=role,
|
||||
model="local-main",
|
||||
content="Task completed. Reusable lesson: streamed tasks need post-processing.",
|
||||
reasoning_content=None,
|
||||
raw={},
|
||||
latency_ms=1.0,
|
||||
)
|
||||
else:
|
||||
raise AssertionError(f"unexpected role: {role}")
|
||||
return ModelResponse(
|
||||
role=role,
|
||||
model="local-main",
|
||||
content=json.dumps(content),
|
||||
reasoning_content=None,
|
||||
raw={},
|
||||
latency_ms=1.0,
|
||||
)
|
||||
|
||||
async def fake_stream_chat(self, role, messages):
|
||||
assert role == "thinker"
|
||||
yield {"type": "content_delta", "delta": "streamed answer"}
|
||||
|
||||
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
|
||||
monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat)
|
||||
with TestClient(create_app()) as client:
|
||||
with client.stream(
|
||||
"POST",
|
||||
"/v1/chat/stream",
|
||||
json={"message": "remember this", "workspace": str(tmp_path), "debug": True},
|
||||
) as response:
|
||||
body = "".join(response.iter_text())
|
||||
task_id = re.search(r'"task_id"\s*:\s*"([^"]+)"', body).group(1)
|
||||
events = []
|
||||
for _ in range(20):
|
||||
events = client.get(f"/v1/tasks/{task_id}/events").json()
|
||||
if any(event["event_type"] == "reflection_completed" for event in events):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
event_types = [event["event_type"] for event in events]
|
||||
memory = client.get("/v1/memory", params={"workspace": str(tmp_path)}).json()
|
||||
experience = client.get("/v1/experience").json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "event: done" in body
|
||||
assert "memory_policy_decision" in event_types
|
||||
assert "memory_stored" in event_types
|
||||
assert "reflection_completed" in event_types
|
||||
assert memory["results"][0]["text"] == "User wants streamed chats to retain memory."
|
||||
assert len(experience) == 1
|
||||
|
||||
|
||||
def test_stream_chat_endpoint_executes_tool_before_streaming_answer(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
||||
(tmp_path / "note.txt").write_text("stream tool content")
|
||||
@@ -115,6 +189,49 @@ def test_stream_chat_endpoint_executes_tool_before_streaming_answer(tmp_path, mo
|
||||
assert "event: done" in body
|
||||
|
||||
|
||||
def test_stream_chat_injects_candidate_skill_summary(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
||||
captured_messages = []
|
||||
|
||||
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
|
||||
assert role == "action"
|
||||
captured_messages.extend(messages)
|
||||
return ModelResponse(
|
||||
role=role,
|
||||
model="local-main",
|
||||
content=json.dumps(
|
||||
{
|
||||
"kind": "action_directive",
|
||||
"intent": "answer directly",
|
||||
"risk_level": "none",
|
||||
"actions": [],
|
||||
}
|
||||
),
|
||||
reasoning_content=None,
|
||||
raw={},
|
||||
latency_ms=1.0,
|
||||
)
|
||||
|
||||
async def fake_stream_chat(self, role, messages):
|
||||
yield {"type": "content_delta", "delta": "ok"}
|
||||
|
||||
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
|
||||
monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat)
|
||||
client = TestClient(create_app())
|
||||
|
||||
with client.stream(
|
||||
"POST",
|
||||
"/v1/chat/stream",
|
||||
json={"message": "analyze repository structure", "workspace": str(tmp_path), "debug": True},
|
||||
) as response:
|
||||
body = "".join(response.iter_text())
|
||||
|
||||
assert response.status_code == 200
|
||||
assert "event: done" in body
|
||||
assert any("Active skill:" in message["content"] for message in captured_messages)
|
||||
assert any("analyze_project" in message["content"] for message in captured_messages)
|
||||
|
||||
|
||||
def test_stream_chat_requests_approval_for_directory_outside_workspace(tmp_path, monkeypatch):
|
||||
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
|
||||
workspace = tmp_path / "workspace"
|
||||
|
||||
Reference in New Issue
Block a user