Stabilize streaming runtime completion

This commit is contained in:
2026-05-21 23:51:23 +08:00
parent 9e5d5bbd5b
commit e6b82f0376
5 changed files with 261 additions and 441 deletions
+117
View File
@@ -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"