Request approval for external paths and show token speed
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user