Files
wehub-resource-sync bcbd1bdb22
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

295 lines
10 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import asyncio
import os
import uuid
import boto3
import pytest
from mirage.core.s3.write import write_bytes
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import DriftPolicy, MountMode
from mirage.workspace import Workspace
from mirage.workspace.snapshot import ContentDriftError
LIVE_BUCKET = os.environ.get("MIRAGE_LIVE_S3_BUCKET") or os.environ.get(
"AWS_S3_BUCKET")
LIVE_REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
LIVE_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID")
LIVE_SECRET = os.environ.get("AWS_SECRET_ACCESS_KEY")
_LIVE_READY = bool(LIVE_BUCKET and LIVE_KEY_ID and LIVE_SECRET)
pytestmark = pytest.mark.skipif(
not _LIVE_READY,
reason=("set MIRAGE_LIVE_S3_BUCKET (or AWS_S3_BUCKET) plus "
"AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY to run live S3 tests"),
)
def _config() -> S3Config:
return S3Config(
bucket=LIVE_BUCKET,
region=LIVE_REGION,
aws_access_key_id=LIVE_KEY_ID,
aws_secret_access_key=LIVE_SECRET,
)
def _boto_client():
return boto3.client(
"s3",
region_name=LIVE_REGION,
aws_access_key_id=LIVE_KEY_ID,
aws_secret_access_key=LIVE_SECRET,
)
def _versioning_enabled() -> bool:
try:
resp = _boto_client().get_bucket_versioning(Bucket=LIVE_BUCKET)
except Exception:
return False
return resp.get("Status") == "Enabled"
def _probe_key() -> str:
return f"mirage-drift-probe-{uuid.uuid4().hex[:8]}.txt"
def _mount(prefix: str = "/s3/") -> dict:
return {prefix: (S3Resource(_config()), MountMode.WRITE)}
def _override(prefix: str = "/s3/") -> dict:
return {prefix: S3Resource(_config())}
def _cleanup_key(key: str) -> None:
client = _boto_client()
try:
if _versioning_enabled():
versions = client.list_object_versions(Bucket=LIVE_BUCKET,
Prefix=key).get(
"Versions", [])
for v in versions:
if v.get("Key") == key:
client.delete_object(Bucket=LIVE_BUCKET,
Key=key,
VersionId=v["VersionId"])
markers = client.list_object_versions(Bucket=LIVE_BUCKET,
Prefix=key).get(
"DeleteMarkers", [])
for m in markers:
if m.get("Key") == key:
client.delete_object(Bucket=LIVE_BUCKET,
Key=key,
VersionId=m["VersionId"])
else:
client.delete_object(Bucket=LIVE_BUCKET, Key=key)
except Exception:
pass
def test_live_strict_raises_on_etag_drift(tmp_path):
"""Snapshot with a recorded read, mutate the live object via boto so
its ETag changes, load with STRICT: the eager drift check must raise
ContentDriftError before any user-visible read.
Requires versioning OFF: on a versioned bucket, stat() captures a
revision and STRICT installs a pin (which intentionally bypasses
drift detection). That path is covered by the version-pin test.
"""
if _versioning_enabled():
pytest.skip(f"bucket {LIVE_BUCKET} is versioned; STRICT will pin "
"instead of drift-checking. See the version-pin test "
"for the versioned path.")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v1 bytes\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
result = asyncio.run(src.execute(f"cat {probe}"))
assert b"v1 bytes" in result.stdout
snap = tmp_path / "drift.tar"
asyncio.run(src.snapshot(snap))
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v2 mutated\n")
dst = Workspace.load(snap, resources=_override())
with pytest.raises(ContentDriftError) as exc_info:
asyncio.run(dst.execute(f"cat {probe}"))
assert exc_info.value.path == probe
assert (exc_info.value.snapshot_fingerprint
!= exc_info.value.live_fingerprint)
finally:
_cleanup_key(key)
def test_live_no_drift_passes(tmp_path):
"""Control: snapshot + load with identical bytes on the bucket must
succeed. Guards against false-positive drift on the live ETag path.
"""
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"stable\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
asyncio.run(src.execute(f"cat {probe}"))
snap = tmp_path / "stable.tar"
asyncio.run(src.snapshot(snap))
dst = Workspace.load(snap, resources=_override())
result = asyncio.run(dst.execute(f"cat {probe}"))
assert b"stable" in result.stdout
finally:
_cleanup_key(key)
def test_live_pin_records_agent_version_not_snapshot_time_version(tmp_path):
"""Race-fix regression test.
Agent reads V1 at T1. Between T1 and snapshot at T3, an external
actor mutates the object to V2. Old design (live stat at snapshot
time) would capture V2's VersionId and pin replay to V2, serving
bytes the agent never saw. New design records VersionId at READ
time (from the GET response headers), so the snapshot pins to V1
and serves V1 bytes on cache miss.
Skipped on non-versioned buckets (no revision = no pin).
"""
if not _versioning_enabled():
pytest.skip("requires versioning")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v1\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
result = asyncio.run(src.execute(f"cat {probe}"))
assert b"v1" in result.stdout
# Race: upstream changes BEFORE snapshot fires
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"v2-racy\n")
snap = tmp_path / "racy.tar"
asyncio.run(src.snapshot(snap))
dst = Workspace.load(snap, resources=_override())
dst._cache.evict_paths([probe])
result = asyncio.run(dst.execute(f"cat {probe}"))
assert result.stdout == b"v1\n", (
f"snapshot pinned the wrong VersionId; served {result.stdout!r} "
"instead of the V1 the agent actually saw")
finally:
_cleanup_key(key)
def test_live_version_pin_serves_original_on_versioned_bucket(tmp_path):
"""End-to-end pin: with bucket versioning enabled, snapshot captures
the live VersionId. After the bucket head moves on, STRICT load
serves the original recorded bytes via the pin, not the current
head, and does not raise.
Skipped (xfail) when the bucket is not versioned; the pin path
requires real S3 versioning to exercise.
"""
if not _versioning_enabled():
pytest.skip(
f"bucket {LIVE_BUCKET} does not have versioning enabled; "
"run `aws s3api put-bucket-versioning --bucket "
f"{LIVE_BUCKET} --versioning-configuration Status=Enabled` "
"to exercise the pin path")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"original\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
result = asyncio.run(src.execute(f"cat {probe}"))
assert b"original" in result.stdout
snap = tmp_path / "pin.tar"
asyncio.run(src.snapshot(snap))
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"mutated\n")
dst = Workspace.load(snap, resources=_override())
dst._cache.evict_paths([probe])
result = asyncio.run(dst.execute(f"cat {probe}"))
assert result.stdout == b"original\n", (
"pinned read should serve the recorded version, "
f"got {result.stdout!r}")
finally:
_cleanup_key(key)
def test_live_off_policy_serves_current(tmp_path):
"""drift_policy=OFF must skip both the drift check and any pin
installation, evict the snapshot cache, and serve the live bytes.
Smokes the regression where the pin was installed even when the
caller explicitly asked for live state.
"""
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"original\n")
try:
src = Workspace(_mount(), mode=MountMode.WRITE)
asyncio.run(src.execute(f"cat {probe}"))
snap = tmp_path / "off.tar"
asyncio.run(src.snapshot(snap))
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"mutated\n")
dst = Workspace.load(snap,
resources=_override(),
drift_policy=DriftPolicy.OFF)
assert dst.revisions == {}
result = asyncio.run(dst.execute(f"cat {probe}"))
assert b"mutated" in result.stdout
finally:
_cleanup_key(key)
def test_live_stat_populates_revision_when_versioned(tmp_path):
"""Smoke: on a versioned bucket, S3Resource.stat must populate
FileStat.revision so capture_fingerprints records it. Tightens the
contract that downstream pin tests rely on.
"""
if not _versioning_enabled():
pytest.skip("bucket not versioned")
key = _probe_key()
probe = f"/s3/{key}"
client = _boto_client()
client.put_object(Bucket=LIVE_BUCKET, Key=key, Body=b"x\n")
try:
resource = S3Resource(_config())
asyncio.run(write_bytes(resource.accessor, probe, b"x\n"))
stat = asyncio.run(resource._ops["stat"](resource.accessor, probe))
assert stat.fingerprint is not None
assert stat.revision is not None, (
"versioned bucket head should carry a VersionId")
finally:
_cleanup_key(key)