Request approval for external paths and show token speed

This commit is contained in:
2026-05-21 23:29:33 +08:00
parent 234a3c957d
commit 9e5d5bbd5b
10 changed files with 244 additions and 20 deletions
+53
View File
@@ -47,6 +47,10 @@ def test_stream_chat_endpoint_emits_sse_reasoning_and_content(tmp_path, monkeypa
assert "event: reasoning_delta" in body
assert "event: content_delta" in body
assert "event: done" in body
assert '"generation_stats":' in body
assert '"min_tokens_per_second":' in body
assert '"avg_tokens_per_second":' in body
assert '"max_tokens_per_second":' in body
assert "thinking" in body
assert "answer" in body
@@ -111,6 +115,55 @@ def test_stream_chat_endpoint_executes_tool_before_streaming_answer(tmp_path, mo
assert "event: done" in body
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"
outside = tmp_path / "outside"
workspace.mkdir()
outside.mkdir()
(outside / "note.txt").write_text("external")
async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None):
assert role == "action"
return ModelResponse(
role=role,
model="local-main",
content=json.dumps(
{
"kind": "action_directive",
"intent": "list external directory",
"risk_level": "low",
"actions": [
{
"tool": "list_dir",
"args": {"path": str(outside)},
"reason": "User asked for that directory",
}
],
}
),
reasoning_content=None,
raw={},
latency_ms=1.0,
)
monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat)
client = TestClient(create_app())
with client.stream(
"POST",
"/v1/chat/stream",
json={"message": "show outside", "workspace": str(workspace), "debug": True},
) as response:
body = "".join(response.iter_text())
assert response.status_code == 200
assert "event: tool_approval_requested" in body
assert '"tool": "list_dir"' in body
assert str(outside) in body
assert '"status": "waiting_for_approval"' in body
def test_continue_stream_executes_approved_tool_and_streams_answer(tmp_path, monkeypatch):
monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3"))
action_calls = 0
+20
View File
@@ -102,6 +102,26 @@ async def test_tool_gateway_lists_workspace_directory(tmp_path):
assert escaped.ok is False
@pytest.mark.asyncio
async def test_tool_gateway_requests_approval_for_directory_outside_workspace(tmp_path):
workspace = tmp_path / "workspace"
outside = tmp_path / "outside"
workspace.mkdir()
outside.mkdir()
(outside / "note.txt").write_text("external")
gateway = ToolGateway.default(str(workspace))
action = {"tool": "list_dir", "args": {"path": str(outside)}}
blocked = await gateway.run_action(action)
approved = await gateway.run_action(action, approved=True)
assert blocked.ok is False
assert blocked.metadata["requires_approval"] is True
assert blocked.metadata["path"] == str(outside)
assert approved.ok is True
assert "note.txt" in approved.output
@pytest.mark.asyncio
async def test_tool_gateway_searches_file_contents(tmp_path):
(tmp_path / "src").mkdir()