636 lines
21 KiB
Python
636 lines
21 KiB
Python
"""Tests for the storage module - FileStorage and ConcurrentStorage backends.
|
|
|
|
DEPRECATED: FileStorage and ConcurrentStorage are deprecated.
|
|
New sessions use unified storage at sessions/{session_id}/state.json.
|
|
These tests are kept for backward compatibility verification only.
|
|
"""
|
|
|
|
import json
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from framework.schemas.run import Run, RunMetrics, RunStatus
|
|
from framework.storage.backend import FileStorage
|
|
from framework.storage.concurrent import CacheEntry, ConcurrentStorage
|
|
|
|
# === HELPER FUNCTIONS ===
|
|
|
|
|
|
def create_test_run(
|
|
run_id: str = "test_run_1",
|
|
goal_id: str = "test_goal",
|
|
status: RunStatus = RunStatus.COMPLETED,
|
|
nodes_executed: list[str] | None = None,
|
|
) -> Run:
|
|
"""Create a test Run object with minimal required fields."""
|
|
metrics = RunMetrics(
|
|
total_decisions=1,
|
|
successful_decisions=1,
|
|
failed_decisions=0,
|
|
nodes_executed=nodes_executed or ["node_1"],
|
|
)
|
|
return Run(
|
|
id=run_id,
|
|
goal_id=goal_id,
|
|
status=status,
|
|
metrics=metrics,
|
|
narrative="Test run completed.",
|
|
)
|
|
|
|
|
|
# === FILESTORAGE TESTS ===
|
|
|
|
|
|
@pytest.mark.skip(reason="FileStorage is deprecated - use unified session storage")
|
|
class TestFileStorageBasics:
|
|
"""Test basic FileStorage operations."""
|
|
|
|
def test_init_creates_directories(self, tmp_path: Path):
|
|
"""FileStorage should create the directory structure on init."""
|
|
FileStorage(tmp_path)
|
|
|
|
assert (tmp_path / "runs").exists()
|
|
assert (tmp_path / "summaries").exists()
|
|
assert (tmp_path / "indexes" / "by_goal").exists()
|
|
assert (tmp_path / "indexes" / "by_status").exists()
|
|
assert (tmp_path / "indexes" / "by_node").exists()
|
|
|
|
def test_init_with_string_path(self, tmp_path: Path):
|
|
"""FileStorage should accept string paths."""
|
|
storage = FileStorage(str(tmp_path))
|
|
assert storage.base_path == tmp_path
|
|
|
|
|
|
@pytest.mark.skip(reason="FileStorage is deprecated - use unified session storage")
|
|
class TestFileStorageRunOperations:
|
|
"""Test FileStorage run CRUD operations."""
|
|
|
|
def test_save_and_load_run(self, tmp_path: Path):
|
|
"""Test saving and loading a run."""
|
|
storage = FileStorage(tmp_path)
|
|
run = create_test_run()
|
|
|
|
storage.save_run(run)
|
|
loaded = storage.load_run(run.id)
|
|
|
|
assert loaded is not None
|
|
assert loaded.id == run.id
|
|
assert loaded.goal_id == run.goal_id
|
|
assert loaded.status == run.status
|
|
|
|
def test_load_nonexistent_run_returns_none(self, tmp_path: Path):
|
|
"""Loading a nonexistent run should return None."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
result = storage.load_run("nonexistent_id")
|
|
assert result is None
|
|
|
|
def test_save_creates_json_file(self, tmp_path: Path):
|
|
"""Saving a run should create a JSON file."""
|
|
storage = FileStorage(tmp_path)
|
|
run = create_test_run(run_id="my_run")
|
|
|
|
storage.save_run(run)
|
|
|
|
run_file = tmp_path / "runs" / "my_run.json"
|
|
assert run_file.exists()
|
|
|
|
# Verify it's valid JSON
|
|
with open(run_file, encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
assert data["id"] == "my_run"
|
|
|
|
def test_save_creates_summary(self, tmp_path: Path):
|
|
"""Saving a run should also create a summary file."""
|
|
storage = FileStorage(tmp_path)
|
|
run = create_test_run(run_id="my_run")
|
|
|
|
storage.save_run(run)
|
|
|
|
summary_file = tmp_path / "summaries" / "my_run.json"
|
|
assert summary_file.exists()
|
|
|
|
def test_load_summary(self, tmp_path: Path):
|
|
"""Test loading a run summary."""
|
|
storage = FileStorage(tmp_path)
|
|
run = create_test_run()
|
|
|
|
storage.save_run(run)
|
|
summary = storage.load_summary(run.id)
|
|
|
|
assert summary is not None
|
|
assert summary.run_id == run.id
|
|
assert summary.goal_id == run.goal_id
|
|
assert summary.status == run.status
|
|
|
|
def test_load_summary_fallback_to_run(self, tmp_path: Path):
|
|
"""If summary file is missing, load_summary should compute from run."""
|
|
storage = FileStorage(tmp_path)
|
|
run = create_test_run()
|
|
|
|
storage.save_run(run)
|
|
|
|
# Delete the summary file
|
|
summary_file = tmp_path / "summaries" / f"{run.id}.json"
|
|
summary_file.unlink()
|
|
|
|
# Should still work by computing from run
|
|
summary = storage.load_summary(run.id)
|
|
assert summary is not None
|
|
assert summary.run_id == run.id
|
|
|
|
def test_delete_run(self, tmp_path: Path):
|
|
"""Test deleting a run."""
|
|
storage = FileStorage(tmp_path)
|
|
run = create_test_run()
|
|
|
|
storage.save_run(run)
|
|
assert storage.load_run(run.id) is not None
|
|
|
|
result = storage.delete_run(run.id)
|
|
|
|
assert result is True
|
|
assert storage.load_run(run.id) is None
|
|
|
|
def test_delete_nonexistent_run_returns_false(self, tmp_path: Path):
|
|
"""Deleting a nonexistent run should return False."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
result = storage.delete_run("nonexistent")
|
|
assert result is False
|
|
|
|
|
|
@pytest.mark.skip(reason="FileStorage is deprecated - use unified session storage")
|
|
class TestFileStorageIndexing:
|
|
"""Test FileStorage index operations."""
|
|
|
|
def test_index_by_goal(self, tmp_path: Path):
|
|
"""Runs should be indexed by goal_id."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
run1 = create_test_run(run_id="run_1", goal_id="goal_a")
|
|
run2 = create_test_run(run_id="run_2", goal_id="goal_a")
|
|
run3 = create_test_run(run_id="run_3", goal_id="goal_b")
|
|
|
|
storage.save_run(run1)
|
|
storage.save_run(run2)
|
|
storage.save_run(run3)
|
|
|
|
goal_a_runs = storage.get_runs_by_goal("goal_a")
|
|
goal_b_runs = storage.get_runs_by_goal("goal_b")
|
|
|
|
assert len(goal_a_runs) == 2
|
|
assert "run_1" in goal_a_runs
|
|
assert "run_2" in goal_a_runs
|
|
assert len(goal_b_runs) == 1
|
|
assert "run_3" in goal_b_runs
|
|
|
|
def test_index_by_status(self, tmp_path: Path):
|
|
"""Runs should be indexed by status."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
run1 = create_test_run(run_id="run_1", status=RunStatus.COMPLETED)
|
|
run2 = create_test_run(run_id="run_2", status=RunStatus.FAILED)
|
|
run3 = create_test_run(run_id="run_3", status=RunStatus.COMPLETED)
|
|
|
|
storage.save_run(run1)
|
|
storage.save_run(run2)
|
|
storage.save_run(run3)
|
|
|
|
completed = storage.get_runs_by_status(RunStatus.COMPLETED)
|
|
failed = storage.get_runs_by_status(RunStatus.FAILED)
|
|
|
|
assert len(completed) == 2
|
|
assert len(failed) == 1
|
|
|
|
def test_index_by_status_string(self, tmp_path: Path):
|
|
"""get_runs_by_status should accept string status."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
run = create_test_run(status=RunStatus.RUNNING)
|
|
storage.save_run(run)
|
|
|
|
runs = storage.get_runs_by_status("running")
|
|
assert len(runs) == 1
|
|
|
|
def test_index_by_node(self, tmp_path: Path):
|
|
"""Runs should be indexed by executed nodes."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
run1 = create_test_run(run_id="run_1", nodes_executed=["node_a", "node_b"])
|
|
run2 = create_test_run(run_id="run_2", nodes_executed=["node_a", "node_c"])
|
|
|
|
storage.save_run(run1)
|
|
storage.save_run(run2)
|
|
|
|
node_a_runs = storage.get_runs_by_node("node_a")
|
|
node_b_runs = storage.get_runs_by_node("node_b")
|
|
node_c_runs = storage.get_runs_by_node("node_c")
|
|
|
|
assert len(node_a_runs) == 2
|
|
assert len(node_b_runs) == 1
|
|
assert len(node_c_runs) == 1
|
|
|
|
def test_delete_removes_from_indexes(self, tmp_path: Path):
|
|
"""Deleting a run should remove it from all indexes."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
run = create_test_run(
|
|
run_id="run_1",
|
|
goal_id="goal_a",
|
|
status=RunStatus.COMPLETED,
|
|
nodes_executed=["node_1"],
|
|
)
|
|
storage.save_run(run)
|
|
|
|
# Verify indexed
|
|
assert "run_1" in storage.get_runs_by_goal("goal_a")
|
|
assert "run_1" in storage.get_runs_by_status(RunStatus.COMPLETED)
|
|
assert "run_1" in storage.get_runs_by_node("node_1")
|
|
|
|
# Delete
|
|
storage.delete_run("run_1")
|
|
|
|
# Verify removed from indexes
|
|
assert "run_1" not in storage.get_runs_by_goal("goal_a")
|
|
assert "run_1" not in storage.get_runs_by_status(RunStatus.COMPLETED)
|
|
assert "run_1" not in storage.get_runs_by_node("node_1")
|
|
|
|
def test_empty_index_returns_empty_list(self, tmp_path: Path):
|
|
"""Querying an empty index should return empty list."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
assert storage.get_runs_by_goal("nonexistent") == []
|
|
assert storage.get_runs_by_status("nonexistent") == []
|
|
assert storage.get_runs_by_node("nonexistent") == []
|
|
|
|
|
|
@pytest.mark.skip(reason="FileStorage is deprecated - use unified session storage")
|
|
class TestFileStorageListOperations:
|
|
"""Test FileStorage list operations."""
|
|
|
|
def test_list_all_runs(self, tmp_path: Path):
|
|
"""Test listing all run IDs."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
storage.save_run(create_test_run(run_id="run_1"))
|
|
storage.save_run(create_test_run(run_id="run_2"))
|
|
storage.save_run(create_test_run(run_id="run_3"))
|
|
|
|
all_runs = storage.list_all_runs()
|
|
|
|
assert len(all_runs) == 3
|
|
assert set(all_runs) == {"run_1", "run_2", "run_3"}
|
|
|
|
def test_list_all_goals(self, tmp_path: Path):
|
|
"""Test listing all goal IDs that have runs."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
storage.save_run(create_test_run(run_id="run_1", goal_id="goal_a"))
|
|
storage.save_run(create_test_run(run_id="run_2", goal_id="goal_b"))
|
|
storage.save_run(create_test_run(run_id="run_3", goal_id="goal_a"))
|
|
|
|
all_goals = storage.list_all_goals()
|
|
|
|
assert len(all_goals) == 2
|
|
assert set(all_goals) == {"goal_a", "goal_b"}
|
|
|
|
def test_get_stats(self, tmp_path: Path):
|
|
"""Test getting storage statistics."""
|
|
storage = FileStorage(tmp_path)
|
|
|
|
storage.save_run(create_test_run(run_id="run_1", goal_id="goal_a"))
|
|
storage.save_run(create_test_run(run_id="run_2", goal_id="goal_b"))
|
|
|
|
stats = storage.get_stats()
|
|
|
|
assert stats["total_runs"] == 2
|
|
assert stats["total_goals"] == 2
|
|
assert stats["storage_path"] == str(tmp_path)
|
|
|
|
|
|
# === CACHE ENTRY TESTS ===
|
|
|
|
|
|
class TestCacheEntry:
|
|
"""Test CacheEntry dataclass."""
|
|
|
|
def test_is_expired_false_when_fresh(self):
|
|
"""Cache entry should not be expired when fresh."""
|
|
entry = CacheEntry(value="test", timestamp=time.time())
|
|
assert entry.is_expired(ttl=60.0) is False
|
|
|
|
def test_is_expired_true_when_old(self):
|
|
"""Cache entry should be expired when older than TTL."""
|
|
old_timestamp = time.time() - 120 # 2 minutes ago
|
|
entry = CacheEntry(value="test", timestamp=old_timestamp)
|
|
assert entry.is_expired(ttl=60.0) is True
|
|
|
|
|
|
# === CONCURRENTSTORAGE TESTS ===
|
|
|
|
|
|
@pytest.mark.skip(reason="ConcurrentStorage is deprecated - wraps deprecated FileStorage")
|
|
class TestConcurrentStorageBasics:
|
|
"""Test basic ConcurrentStorage operations."""
|
|
|
|
def test_init(self, tmp_path: Path):
|
|
"""Test ConcurrentStorage initialization."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
|
|
assert storage.base_path == tmp_path
|
|
assert storage._running is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_start_and_stop(self, tmp_path: Path):
|
|
"""Test starting and stopping the storage."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
|
|
await storage.start()
|
|
assert storage._running is True
|
|
assert storage._batch_task is not None
|
|
|
|
await storage.stop()
|
|
assert storage._running is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_double_start_is_idempotent(self, tmp_path: Path):
|
|
"""Starting twice should be safe."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
|
|
await storage.start()
|
|
await storage.start() # Should not raise
|
|
assert storage._running is True
|
|
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_double_stop_is_idempotent(self, tmp_path: Path):
|
|
"""Stopping twice should be safe."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
|
|
await storage.start()
|
|
await storage.stop()
|
|
await storage.stop() # Should not raise
|
|
assert storage._running is False
|
|
|
|
|
|
@pytest.mark.skip(reason="ConcurrentStorage is deprecated - wraps deprecated FileStorage")
|
|
class TestConcurrentStorageRunOperations:
|
|
"""Test ConcurrentStorage run operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_and_load_run(self, tmp_path: Path):
|
|
"""Test async save and load of a run."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run = create_test_run()
|
|
await storage.save_run(run, immediate=True)
|
|
|
|
loaded = await storage.load_run(run.id)
|
|
|
|
assert loaded is not None
|
|
assert loaded.id == run.id
|
|
assert loaded.goal_id == run.goal_id
|
|
finally:
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_run_uses_cache(self, tmp_path: Path):
|
|
"""Second load should use cached value."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run = create_test_run()
|
|
await storage.save_run(run, immediate=True)
|
|
|
|
# First load
|
|
loaded1 = await storage.load_run(run.id)
|
|
# Second load (should use cache)
|
|
loaded2 = await storage.load_run(run.id, use_cache=True)
|
|
|
|
assert loaded1 is not None
|
|
assert loaded2 is not None
|
|
# Cache should return same object
|
|
assert loaded1 is loaded2
|
|
finally:
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_load_run_bypass_cache(self, tmp_path: Path):
|
|
"""Load with use_cache=False should bypass cache."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run = create_test_run()
|
|
await storage.save_run(run, immediate=True)
|
|
|
|
loaded1 = await storage.load_run(run.id)
|
|
loaded2 = await storage.load_run(run.id, use_cache=False)
|
|
|
|
assert loaded1 is not None
|
|
assert loaded2 is not None
|
|
# Fresh load should be different object
|
|
assert loaded1 is not loaded2
|
|
finally:
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_run(self, tmp_path: Path):
|
|
"""Test async delete of a run."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run = create_test_run()
|
|
await storage.save_run(run, immediate=True)
|
|
|
|
result = await storage.delete_run(run.id)
|
|
|
|
assert result is True
|
|
loaded = await storage.load_run(run.id)
|
|
assert loaded is None
|
|
finally:
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_clears_cache(self, tmp_path: Path):
|
|
"""Deleting a run should clear it from cache."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run = create_test_run()
|
|
await storage.save_run(run, immediate=True)
|
|
|
|
# Load to populate cache
|
|
await storage.load_run(run.id)
|
|
assert f"run:{run.id}" in storage._cache
|
|
|
|
# Delete
|
|
await storage.delete_run(run.id)
|
|
|
|
# Cache should be cleared
|
|
assert f"run:{run.id}" not in storage._cache
|
|
finally:
|
|
await storage.stop()
|
|
|
|
|
|
@pytest.mark.skip(reason="ConcurrentStorage is deprecated - wraps deprecated FileStorage")
|
|
class TestConcurrentStorageQueryOperations:
|
|
"""Test ConcurrentStorage query operations."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_runs_by_goal(self, tmp_path: Path):
|
|
"""Test async query by goal."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run1 = create_test_run(run_id="run_1", goal_id="goal_a")
|
|
run2 = create_test_run(run_id="run_2", goal_id="goal_a")
|
|
|
|
await storage.save_run(run1, immediate=True)
|
|
await storage.save_run(run2, immediate=True)
|
|
|
|
runs = await storage.get_runs_by_goal("goal_a")
|
|
|
|
assert len(runs) == 2
|
|
finally:
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_runs_by_status(self, tmp_path: Path):
|
|
"""Test async query by status."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
run = create_test_run(status=RunStatus.FAILED)
|
|
await storage.save_run(run, immediate=True)
|
|
|
|
runs = await storage.get_runs_by_status(RunStatus.FAILED)
|
|
|
|
assert len(runs) == 1
|
|
finally:
|
|
await storage.stop()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_all_runs(self, tmp_path: Path):
|
|
"""Test async list all runs."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
await storage.save_run(create_test_run(run_id="run_1"), immediate=True)
|
|
await storage.save_run(create_test_run(run_id="run_2"), immediate=True)
|
|
|
|
runs = await storage.list_all_runs()
|
|
|
|
assert len(runs) == 2
|
|
finally:
|
|
await storage.stop()
|
|
|
|
|
|
@pytest.mark.skip(reason="ConcurrentStorage is deprecated - wraps deprecated FileStorage")
|
|
class TestConcurrentStorageCacheManagement:
|
|
"""Test ConcurrentStorage cache management."""
|
|
|
|
def test_clear_cache(self, tmp_path: Path):
|
|
"""Test clearing the cache."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
storage._cache["test_key"] = CacheEntry(value="test", timestamp=time.time())
|
|
|
|
storage.clear_cache()
|
|
|
|
assert len(storage._cache) == 0
|
|
|
|
def test_invalidate_cache(self, tmp_path: Path):
|
|
"""Test invalidating a specific cache entry."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
storage._cache["key1"] = CacheEntry(value="test1", timestamp=time.time())
|
|
storage._cache["key2"] = CacheEntry(value="test2", timestamp=time.time())
|
|
|
|
storage.invalidate_cache("key1")
|
|
|
|
assert "key1" not in storage._cache
|
|
assert "key2" in storage._cache
|
|
|
|
def test_get_cache_stats(self, tmp_path: Path):
|
|
"""Test getting cache statistics."""
|
|
storage = ConcurrentStorage(tmp_path, cache_ttl=60.0)
|
|
|
|
# Add fresh entry
|
|
storage._cache["fresh"] = CacheEntry(value="test", timestamp=time.time())
|
|
# Add expired entry
|
|
storage._cache["expired"] = CacheEntry(value="test", timestamp=time.time() - 120)
|
|
|
|
stats = storage.get_cache_stats()
|
|
|
|
assert stats["total_entries"] == 2
|
|
assert stats["expired_entries"] == 1
|
|
assert stats["valid_entries"] == 1
|
|
|
|
|
|
@pytest.mark.skip(reason="ConcurrentStorage is deprecated - wraps deprecated FileStorage")
|
|
class TestConcurrentStorageSyncAPI:
|
|
"""Test ConcurrentStorage synchronous API for backward compatibility."""
|
|
|
|
def test_save_run_sync(self, tmp_path: Path):
|
|
"""Test synchronous save."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
run = create_test_run()
|
|
|
|
storage.save_run_sync(run)
|
|
|
|
# Verify saved
|
|
loaded = storage.load_run_sync(run.id)
|
|
assert loaded is not None
|
|
assert loaded.id == run.id
|
|
|
|
def test_load_run_sync(self, tmp_path: Path):
|
|
"""Test synchronous load."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
run = create_test_run()
|
|
|
|
storage.save_run_sync(run)
|
|
loaded = storage.load_run_sync(run.id)
|
|
|
|
assert loaded is not None
|
|
|
|
def test_load_run_sync_nonexistent(self, tmp_path: Path):
|
|
"""Synchronous load of nonexistent run returns None."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
|
|
loaded = storage.load_run_sync("nonexistent")
|
|
assert loaded is None
|
|
|
|
|
|
@pytest.mark.skip(reason="ConcurrentStorage is deprecated - wraps deprecated FileStorage")
|
|
class TestConcurrentStorageStats:
|
|
"""Test ConcurrentStorage statistics."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_stats(self, tmp_path: Path):
|
|
"""Test getting async storage stats."""
|
|
storage = ConcurrentStorage(tmp_path)
|
|
await storage.start()
|
|
|
|
try:
|
|
await storage.save_run(create_test_run(), immediate=True)
|
|
|
|
stats = await storage.get_stats()
|
|
|
|
assert stats["total_runs"] == 1
|
|
assert "cache" in stats
|
|
assert "pending_writes" in stats
|
|
assert stats["running"] is True
|
|
finally:
|
|
await storage.stop()
|