feat(v2): stream execution revisions over sse

This commit is contained in:
2026-07-27 22:40:16 +02:00
parent 717653b329
commit 3b7ef2e874
2 changed files with 109 additions and 9 deletions
+78 -2
View File
@@ -5,12 +5,13 @@ from pathlib import Path
import httpx
import pytest
from backup_tool.api.app import create_app
from backup_tool.api.app import create_app, execution_events_stream
from backup_tool.config import Settings
from backup_tool.db.models import Execution
from backup_tool.db.models import Execution, Repository, Source
from backup_tool.execution import (
claim,
complete_cancellation,
enqueue,
heartbeat,
recover_stale,
request_cancellation,
@@ -157,6 +158,10 @@ async def test_local_source_probe_archive_and_repository_targeted_job(
assert retried.status_code == 202
assert retried.json()["id"] == execution.json()["id"]
assert retried.json()["attempt"] == 2
cancelled = await client.post(
f"/api/v2/executions/{execution.json()['id']}/cancel", headers=headers
)
assert cancelled.status_code == 202
stream = await client.get(
f"/api/v2/executions/{execution.json()['id']}/events", headers=headers
)
@@ -172,6 +177,77 @@ async def test_local_source_probe_archive_and_repository_targeted_job(
).status_code == 409
@pytest.mark.asyncio
async def test_execution_sse_replays_later_redacted_revision(tmp_path: Path) -> None:
source_root = tmp_path / "sources"
source_root.mkdir()
data_dir = tmp_path / "data"
data_dir.mkdir()
key = tmp_path / "master.key"
key.write_bytes(b"x" * 32)
key.chmod(0o600)
repositories = tmp_path / "repositories"
restore = tmp_path / "restore"
repositories.mkdir()
restore.mkdir()
settings = Settings(
data_dir=data_dir,
database_url=f"sqlite+aiosqlite:///{data_dir / 'db.sqlite'}",
repository_roots=(repositories,),
local_source_roots=(source_root,),
restore_roots=(restore,),
master_key_file=key,
)
app = create_app(settings)
from backup_tool.db.models import Base, Job
async with app.state.engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
async with app.state.sessions() as db:
repository = Repository(
name="stream-repository",
root=str(repositories / "stream"),
format_version=1,
compression="none",
encryption="none",
)
source = Source(
name="stream-source",
kind="local",
public_config={"root": str(source_root)},
secret_refs=[],
)
db.add_all([repository, source])
await db.flush()
job = Job(
name="stream-job",
source_id=source.id,
repository_id=repository.id,
requested_mode="full",
exclusions=[],
retention={},
)
db.add(job)
await db.commit()
execution = await enqueue(db, job.id)
execution_id = execution.id
stream = execution_events_stream(app.state.sessions, execution_id, None)
first = await anext(stream)
first_id = first.split("\\n", 1)[0].removeprefix("id: ")
assert "queued" in first
async with app.state.sessions() as db:
assert await request_cancellation(db, execution_id) is not None
second = await anext(stream)
assert second.split("\\n", 1)[0].removeprefix("id: ") != first_id
assert "cancelled" in second
replay = execution_events_stream(app.state.sessions, execution_id, first_id)
assert "cancelled" in await anext(replay)
await stream.aclose()
await replay.aclose()
@pytest.mark.asyncio
async def test_local_source_rejects_unallowlisted_root(tmp_path: Path) -> None:
allowed = tmp_path / "allowed"