11from __future__ import annotations
22
3- from dataclasses import dataclass
3+ import enum
4+ from dataclasses import dataclass , field
45from pathlib import Path
56from typing import Any
67
1112 AgentTask ,
1213 FilesystemAccess ,
1314 McpServerConfig ,
15+ PermissionMode ,
1416 PermissionProfile ,
1517)
1618from agent_runtime_kit ._errors import UnsupportedTaskInputError
1719from agent_runtime_kit .adapters import AntigravityAgentRuntime
1820from agent_runtime_kit .testing import RecordingEventSink
1921
2022
23+ class FakeBuiltinTools (str , enum .Enum ):
24+ """Mirror the real ``BuiltinTools`` enum values so validation behaves the same."""
25+
26+ LIST_DIR = "list_directory"
27+ VIEW_FILE = "view_file"
28+ EDIT_FILE = "edit_file"
29+ RUN_COMMAND = "run_command"
30+ START_SUBAGENT = "start_subagent"
31+ FINISH = "finish"
32+
33+ @staticmethod
34+ def read_only () -> list [FakeBuiltinTools ]:
35+ return [FakeBuiltinTools .LIST_DIR , FakeBuiltinTools .VIEW_FILE , FakeBuiltinTools .FINISH ]
36+
37+ @staticmethod
38+ def nondestructive () -> list [FakeBuiltinTools ]:
39+ return [
40+ FakeBuiltinTools .LIST_DIR ,
41+ FakeBuiltinTools .VIEW_FILE ,
42+ FakeBuiltinTools .EDIT_FILE ,
43+ FakeBuiltinTools .FINISH ,
44+ ]
45+
46+ @staticmethod
47+ def all_tools () -> list [FakeBuiltinTools ]:
48+ return list (FakeBuiltinTools )
49+
50+
2151class FakeTypes :
52+ BuiltinTools = FakeBuiltinTools
53+
2254 @dataclass
2355 class Text :
2456 text : str
@@ -37,31 +69,20 @@ class ToolResult:
3769 name : str
3870 result : str
3971 args : dict [str , Any ] | None = None
72+ error : str | None = None
73+ exception : Exception | None = None
4074
4175 @dataclass
4276 class CapabilitiesConfig :
43- enabled_tools : list [Any ]
77+ enabled_tools : list [Any ] | None = None
78+ disabled_tools : list [Any ] | None = None
4479 enable_subagents : bool = False
4580
4681 @dataclass
4782 class McpStdioServer :
83+ name : str
4884 command : str
49- args : list [str ]
50-
51- class BuiltinTools :
52- START_SUBAGENT = "start_subagent"
53-
54- @staticmethod
55- def read_only () -> list [str ]:
56- return ["read" ]
57-
58- @staticmethod
59- def nondestructive () -> list [str ]:
60- return ["read" , "search" ]
61-
62- @staticmethod
63- def all_tools () -> list [str ]:
64- return ["read" , "write" , "start_subagent" ]
85+ args : list [str ] = field (default_factory = list )
6586
6687
6788class FakePolicy :
@@ -75,8 +96,26 @@ def __init__(self, **kwargs: Any) -> None:
7596 self .kwargs = kwargs
7697
7798
99+ class FakeUsage :
100+ prompt_token_count = 5
101+ candidates_token_count = 7
102+ thoughts_token_count = 2
103+ cached_content_token_count = 1
104+ total_token_count = 14
105+
106+
107+ class FakeResponse :
108+ def __init__ (self , prompt : str , chunks_factory : Any ) -> None :
109+ self .chunks = chunks_factory (prompt )
110+ self .usage_metadata = FakeUsage ()
111+
112+ async def structured_output (self ) -> dict [str , bool ]:
113+ return {"ok" : True }
114+
115+
78116class FakeAgent :
79117 last_config : FakeConfig | None = None
118+ chunks_factory : Any = None
80119
81120 def __init__ (self , config : FakeConfig ) -> None :
82121 FakeAgent .last_config = config
@@ -89,24 +128,7 @@ async def __aexit__(self, *args: object) -> None:
89128 return None
90129
91130 async def chat (self , prompt : str ) -> FakeResponse :
92- return FakeResponse (prompt )
93-
94-
95- class FakeUsage :
96- prompt_token_count = 5
97- candidates_token_count = 7
98- thoughts_token_count = 2
99- cached_content_token_count = 1
100- total_token_count = 14
101-
102-
103- class FakeResponse :
104- def __init__ (self , prompt : str ) -> None :
105- self .chunks = _chunks (prompt )
106- self .usage_metadata = FakeUsage ()
107-
108- async def structured_output (self ) -> dict [str , bool ]:
109- return {"ok" : True }
131+ return FakeResponse (prompt , FakeAgent .chunks_factory or _chunks )
110132
111133
112134async def _chunks (prompt : str ):
@@ -116,17 +138,28 @@ async def _chunks(prompt: str):
116138 yield FakeTypes .ToolResult ("Read" , "ok" , {"path" : "README.md" })
117139
118140
119- @pytest .mark .asyncio
120- async def test_antigravity_runtime_runs_with_injected_sdk (tmp_path : Path ) -> None :
121- sink = RecordingEventSink ()
122- runtime = AntigravityAgentRuntime (
123- api_key = "key" ,
141+ def make_runtime (* , data_dir : Path , api_key : str = "key" ) -> AntigravityAgentRuntime :
142+ return AntigravityAgentRuntime (
143+ api_key = api_key ,
144+ data_dir = data_dir ,
124145 agent_cls = FakeAgent ,
125146 config_cls = FakeConfig ,
126147 types_module = FakeTypes ,
127148 policy_module = FakePolicy ,
128149 )
129150
151+
152+ @pytest .fixture (autouse = True )
153+ def _reset_agent () -> None :
154+ FakeAgent .last_config = None
155+ FakeAgent .chunks_factory = None
156+
157+
158+ @pytest .mark .asyncio
159+ async def test_antigravity_runtime_runs_with_injected_sdk (tmp_path : Path ) -> None :
160+ sink = RecordingEventSink ()
161+ runtime = make_runtime (data_dir = tmp_path )
162+
130163 result = await runtime .run (
131164 AgentTask (
132165 goal = "task" ,
@@ -149,15 +182,141 @@ async def test_antigravity_runtime_runs_with_injected_sdk(tmp_path: Path) -> Non
149182
150183
151184@pytest .mark .asyncio
152- async def test_antigravity_rejects_mcp_env () -> None :
153- runtime = AntigravityAgentRuntime (
154- api_key = "key" ,
155- agent_cls = FakeAgent ,
156- config_cls = FakeConfig ,
157- types_module = FakeTypes ,
158- policy_module = FakePolicy ,
185+ async def test_antigravity_mcp_server_gets_name (tmp_path : Path ) -> None :
186+ runtime = make_runtime (data_dir = tmp_path )
187+
188+ await runtime .run (
189+ AgentTask (
190+ goal = "task" ,
191+ mcp_servers = (McpServerConfig (name = "fs" , command = "mcp" , args = ("--root" ,)),),
192+ )
159193 )
160194
195+ assert FakeAgent .last_config is not None
196+ servers = FakeAgent .last_config .kwargs ["mcp_servers" ]
197+ assert servers [0 ].name == "fs"
198+ assert servers [0 ].command == "mcp"
199+
200+
201+ @pytest .mark .asyncio
202+ async def test_antigravity_default_mode_uses_nondestructive (tmp_path : Path ) -> None :
203+ runtime = make_runtime (data_dir = tmp_path )
204+
205+ await runtime .run (AgentTask (goal = "task" ))
206+
207+ capabilities = FakeAgent .last_config .kwargs ["capabilities" ] # type: ignore[union-attr]
208+ assert capabilities .enabled_tools == FakeBuiltinTools .nondestructive ()
209+
210+
211+ @pytest .mark .asyncio
212+ async def test_antigravity_permissive_uses_all_tools (tmp_path : Path ) -> None :
213+ runtime = make_runtime (data_dir = tmp_path )
214+
215+ await runtime .run (
216+ AgentTask (goal = "task" , permissions = PermissionProfile (mode = PermissionMode .PERMISSIVE ))
217+ )
218+
219+ config = FakeAgent .last_config
220+ assert config is not None
221+ assert config .kwargs ["capabilities" ].enabled_tools == FakeBuiltinTools .all_tools ()
222+ assert config .kwargs ["capabilities" ].enable_subagents is True
223+ assert config .kwargs ["policies" ] == ["allow_all" ]
224+
225+
226+ @pytest .mark .asyncio
227+ async def test_antigravity_strict_uses_read_only_and_no_policies (tmp_path : Path ) -> None :
228+ runtime = make_runtime (data_dir = tmp_path )
229+
230+ await runtime .run (
231+ AgentTask (goal = "task" , permissions = PermissionProfile (mode = PermissionMode .STRICT ))
232+ )
233+
234+ config = FakeAgent .last_config
235+ assert config is not None
236+ assert config .kwargs ["capabilities" ].enabled_tools == FakeBuiltinTools .read_only ()
237+ assert config .kwargs ["policies" ] == []
238+
239+
240+ @pytest .mark .asyncio
241+ async def test_antigravity_disallowed_tools_map_to_disabled (tmp_path : Path ) -> None :
242+ runtime = make_runtime (data_dir = tmp_path )
243+
244+ await runtime .run (
245+ AgentTask (
246+ goal = "task" ,
247+ permissions = PermissionProfile (disallowed_tools = ("run_command" ,)),
248+ )
249+ )
250+
251+ config = FakeAgent .last_config
252+ assert config is not None
253+ capabilities = config .kwargs ["capabilities" ]
254+ assert capabilities .disabled_tools == ["run_command" ]
255+ # enabled_tools and disabled_tools are mutually exclusive in the real SDK.
256+ assert capabilities .enabled_tools is None
257+
258+
259+ @pytest .mark .asyncio
260+ async def test_antigravity_rejects_allow_and_deny_list_together (tmp_path : Path ) -> None :
261+ runtime = make_runtime (data_dir = tmp_path )
262+
263+ with pytest .raises (UnsupportedTaskInputError ):
264+ await runtime .run (
265+ AgentTask (
266+ goal = "task" ,
267+ permissions = PermissionProfile (
268+ allowed_tools = ("view_file" ,), disallowed_tools = ("run_command" ,)
269+ ),
270+ )
271+ )
272+
273+
274+ @pytest .mark .asyncio
275+ async def test_antigravity_invalid_allowed_tool_raises (tmp_path : Path ) -> None :
276+ runtime = make_runtime (data_dir = tmp_path )
277+
278+ with pytest .raises (UnsupportedTaskInputError ):
279+ await runtime .run (
280+ AgentTask (goal = "task" , permissions = PermissionProfile (allowed_tools = ("Read" ,)))
281+ )
282+
283+
284+ @pytest .mark .asyncio
285+ async def test_antigravity_tool_result_error_status (tmp_path : Path ) -> None :
286+ async def error_chunks (prompt : str ):
287+ yield FakeTypes .Text ("partial" )
288+ yield FakeTypes .ToolResult ("Edit" , "" , {"p" : "x" }, error = "disk full" )
289+
290+ FakeAgent .chunks_factory = error_chunks
291+ runtime = make_runtime (data_dir = tmp_path )
292+
293+ result = await runtime .run (AgentTask (goal = "task" ))
294+
295+ assert result .tool_calls [0 ].status == "error"
296+
297+
298+ @pytest .mark .asyncio
299+ async def test_antigravity_rejects_budget (tmp_path : Path ) -> None :
300+ runtime = make_runtime (data_dir = tmp_path )
301+
302+ with pytest .raises (UnsupportedTaskInputError ):
303+ await runtime .run (AgentTask (goal = "task" , budget_usd = 1.0 ))
304+
305+
306+ @pytest .mark .asyncio
307+ async def test_antigravity_rejects_network (tmp_path : Path ) -> None :
308+ runtime = make_runtime (data_dir = tmp_path )
309+
310+ with pytest .raises (UnsupportedTaskInputError ):
311+ await runtime .run (
312+ AgentTask (goal = "task" , permissions = PermissionProfile (network = True ))
313+ )
314+
315+
316+ @pytest .mark .asyncio
317+ async def test_antigravity_rejects_mcp_env (tmp_path : Path ) -> None :
318+ runtime = make_runtime (data_dir = tmp_path )
319+
161320 with pytest .raises (UnsupportedTaskInputError ):
162321 await runtime .run (
163322 AgentTask (
@@ -167,15 +326,43 @@ async def test_antigravity_rejects_mcp_env() -> None:
167326 )
168327
169328
170- def test_antigravity_availability_uses_injected_sdk () -> None :
329+ @pytest .mark .asyncio
330+ async def test_antigravity_missing_api_key_raises_unavailable (
331+ tmp_path : Path , monkeypatch : pytest .MonkeyPatch
332+ ) -> None :
333+ from agent_runtime_kit ._errors import AgentRuntimeUnavailableError
334+
335+ monkeypatch .delenv ("GEMINI_API_KEY" , raising = False )
336+ monkeypatch .delenv ("GOOGLE_API_KEY" , raising = False )
171337 runtime = AntigravityAgentRuntime (
172- api_key = "key" ,
338+ api_key = None ,
339+ data_dir = tmp_path ,
173340 agent_cls = FakeAgent ,
174341 config_cls = FakeConfig ,
175342 types_module = FakeTypes ,
176343 policy_module = FakePolicy ,
177344 )
178345
346+ with pytest .raises (AgentRuntimeUnavailableError ):
347+ await runtime .run (AgentTask (goal = "task" ))
348+
349+
350+ def test_antigravity_data_dir_is_private (tmp_path : Path ) -> None :
351+ import os
352+ import stat
353+
354+ runtime = make_runtime (data_dir = tmp_path )
355+
356+ path = runtime ._runtime_dir ("antigravity-sessions" )
357+
358+ assert path .parent == tmp_path
359+ mode = stat .S_IMODE (os .stat (path ).st_mode )
360+ assert mode == 0o700
361+
362+
363+ def test_antigravity_availability_uses_injected_sdk (tmp_path : Path ) -> None :
364+ runtime = make_runtime (data_dir = tmp_path )
365+
179366 diagnostic = runtime .availability ()
180367
181368 assert diagnostic .kind is AgentRuntimeKind .ANTIGRAVITY_AGENT_SDK
0 commit comments