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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# ========= 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. =========
+208
View File
@@ -0,0 +1,208 @@
# ========= 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 pytest
from mirage.server.auth.config import (AuthMode, JWTConfig,
resolve_auth_config,
resolve_local_token)
from mirage.server.daemon_config import ALLOWED_KEYS
@pytest.mark.no_host_override
def test_resolve_local_token_env_wins(tmp_path):
f = tmp_path / "auth_token"
f.write_text("from-file")
got = resolve_local_token(env={"MIRAGE_AUTH_TOKEN": "from-env"},
token_file=f)
assert got == "from-env"
@pytest.mark.no_host_override
def test_resolve_local_token_falls_back_to_file(tmp_path):
f = tmp_path / "auth_token"
f.write_text("from-file")
got = resolve_local_token(env={}, token_file=f)
assert got == "from-file"
@pytest.mark.no_host_override
def test_resolve_local_token_returns_none_when_no_source(tmp_path):
f = tmp_path / "missing"
got = resolve_local_token(env={}, token_file=f)
assert got is None
@pytest.mark.no_host_override
def test_resolve_auth_config_default_is_local_no_token(tmp_path):
cfg = resolve_auth_config(env={}, token_file=tmp_path / "missing")
assert cfg.mode == "local"
assert cfg.local_token is None
assert cfg.bearer_token is None
assert cfg.jwt is None
@pytest.mark.no_host_override
def test_resolve_auth_config_local_with_env(tmp_path):
cfg = resolve_auth_config(env={"MIRAGE_AUTH_TOKEN": "lt"},
token_file=tmp_path / "missing")
assert cfg.mode == "local"
assert cfg.local_token == "lt"
@pytest.mark.no_host_override
def test_resolve_auth_config_token_mode_requires_env(tmp_path):
with pytest.raises(RuntimeError, match="MIRAGE_AUTH_TOKEN"):
resolve_auth_config(env={"MIRAGE_AUTH_MODE": "token"},
token_file=tmp_path / "missing")
@pytest.mark.no_host_override
def test_resolve_auth_config_token_mode_uses_env_token(tmp_path):
cfg = resolve_auth_config(env={
"MIRAGE_AUTH_MODE": "token",
"MIRAGE_AUTH_TOKEN": "operator-pat"
},
token_file=tmp_path / "missing")
assert cfg.mode == "token"
assert cfg.bearer_token == "operator-pat"
assert cfg.local_token is None
assert cfg.jwt is None
@pytest.mark.no_host_override
def test_resolve_auth_config_jwt_mode_requires_key(tmp_path):
with pytest.raises(RuntimeError, match="MIRAGE_JWT_PUBKEY"):
resolve_auth_config(env={
"MIRAGE_AUTH_MODE": "jwt",
"MIRAGE_JWT_ALG": "RS256"
},
token_file=tmp_path / "missing")
@pytest.mark.no_host_override
def test_resolve_auth_config_jwt_mode_requires_alg(tmp_path):
with pytest.raises(RuntimeError, match="MIRAGE_JWT_ALG"):
resolve_auth_config(env={
"MIRAGE_AUTH_MODE": "jwt",
"MIRAGE_JWT_PUBKEY": "-----BEGIN"
},
token_file=tmp_path / "missing")
@pytest.mark.no_host_override
def test_resolve_auth_config_jwt_mode_inline_key(tmp_path):
cfg = resolve_auth_config(env={
"MIRAGE_AUTH_MODE": "jwt",
"MIRAGE_JWT_PUBKEY":
"-----BEGIN PUBLIC KEY-----\nFAKE\n-----END PUBLIC KEY-----",
"MIRAGE_JWT_ALG": "RS256",
"MIRAGE_JWT_ISSUER": "https://issuer.example",
"MIRAGE_JWT_AUDIENCE": "mirage-daemon",
"MIRAGE_JWT_AUTHORIZED_PARTIES":
"https://app.example,https://other.example",
"MIRAGE_JWT_CLOCK_SKEW_SECONDS": "12",
},
token_file=tmp_path / "missing")
assert cfg.mode == "jwt"
assert cfg.jwt is not None
assert isinstance(cfg.jwt, JWTConfig)
assert "FAKE" in cfg.jwt.key
assert cfg.jwt.algorithm == "RS256"
assert cfg.jwt.issuer == "https://issuer.example"
assert cfg.jwt.audience == "mirage-daemon"
assert cfg.jwt.authorized_parties == ("https://app.example",
"https://other.example")
assert cfg.jwt.clock_skew_seconds == 12
@pytest.mark.no_host_override
def test_resolve_auth_config_jwt_mode_pubkey_from_file(tmp_path):
key_file = tmp_path / "jwt.pub"
key_file.write_text(
"-----BEGIN PUBLIC KEY-----\nFROMFILE\n-----END PUBLIC KEY-----")
cfg = resolve_auth_config(env={
"MIRAGE_AUTH_MODE": "jwt",
"MIRAGE_JWT_PUBKEY_FILE": str(key_file),
"MIRAGE_JWT_ALG": "RS256",
},
token_file=tmp_path / "missing")
assert cfg.mode == "jwt"
assert cfg.jwt is not None
assert "FROMFILE" in cfg.jwt.key
assert cfg.jwt.clock_skew_seconds == 5 # default mirrors Clerk
@pytest.mark.no_host_override
def test_resolve_auth_config_unknown_mode_raises(tmp_path):
with pytest.raises(RuntimeError, match="MIRAGE_AUTH_MODE"):
resolve_auth_config(env={"MIRAGE_AUTH_MODE": "wat"},
token_file=tmp_path / "missing")
def test_auth_mode_from_config_table(tmp_path):
cfg = resolve_auth_config(env={"MIRAGE_AUTH_TOKEN": "tok"},
table={"auth_mode": "token"})
assert cfg.mode == AuthMode.TOKEN
assert cfg.bearer_token == "tok"
def test_env_auth_mode_beats_config_table(tmp_path):
cfg = resolve_auth_config(env={"MIRAGE_AUTH_MODE": "local"},
token_file=tmp_path / "missing",
table={"auth_mode": "token"})
assert cfg.mode == AuthMode.LOCAL
def test_jwt_settings_from_config_table(tmp_path):
key_file = tmp_path / "pub.pem"
key_file.write_text("KEYDATA")
cfg = resolve_auth_config(env={},
table={
"auth_mode": "jwt",
"jwt_pubkey_file": str(key_file),
"jwt_alg": "RS256",
"jwt_issuer": "https://issuer",
"jwt_audience": "aud",
"jwt_authorized_parties": "a,b",
"jwt_clock_skew": 9,
})
assert cfg.mode == AuthMode.JWT
assert cfg.jwt is not None
assert cfg.jwt.key == "KEYDATA"
assert cfg.jwt.algorithm == "RS256"
assert cfg.jwt.issuer == "https://issuer"
assert cfg.jwt.audience == "aud"
assert cfg.jwt.authorized_parties == ("a", "b")
assert cfg.jwt.clock_skew_seconds == 9
def test_env_jwt_alg_beats_config_table(tmp_path):
key_file = tmp_path / "pub.pem"
key_file.write_text("KEYDATA")
cfg = resolve_auth_config(env={
"MIRAGE_AUTH_MODE": "jwt",
"MIRAGE_JWT_PUBKEY_FILE": str(key_file),
"MIRAGE_JWT_ALG": "ES256",
},
table={"jwt_alg": "RS256"})
assert cfg.jwt is not None
assert cfg.jwt.algorithm == "ES256"
def test_secret_keys_have_no_config_key():
assert "jwt_pubkey" not in ALLOWED_KEYS
assert "MIRAGE_AUTH_TOKEN" not in ALLOWED_KEYS
assert "auth_mode" in ALLOWED_KEYS
assert "jwt_alg" in ALLOWED_KEYS
+202
View File
@@ -0,0 +1,202 @@
# ========= 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 time
from dataclasses import dataclass
import jwt as pyjwt
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from mirage.server.auth.config import JWTConfig
from mirage.server.auth.jwt import JWTVerificationError, verify_jwt
@dataclass
class KeyPair:
private_pem: bytes
public_pem: bytes
@pytest.fixture(scope="module")
def rsa_keys() -> KeyPair:
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=2048)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
public_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
return KeyPair(private_pem=private_pem, public_pem=public_pem)
def _make_cfg(rsa_keys: KeyPair, **overrides) -> JWTConfig:
base = dict(
key=rsa_keys.public_pem.decode(),
algorithm="RS256",
issuer=None,
audience=None,
authorized_parties=(),
clock_skew_seconds=5,
)
base.update(overrides)
return JWTConfig(**base)
def _sign(rsa_keys: KeyPair,
claims: dict,
*,
alg: str = "RS256",
headers: dict | None = None) -> str:
return pyjwt.encode(claims,
rsa_keys.private_pem,
algorithm=alg,
headers=headers)
@pytest.mark.no_host_override
def test_verify_jwt_accepts_valid_rs256(rsa_keys):
cfg = _make_cfg(rsa_keys)
token = _sign(rsa_keys, {"sub": "user-1", "exp": int(time.time()) + 60})
claims = verify_jwt(token, cfg)
assert claims["sub"] == "user-1"
@pytest.mark.no_host_override
def test_verify_jwt_rejects_alg_none(rsa_keys):
cfg = _make_cfg(rsa_keys)
token = pyjwt.encode({
"sub": "x",
"exp": int(time.time()) + 60
},
key="",
algorithm="none")
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_rejects_alg_confusion(rsa_keys):
cfg = _make_cfg(rsa_keys, algorithm="RS256")
token = pyjwt.encode({
"sub": "x",
"exp": int(time.time()) + 60
},
key="shared-secret",
algorithm="HS256")
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_rejects_missing_exp(rsa_keys):
cfg = _make_cfg(rsa_keys)
token = _sign(rsa_keys, {"sub": "x"})
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_rejects_expired(rsa_keys):
cfg = _make_cfg(rsa_keys, clock_skew_seconds=0)
token = _sign(rsa_keys, {"sub": "x", "exp": int(time.time()) - 60})
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_accepts_within_clock_skew(rsa_keys):
cfg = _make_cfg(rsa_keys, clock_skew_seconds=30)
token = _sign(rsa_keys, {"sub": "x", "exp": int(time.time()) - 5})
claims = verify_jwt(token, cfg)
assert claims["sub"] == "x"
@pytest.mark.no_host_override
def test_verify_jwt_rejects_wrong_issuer(rsa_keys):
cfg = _make_cfg(rsa_keys, issuer="https://issuer.example")
token = _sign(
rsa_keys, {
"sub": "x",
"exp": int(time.time()) + 60,
"iss": "https://attacker.example",
})
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_rejects_wrong_audience(rsa_keys):
cfg = _make_cfg(rsa_keys, audience="mirage-daemon")
token = _sign(rsa_keys, {
"sub": "x",
"exp": int(time.time()) + 60,
"aud": "something-else",
})
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_rejects_unauthorized_party(rsa_keys):
cfg = _make_cfg(rsa_keys, authorized_parties=("https://app.example", ))
token = _sign(
rsa_keys, {
"sub": "x",
"exp": int(time.time()) + 60,
"azp": "https://attacker.example",
})
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_accepts_matching_authorized_party(rsa_keys):
cfg = _make_cfg(rsa_keys,
authorized_parties=("https://app.example",
"https://other.example"))
token = _sign(rsa_keys, {
"sub": "x",
"exp": int(time.time()) + 60,
"azp": "https://other.example",
})
claims = verify_jwt(token, cfg)
assert claims["sub"] == "x"
@pytest.mark.no_host_override
def test_verify_jwt_rejects_bad_typ_header(rsa_keys):
cfg = _make_cfg(rsa_keys)
token = _sign(rsa_keys, {
"sub": "x",
"exp": int(time.time()) + 60
},
headers={"typ": "NotAJWT"})
with pytest.raises(JWTVerificationError):
verify_jwt(token, cfg)
@pytest.mark.no_host_override
def test_verify_jwt_accepts_missing_typ_header(rsa_keys):
# Some issuers omit `typ` entirely. PyJWT does not require it,
# and we should not force it when it's absent.
cfg = _make_cfg(rsa_keys)
token = _sign(rsa_keys, {"sub": "x", "exp": int(time.time()) + 60})
claims = verify_jwt(token, cfg)
assert claims["sub"] == "x"
+200
View File
@@ -0,0 +1,200 @@
# ========= 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 time
from dataclasses import dataclass
import jwt as pyjwt
import pytest
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from httpx import ASGITransport, AsyncClient
from mirage.server import build_app
from mirage.server.auth.config import AuthConfig, JWTConfig
@dataclass
class KeyPair:
private_pem: bytes
public_pem: bytes
@pytest.fixture(scope="module")
def rsa_keys() -> KeyPair:
private_key = rsa.generate_private_key(public_exponent=65537,
key_size=2048)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption(),
)
public_pem = private_key.public_key().public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo,
)
return KeyPair(private_pem=private_pem, public_pem=public_pem)
def _client(app, headers=None):
transport = ASGITransport(app=app)
return AsyncClient(transport=transport,
base_url="http://test",
headers=headers or {})
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_local_mode_accepts_correct_bearer():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="local",
local_token="correct-token"))
async with _client(app, {"Authorization": "Bearer correct-token"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_local_mode_rejects_wrong_bearer():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="local",
local_token="correct-token"))
async with _client(app, {"Authorization": "Bearer wrong-token"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_local_mode_rejects_missing_header():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="local",
local_token="correct-token"))
async with _client(app) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_local_mode_no_token_lets_everything_through():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="local", local_token=None))
async with _client(app) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_token_mode_accepts_correct_token():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="token",
bearer_token="operator-pat"))
async with _client(app, {"Authorization": "Bearer operator-pat"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_token_mode_rejects_wrong_token():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="token",
bearer_token="operator-pat"))
async with _client(app, {"Authorization": "Bearer something-else"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_token_mode_rejects_jwt_shaped_value():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="token",
bearer_token="operator-pat"))
fake_jwt = "aaaa.bbbb.cccc"
async with _client(app, {"Authorization": f"Bearer {fake_jwt}"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_jwt_mode_accepts_valid_signed(rsa_keys):
jwt_cfg = JWTConfig(key=rsa_keys.public_pem.decode(), algorithm="RS256")
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="jwt", jwt=jwt_cfg))
token = pyjwt.encode({
"sub": "agent",
"exp": int(time.time()) + 60
},
rsa_keys.private_pem,
algorithm="RS256")
async with _client(app, {"Authorization": f"Bearer {token}"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_jwt_mode_rejects_opaque_bearer(rsa_keys):
jwt_cfg = JWTConfig(key=rsa_keys.public_pem.decode(), algorithm="RS256")
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="jwt", jwt=jwt_cfg))
async with _client(app, {"Authorization": "Bearer not-a-jwt"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_jwt_mode_rejects_expired(rsa_keys):
jwt_cfg = JWTConfig(key=rsa_keys.public_pem.decode(),
algorithm="RS256",
clock_skew_seconds=0)
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="jwt", jwt=jwt_cfg))
token = pyjwt.encode({
"sub": "agent",
"exp": int(time.time()) - 60
},
rsa_keys.private_pem,
algorithm="RS256")
async with _client(app, {"Authorization": f"Bearer {token}"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_health_endpoint_always_open():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="local",
local_token="some-token"))
async with _client(app) as c:
r = await c.get("/v1/health")
assert r.status_code == 200
@pytest.mark.no_auth_override
@pytest.mark.asyncio
async def test_authorization_header_without_bearer_prefix_rejected():
app = build_app(idle_grace_seconds=10.0,
auth_config=AuthConfig(mode="local",
local_token="correct-token"))
async with _client(app, {"Authorization": "correct-token"}) as c:
r = await c.get("/v1/workspaces")
assert r.status_code == 401
+60
View File
@@ -0,0 +1,60 @@
# ========= 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 os
import stat
import pytest
from mirage.server.auth.storage import (default_token_file, ensure_token_file,
read_token_file)
from mirage.server.env import ENV_HOME
@pytest.mark.no_host_override
def test_ensure_token_file_creates_with_0o600(tmp_path):
target = tmp_path / "subdir" / "auth_token"
token = ensure_token_file(target)
assert target.exists()
assert target.read_text().strip() == token
assert token, "minted token must be non-empty"
mode = stat.S_IMODE(os.stat(target).st_mode)
assert mode == 0o600, f"expected 0o600, got {oct(mode)}"
@pytest.mark.no_host_override
def test_ensure_token_file_is_idempotent(tmp_path):
target = tmp_path / "auth_token"
first = ensure_token_file(target)
second = ensure_token_file(target)
assert first == second, "second call must reuse existing token"
@pytest.mark.no_host_override
def test_read_token_file_returns_none_when_missing(tmp_path):
target = tmp_path / "absent"
assert read_token_file(target) is None
@pytest.mark.no_host_override
def test_read_token_file_returns_stripped_contents(tmp_path):
target = tmp_path / "auth_token"
target.write_text(" abc-def \n")
assert read_token_file(target) == "abc-def"
@pytest.mark.no_host_override
def test_default_token_file_follows_mirage_home(tmp_path, monkeypatch):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
assert default_token_file() == tmp_path / "auth_token"
+48
View File
@@ -0,0 +1,48 @@
# ========= 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 pytest
@pytest.fixture(autouse=True)
def _allow_asgi_test_host(monkeypatch, request):
# Existing server tests drive the app through httpx.ASGITransport
# with base_url="http://test", which sends Host=test. We extend
# the allowlist to include that synthetic host rather than disabling
# enforcement entirely, so the middleware still rejects anything
# else (e.g. a future regression test that drops in an unexpected
# Host header). Tests that exercise rejection paths opt out with
# the @pytest.mark.no_host_override marker.
if "no_host_override" in request.keywords:
return
monkeypatch.setenv("MIRAGE_ALLOWED_HOSTS", "test,127.0.0.1,localhost,::1")
@pytest.fixture(autouse=True)
def _disable_auth_for_legacy_tests(monkeypatch, request, tmp_path_factory):
# Existing server tests don't send Authorization headers. With the
# auth middleware now installed by build_app, we keep them passing
# by forcing the daemon into mode=local with no token configured
# (env unset + MIRAGE_HOME pointed at an empty temp dir, so the
# default token file does not exist), which the middleware treats
# as "no auth" and lets every request through with a startup
# warning. Redirecting MIRAGE_HOME also keeps these tests from
# touching the real ~/.mirage tree. Tests that exercise auth paths
# opt out with the @pytest.mark.no_auth_override marker.
if "no_auth_override" in request.keywords:
return
monkeypatch.setenv("MIRAGE_AUTH_MODE", "local")
monkeypatch.delenv("MIRAGE_AUTH_TOKEN", raising=False)
monkeypatch.setenv("MIRAGE_HOME",
str(tmp_path_factory.mktemp("mirage_home")))
+68
View File
@@ -0,0 +1,68 @@
# ========= 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 os
import pytest
from mirage.server.app import _remove_pid_file, _write_pid_file, build_app
from mirage.server.daemon_config import DaemonConfigError
from mirage.server.env import ENV_HOME, ENV_PID_FILE
def test_build_app_pid_file_explicit_wins(tmp_path):
target = tmp_path / "custom" / "daemon.pid"
app = build_app(pid_file=target)
assert app.state.pid_file == target
def test_build_app_pid_file_from_env(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_PID_FILE, str(tmp_path / "env.pid"))
app = build_app()
assert app.state.pid_file == tmp_path / "env.pid"
def test_build_app_roots_follow_mirage_home(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv(ENV_PID_FILE, raising=False)
app = build_app()
assert app.state.pid_file == tmp_path / "daemon.pid"
assert app.state.snapshot_root == tmp_path / "snapshots"
def test_write_and_remove_pid_file_creates_parents(tmp_path):
target = tmp_path / "nested" / "daemon.pid"
_write_pid_file(target)
assert target.read_text() == str(os.getpid())
_remove_pid_file(target)
assert not target.exists()
def test_remove_pid_file_missing_is_quiet(tmp_path):
_remove_pid_file(tmp_path / "does_not_exist.pid")
def test_build_app_rejects_unknown_config_key(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv(ENV_PID_FILE, raising=False)
(tmp_path / "config.toml").write_text('[daemon]\ntypo_key = "x"\n')
with pytest.raises(DaemonConfigError, match="typo_key"):
build_app()
def test_build_app_accepts_valid_config(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.delenv(ENV_PID_FILE, raising=False)
(tmp_path / "config.toml").write_text('[daemon]\nurl = "http://h:1"\n')
build_app()
+67
View File
@@ -0,0 +1,67 @@
# ========= 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 pytest
from mirage.server.daemon_config import (ALLOWED_KEYS, DaemonConfigError,
read_daemon_table,
validate_daemon_table)
def test_read_daemon_table_missing_file(tmp_path):
assert read_daemon_table(tmp_path) == {}
def test_read_daemon_table_no_daemon_section(tmp_path):
(tmp_path / "config.toml").write_text("[other]\nx = 1\n")
assert read_daemon_table(tmp_path) == {}
def test_read_daemon_table_reads_keys(tmp_path):
(tmp_path / "config.toml"
).write_text('[daemon]\nurl = "http://h:1"\npid_file = "/tmp/p.pid"\n')
table = read_daemon_table(tmp_path)
assert table["url"] == "http://h:1"
assert table["pid_file"] == "/tmp/p.pid"
def test_read_daemon_table_malformed_toml(tmp_path):
(tmp_path / "config.toml").write_text("[daemon\nnot toml")
with pytest.raises(DaemonConfigError, match="config.toml"):
read_daemon_table(tmp_path)
def test_allowed_keys_contents():
assert "pid_file" in ALLOWED_KEYS
assert "MIRAGE_HOME" not in ALLOWED_KEYS
def test_validate_accepts_known_keys():
validate_daemon_table({"url": "http://h:1", "idle_grace_seconds": 45})
def test_validate_rejects_unknown_keys():
with pytest.raises(DaemonConfigError, match="typo_key"):
validate_daemon_table({"typo_key": "x", "url": "http://h:1"})
def test_validate_rejects_wrong_type():
with pytest.raises(DaemonConfigError, match="pid_file"):
validate_daemon_table({"pid_file": 123})
def test_validate_accepts_numeric_grace_and_rejects_string():
validate_daemon_table({"idle_grace_seconds": 12.5})
with pytest.raises(DaemonConfigError, match="idle_grace_seconds"):
validate_daemon_table({"idle_grace_seconds": "soon"})
+224
View File
@@ -0,0 +1,224 @@
# ========= 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 json
import pytest
from httpx import ASGITransport, AsyncClient
from mirage.server import build_app
def _minimal_config() -> dict:
return {
"config": {
"mounts": {
"/": {
"resource": "ram",
"mode": "WRITE"
}
},
},
}
async def _create_workspace(client: AsyncClient) -> str:
r = await client.post("/v1/workspaces", json=_minimal_config())
assert r.status_code == 201
return r.json()["id"]
@pytest.mark.asyncio
async def test_execute_sync_returns_io_result():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute",
json={"command": "echo hello"},
)
assert r.status_code == 200, r.text
body = r.json()
assert body["kind"] == "io"
assert body["exit_code"] == 0
assert body["stdout"].startswith("hello")
assert "X-Mirage-Job-Id" in r.headers
@pytest.mark.asyncio
async def test_execute_sync_records_a_job_in_done_state():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute",
json={"command": "echo done-marker"},
)
job_id = r.headers["X-Mirage-Job-Id"]
rj = await client.get(f"/v1/jobs/{job_id}")
assert rj.status_code == 200
body = rj.json()
assert body["status"] == "done"
assert body["result"]["stdout"].startswith("done-marker")
@pytest.mark.asyncio
async def test_execute_background_returns_job_id_immediately():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute?background=true",
json={"command": "sleep 0.3 && echo bg-done"},
)
assert r.status_code == 202, r.text
body = r.json()
assert body["job_id"].startswith("job_")
assert body["workspace_id"] == wid
@pytest.mark.asyncio
async def test_background_job_completes_and_result_is_readable():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute?background=true",
json={"command": "echo finished"},
)
job_id = r.json()["job_id"]
rw = await client.post(f"/v1/jobs/{job_id}/wait", json={})
body = rw.json()
assert body["status"] == "done"
assert body["result"]["stdout"].startswith("finished")
@pytest.mark.asyncio
async def test_wait_with_timeout_returns_running_status():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute?background=true",
json={"command": "sleep 1.0"},
)
job_id = r.json()["job_id"]
rw = await client.post(f"/v1/jobs/{job_id}/wait",
json={"timeout_s": 0.1})
assert rw.status_code == 200
assert rw.json()["status"] == "running"
@pytest.mark.asyncio
async def test_cancel_running_job():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute?background=true",
json={"command": "sleep 5.0"},
)
job_id = r.json()["job_id"]
await asyncio.sleep(0.05)
rd = await client.delete(f"/v1/jobs/{job_id}")
assert rd.status_code == 200
await asyncio.sleep(0.1)
rg = await client.get(f"/v1/jobs/{job_id}")
status = rg.json()["status"]
assert status in ("canceled", "failed")
@pytest.mark.asyncio
async def test_list_jobs_filtered_by_workspace():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid_a = await _create_workspace(client)
wid_b = await _create_workspace(client)
await client.post(
f"/v1/workspaces/{wid_a}/execute",
json={"command": "echo a"},
)
await client.post(
f"/v1/workspaces/{wid_b}/execute",
json={"command": "echo b"},
)
r = await client.get("/v1/jobs")
assert len(r.json()) == 2
r = await client.get(f"/v1/jobs?workspace_id={wid_a}")
jobs = r.json()
assert len(jobs) == 1
assert jobs[0]["workspace_id"] == wid_a
@pytest.mark.asyncio
async def test_execute_with_stdin_multipart():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/execute",
data={"request": json.dumps({"command": "wc -l"})},
files={
"stdin":
("stdin.bin", b"a\nb\nc\n", "application/octet-stream"),
},
)
assert r.status_code == 200, r.text
body = r.json()
assert body["exit_code"] == 0
assert body["stdout"].strip().startswith("3")
@pytest.mark.asyncio
async def test_unknown_workspace_404():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
r = await client.post(
"/v1/workspaces/ws_doesnotexist/execute",
json={"command": "echo hi"},
)
assert r.status_code == 404
@pytest.mark.asyncio
async def test_unknown_job_404():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
r = await client.get("/v1/jobs/job_doesnotexist")
assert r.status_code == 404
+227
View File
@@ -0,0 +1,227 @@
# ========= 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 pytest
from httpx import ASGITransport, AsyncClient
from mirage.server import build_app
from mirage.server.host_validation import (is_host_allowed,
parse_allowed_hosts,
resolve_allowed_hosts, strip_port)
from mirage.server.host_validation_constants import DEFAULT_ALLOWED_HOSTS
def test_parse_allowed_hosts_defaults_when_missing():
assert parse_allowed_hosts(None) == list(DEFAULT_ALLOWED_HOSTS)
assert parse_allowed_hosts("") == list(DEFAULT_ALLOWED_HOSTS)
assert parse_allowed_hosts(" ") == list(DEFAULT_ALLOWED_HOSTS)
def test_parse_allowed_hosts_csv():
assert parse_allowed_hosts("a,b,c") == ["a", "b", "c"]
assert parse_allowed_hosts(" a , b , c ") == ["a", "b", "c"]
def test_parse_allowed_hosts_wildcard_passthrough():
assert parse_allowed_hosts("*") == ["*"]
assert parse_allowed_hosts("*,localhost") == ["*", "localhost"]
def test_strip_port_well_formed():
assert strip_port("127.0.0.1") == "127.0.0.1"
assert strip_port("127.0.0.1:8765") == "127.0.0.1"
assert strip_port("localhost:8765") == "localhost"
def test_strip_port_ipv6_brackets():
assert strip_port("[::1]") == "::1"
assert strip_port("[::1]:8765") == "::1"
def test_strip_port_malformed_bracketed_returns_raw():
assert strip_port("[::1]evil") == "[::1]evil"
assert strip_port("[::1]:8765x") == "[::1]:8765x"
assert strip_port("[::1].attacker.tld") == "[::1].attacker.tld"
def test_strip_port_unclosed_bracket_returns_raw():
assert strip_port("[::1") == "[::1"
def test_strip_port_non_digit_port_returns_raw():
assert strip_port("127.0.0.1:8765x") == "127.0.0.1:8765x"
def test_strip_port_empty_host_returns_raw():
assert strip_port(":8765") == ":8765"
assert strip_port("[]") == "[]"
def test_is_host_allowed_malformed_fail_closed():
allowed = list(DEFAULT_ALLOWED_HOSTS)
assert is_host_allowed("[::1]evil", allowed) is False
assert is_host_allowed("[::1]:8765x", allowed) is False
assert is_host_allowed("[::1].attacker.tld", allowed) is False
def test_is_host_allowed_accepts_loopback():
allowed = list(DEFAULT_ALLOWED_HOSTS)
assert is_host_allowed("[::1]", allowed) is True
assert is_host_allowed("[::1]:8765", allowed) is True
assert is_host_allowed("127.0.0.1:8765", allowed) is True
def test_resolve_allowed_hosts_explicit_wins(monkeypatch):
monkeypatch.setenv("MIRAGE_ALLOWED_HOSTS", "elsewhere")
assert resolve_allowed_hosts(["override.example"]) == ["override.example"]
def test_resolve_allowed_hosts_env_when_arg_missing(monkeypatch):
monkeypatch.setenv("MIRAGE_ALLOWED_HOSTS", "foo,bar")
assert resolve_allowed_hosts(None) == ["foo", "bar"]
def test_resolve_allowed_hosts_defaults_when_env_unset(monkeypatch):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
assert resolve_allowed_hosts(None) == list(DEFAULT_ALLOWED_HOSTS)
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_default_rejects_unknown_host(monkeypatch):
# No env, no explicit arg: middleware enforces loopback allowlist.
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://attacker.example") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 400
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_default_accepts_loopback_host(monkeypatch):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://127.0.0.1") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 200
async with AsyncClient(transport=transport,
base_url="http://localhost") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_default_accepts_ipv6_loopback_host(monkeypatch):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
for base in ("http://[::1]", "http://[::1]:8765"):
async with AsyncClient(transport=transport, base_url=base) as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_default_rejects_malformed_bracketed_host(monkeypatch):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://127.0.0.1") as client:
r = await client.get("/v1/workspaces", headers={"host": "[::1]evil"})
assert r.status_code == 400
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_default_rejects_non_digit_port(monkeypatch):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://127.0.0.1") as client:
r = await client.get("/v1/workspaces",
headers={"host": "127.0.0.1:8765x"})
assert r.status_code == 400
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_env_override_extends_allowlist(monkeypatch):
monkeypatch.setenv("MIRAGE_ALLOWED_HOSTS",
"127.0.0.1,localhost,daemon.mirage.local")
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://daemon.mirage.local") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 200
async with AsyncClient(transport=transport,
base_url="http://attacker.example") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 400
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_explicit_wildcard_disables_enforcement(monkeypatch):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0, allowed_hosts=["*"])
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://anything.example") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 200
@pytest.mark.no_host_override
@pytest.mark.asyncio
async def test_rejection_emits_log_warning(monkeypatch, caplog):
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
caplog.set_level("WARNING", logger="mirage.server.host_validation")
async with AsyncClient(transport=transport,
base_url="http://attacker.example") as client:
r = await client.get("/v1/workspaces")
assert r.status_code == 400
rejection_logs = [
rec for rec in caplog.records
if "attacker.example" in rec.getMessage()
]
assert rejection_logs, "expected a warning log for the rejected host"
assert rejection_logs[0].levelname == "WARNING"
def test_resolve_allowed_hosts_config_beats_default(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_ALLOWED_HOSTS", raising=False)
(tmp_path / "config.toml"
).write_text('[daemon]\nallowed_hosts = "example.com,api.example.com"\n')
assert resolve_allowed_hosts() == ["example.com", "api.example.com"]
def test_resolve_allowed_hosts_env_beats_config(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.setenv("MIRAGE_ALLOWED_HOSTS", "env.example.com")
(tmp_path / "config.toml"
).write_text('[daemon]\nallowed_hosts = "file.example.com"\n')
assert resolve_allowed_hosts() == ["env.example.com"]
+194
View File
@@ -0,0 +1,194 @@
# ========= 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. =========
from pathlib import Path
import pytest
from mirage.server.env import ENV_HOME, ENV_PID_FILE
from mirage.server.paths import (PathOutsideRootError, mirage_home,
pid_file_path, resolve_within_root,
snapshot_root_path, validate_path_segment,
version_root_path)
def test_resolve_within_root_relative(tmp_path):
out = resolve_within_root(tmp_path, "seed.tar")
assert out == (tmp_path / "seed.tar")
def test_resolve_within_root_absolute_inside(tmp_path):
inside = tmp_path / "nested" / "a.tar"
assert resolve_within_root(tmp_path, str(inside)) == inside
def test_resolve_within_root_returns_root(tmp_path):
assert resolve_within_root(tmp_path, ".") == tmp_path
def test_resolve_within_root_rejects_traversal(tmp_path):
with pytest.raises(PathOutsideRootError):
resolve_within_root(tmp_path, "../../etc/passwd")
def test_resolve_within_root_rejects_absolute_outside(tmp_path):
with pytest.raises(PathOutsideRootError):
resolve_within_root(tmp_path, "/etc/passwd")
def test_resolve_within_root_rejects_sibling_prefix(tmp_path):
sibling = str(tmp_path) + "-evil"
with pytest.raises(PathOutsideRootError):
resolve_within_root(tmp_path, sibling)
def test_validate_path_segment_accepts_safe():
assert validate_path_segment("ws_abc123") == "ws_abc123"
assert validate_path_segment("a.b-c_d") == "a.b-c_d"
@pytest.mark.parametrize("bad", ["", ".", "..", "a/b", "a\\b", "a b", "a$b"])
def test_validate_path_segment_rejects_bad(bad):
with pytest.raises(PathOutsideRootError):
validate_path_segment(bad)
def test_mirage_home_defaults_to_dot_mirage(monkeypatch):
monkeypatch.delenv(ENV_HOME, raising=False)
assert mirage_home() == Path.home() / ".mirage"
def test_mirage_home_honors_env(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
assert mirage_home() == tmp_path
def test_pid_file_defaults_under_home(monkeypatch, tmp_path):
monkeypatch.delenv(ENV_PID_FILE, raising=False)
monkeypatch.setenv(ENV_HOME, str(tmp_path))
assert pid_file_path() == tmp_path / "daemon.pid"
def test_pid_file_env_wins_over_home(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
monkeypatch.setenv(ENV_PID_FILE, "/run/mirage/daemon.pid")
assert pid_file_path() == Path("/run/mirage/daemon.pid")
def test_pid_file_explicit_wins_over_env(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_PID_FILE, "/run/mirage/daemon.pid")
assert pid_file_path(tmp_path / "x.pid") == tmp_path / "x.pid"
def test_roots_follow_mirage_home(monkeypatch, tmp_path):
monkeypatch.setenv(ENV_HOME, str(tmp_path))
assert version_root_path() == tmp_path / "repos"
assert snapshot_root_path() == tmp_path / "snapshots"
def test_mirage_home_relative_env_is_absolutized(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
monkeypatch.setenv(ENV_HOME, "mhome")
assert mirage_home() == tmp_path / "mhome"
def test_pid_file_relative_env_is_absolutized(monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
monkeypatch.setenv(ENV_PID_FILE, "rel/daemon.pid")
assert pid_file_path() == tmp_path / "rel" / "daemon.pid"
def test_pid_file_explicit_relative_is_absolutized(monkeypatch, tmp_path):
monkeypatch.delenv(ENV_PID_FILE, raising=False)
monkeypatch.chdir(tmp_path)
assert pid_file_path("x.pid") == tmp_path / "x.pid"
def _write_config(home, body):
(home / "config.toml").write_text(f"[daemon]\n{body}\n")
def test_pid_file_config_beats_default(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_PID_FILE", raising=False)
_write_config(tmp_path, 'pid_file = "/tmp/from-config.pid"')
assert pid_file_path() == Path("/tmp/from-config.pid")
def test_pid_file_env_beats_config(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.setenv("MIRAGE_PID_FILE", "/tmp/from-env.pid")
_write_config(tmp_path, 'pid_file = "/tmp/from-config.pid"')
assert pid_file_path() == Path("/tmp/from-env.pid")
def test_pid_file_explicit_beats_env(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_PID_FILE", "/tmp/from-env.pid")
assert pid_file_path("/tmp/explicit.pid") == Path("/tmp/explicit.pid")
def test_pid_file_default_when_unset(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_PID_FILE", raising=False)
assert pid_file_path() == tmp_path / "daemon.pid"
def test_version_root_config_beats_default(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_VERSION_ROOT", raising=False)
_write_config(tmp_path, 'version_root = "/data/repos"')
assert version_root_path() == Path("/data/repos")
def test_version_root_env_beats_config(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.setenv("MIRAGE_VERSION_ROOT", "/env/repos")
_write_config(tmp_path, 'version_root = "/data/repos"')
assert version_root_path() == Path("/env/repos")
def test_snapshot_root_config_beats_default(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_SNAPSHOT_ROOT", raising=False)
_write_config(tmp_path, 'snapshot_root = "/data/snaps"')
assert snapshot_root_path() == Path("/data/snaps")
def test_snapshot_root_default_when_unset(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_SNAPSHOT_ROOT", raising=False)
assert snapshot_root_path() == tmp_path / "snapshots"
def test_version_root_explicit_beats_env(monkeypatch):
monkeypatch.setenv("MIRAGE_VERSION_ROOT", "/env/repos")
assert version_root_path("/explicit/repos") == Path("/explicit/repos")
def test_version_root_default_when_unset(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.delenv("MIRAGE_VERSION_ROOT", raising=False)
assert version_root_path() == tmp_path / "repos"
def test_snapshot_root_env_beats_config(tmp_path, monkeypatch):
monkeypatch.setenv("MIRAGE_HOME", str(tmp_path))
monkeypatch.setenv("MIRAGE_SNAPSHOT_ROOT", "/env/snaps")
_write_config(tmp_path, 'snapshot_root = "/data/snaps"')
assert snapshot_root_path() == Path("/env/snaps")
def test_snapshot_root_explicit_beats_env(monkeypatch):
monkeypatch.setenv("MIRAGE_SNAPSHOT_ROOT", "/env/snaps")
assert snapshot_root_path("/explicit/snaps") == Path("/explicit/snaps")
+137
View File
@@ -0,0 +1,137 @@
# ========= 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 pytest
from httpx import ASGITransport, AsyncClient
from mirage.server import build_app
def _minimal_config() -> dict:
return {
"config": {
"mounts": {
"/": {
"resource": "ram",
"mode": "WRITE"
}
},
},
}
async def _create_workspace(client: AsyncClient) -> str:
r = await client.post("/v1/workspaces", json=_minimal_config())
return r.json()["id"]
@pytest.mark.asyncio
async def test_create_list_delete_session_round_trip():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(f"/v1/workspaces/{wid}/sessions",
json={"session_id": "agent_a"})
assert r.status_code == 201, r.text
assert r.json()["session_id"] == "agent_a"
r = await client.get(f"/v1/workspaces/{wid}/sessions")
ids = {s["session_id"] for s in r.json()}
assert "agent_a" in ids
assert "default" in ids
r = await client.delete(f"/v1/workspaces/{wid}/sessions/agent_a")
assert r.status_code == 200
r = await client.get(f"/v1/workspaces/{wid}/sessions")
ids = {s["session_id"] for s in r.json()}
assert "agent_a" not in ids
@pytest.mark.asyncio
async def test_create_session_without_id_auto_assigns():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(f"/v1/workspaces/{wid}/sessions", json={})
assert r.status_code == 201
sid = r.json()["session_id"]
assert sid.startswith("sess_")
@pytest.mark.asyncio
async def test_create_session_collision_409():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
await client.post(f"/v1/workspaces/{wid}/sessions",
json={"session_id": "dup"})
r = await client.post(f"/v1/workspaces/{wid}/sessions",
json={"session_id": "dup"})
assert r.status_code == 409
@pytest.mark.asyncio
async def test_delete_unknown_session_404():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.delete(f"/v1/workspaces/{wid}/sessions/nonexistent")
assert r.status_code == 404
@pytest.mark.asyncio
async def test_create_session_with_allowed_mounts():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid = await _create_workspace(client)
r = await client.post(
f"/v1/workspaces/{wid}/sessions",
json={
"session_id": "agent_a",
"allowed_mounts": ["/"],
},
)
assert r.status_code == 201, r.text
registry = app.state.registry
sess = registry.get(wid).runner.ws.get_session("agent_a")
assert sess.allowed_mounts is not None
assert "/" in sess.allowed_mounts
@pytest.mark.asyncio
async def test_session_isolated_per_workspace():
app = build_app(idle_grace_seconds=10.0)
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport,
base_url="http://test") as client:
wid_a = await _create_workspace(client)
wid_b = await _create_workspace(client)
await client.post(f"/v1/workspaces/{wid_a}/sessions",
json={"session_id": "only_in_a"})
r = await client.get(f"/v1/workspaces/{wid_b}/sessions")
ids = {s["session_id"] for s in r.json()}
assert "only_in_a" not in ids
+271
View File
@@ -0,0 +1,271 @@
# ========= 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 pytest
from httpx import ASGITransport, AsyncClient
from mirage.server import build_app
def _minimal_config() -> dict:
return {
"config": {
"mounts": {
"/": {
"resource": "ram",
"mode": "WRITE"
}
},
},
}
def _client(tmp_path):
app = build_app(idle_grace_seconds=30.0,
version_root=str(tmp_path / "repos"))
return AsyncClient(transport=ASGITransport(app=app),
base_url="http://test")
async def _create_ws(client) -> str:
r = await client.post("/v1/workspaces", json=_minimal_config())
assert r.status_code == 201, r.text
return r.json()["id"]
async def _write(client, wid: str, command: str) -> None:
r = await client.post(f"/v1/workspaces/{wid}/execute",
json={"command": command})
assert r.status_code == 200, r.text
async def _cat(client, wid: str, path: str) -> str:
r = await client.post(f"/v1/workspaces/{wid}/execute",
json={"command": f"cat {path}"})
assert r.status_code == 200, r.text
return r.json()["stdout"]
@pytest.mark.asyncio
async def test_commit_log_checkout_flow(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo v1 > /notes.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"message": "first"})
assert r.status_code == 200, r.text
v1 = r.json()["version"]
assert r.json()["branch"] == "main"
await _write(client, wid, "echo v2 > /notes.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"message": "second"})
assert r.status_code == 200, r.text
r = await client.get(f"/v1/workspaces/{wid}/versions")
assert r.status_code == 200
log = r.json()
assert [e["message"] for e in log] == ["second", "first"]
assert await _cat(client, wid, "/notes.txt") == "v2\n"
r = await client.post(f"/v1/workspaces/{wid}/checkout",
json={"ref": v1})
assert r.status_code == 200, r.text
assert await _cat(client, wid, "/notes.txt") == "v1\n"
@pytest.mark.asyncio
async def test_diff_endpoint_follows_git(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo one > /a.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"message": "first"})
v1 = r.json()["version"]
await _write(client, wid, "echo two > /a.txt")
await _write(client, wid, "echo new > /b.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"message": "second"})
v2 = r.json()["version"]
r = await client.get(f"/v1/workspaces/{wid}/diff",
params={
"a": v1,
"b": v2
})
assert r.status_code == 200, r.text
assert r.json()["modified"] == ["a.txt"]
assert r.json()["added"] == ["b.txt"]
await _write(client, wid, "echo three > /a.txt")
r = await client.get(f"/v1/workspaces/{wid}/diff", params={"a": v2})
assert r.status_code == 200, r.text
assert r.json()["modified"] == ["a.txt"]
r = await client.get(f"/v1/workspaces/{wid}/diff")
assert r.status_code == 200, r.text
assert r.json()["modified"] == ["a.txt"]
@pytest.mark.asyncio
async def test_diff_bad_ref_404(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo x > /x.txt")
await client.post(f"/v1/workspaces/{wid}/commit", json={})
r = await client.get(f"/v1/workspaces/{wid}/diff",
params={"a": "deadbeef" * 5})
assert r.status_code == 404
@pytest.mark.asyncio
async def test_branch_endpoint_diverges(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo one > /a.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"message": "first"})
v1 = r.json()["version"]
r = await client.post(f"/v1/workspaces/{wid}/branch",
json={"name": "exp"})
assert r.status_code == 201, r.text
assert r.json()["branch"] == "exp"
assert r.json()["version"] == v1
await _write(client, wid, "echo two > /a.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={
"message": "on exp",
"branch": "exp"
})
assert r.status_code == 200, r.text
exp_log = (await client.get(f"/v1/workspaces/{wid}/versions",
params={"branch": "exp"})).json()
main_log = (await client.get(f"/v1/workspaces/{wid}/versions",
params={"branch": "main"})).json()
assert [e["message"] for e in exp_log] == ["on exp", "first"]
assert [e["message"] for e in main_log] == ["first"]
@pytest.mark.asyncio
async def test_branch_duplicate_409(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo x > /x.txt")
await client.post(f"/v1/workspaces/{wid}/commit", json={})
await client.post(f"/v1/workspaces/{wid}/branch", json={"name": "exp"})
r = await client.post(f"/v1/workspaces/{wid}/branch",
json={"name": "exp"})
assert r.status_code == 409
@pytest.mark.asyncio
async def test_branch_from_missing_404(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo x > /x.txt")
await client.post(f"/v1/workspaces/{wid}/commit", json={})
r = await client.post(f"/v1/workspaces/{wid}/branch",
json={
"name": "exp",
"from_branch": "ghost"
})
assert r.status_code == 404
@pytest.mark.asyncio
async def test_commit_unknown_branch_404(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo x > /x.txt")
await client.post(f"/v1/workspaces/{wid}/commit", json={})
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"branch": "ghost"})
assert r.status_code == 404
@pytest.mark.asyncio
async def test_clone_from_version_creates_new_workspace(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo base > /b.txt")
r = await client.post(f"/v1/workspaces/{wid}/commit",
json={"message": "base"})
version = r.json()["version"]
r = await client.post("/v1/workspaces/clone",
json={
"source_id": wid,
"at": version
})
assert r.status_code == 201, r.text
new_id = r.json()["id"]
assert new_id != wid
assert await _cat(client, new_id, "/b.txt") == "base\n"
@pytest.mark.asyncio
async def test_clone_live_workspace(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo live > /l.txt")
r = await client.post("/v1/workspaces/clone", json={"source_id": wid})
assert r.status_code == 201, r.text
new_id = r.json()["id"]
assert new_id != wid
assert await _cat(client, new_id, "/l.txt") == "live\n"
@pytest.mark.asyncio
async def test_log_empty_when_no_commits(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
r = await client.get(f"/v1/workspaces/{wid}/versions")
assert r.status_code == 200
assert r.json() == []
@pytest.mark.asyncio
async def test_commit_unknown_workspace_404(tmp_path):
async with _client(tmp_path) as client:
r = await client.post("/v1/workspaces/nope/commit", json={})
assert r.status_code == 404
@pytest.mark.asyncio
async def test_checkout_bad_ref_404(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
await _write(client, wid, "echo x > /x.txt")
await client.post(f"/v1/workspaces/{wid}/commit", json={})
r = await client.post(f"/v1/workspaces/{wid}/checkout",
json={"ref": "deadbeef" * 5})
assert r.status_code == 404
@pytest.mark.asyncio
async def test_clone_duplicate_id_409(tmp_path):
async with _client(tmp_path) as client:
wid = await _create_ws(client)
r = await client.post("/v1/workspaces/clone",
json={
"source_id": wid,
"id": wid
})
assert r.status_code == 409
@@ -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())
+303
View File
@@ -0,0 +1,303 @@
# ========= 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 pytest
from mirage.resource.ram import RAMResource
from mirage.server.version.api import (branch, checkout, commit, commit_state,
diff_live_vs_ref, read_version,
resolve_ref, snapshot_tree, status,
status_state, version_diff, version_log)
from mirage.server.version.backend import LocalBackend
from mirage.server.version.errors import NoSuchBranchError
from mirage.server.version.state_tree import META_PATH
from mirage.server.version.store import VersionStore
from mirage.types import CacheKey, MountMode, StateKey
from mirage.workspace import Workspace
from mirage.workspace.snapshot import to_state_dict
def _cache_entry(data: bytes) -> dict:
return {
CacheKey.KEY: "k",
CacheKey.DATA: data,
CacheKey.FINGERPRINT: None,
CacheKey.TTL: None,
CacheKey.CACHED_AT: 0.0,
CacheKey.SIZE: len(data),
}
@pytest.mark.asyncio
async def test_snapshot_tree_contains_files_and_meta(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hello > /m/a.txt")
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
tree = await snapshot_tree(store, ws)
contents = await store.read_tree(tree)
assert META_PATH in contents
assert await store.read_blob(contents["m/a.txt"]) == b"hello\n"
@pytest.mark.asyncio
async def test_commit_advances_branch_and_links_parent(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
c1 = await commit(store, ws, branch="main", message="first")
await ws.execute("echo two > /m/a.txt")
c2 = await commit(store, ws, branch="main", message="second")
assert await store.head("main") == c2
assert (await store.read_commit(c2)).parents == [c1]
assert await store.log("main") == [c2, c1]
@pytest.mark.asyncio
async def test_version_log_lists_messages_newest_first(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
await commit(store, ws, message="first")
await ws.execute("echo two > /m/a.txt")
await commit(store, ws, message="second")
log = await version_log(store, "main")
assert [entry["message"] for entry in log] == ["second", "first"]
@pytest.mark.asyncio
async def test_version_diff_reports_changed_files_only(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
c1 = await commit(store, ws, message="first")
await ws.execute("echo two > /m/a.txt")
await ws.execute("echo new > /m/b.txt")
c2 = await commit(store, ws, message="second")
diff = await version_diff(store, c1, c2)
assert diff["modified"] == ["m/a.txt"]
assert diff["added"] == ["m/b.txt"]
assert META_PATH not in diff["modified"]
@pytest.mark.asyncio
async def test_diff_live_vs_ref_reports_changes_against_version(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
c1 = await commit(store, ws, branch="main", message="first")
await ws.execute("echo two > /m/a.txt")
await ws.execute("echo new > /m/b.txt")
by_oid = await diff_live_vs_ref(store, await to_state_dict(ws), c1)
assert by_oid["modified"] == ["m/a.txt"]
assert by_oid["added"] == ["m/b.txt"]
by_branch = await diff_live_vs_ref(store, await to_state_dict(ws), "main")
assert by_branch == by_oid
@pytest.mark.asyncio
async def test_status_reports_uncommitted_changes(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
await commit(store, ws, message="first")
await ws.execute("echo changed > /m/a.txt")
st = await status(store, ws, "main")
assert st["modified"] == ["m/a.txt"]
@pytest.mark.asyncio
async def test_diff_ignores_cache_churn(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
s1 = await to_state_dict(ws)
s1[StateKey.CACHE][CacheKey.ENTRIES] = [_cache_entry(b"AAA")]
c1 = await commit_state(store, s1, message="first")
s2 = await to_state_dict(ws)
s2[StateKey.CACHE][CacheKey.ENTRIES] = [_cache_entry(b"BBB")]
c2 = await commit_state(store, s2, message="second")
assert await version_diff(store, c1, c2) == {
"added": [],
"modified": [],
"deleted": [],
}
@pytest.mark.asyncio
async def test_status_state_reports_uncommitted_changes(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
await commit(store, ws, message="first")
await ws.execute("echo changed > /m/a.txt")
st = await status_state(store, await to_state_dict(ws), "main")
assert st["modified"] == ["m/a.txt"]
@pytest.mark.asyncio
async def test_status_state_no_commit_yet_lists_all_as_added(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
st = await status_state(store, await to_state_dict(ws), "main")
assert st == {"added": ["m/a.txt"], "modified": [], "deleted": []}
@pytest.mark.asyncio
async def test_status_ignores_cache_churn(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
s1 = await to_state_dict(ws)
s1[StateKey.CACHE][CacheKey.ENTRIES] = [_cache_entry(b"AAA")]
await commit_state(store, s1, message="first")
live = await to_state_dict(ws)
live[StateKey.CACHE][CacheKey.ENTRIES] = [_cache_entry(b"BBB")]
st = await status_state(store, live, "main")
assert st == {"added": [], "modified": [], "deleted": []}
@pytest.mark.asyncio
async def test_resolve_ref_branch_and_oid(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
c1 = await commit(store, ws, branch="main", message="first")
assert await resolve_ref(store, "main") == c1
assert await resolve_ref(store, c1) == c1
assert await resolve_ref(store, c1.decode()) == c1
@pytest.mark.asyncio
async def test_commit_state_creates_version_from_state(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo hi > /m/a.txt")
version = await commit_state(store,
await to_state_dict(ws),
branch="main",
message="from state")
entries, _ = await read_version(store, version)
assert entries["m/a.txt"] == b"hi\n"
@pytest.mark.asyncio
async def test_commit_to_unknown_branch_errors(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
await commit(store, ws, branch="main", message="first")
with pytest.raises(NoSuchBranchError):
await commit(store, ws, branch="exp", message="oops")
@pytest.mark.asyncio
async def test_commit_diverges_after_branch_created(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
main_head = await commit(store, ws, branch="main", message="first")
await branch(store, "exp", from_branch="main")
await ws.execute("echo two > /m/a.txt")
exp_head = await commit(store, ws, branch="exp", message="on exp")
assert (await store.read_commit(exp_head)).parents == [main_head]
assert await store.head("main") == main_head
@pytest.mark.asyncio
async def test_branch_creates_line_at_current(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo one > /m/a.txt")
c1 = await commit(store, ws, branch="main", message="first")
await branch(store, "exp", from_branch="main")
assert await store.head("exp") == c1
assert "exp" in await store.branches()
@pytest.mark.asyncio
async def test_read_version_reads_back_files_and_meta(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo hello > /m/a.txt")
version = await commit(store, ws, message="first")
entries, meta = await read_version(store, version)
assert entries["m/a.txt"] == b"hello\n"
assert META_PATH not in entries
assert "/m/" in [m["prefix"] for m in meta["mounts"]]
@pytest.mark.asyncio
async def test_checkout_rebuilds_content_in_place(tmp_path):
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
await ws.execute("echo original > /m/a.txt")
await commit(store, ws, branch="main", message="first")
await ws.execute("echo mutated > /m/a.txt")
await ws.execute("echo extra > /m/b.txt")
await checkout(store, ws, "main")
result = await ws.execute("cat /m/a.txt")
assert (await result.stdout_str()) == "original\n"
assert await status(store, ws, "main") == {
"added": [],
"modified": [],
"deleted": [],
}
@@ -0,0 +1,45 @@
# ========= 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. =========
from pathlib import Path
from dulwich.objects import Blob
from mirage.server.version.backend import LocalBackend
def test_open_repo_creates_bare_repo_under_workspace_id(tmp_path: Path):
backend = LocalBackend(tmp_path)
backend.open_repo("ws1")
assert (tmp_path / "ws1" / "objects").is_dir()
assert (tmp_path / "ws1" / "HEAD").exists()
def test_open_repo_reuses_existing(tmp_path: Path):
backend = LocalBackend(tmp_path)
repo1 = backend.open_repo("ws1")
blob = Blob.from_string(b"x")
repo1.object_store.add_object(blob)
repo2 = backend.open_repo("ws1")
assert blob.id in repo2.object_store
def test_distinct_workspace_ids_are_isolated(tmp_path: Path):
backend = LocalBackend(tmp_path)
repo_a = backend.open_repo("a")
repo_b = backend.open_repo("b")
blob = Blob.from_string(b"only-in-a")
repo_a.object_store.add_object(blob)
assert blob.id in repo_a.object_store
assert blob.id not in repo_b.object_store
@@ -0,0 +1,204 @@
# ========= 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 io
import pytest
from mirage.resource.ram import RAMResource
from mirage.server.version.state_tree import (blob_to_meta, meta_to_blob,
to_state, to_tree_inputs,
tree_inputs_from_state)
from mirage.types import (CacheKey, FingerprintKey, MountKey, MountMode,
SessionKey, StateKey)
from mirage.workspace import Workspace
from mirage.workspace.snapshot.manifest import split_manifest_and_blobs
from mirage.workspace.snapshot.state import to_state_dict
from mirage.workspace.snapshot.tar_io import read_tar, write_tar
def _mount_files(state: dict, prefix: str) -> dict:
for mount in state["mounts"]:
if mount["prefix"] == prefix:
return mount["resource_state"]["files"]
raise KeyError(prefix)
@pytest.mark.asyncio
async def test_to_tree_inputs_ram_files():
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hello > /m/a.txt")
await ws.execute("mkdir -p /m/sub && echo world > /m/sub/b.txt")
entries, meta = await to_tree_inputs(ws)
assert entries["m/a.txt"] == b"hello\n"
assert entries["m/sub/b.txt"] == b"world\n"
prefixes = [m[MountKey.PREFIX] for m in meta["mounts"]]
assert "/m/" in prefixes
@pytest.mark.asyncio
async def test_to_state_round_trips_files():
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hello > /m/a.txt")
await ws.execute("mkdir -p /m/sub && echo world > /m/sub/b.txt")
original_files = _mount_files(await to_state_dict(ws), "/m/")
entries, meta = await to_tree_inputs(ws)
state = to_state(entries, meta)
assert _mount_files(state, "/m/") == original_files
@pytest.mark.asyncio
async def test_tree_inputs_from_state_matches_ws_path():
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hi > /m/a.txt")
entries_ws, _ = await to_tree_inputs(ws)
entries_state, _ = tree_inputs_from_state(await to_state_dict(ws))
assert entries_ws == entries_state
assert entries_state["m/a.txt"] == b"hi\n"
@pytest.mark.asyncio
async def test_to_state_is_tar_loadable():
ws = Workspace({"/m": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hello > /m/a.txt")
entries, meta = await to_tree_inputs(ws)
state = to_state(entries, meta)
manifest, blobs = split_manifest_and_blobs(state)
buf = io.BytesIO()
write_tar(buf, manifest, blobs)
buf.seek(0)
restored = read_tar(buf)
assert _mount_files(restored, "/m/")["/a.txt"] == b"hello\n"
@pytest.mark.asyncio
async def test_cache_fingerprints_sessions_round_trip():
ws = Workspace({"/": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hi > /a.txt")
state = await to_state_dict(ws)
state[StateKey.CACHE][CacheKey.ENTRIES] = [{
CacheKey.KEY: "/a.txt",
CacheKey.DATA: b"cached-bytes",
CacheKey.FINGERPRINT: "etag-1",
CacheKey.TTL: None,
CacheKey.CACHED_AT: 123.0,
CacheKey.SIZE: 12,
}]
state[StateKey.FINGERPRINTS] = [{
FingerprintKey.PATH: "/a.txt",
FingerprintKey.MOUNT_PREFIX: "/",
FingerprintKey.FINGERPRINT: "etag-1",
FingerprintKey.REVISION: "v1",
}]
state[StateKey.SESSIONS] = [{
SessionKey.SESSION_ID: "agent_a",
SessionKey.CWD: "/sub",
SessionKey.ENV: {
"FOO": "bar"
},
}]
entries, meta = tree_inputs_from_state(state)
meta = blob_to_meta(meta_to_blob(meta))
restored = to_state(entries, meta)
cache_entries = restored[StateKey.CACHE][CacheKey.ENTRIES]
assert len(cache_entries) == 1
assert cache_entries[0][CacheKey.DATA] == b"cached-bytes"
assert cache_entries[0][CacheKey.KEY] == "/a.txt"
assert restored[StateKey.FINGERPRINTS][0][FingerprintKey.REVISION] == "v1"
assert restored[StateKey.SESSIONS][0][SessionKey.CWD] == "/sub"
assert restored[StateKey.SESSIONS][0][SessionKey.ENV] == {"FOO": "bar"}
files = _mount_files(restored, "/")
assert files["/a.txt"] == b"hi\n"
assert all(".mirage-cache" not in k for k in files)
@pytest.mark.asyncio
async def test_cache_and_pins_survive_tar():
ws = Workspace({"/": (RAMResource(), MountMode.WRITE)},
mode=MountMode.WRITE)
await ws.execute("echo hi > /a.txt")
state = await to_state_dict(ws)
state[StateKey.CACHE][CacheKey.ENTRIES] = [{
CacheKey.KEY: "/a.txt",
CacheKey.DATA: b"cached-bytes",
CacheKey.FINGERPRINT: "etag-1",
CacheKey.TTL: None,
CacheKey.CACHED_AT: 1.0,
CacheKey.SIZE: 12,
}]
state[StateKey.FINGERPRINTS] = [{
FingerprintKey.PATH: "/a.txt",
FingerprintKey.MOUNT_PREFIX: "/",
FingerprintKey.REVISION: "v1",
}]
state[StateKey.SESSIONS] = [{
SessionKey.SESSION_ID: "agent_a",
SessionKey.CWD: "/sub",
SessionKey.ENV: {
"FOO": "bar"
},
}]
entries, meta = tree_inputs_from_state(state)
meta = blob_to_meta(meta_to_blob(meta))
rebuilt = to_state(entries, meta)
manifest, blobs = split_manifest_and_blobs(rebuilt)
buf = io.BytesIO()
write_tar(buf, manifest, blobs)
buf.seek(0)
restored = read_tar(buf)
ce = restored[StateKey.CACHE][CacheKey.ENTRIES]
assert ce[0][CacheKey.DATA] == b"cached-bytes"
assert restored[StateKey.FINGERPRINTS][0][FingerprintKey.REVISION] == "v1"
sessions = {
s[SessionKey.SESSION_ID]: s
for s in restored[StateKey.SESSIONS]
}
assert sessions["agent_a"][SessionKey.CWD] == "/sub"
def test_meta_blob_round_trip():
meta = {
"mounts": [],
"pins": {
"/s3/a.txt": {
"rev": "v123",
"fp": "etag-abc"
}
},
}
parsed = blob_to_meta(meta_to_blob(meta))
assert parsed["pins"]["/s3/a.txt"] == {"rev": "v123", "fp": "etag-abc"}
+158
View File
@@ -0,0 +1,158 @@
# ========= 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. =========
from pathlib import Path
import pytest
from mirage.server.version.backend import LocalBackend
from mirage.server.version.errors import HeadMovedError
from mirage.server.version.store import VersionStore
@pytest.mark.asyncio
async def test_open_creates_bare_repo(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
assert store is not None
assert (tmp_path / "ws" / "objects").is_dir()
assert (tmp_path / "ws" / "HEAD").is_file()
@pytest.mark.asyncio
async def test_open_reuses_existing_repo(tmp_path: Path):
backend = LocalBackend(tmp_path)
await VersionStore.open(backend, "ws")
reopened = await VersionStore.open(backend, "ws")
assert reopened is not None
@pytest.mark.asyncio
async def test_blob_roundtrip(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
oid = await store.write_blob(b"hello")
assert await store.read_blob(oid) == b"hello"
@pytest.mark.asyncio
async def test_identical_blobs_dedup(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
first = await store.write_blob(b"same-bytes")
second = await store.write_blob(b"same-bytes")
assert first == second
@pytest.mark.asyncio
async def test_tree_roundtrip(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
a = await store.write_blob(b"aaa")
b = await store.write_blob(b"bbb")
tree = await store.write_tree({"a.txt": a, "dir/b.txt": b})
assert await store.read_tree(tree) == {"a.txt": a, "dir/b.txt": b}
@pytest.mark.asyncio
async def test_tree_nested_paths(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
x = await store.write_blob(b"x")
y = await store.write_blob(b"y")
z = await store.write_blob(b"z")
tree = await store.write_tree({
"top.txt": x,
"d/one.txt": y,
"d/sub/two.txt": z,
})
assert await store.read_tree(tree) == {
"top.txt": x,
"d/one.txt": y,
"d/sub/two.txt": z,
}
@pytest.mark.asyncio
async def test_commit_advances_branch(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
t1 = await store.write_tree({"a.txt": await store.write_blob(b"v1")})
c1 = await store.commit(t1, parents=[], branch="main", message="first")
assert await store.head("main") == c1
t2 = await store.write_tree({"a.txt": await store.write_blob(b"v2")})
c2 = await store.commit(t2, parents=[c1], branch="main", message="second")
assert await store.head("main") == c2
commit = await store.read_commit(c2)
assert commit.parents == [c1]
assert commit.message == b"second"
@pytest.mark.asyncio
async def test_commit_rejects_stale_head(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
c1 = await store.commit(await store.write_tree(
{"a.txt": await store.write_blob(b"v1")}),
parents=[],
branch="main",
message="first")
c2 = await store.commit(await store.write_tree(
{"a.txt": await store.write_blob(b"v2")}),
parents=[c1],
branch="main",
message="second")
assert await store.head("main") == c2
with pytest.raises(HeadMovedError):
await store.commit(await store.write_tree(
{"a.txt": await store.write_blob(b"v3")}),
parents=[c1],
branch="main",
message="stale")
assert await store.head("main") == c2
@pytest.mark.asyncio
async def test_branches_and_log(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
t1 = await store.write_tree({"a.txt": await store.write_blob(b"v1")})
c1 = await store.commit(t1, parents=[], branch="main", message="first")
t2 = await store.write_tree({"a.txt": await store.write_blob(b"v2")})
c2 = await store.commit(t2, parents=[c1], branch="main", message="second")
assert await store.branches() == ["main"]
assert await store.log("main") == [c2, c1]
@pytest.mark.asyncio
async def test_tree_diff(tmp_path: Path):
store = await VersionStore.open(LocalBackend(tmp_path), "ws")
keep = await store.write_blob(b"keep")
before = await store.write_blob(b"before")
after = await store.write_blob(b"after")
gone = await store.write_blob(b"gone")
new = await store.write_blob(b"new")
tree_a = await store.write_tree({
"keep.txt": keep,
"change.txt": before,
"gone.txt": gone,
})
tree_b = await store.write_tree({
"keep.txt": keep,
"change.txt": after,
"new.txt": new,
})
assert await store.diff(tree_a, tree_b) == {
"added": ["new.txt"],
"modified": ["change.txt"],
"deleted": ["gone.txt"],
}