chore: import upstream snapshot with attribution
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,415 @@
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
|
||||
from mirage.server import build_app
|
||||
from mirage.server.registry import WorkspaceRegistry
|
||||
|
||||
|
||||
def _minimal_config() -> dict:
|
||||
return {
|
||||
"config": {
|
||||
"mounts": {
|
||||
"/": {
|
||||
"resource": "ram",
|
||||
"mode": "WRITE"
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _make_app_with_short_grace(grace: float = 0.2, snapshot_root=None):
|
||||
exit_event = asyncio.Event()
|
||||
app = build_app(idle_grace_seconds=grace,
|
||||
exit_event=exit_event,
|
||||
snapshot_root=snapshot_root)
|
||||
return app, exit_event
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_list_get_delete_round_trip():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
assert r.status_code == 201, r.text
|
||||
detail = r.json()
|
||||
wid = detail["id"]
|
||||
assert wid.startswith("ws_")
|
||||
assert detail["mode"] == "write"
|
||||
assert any(m["prefix"] == "/" for m in detail["mounts"])
|
||||
|
||||
r = await client.get("/v1/workspaces")
|
||||
assert r.status_code == 200
|
||||
briefs = r.json()
|
||||
assert len(briefs) == 1
|
||||
assert briefs[0]["id"] == wid
|
||||
assert briefs[0]["mount_count"] == 1
|
||||
|
||||
r = await client.get(f"/v1/workspaces/{wid}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["id"] == wid
|
||||
|
||||
r = await client.delete(f"/v1/workspaces/{wid}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["id"] == wid
|
||||
|
||||
r = await client.get(f"/v1/workspaces/{wid}")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_with_explicit_id():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
body = {**_minimal_config(), "id": "myws"}
|
||||
r = await client.post("/v1/workspaces", json=body)
|
||||
assert r.status_code == 201
|
||||
assert r.json()["id"] == "myws"
|
||||
|
||||
r = await client.post("/v1/workspaces", json=body)
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_verbose_includes_internals():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
|
||||
r = await client.get(f"/v1/workspaces/{wid}")
|
||||
assert r.json()["internals"] is None
|
||||
|
||||
r = await client.get(f"/v1/workspaces/{wid}?verbose=true")
|
||||
internals = r.json()["internals"]
|
||||
assert internals is not None
|
||||
assert "cache_bytes" in internals
|
||||
assert "cache_entries" in internals
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.skipif(not os.environ.get("REDIS_URL"),
|
||||
reason="REDIS_URL not set")
|
||||
async def test_get_verbose_internals_with_redis_cache():
|
||||
body = {
|
||||
"config": {
|
||||
"mounts": {
|
||||
"/": {
|
||||
"resource": "ram",
|
||||
"mode": "WRITE"
|
||||
}
|
||||
},
|
||||
"cache": {
|
||||
"type": "redis",
|
||||
"url": os.environ["REDIS_URL"],
|
||||
"key_prefix": "test:summary:",
|
||||
},
|
||||
},
|
||||
}
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=body)
|
||||
assert r.status_code == 201, r.text
|
||||
wid = r.json()["id"]
|
||||
|
||||
r = await client.get(f"/v1/workspaces/{wid}?verbose=true")
|
||||
assert r.status_code == 200, r.text
|
||||
internals = r.json()["internals"]
|
||||
assert internals is not None
|
||||
# Redis cache does not track size or entries; the summary must
|
||||
# report them as untracked instead of reaching into RAM-store
|
||||
# internals or conflating "not tracked" with an empty cache.
|
||||
assert internals["cache_bytes"] is None
|
||||
assert internals["cache_entries"] is None
|
||||
|
||||
await client.delete(f"/v1/workspaces/{wid}")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clone_returns_new_workspace_with_same_mounts():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
|
||||
r = await client.post(f"/v1/workspaces/{wid}/clone", json={})
|
||||
assert r.status_code == 201, r.text
|
||||
clone = r.json()
|
||||
assert clone["id"] != wid
|
||||
assert clone["id"].startswith("ws_")
|
||||
assert {m["prefix"] for m in clone["mounts"]} == {"/"}
|
||||
|
||||
r = await client.get("/v1/workspaces")
|
||||
assert len(r.json()) == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_clone_with_explicit_id_409_on_collision():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
body = {**_minimal_config(), "id": "src"}
|
||||
await client.post("/v1/workspaces", json=body)
|
||||
|
||||
body2 = {**_minimal_config(), "id": "other"}
|
||||
await client.post("/v1/workspaces", json=body2)
|
||||
|
||||
r = await client.post("/v1/workspaces/src/clone", json={"id": "other"})
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_endpoint():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.get("/v1/health")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "ok"
|
||||
assert body["workspaces"] == 0
|
||||
assert body["uptime_s"] >= 0
|
||||
|
||||
await client.post("/v1/workspaces", json=_minimal_config())
|
||||
r = await client.get("/v1/health")
|
||||
assert r.json()["workspaces"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_writes_tar_to_path(tmp_path):
|
||||
app, _ = _make_app_with_short_grace(grace=10.0, snapshot_root=tmp_path)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
target = tmp_path / "snap.tar"
|
||||
r = await client.post(f"/v1/workspaces/{wid}/snapshot",
|
||||
json={"path": str(target)})
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
assert body["path"] == str(target)
|
||||
assert body["size"] > 0
|
||||
assert target.exists()
|
||||
assert target.stat().st_size == body["size"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_load_round_trip(tmp_path):
|
||||
app, _ = _make_app_with_short_grace(grace=10.0, snapshot_root=tmp_path)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
target = tmp_path / "snap.tar"
|
||||
await client.post(f"/v1/workspaces/{wid}/snapshot",
|
||||
json={"path": str(target)})
|
||||
|
||||
r = await client.post("/v1/workspaces/load",
|
||||
json={"path": str(target)})
|
||||
assert r.status_code == 201, r.text
|
||||
new_id = r.json()["id"]
|
||||
assert new_id != wid
|
||||
|
||||
r = await client.get(f"/v1/workspaces/{new_id}")
|
||||
assert r.status_code == 200
|
||||
assert {m["prefix"] for m in r.json()["mounts"]} == {"/"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_snapshot_rejects_path_outside_root(tmp_path):
|
||||
app, _ = _make_app_with_short_grace(grace=10.0, snapshot_root=tmp_path)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
r = await client.post(f"/v1/workspaces/{wid}/snapshot",
|
||||
json={"path": "../escape.tar"})
|
||||
assert r.status_code == 400, r.text
|
||||
assert not (tmp_path.parent / "escape.tar").exists()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_missing_path_returns_400(tmp_path):
|
||||
app, _ = _make_app_with_short_grace(grace=10.0, snapshot_root=tmp_path)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces/load",
|
||||
json={"path": str(tmp_path / "nope.tar")})
|
||||
assert r.status_code == 400, r.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_two_workspaces_run_in_isolation():
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid_a = r.json()["id"]
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid_b = r.json()["id"]
|
||||
registry = app.state.registry
|
||||
runner_a = registry.get(wid_a).runner
|
||||
runner_b = registry.get(wid_b).runner
|
||||
|
||||
slow = asyncio.create_task(
|
||||
runner_a.call(runner_a.ws.execute("sleep 1.0")))
|
||||
await asyncio.sleep(0.05)
|
||||
start = time.monotonic()
|
||||
result = await runner_b.call(runner_b.ws.execute("echo quick"))
|
||||
elapsed = time.monotonic() - start
|
||||
assert result.exit_code == 0
|
||||
assert elapsed < 0.5, (
|
||||
f"workspace B took {elapsed:.2f}s while A was sleeping; "
|
||||
"isolation violated")
|
||||
await slow
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_idle_shutdown_event_fires_after_grace():
|
||||
app, exit_event = _make_app_with_short_grace(grace=0.2)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
await client.delete(f"/v1/workspaces/{wid}")
|
||||
await asyncio.wait_for(exit_event.wait(), timeout=2.0)
|
||||
assert exit_event.is_set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_idle_timer_canceled_when_new_workspace_created():
|
||||
app, exit_event = _make_app_with_short_grace(grace=0.5)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
r = await client.post("/v1/workspaces", json=_minimal_config())
|
||||
wid = r.json()["id"]
|
||||
await client.delete(f"/v1/workspaces/{wid}")
|
||||
await asyncio.sleep(0.1)
|
||||
assert not exit_event.is_set()
|
||||
await client.post("/v1/workspaces", json=_minimal_config())
|
||||
await asyncio.sleep(0.6)
|
||||
assert not exit_event.is_set()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_workspace_bridges_fuse_through_manager(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def _fake_add(self, prefix, mountpoint=None):
|
||||
calls.append((prefix, mountpoint))
|
||||
return mountpoint or "/tmp/fake"
|
||||
|
||||
monkeypatch.setattr("mirage.workspace.workspace.Workspace.add_fuse_mount",
|
||||
_fake_add)
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
body = {
|
||||
"config": {
|
||||
"mounts": {
|
||||
"/data/": {
|
||||
"resource": "ram",
|
||||
"fuse": True
|
||||
},
|
||||
"/pinned/": {
|
||||
"resource": "ram",
|
||||
"fuse": "/tmp/pinned"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
r = await client.post("/v1/workspaces", json=body)
|
||||
assert r.status_code == 201, r.text
|
||||
assert ("/data/", None) in calls
|
||||
assert any(mp == "/tmp/pinned" for _, mp in calls)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_workspace_rolls_back_on_fuse_failure(monkeypatch):
|
||||
closed = []
|
||||
|
||||
def _boom_add(self, prefix, mountpoint=None):
|
||||
if not closed:
|
||||
orig = self.close
|
||||
|
||||
async def _spy():
|
||||
closed.append(True)
|
||||
await orig()
|
||||
|
||||
self.close = _spy
|
||||
raise ValueError("boom collision")
|
||||
|
||||
monkeypatch.setattr("mirage.workspace.workspace.Workspace.add_fuse_mount",
|
||||
_boom_add)
|
||||
app, _ = _make_app_with_short_grace(grace=10.0)
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport,
|
||||
base_url="http://test") as client:
|
||||
body = {
|
||||
"config": {
|
||||
"mounts": {
|
||||
"/data/": {
|
||||
"resource": "ram",
|
||||
"fuse": True
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
r = await client.post("/v1/workspaces", json=body)
|
||||
assert r.status_code == 409, r.text
|
||||
|
||||
r = await client.get("/v1/workspaces")
|
||||
assert r.json() == []
|
||||
assert closed == [True]
|
||||
|
||||
|
||||
def test_registry_zero_grace_fires_immediately():
|
||||
|
||||
async def _run():
|
||||
registry = WorkspaceRegistry(idle_grace_seconds=0)
|
||||
from mirage import MountMode, Workspace
|
||||
from mirage.resource.ram import RAMResource
|
||||
ws = Workspace({"/": (RAMResource(), MountMode.WRITE)})
|
||||
entry = registry.add(ws)
|
||||
await registry.remove(entry.id)
|
||||
assert registry.exit_event.is_set()
|
||||
|
||||
asyncio.run(_run())
|
||||
Reference in New Issue
Block a user