docs: add product archive operations runbook

This commit is contained in:
2026-09-12 17:45:35 +08:00
parent cd9308ae8f
commit fabc6d69f2
2 changed files with 227 additions and 0 deletions
+87
View File
@@ -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()