docs: add product archive operations runbook
This commit is contained in:
@@ -6,6 +6,7 @@ import shutil
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
@@ -23,6 +24,10 @@ from service.product_archive import ( # noqa: E402
|
||||
validate_word_locations_db,
|
||||
)
|
||||
from service.product_archive_store import ProductArchiveStore # noqa: E402
|
||||
from service.cleanup_service import CleanupService # noqa: E402
|
||||
from service.metadata_store import MetadataStore # noqa: E402
|
||||
from service.storage import Storage # noqa: E402
|
||||
from service.schemas import JobStatus # noqa: E402
|
||||
from service import app as service_app # noqa: E402
|
||||
|
||||
|
||||
@@ -464,6 +469,88 @@ def test_delete_rejects_failed_cleanup_product_until_restore(product_archive_cli
|
||||
assert soft_deleted.json()["status"] == "pending_cleanup"
|
||||
|
||||
|
||||
def test_only_visible_inserted_wordcloud_is_retained_after_30_day_cleanup(
|
||||
product_archive_client, prepared_wordcloud_job, tmp_path, monkeypatch
|
||||
):
|
||||
archived_at = datetime.now(timezone.utc)
|
||||
created_at = archived_at - timedelta(days=31)
|
||||
assets_dir = tmp_path / "e2e-assets"
|
||||
assets_dir.mkdir(parents=True)
|
||||
monkeypatch.setattr(service_app, "ASSETS_DIR", assets_dir)
|
||||
|
||||
source_specs = {
|
||||
"job-visible": "visible",
|
||||
"job-hidden": "hidden",
|
||||
"job-detached": None,
|
||||
}
|
||||
source_assets: dict[str, dict[str, str]] = {}
|
||||
source_paths: dict[str, Path] = {}
|
||||
metadata_store = MetadataStore(tmp_path / "e2e-metadata" / "app.db")
|
||||
storage = Storage(tmp_path / "e2e-workspace")
|
||||
|
||||
for job_id, placement in source_specs.items():
|
||||
asset_id = f"asset_{job_id.replace('-', '_')}"
|
||||
asset_dir = assets_dir / asset_id[:2] / asset_id
|
||||
asset_dir.mkdir(parents=True)
|
||||
(asset_dir / "meta.json").write_text(
|
||||
json.dumps({"asset_id": asset_id, "type": "wordcloud", "job_id": job_id}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
source_assets[job_id] = asset_id
|
||||
source_paths[job_id] = storage.job_root(job_id) / "output" / "word_locations.sqlite"
|
||||
source_paths[job_id].parent.mkdir(parents=True, exist_ok=True)
|
||||
with sqlite3.connect(source_paths[job_id]) as connection:
|
||||
connection.execute("CREATE TABLE word_locations (id INTEGER PRIMARY KEY, name TEXT)")
|
||||
connection.execute("INSERT INTO word_locations (name) VALUES (?)", (job_id,))
|
||||
metadata_store.upsert_job(
|
||||
JobStatus(
|
||||
job_id=job_id,
|
||||
status="success",
|
||||
stage="done",
|
||||
progress_percent=100,
|
||||
message="done",
|
||||
artifacts={"db": str(source_paths[job_id])},
|
||||
created_at=created_at,
|
||||
updated_at=created_at,
|
||||
)
|
||||
)
|
||||
|
||||
def resolve_job(job_id: str):
|
||||
return SimpleNamespace(
|
||||
status="success",
|
||||
artifacts={"db": str(source_paths[job_id])},
|
||||
)
|
||||
|
||||
monkeypatch.setattr(service_app, "_resolve_job_status", resolve_job)
|
||||
product = create_product(product_archive_client)
|
||||
response = archive_product_version(product_archive_client, product["product_id"], {
|
||||
"layers": [
|
||||
{"id": "shown", "visible": True},
|
||||
{"id": "hidden", "visible": False},
|
||||
],
|
||||
"elements": [
|
||||
{"type": "sticker", "assetId": source_assets["job-visible"], "layerId": "shown"},
|
||||
{"type": "sticker", "assetId": source_assets["job-hidden"], "layerId": "hidden"},
|
||||
],
|
||||
})
|
||||
|
||||
assert response.status_code == 201
|
||||
archived = response.json()["wordcloud_archives"]
|
||||
assert [item["source_job_id"] for item in archived] == ["job-visible"]
|
||||
visible_snapshot = Path(archived[0]["db_path"])
|
||||
assert visible_snapshot.exists()
|
||||
|
||||
cleanup = CleanupService(storage, metadata_store, product_archive_client.product_store)
|
||||
report = cleanup.apply(now=archived_at + timedelta(days=31))
|
||||
|
||||
assert "job-visible" not in report.deleted_job_ids
|
||||
assert {"job-hidden", "job-detached"} <= set(report.deleted_job_ids)
|
||||
assert not storage.job_root("job-hidden").exists()
|
||||
assert not storage.job_root("job-detached").exists()
|
||||
assert storage.job_root("job-visible").exists()
|
||||
assert visible_snapshot.exists()
|
||||
|
||||
|
||||
def test_archive_failure_during_final_move_removes_staging_files(product_archive_client, prepared_wordcloud_job, monkeypatch):
|
||||
product = create_product(product_archive_client)
|
||||
service = service_app._product_archive_service()
|
||||
|
||||
Reference in New Issue
Block a user