8f10353f0c
CI / lint (push) Has been cancelled
CI / js-syntax (push) Successful in 10m24s
CI / deps-audit (push) Has been cancelled
CI / test (push) Failing after 24m55s
CI / sast-bandit (push) Failing after 11m13s
CI / trivy (push) Failing after 9m32s
Docker Publish / build-and-push (push) Failing after 34m3s
122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
from __future__ import annotations
|
|
|
|
import time
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from app.core.models import Job
|
|
from app.core.registry import _jobs
|
|
from app.pipeline.collect import sweep_old_jobs
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_registry():
|
|
_jobs.clear()
|
|
yield
|
|
_jobs.clear()
|
|
|
|
|
|
def _mkdir(jobs_dir: Path, name: str) -> Path:
|
|
d = jobs_dir / name
|
|
d.mkdir(parents=True)
|
|
(d / "marker").write_bytes(b"x")
|
|
return d
|
|
|
|
|
|
def test_skip_active_job_even_if_old(tmp_path: Path):
|
|
"""An active (non-terminal) job's directory must never be swept,
|
|
even if its created_at predates the TTL cutoff."""
|
|
d = _mkdir(tmp_path, "abcdefabcdef")
|
|
job = Job(id="abcdefabcdef")
|
|
job.status = "separating"
|
|
job.created_at = time.time() - 999_999 # ancient
|
|
_jobs[job.id] = job
|
|
|
|
with patch("app.pipeline.collect.JOB_TTL_SECONDS", 60):
|
|
sweep_old_jobs(tmp_path)
|
|
|
|
assert d.is_dir()
|
|
assert job.id in _jobs
|
|
|
|
|
|
def test_sweeps_terminal_old_job(tmp_path: Path):
|
|
d = _mkdir(tmp_path, "abcdefabcdee")
|
|
job = Job(id="abcdefabcdee")
|
|
job.status = "done"
|
|
job.created_at = time.time() - 999_999
|
|
_jobs[job.id] = job
|
|
|
|
with patch("app.pipeline.collect.JOB_TTL_SECONDS", 60):
|
|
sweep_old_jobs(tmp_path)
|
|
|
|
assert not d.exists()
|
|
assert job.id not in _jobs
|
|
|
|
|
|
def test_sweep_disabled_under_desktop(monkeypatch):
|
|
"""The desktop shell (STEMDECK_DESKTOP=1) opts out of the TTL sweep so a
|
|
user's curated library isn't purged; the server/Docker default keeps it."""
|
|
from app.main import _sweep_disabled
|
|
|
|
monkeypatch.delenv("STEMDECK_PERSIST_LIBRARY", raising=False)
|
|
monkeypatch.setenv("STEMDECK_DESKTOP", "1")
|
|
assert _sweep_disabled() is True
|
|
monkeypatch.delenv("STEMDECK_DESKTOP", raising=False)
|
|
assert _sweep_disabled() is False
|
|
|
|
|
|
def test_sweep_disabled_under_persistent_library(monkeypatch):
|
|
"""A self-hosted server (run.sh) opts into a persistent library
|
|
(STEMDECK_PERSIST_LIBRARY=1) so its processed tracks aren't purged."""
|
|
from app.main import _sweep_disabled
|
|
|
|
monkeypatch.delenv("STEMDECK_DESKTOP", raising=False)
|
|
monkeypatch.setenv("STEMDECK_PERSIST_LIBRARY", "1")
|
|
assert _sweep_disabled() is True
|
|
monkeypatch.delenv("STEMDECK_PERSIST_LIBRARY", raising=False)
|
|
assert _sweep_disabled() is False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_sweep_loop_returns_immediately_under_desktop(monkeypatch):
|
|
"""In desktop mode the loop returns at once instead of entering the hourly
|
|
cycle (wait_for would time out if it looped)."""
|
|
import asyncio
|
|
|
|
from app.main import _sweep_loop
|
|
|
|
monkeypatch.setenv("STEMDECK_DESKTOP", "1")
|
|
await asyncio.wait_for(_sweep_loop(), timeout=2)
|
|
|
|
|
|
def test_keeps_recent_terminal_job(tmp_path: Path):
|
|
d = _mkdir(tmp_path, "abcdefabcded")
|
|
job = Job(id="abcdefabcded")
|
|
job.status = "done"
|
|
job.created_at = time.time() # fresh
|
|
_jobs[job.id] = job
|
|
|
|
with patch("app.pipeline.collect.JOB_TTL_SECONDS", 60):
|
|
sweep_old_jobs(tmp_path)
|
|
|
|
assert d.is_dir()
|
|
assert job.id in _jobs
|
|
|
|
|
|
def test_orphan_dir_falls_back_to_mtime(tmp_path: Path):
|
|
"""Directories with no registry entry (e.g. left over from a prior
|
|
server run) still get swept by mtime."""
|
|
d = _mkdir(tmp_path, "abcdefabcdec")
|
|
# Backdate the directory.
|
|
old = time.time() - 999_999
|
|
import os
|
|
|
|
os.utime(d, (old, old))
|
|
|
|
with patch("app.pipeline.collect.JOB_TTL_SECONDS", 60):
|
|
sweep_old_jobs(tmp_path)
|
|
|
|
assert not d.exists()
|