Files
hermes-agent/tests/agent/test_generated_media_durable.py
kshitijk4poor 266499524e fix(gateway): deliver and bound-sync generated media in cache/generated
The salvage moved generated images/videos to cache/generated/<kind>/ so the
24h sweep stops deleting the only copy (#126445), but two consumers still
only knew the old swept dirs:

- Strict media delivery (HERMES_MEDIA_DELIVERY_STRICT=1) allowlists cache
  roots via _MEDIA_DELIVERY_CACHE_SUBDIRS; without "generated" a day-old
  generated image was rejected once the recency window lapsed, i.e. the
  move broke re-sending artifacts on public gateways.
- iter_cache_files (Modal/remote resync) walked the new never-swept tree in
  full, so every sync re-uploaded the whole generation history. Only files
  within the sweep window (MEDIA_CACHE_MAX_AGE_HOURS) are synced now.

GENERATED_SUBDIR and MEDIA_CACHE_MAX_AGE_HOURS live in agent/provider_media
as the single source for the image/video writers, the gateway allowlist and
cleanup default, and credential_files. Also trims duplicated comments and
fixes the pet orchestrate comments (nothing prunes cache/generated/images).
Tests use direct function refs instead of importlib strings.
2026-10-02 01:18:03 +05:30

78 lines
3.1 KiB
Python

"""Generated image/video output must survive the gateway media sweep (#126445).
``save_b64_image`` / ``save_*_video`` write the ONLY copy of base64-returning
provider output. The hourly gateway housekeeping deletes 24h-old files from
``cache/images`` / ``cache/videos`` (transient inbound media), so generated
deliverables live in ``cache/generated/<kind>/`` instead — otherwise the
transcript and the Artifacts panel point at deleted files a day later, with no
way to recover the bytes.
Living outside the swept dirs means ``cache/generated`` needs its own
credential-files mount/sync entry: Docker/SSH/Modal backends only reach paths
that map through ``tools.credential_files``, and an unmapped path also drops
``agent_visible_image`` from the provider result.
RED on main: every test here fails — the writers target the swept caches and
``cache/generated`` is not mounted at all.
"""
from __future__ import annotations
import base64
import os
import time
from pathlib import Path
import pytest
from agent.image_gen_provider import save_b64_image
from agent.video_gen_provider import save_b64_video
from gateway.platforms import base
PNG_1PX = bytes.fromhex(
"89504e470d0a1a0a0000000d49484452000000010000000108020000009077"
"53de00000010494441547801635c0e000000feff03000006000557bfabd400"
"00000049454e44ae426082"
)
def _backdate_hours(path, hours: float = 25.0) -> None:
old = time.time() - hours * 3600
os.utime(path, (old, old))
@pytest.mark.parametrize(
"save, sweeper",
[(save_b64_image, base.cleanup_image_cache), (save_b64_video, base.cleanup_video_cache)],
ids=["image", "video"],
)
def test_old_generated_media_is_not_swept(tmp_path, monkeypatch, save, sweeper):
# Profile home so the check-time per-profile cache roots (built from
# _MEDIA_DELIVERY_CACHE_SUBDIRS) cover it; the import-time roots do not.
home = tmp_path / "profiles" / "p"
home.mkdir(parents=True)
monkeypatch.setattr(base, "_HERMES_ROOT", tmp_path)
monkeypatch.setenv("HERMES_HOME", str(home))
path = save(base64.b64encode(PNG_1PX).decode(), prefix="red_test")
_backdate_hours(path)
removed = sweeper(max_age_hours=24)
assert path.exists(), f"25h-old generated media was swept ({removed} removed): {path}"
# Still deliverable on a strict gateway with recency trust off: only the
# cache allowlist can vouch for a day-old file.
monkeypatch.setenv("HERMES_MEDIA_DELIVERY_STRICT", "1")
monkeypatch.setenv("HERMES_MEDIA_TRUST_RECENT_FILES", "0")
assert base.validate_media_delivery_path(str(path)) == str(path.resolve())
def test_generated_dir_is_mounted_from_the_writers_location(tmp_path, monkeypatch):
"""Out-of-sweep is only half the job: remote backends must still see the file."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
from tools.credential_files import get_cache_directory_mounts
path = save_b64_image(base64.b64encode(PNG_1PX).decode(), prefix="red_test")
mounts = {m["container_path"]: m["host_path"] for m in get_cache_directory_mounts()}
assert "/root/.hermes/cache/generated" in mounts, sorted(mounts)
assert path.is_relative_to(Path(mounts["/root/.hermes/cache/generated"]).resolve())