Files
wehub-resource-sync bcbd1bdb22
Test (Python) / test-python-gate (push) Blocked by required conditions
Test (TypeScript) / test-typescript-gate (push) Blocked by required conditions
CLI exit codes / cli-gate (push) Blocked by required conditions
Test (Install) / test-install-gate (push) Blocked by required conditions
Integ / integ-gate (push) Blocked by required conditions
CLI exit codes / Python CLI (push) Waiting to run
Integ / changes (push) Has been skipped
Test (Install) / python-minimal (3.11) (push) Waiting to run
Test (Install) / python-minimal (3.12) (push) Waiting to run
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Waiting to run
Integ / integ-ts (push) Waiting to run
Integ / integ (push) Waiting to run
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Waiting to run
Integ / integ-database (push) Waiting to run
Test (Install) / python-extra (email, mirage.resource.email) (push) Waiting to run
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Waiting to run
Integ / integ-ssh (push) Waiting to run
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
CLI exit codes / TypeScript CLI (push) Waiting to run
CLI exit codes / Cross-language snapshot interop (push) Waiting to run
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Waiting to run
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Waiting to run
Test (Python) / changes (push) Has been skipped
Integ / integ-database-ts (push) Waiting to run
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Waiting to run
Integ / integ-data (push) Waiting to run
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Waiting to run
Test (Python) / test (push) Waiting to run
Integ / integ-fuse (push) Waiting to run
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Waiting to run
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Waiting to run
Test (Python) / audit (push) Waiting to run
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Waiting to run
Integ / integ-ssh-ts (push) Waiting to run
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Waiting to run
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Waiting to run
Test (TypeScript) / changes (push) Has been skipped
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Waiting to run
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Waiting to run
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Waiting to run
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Waiting to run
Test (TypeScript) / test (push) Waiting to run
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Waiting to run
Test (TypeScript) / python-fs-shim (push) Waiting to run
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Waiting to run
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Waiting to run
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Waiting to run
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Waiting to run
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Waiting to run
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Waiting to run
Test (Install) / ts-minimal (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

416 lines
15 KiB
Python

# ========= 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())