aisolate-client 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/PKG-INFO +2 -2
  2. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/README.md +1 -1
  3. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/pyproject.toml +1 -1
  4. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/exception.py +26 -0
  5. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/sandbox.py +18 -0
  6. aisolate_client-0.3.2/src/aisolate/client/tests/integration/conftest.py +24 -0
  7. aisolate_client-0.3.2/src/aisolate/common/output_window.py +58 -0
  8. aisolate_client-0.3.2/src/aisolate/common/tests/fake_vm.py +118 -0
  9. aisolate_client-0.3.2/src/aisolate/common/tests/test_output_window.py +74 -0
  10. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/PKG-INFO +2 -2
  11. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/SOURCES.txt +3 -0
  12. aisolate_client-0.3.0/src/aisolate/client/tests/integration/conftest.py +0 -101
  13. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/setup.cfg +0 -0
  14. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/__init__.py +0 -0
  15. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/__init__.py +0 -0
  16. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/gateway_client.py +0 -0
  17. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/models.py +0 -0
  18. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/remote_executer.py +0 -0
  19. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/sandbox_manager.py +0 -0
  20. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/__init__.py +0 -0
  21. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/__init__.py +0 -0
  22. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/test_gateway_client.py +0 -0
  23. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/integration/test_sandbox.py +0 -0
  24. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/test_gateway_client_demux.py +0 -0
  25. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/client/tests/test_sandbox_workspace_prefix.py +0 -0
  26. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/__init__.py +0 -0
  27. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/codec.py +0 -0
  28. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/errors.py +0 -0
  29. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/models.py +0 -0
  30. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/__init__.py +0 -0
  31. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/fixtures.py +0 -0
  32. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/test_codec_channel.py +0 -0
  33. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate/common/tests/utils.py +0 -0
  34. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/dependency_links.txt +0 -0
  35. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/requires.txt +0 -0
  36. {aisolate_client-0.3.0 → aisolate_client-0.3.2}/src/aisolate_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisolate-client
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Client library for sandbox environments
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -16,7 +16,7 @@ Client library for sandbox environments
16
16
  ## Local Installation
17
17
 
18
18
  ```bash
19
- pip install path/to/aisolate_client-0.3.0-py3-none-any.whl
19
+ pip install path/to/aisolate_client-0.3.2-py3-none-any.whl
20
20
  ```
21
21
 
22
22
  ## Usage
@@ -5,7 +5,7 @@ Client library for sandbox environments
5
5
  ## Local Installation
6
6
 
7
7
  ```bash
8
- pip install path/to/aisolate_client-0.3.0-py3-none-any.whl
8
+ pip install path/to/aisolate_client-0.3.2-py3-none-any.whl
9
9
  ```
10
10
 
11
11
  ## Usage
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aisolate-client"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Client library for sandbox environments"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -48,3 +48,29 @@ class SandboxError(Exception):
48
48
  """Raised for any errors related to the Sandbox lifecycle or execution."""
49
49
 
50
50
  pass
51
+
52
+
53
+ def is_infrastructure_fault(exc: BaseException) -> bool:
54
+ """True when a sandbox call failed because the sandbox itself is broken —
55
+ VM gone, websocket dropped, gateway 5xx, or a request timeout — rather than
56
+ because executed user code raised.
57
+
58
+ Executed user-code errors come back as kernel *results* (the kernel captures
59
+ the traceback), never as these exceptions, so a caught ``Sandbox*Error`` is
60
+ always an infrastructure fault the run can recover from by re-provisioning a
61
+ fresh VM. A gateway 4xx (client misuse, e.g. a bad request) is deliberately
62
+ excluded — re-provisioning would not help.
63
+ """
64
+ if isinstance(
65
+ exc,
66
+ (
67
+ SandboxConnectionError,
68
+ SandboxTimeoutError,
69
+ SandboxWebSocketError,
70
+ SandboxError,
71
+ ),
72
+ ):
73
+ return True
74
+ if isinstance(exc, SandboxAPIError):
75
+ return exc.status_code is None or exc.status_code >= 500
76
+ return False
@@ -222,6 +222,24 @@ class Sandbox:
222
222
  """Async context manager exit point. Closes the sandbox."""
223
223
  await self._async_close(exc_type, exc, tb)
224
224
 
225
+ async def areset(self) -> "Sandbox":
226
+ """Force a fresh VM under the same id: tear the current sandbox down
227
+ (best-effort — the VM may already be gone server-side) and start a new
228
+ one.
229
+
230
+ The recovery path when the gateway reports the VM lost (guest
231
+ crash/OOM). A plain ``astart()`` cannot do this: it no-ops while
232
+ ``_is_active`` is set, and that client-side flag cannot observe a
233
+ server-side VM death. Dropping ``_client`` forces ``_async_start`` to
234
+ build a fresh connection rather than reuse the broken one.
235
+ """
236
+ with contextlib.suppress(Exception):
237
+ await self._async_close(None, None, None)
238
+ self._is_active = False
239
+ self._client = None
240
+ await self._async_start()
241
+ return self
242
+
225
243
  def run_code(
226
244
  self,
227
245
  code: str,
@@ -0,0 +1,24 @@
1
+ import importlib
2
+
3
+ import pytest_asyncio
4
+
5
+ # The driver lives in the shared test-support module so the agent e2e
6
+ # harness can use the same no-VM wiring.
7
+ from aisolate.common.tests.fake_vm import FakeDriver
8
+
9
+ # Import fixtures from common fixtures module
10
+ from aisolate.common.tests.fixtures import *
11
+
12
+
13
+ @pytest_asyncio.fixture(autouse=True)
14
+ async def _inject_fake_vm_driver(kernel_gateway_proc):
15
+ """Set the driver override BEFORE gateway_test_client enters the app
16
+ lifespan (autouse fixtures at the same scope run first), clear it and
17
+ close the fake sandbox stacks after the client has torn down."""
18
+ gateway_app = importlib.import_module("aisolate.gateway.app.main").app
19
+ driver = FakeDriver()
20
+ gateway_app.state.vm_driver_override = driver
21
+ yield
22
+ del gateway_app.state.vm_driver_override
23
+ for stack in driver.stacks:
24
+ await stack.aclose()
@@ -0,0 +1,58 @@
1
+ """The one honest output window, shared by every execution lane.
2
+
3
+ ``window_stream`` is the single windowing policy for observation channels
4
+ (shell stdout/stderr, jupyter streams and expression results): output at or
5
+ under the budget passes through BYTE-IDENTICAL; anything larger keeps head
6
+ AND tail and carries one explicit footer in the ``[TRUNCATED …]`` marker
7
+ family the agent prompts promise ("truncation is always explicit").
8
+
9
+ This module is imported by the sandbox guest (``ShellExecutor``) and by the
10
+ agent host (``RemotePythonExecutor``) — imports must stay stdlib-only, like
11
+ ``codec.py``.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ #: Default window per stream/field, in characters (~1.5k tokens). Chosen for
17
+ #: the post ``-ub 2048``/``--swa-full`` era: a complete 31-row table
18
+ #: (~2.4 KB) is a normal observation and must pass whole.
19
+ DEFAULT_OUTPUT_WINDOW = 6000
20
+
21
+ #: Env var that overrides the window — delivered via the context-creation
22
+ #: env (shell lane) and read at construction (jupyter lane).
23
+ OUTPUT_WINDOW_ENV = "AISOLATE_OUTPUT_WINDOW"
24
+
25
+
26
+ def window_stream(
27
+ text: str,
28
+ *,
29
+ budget: int,
30
+ label: str,
31
+ spool_ref: str | None = None,
32
+ guidance: str = "",
33
+ ) -> str:
34
+ """Window ``text`` to ``budget`` chars: head + footer + tail.
35
+
36
+ Returns ``text`` unchanged (byte-identical — prefix-cache stability of
37
+ the common case) when it fits the budget or the budget is unlimited
38
+ (``<= 0``). Otherwise keeps the first 2/3 and last 1/3 of the budget
39
+ around one footer line naming the stream, the total size, where the
40
+ full output lives (``spool_ref``), and what to do instead of re-running
41
+ (``guidance``).
42
+ """
43
+ if not text or budget <= 0 or len(text) <= budget:
44
+ return text
45
+
46
+ tail = budget // 3
47
+ head = budget - tail
48
+
49
+ footer = f"... [TRUNCATED {label} — {len(text):,} chars total"
50
+ if spool_ref:
51
+ footer += f"; full output: {spool_ref}"
52
+ if guidance:
53
+ footer += f"; {guidance}"
54
+ footer += "]"
55
+
56
+ return f"{text[:head]}\n{footer}\n{text[-tail:]}" if tail else (
57
+ f"{text[:head]}\n{footer}"
58
+ )
@@ -0,0 +1,118 @@
1
+ """In-process stand-in for the VM layer — the no-firecracker test driver.
2
+
3
+ ``FakeDriver`` implements the gateway's ``VMDriver`` protocol by booting the
4
+ REAL sandbox ASGI app in-process (lifespan and all — including its
5
+ DirectKernelExecutor python3 kernel) and handing the gateway a ``VMHandle``
6
+ whose session/WS are ASGI test clients. Injected through the
7
+ ``app.state.vm_driver_override`` seam (``gateway/app/factory.py``): the
8
+ gateway lifespan constructs the real FirecrackerDriver only when no override
9
+ is present, so this fake is the app's one driver for the whole test —
10
+ including lifespan-shutdown teardown of the handles it created.
11
+
12
+ ``process_match`` returns ``False`` for everything: nothing the fake creates
13
+ is a real VM process, so the dummy pid must never be signalled, sampled for
14
+ PSS, or reaped.
15
+
16
+ Shared home for the driver previously defined inline in
17
+ ``client/tests/integration/conftest.py`` (which now imports from here), used
18
+ by the agent e2e harness (``agent/tests/test_e2e/``) as well.
19
+ """
20
+
21
+ import asyncio
22
+ import importlib
23
+ from contextlib import AsyncExitStack
24
+ from pathlib import Path
25
+ from types import SimpleNamespace
26
+ from urllib.parse import unquote
27
+
28
+ from asgi_lifespan import LifespanManager
29
+
30
+ from aisolate.common.tests.utils import WsAliasTestClient
31
+
32
+
33
+ class SingleEncodeTestClient(WsAliasTestClient):
34
+ """Keep pre-quoted query strings single-encoded through the ASGI client.
35
+
36
+ ``async_asgi_testclient`` re-quotes the query it splits off the URL
37
+ (``utils.make_test_headers_path_and_query_string``, ``safe="&="``).
38
+ The GatewayClient and the gateway's proxy routers bake PRE-QUOTED
39
+ queries into their URLs (the vsock session forwards method+url only,
40
+ so the query must ride the URL) — the re-quote turns ``%2F`` into
41
+ ``%252F`` and workspace paths like ``<lineage>/task.md`` degrade into
42
+ literal ``<lineage>%2Ftask.md`` files. Un-quote once before dispatch
43
+ so the client's own re-quote restores the wire encoding.
44
+ """
45
+
46
+ async def open(self, path, **kwargs):
47
+ base, sep, query = path.partition("?")
48
+ if sep and kwargs.get("query_string") is None:
49
+ path = f"{base}?{unquote(query)}"
50
+ return await super().open(path, **kwargs)
51
+
52
+
53
+ class DummyProcess(SimpleNamespace):
54
+ returncode = None
55
+ pid = 12345
56
+
57
+ async def wait(self):
58
+ return 0
59
+
60
+ def kill(self):
61
+ self.returncode = -9
62
+
63
+ def terminate(self):
64
+ return 0
65
+
66
+
67
+ class FakeDriver:
68
+ """In-process VM 'driver': boots the sandbox ASGI app instead of a VM."""
69
+
70
+ def __init__(self):
71
+ # One stack per sandbox to avoid WebSocket reuse issues; closed
72
+ # by the injecting fixture after the gateway client has exited.
73
+ self.stacks = []
74
+
75
+ def process_match(self, cmdline: str) -> bool:
76
+ return False
77
+
78
+ async def create(self, sbx):
79
+ from aisolate.gateway.app.models.sandbox import (
80
+ SandboxConnection,
81
+ SocatProcess,
82
+ VMHandle,
83
+ )
84
+
85
+ sandbox_app = importlib.import_module("aisolate.sandbox.app.main").app
86
+
87
+ stack = AsyncExitStack()
88
+ self.stacks.append(stack)
89
+
90
+ # bring up the ASGI sandbox app so WS paths exist
91
+ await stack.enter_async_context(LifespanManager(sandbox_app))
92
+ # get a test client that supports HTTP + WS
93
+ async_client = await stack.enter_async_context(
94
+ SingleEncodeTestClient(sandbox_app)
95
+ )
96
+ ws = await stack.enter_async_context(async_client.websocket_connect("/execute"))
97
+
98
+ conn = SandboxConnection(
99
+ session=async_client,
100
+ ws=ws,
101
+ base_url="http://sandbox.local",
102
+ )
103
+ socat = SocatProcess(
104
+ process=DummyProcess(),
105
+ socket_path=Path("/tmp/dummy.sock"),
106
+ vsock_port=1080,
107
+ protocol="TCP",
108
+ remote_host="127.0.0.1",
109
+ remote_port=1080,
110
+ )
111
+
112
+ return VMHandle(
113
+ sandbox=sbx,
114
+ vm_proc=DummyProcess(),
115
+ conn=conn,
116
+ socat=socat,
117
+ log_forwarder_task=asyncio.create_task(asyncio.sleep(0)), # dummy task
118
+ )
@@ -0,0 +1,74 @@
1
+ """Unit tests for the shared output window policy."""
2
+
3
+ from aisolate.common.output_window import window_stream
4
+
5
+
6
+ class TestWindowStream:
7
+ def test_under_budget_is_byte_identical(self):
8
+ text = "a" * 100
9
+ assert window_stream(text, budget=100, label="stdout") is text
10
+
11
+ def test_at_budget_boundary_passes_unchanged(self):
12
+ text = "x" * 6000
13
+ assert window_stream(text, budget=6000, label="stdout") == text
14
+
15
+ def test_over_budget_keeps_head_and_tail(self):
16
+ text = "H" * 4000 + "M" * 50000 + "T" * 2000
17
+ out = window_stream(text, budget=6000, label="stdout")
18
+ assert out.startswith("H" * 4000)
19
+ assert out.endswith("T" * 2000)
20
+ assert "M" * 100 not in out
21
+
22
+ def test_footer_names_stream_and_total(self):
23
+ text = "y" * 51_340
24
+ out = window_stream(text, budget=6000, label="stdout")
25
+ assert "[TRUNCATED stdout — 51,340 chars total]" in out
26
+
27
+ def test_footer_carries_spool_ref_and_guidance(self):
28
+ out = window_stream(
29
+ "z" * 10_000,
30
+ budget=600,
31
+ label="stdout",
32
+ spool_ref=".aisolate/out/7.stdout",
33
+ guidance="view a window: sed -n '1,120p' .aisolate/out/7.stdout",
34
+ )
35
+ assert "full output: .aisolate/out/7.stdout" in out
36
+ assert "sed -n '1,120p'" in out
37
+ assert out.count("[TRUNCATED") == 1
38
+
39
+ def test_guidance_without_spool(self):
40
+ out = window_stream(
41
+ "z" * 10_000,
42
+ budget=600,
43
+ label="stdout",
44
+ guidance="the kernel still holds your data",
45
+ )
46
+ assert "the kernel still holds your data" in out
47
+ assert "full output:" not in out
48
+
49
+ def test_head_tail_split_is_two_thirds_one_third(self):
50
+ text = "".join(str(i % 10) for i in range(20_000))
51
+ out = window_stream(text, budget=6000, label="stderr")
52
+ head, _, rest = out.partition("\n... [TRUNCATED")
53
+ tail = rest.partition("]\n")[2]
54
+ assert head == text[:4000]
55
+ assert tail == text[-2000:]
56
+
57
+ def test_zero_or_negative_budget_disables_windowing(self):
58
+ text = "q" * 10_000
59
+ assert window_stream(text, budget=0, label="stdout") == text
60
+ assert window_stream(text, budget=-1, label="stdout") == text
61
+
62
+ def test_empty_text_passes(self):
63
+ assert window_stream("", budget=100, label="stdout") == ""
64
+
65
+ def test_tiny_budget_still_has_head(self):
66
+ out = window_stream("abcdefghij" * 10, budget=2, label="stdout")
67
+ assert out.startswith("ab")
68
+ assert "[TRUNCATED stdout" in out
69
+
70
+ def test_unicode_slicing_is_char_safe(self):
71
+ text = "ä" * 10_000
72
+ out = window_stream(text, budget=600, label="stdout")
73
+ assert out.startswith("ä" * 400)
74
+ assert out.endswith("ä" * 200)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aisolate-client
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Client library for sandbox environments
5
5
  License: Apache-2.0
6
6
  Requires-Python: >=3.10
@@ -16,7 +16,7 @@ Client library for sandbox environments
16
16
  ## Local Installation
17
17
 
18
18
  ```bash
19
- pip install path/to/aisolate_client-0.3.0-py3-none-any.whl
19
+ pip install path/to/aisolate_client-0.3.2-py3-none-any.whl
20
20
  ```
21
21
 
22
22
  ## Usage
@@ -19,9 +19,12 @@ src/aisolate/common/__init__.py
19
19
  src/aisolate/common/codec.py
20
20
  src/aisolate/common/errors.py
21
21
  src/aisolate/common/models.py
22
+ src/aisolate/common/output_window.py
22
23
  src/aisolate/common/tests/__init__.py
24
+ src/aisolate/common/tests/fake_vm.py
23
25
  src/aisolate/common/tests/fixtures.py
24
26
  src/aisolate/common/tests/test_codec_channel.py
27
+ src/aisolate/common/tests/test_output_window.py
25
28
  src/aisolate/common/tests/utils.py
26
29
  src/aisolate_client.egg-info/PKG-INFO
27
30
  src/aisolate_client.egg-info/SOURCES.txt
@@ -1,101 +0,0 @@
1
- import asyncio
2
- import importlib
3
- from contextlib import AsyncExitStack
4
- from pathlib import Path
5
- from types import SimpleNamespace
6
-
7
- import pytest_asyncio
8
- from asgi_lifespan import LifespanManager
9
- from aisolate.common.tests.utils import WsAliasTestClient
10
-
11
- # Import fixtures from common fixtures module
12
- from aisolate.common.tests.fixtures import *
13
-
14
-
15
- class _DummyProcess(SimpleNamespace):
16
- returncode = None
17
- pid = 12345
18
-
19
- async def wait(self):
20
- return 0
21
-
22
- def kill(self):
23
- self.returncode = -9
24
-
25
- def terminate(self):
26
- return 0
27
-
28
-
29
- class FakeDriver:
30
- """In-process VM 'driver': boots the sandbox ASGI app instead of a VM.
31
-
32
- Injected through the ``app.state.vm_driver_override`` seam
33
- (vmm-driver.plan.md WI-5): the gateway lifespan constructs the real
34
- FirecrackerDriver only when no override is present, so this fake is
35
- the app's one driver for the whole test — including lifespan-shutdown
36
- teardown of the handles it created.
37
- """
38
-
39
- def __init__(self):
40
- # One stack per sandbox to avoid WebSocket reuse issues; closed
41
- # by the injecting fixture after the gateway client has exited.
42
- self.stacks = []
43
-
44
- def process_match(self, cmdline: str) -> bool:
45
- # Nothing the fake creates is a real VM process: the dummy pid
46
- # (12345) must never be signalled, sampled for PSS, or reaped.
47
- return False
48
-
49
- async def create(self, sbx):
50
- from aisolate.gateway.app.models.sandbox import (
51
- SandboxConnection,
52
- SocatProcess,
53
- VMHandle,
54
- )
55
-
56
- sandbox_app = importlib.import_module("aisolate.sandbox.app.main").app
57
-
58
- stack = AsyncExitStack()
59
- self.stacks.append(stack)
60
-
61
- # bring up the ASGI sandbox app so WS paths exist
62
- await stack.enter_async_context(LifespanManager(sandbox_app))
63
- # get a test client that supports HTTP + WS
64
- async_client = await stack.enter_async_context(WsAliasTestClient(sandbox_app))
65
- ws = await stack.enter_async_context(async_client.websocket_connect("/execute"))
66
-
67
- conn = SandboxConnection(
68
- session=async_client,
69
- ws=ws,
70
- base_url="http://sandbox.local",
71
- )
72
- socat = SocatProcess(
73
- process=_DummyProcess(),
74
- socket_path=Path("/tmp/dummy.sock"),
75
- vsock_port=1080,
76
- protocol="TCP",
77
- remote_host="127.0.0.1",
78
- remote_port=1080,
79
- )
80
-
81
- return VMHandle(
82
- sandbox=sbx,
83
- vm_proc=_DummyProcess(),
84
- conn=conn,
85
- socat=socat,
86
- log_forwarder_task=asyncio.create_task(asyncio.sleep(0)), # dummy task
87
- )
88
-
89
-
90
- @pytest_asyncio.fixture(autouse=True)
91
- async def _inject_fake_vm_driver(kernel_gateway_proc):
92
- """Set the driver override BEFORE gateway_test_client enters the app
93
- lifespan (autouse fixtures at the same scope run first), clear it and
94
- close the fake sandbox stacks after the client has torn down."""
95
- gateway_app = importlib.import_module("aisolate.gateway.app.main").app
96
- driver = FakeDriver()
97
- gateway_app.state.vm_driver_override = driver
98
- yield
99
- del gateway_app.state.vm_driver_override
100
- for stack in driver.stacks:
101
- await stack.aclose()