| """`ask_user` — the agent poses a multiple-choice question to the user. |
| |
| The tool is a pure UI-control marker: it does no I/O. `execute_tool_block` |
| returns an `ask_user` payload that the agent loop turns into an `ask_user` SSE |
| event and then ends the turn so the chat waits for the user's selection. |
| """ |
| import asyncio |
| import json |
|
|
| from src.agent_tools import ToolBlock, TOOL_TAGS |
| from src.tool_execution import execute_tool_block |
| from src.tool_index import ALWAYS_AVAILABLE, BUILTIN_TOOL_DESCRIPTIONS |
| from src.tool_security import is_public_blocked_tool |
|
|
|
|
| def _run(content): |
| return asyncio.run(execute_tool_block(ToolBlock("ask_user", content))) |
|
|
|
|
| def test_valid_question_returns_ask_user_payload(): |
| content = json.dumps({ |
| "question": "Which database should I use?", |
| "options": [ |
| {"label": "PostgreSQL", "description": "Relational, ACID"}, |
| {"label": "SQLite", "description": "Zero-config, file-based"}, |
| ], |
| }) |
| desc, result = _run(content) |
| assert result.get("exit_code") == 0 |
| assert "error" not in result |
| payload = result["ask_user"] |
| assert payload["question"] == "Which database should I use?" |
| assert [o["label"] for o in payload["options"]] == ["PostgreSQL", "SQLite"] |
| assert payload["options"][0]["description"] == "Relational, ACID" |
| assert payload["multi"] is False |
| assert "PostgreSQL" in result["output"] |
|
|
|
|
| def test_multi_flag_is_carried(): |
| content = json.dumps({ |
| "question": "Which features?", |
| "options": [{"label": "A"}, {"label": "B"}, {"label": "C"}], |
| "multi": True, |
| }) |
| _, result = _run(content) |
| assert result["ask_user"]["multi"] is True |
| assert len(result["ask_user"]["options"]) == 3 |
|
|
|
|
| def test_string_options_are_accepted(): |
| content = json.dumps({"question": "Pick one", "options": ["Yes", "No"]}) |
| _, result = _run(content) |
| labels = [o["label"] for o in result["ask_user"]["options"]] |
| assert labels == ["Yes", "No"] |
|
|
|
|
| def test_options_are_capped_at_six(): |
| content = json.dumps({ |
| "question": "Pick", |
| "options": [{"label": f"opt{i}"} for i in range(10)], |
| }) |
| _, result = _run(content) |
| assert len(result["ask_user"]["options"]) == 6 |
|
|
|
|
| def test_fewer_than_two_options_is_rejected(): |
| content = json.dumps({"question": "Only one?", "options": [{"label": "A"}]}) |
| _, result = _run(content) |
| assert "error" in result |
| assert result.get("exit_code") == 1 |
|
|
|
|
| def test_missing_question_is_rejected(): |
| content = json.dumps({"options": [{"label": "A"}, {"label": "B"}]}) |
| _, result = _run(content) |
| assert "error" in result |
|
|
|
|
| def test_serializer_round_trips_structured_args(): |
| from src.tool_schemas import function_call_to_tool_block |
| args = {"question": "Q?", "options": [{"label": "A"}, {"label": "B"}], "multi": True} |
| block = function_call_to_tool_block("ask_user", json.dumps(args)) |
| assert block is not None |
| assert block.tool_type == "ask_user" |
| assert json.loads(block.content) == args |
|
|
|
|
| def test_registered_everywhere(): |
| |
| assert "ask_user" in TOOL_TAGS |
| |
| assert "ask_user" in ALWAYS_AVAILABLE |
| assert "ask_user" in BUILTIN_TOOL_DESCRIPTIONS |
| |
| from src.tool_schemas import FUNCTION_TOOL_SCHEMAS |
| names = {s["function"]["name"] for s in FUNCTION_TOOL_SCHEMAS} |
| assert "ask_user" in names |
| |
| assert is_public_blocked_tool("ask_user") is False |
|
|