Files
pigway-frame-flow/tests/test_source_libraries.py
T
渡口浪人 ed3b61bc58
Community checks / verify (push) Skipped
Verify Frame Flow / verify (push) Failing after 7m11s
fix: resolve gallery labels from current plans and preserve render snapshots
2026-10-23 23:10:50 +08:00

784 lines
28 KiB
Python

import hashlib
from datetime import UTC, datetime
from io import BytesIO
import pytest
from PIL import Image
from pigway_frame_flow.providers.photo_sources import PhotoSourceInput
@pytest.fixture
def library_source(service, settings, legacy_photo_engine):
root = settings.data_dir.parent / "photos"
root.mkdir()
settings.photo_root = root
Image.new("RGB", (900, 600), (25, 100, 220)).save(root / "photo.jpg")
for source_id in ["family", "travel"]:
service.photo_sources.save(
source_id, PhotoSourceInput(name=source_id, kind="local", location=".")
)
return "family"
def create_plan(
client, source="family", name="Watch", width=410, height=502, **changes
):
response = client.post(
"/api/v1/generation-plans",
json={
"source_id": source,
"name": name,
"width": width,
"height": height,
**changes,
},
)
assert response.status_code == 200, response.text
return response.json()
def bind(client, terminal, source, plan):
response = client.put(
"/api/v1/library-terminals/" + terminal + "/binding",
json={"source_id": source, "plan_id": plan["id"]},
)
assert response.status_code == 200, response.text
def import_picture(client):
response = client.post(
"/api/v1/source-libraries/family/import", json={"item_id": "photo.jpg"}
)
assert response.status_code == 200, response.text
return response.json()
def test_single_binding_and_cross_library_guards(client, service, library_source):
first = create_plan(client)
other = create_plan(client, "travel")
bind(client, "watch", "family", first)
assert (
client.put(
"/api/v1/library-terminals/watch/binding",
json={"source_id": "travel", "plan_id": first["id"]},
).status_code
== 409
)
bind(client, "watch", "travel", other)
assert service.db.one("SELECT COUNT(*) AS n FROM terminal_bindings")["n"] == 1
assert client.delete("/api/v1/generation-plans/" + other["id"]).status_code == 409
assert client.delete("/api/v1/photo-sources/travel").status_code == 409
assert (
client.put(
"/api/v1/library-terminals/living-room/binding",
json={"source_id": "family", "plan_id": first["id"]},
).status_code
== 422
)
assert (
client.put(
"/api/v1/generation-plans/" + other["id"],
json={
k: v
for k, v in {**other, "width": 400}.items()
if k not in ("id", "archived")
},
).status_code
== 422
)
assert (
client.put(
"/api/v1/generation-plans/" + first["id"],
json={"name": "Move", "source_id": "travel"},
).status_code
== 409
)
def test_original_and_versions_share_picture_with_immutable_snapshots(
client, service, library_source
):
picture = import_picture(client)
assert import_picture(client)["id"] == picture["id"]
first = create_plan(client)
create_plan(client, name="Frame", width=1200, height=1600)
versions = client.post(
"/api/v1/source-images/" + picture["id"] + "/generate", json={}
).json()
assert len(versions) == 2
assert all(v["source"] == "generated" for v in versions)
assert all(picture["name"].removesuffix(".jpg") in v["name"] for v in versions)
assert len({v["source_image_id"] for v in versions}) == 1
repeated = client.post(
"/api/v1/source-images/" + picture["id"] + "/generate", json={}
).json()
assert {v["asset_id"] for v in versions} == {v["asset_id"] for v in repeated}
payload = {k: v for k, v in first.items() if k not in ("id", "archived")}
assert (
client.put(
"/api/v1/generation-plans/" + first["id"], json={**payload, "quality": 70}
).status_code
== 200
)
updated = client.post(
"/api/v1/source-images/" + picture["id"] + "/generate",
json={"plan_id": first["id"]},
).json()[0]
assert updated["asset_id"] not in {v["asset_id"] for v in versions}
assert updated["plan"]["quality"] == 70
assert (
next(v for v in versions if v["plan_id"] == first["id"])["plan"]["quality"]
== 85
)
other = create_plan(client, "travel")
assert (
client.post(
"/api/v1/source-images/" + picture["id"] + "/generate",
json={"plan_id": other["id"]},
).status_code
== 409
)
assert client.get("/api/v1/assets").json() == []
assert client.delete("/api/v1/assets/" + picture["id"]).status_code == 409
assert (
client.post(
"/api/v1/devices/living-room/display",
json={"content": {"type": "image", "asset_id": picture["id"]}},
).status_code
== 409
)
assert (
client.put(
"/api/v1/source-images/" + picture["id"] + "/star", json={"starred": True}
).status_code
== 200
)
assert (
client.put(
"/api/v1/source-images/" + picture["id"] + "/exclusion",
json={"excluded": True},
).status_code
== 409
)
assert client.delete("/api/v1/source-images/" + picture["id"]).status_code == 204
assert service.db.one("SELECT COUNT(*) AS n FROM library_versions")["n"] == 0
assert (service.settings.photo_root / "photo.jpg").exists()
def test_pipeline_selects_once_for_frame_and_watch_and_shared_plan(
client, service, library_source, monkeypatch
):
frame = create_plan(client, name="Frame", width=1200, height=1600, caption=True)
watch = create_plan(client)
service.registry.drivers["second-frame"] = service.registry.get("living-room")
for terminal in ["living-room", "second-frame"]:
bind(client, terminal, "family", frame)
bind(client, "watch", "family", watch)
def unavailable_frame(*args, **kwargs):
raise AssertionError("Source generation must not read a frame device")
original_device = service.registry.get("living-room").device
monkeypatch.setattr(
service.registry.get("living-room"), "device", unavailable_frame
)
read = service.photo_sources.read
calls = []
def source_read(*args, **kwargs):
calls.append(1)
data, name, _ = read(*args, **kwargs)
return data, name, {"title": "Caption", "date": "2026-10-23"}
monkeypatch.setattr(service.photo_sources, "read", source_read)
result = client.post("/api/v1/source-libraries/family/run", json={}).json()
assert result["errors"] == []
assert len(calls) == 1
assert len(result["outputs"]) == 3
assert len({v["asset_id"] for v in result["outputs"]}) == 2
monkeypatch.setattr(service.registry.get("living-room"), "device", original_device)
for terminal in ["living-room", "watch"]:
page = client.get("/api/v1/library-terminals/" + terminal + "/images").json()
assert page["total"] == 1
version = page["items"][0]
with Image.open(BytesIO(client.get(version["url"]).content)) as image:
if terminal == "watch":
assert image.size == (410, 502)
assert image.getpixel((200, 490))[2] > 200
else:
assert image.getpixel((10, 1590))[0] > 240
assert client.post(
"/api/v1/library-terminals/watch/images/"
+ result["outputs"][-1]["asset_id"]
+ "/send"
).status_code in (409, 422)
assert service.db.one("SELECT COUNT(*) AS n FROM operations")["n"] == 0
def test_scheduled_pipeline_links_independent_delivery_and_rechecks_binding(
client, app, service, library_source, monkeypatch
):
frame = create_plan(client, name="Frame", width=1200, height=1600)
watch = create_plan(client)
bind(client, "living-room", "family", frame)
bind(client, "watch", "family", watch)
saved = client.post(
"/api/v1/jobs",
json={
"name": "Shared source",
"schedule": {"type": "daily", "time": "07:00"},
"source": {"type": "photo_source", "source_id": "family"},
"library_pipeline": True,
},
).json()
now = datetime.now(UTC)
service.db.execute(
"UPDATE jobs SET next_run=? WHERE id=?", (now.isoformat(), saved["id"])
)
app.state.scheduler.tick(now)
root = service.db.one("SELECT id FROM operations WHERE action='library-generate'")
assert root and service.process_one()
op = client.get("/api/v1/operations/" + root["id"]).json()
assert op["status"] == "COMPLETED"
assert len(op["result"]["outputs"]) == 2
child = service.db.one(
"SELECT id FROM operations WHERE client_id='library-terminal'"
)
assert child
assert (
service.db.one("SELECT parent_operation_id FROM terminal_deliveries")[
"parent_operation_id"
]
== root["id"]
)
client.delete("/api/v1/library-terminals/living-room/binding")
writes = []
monkeypatch.setattr(
service.registry.get("living-room"), "upload", lambda *args: writes.append(args)
)
assert service.process_one()
assert service.operation(child["id"])["status"] == "FAILED"
assert writes == []
assert client.get("/api/v1/library-terminals/watch/images").json()["total"] == 1
def test_preview_is_unsaved_and_references_prevent_deletion(
client, service, library_source
):
picture = import_picture(client)
plan = create_plan(client, name="Frame", width=1200, height=1600)
payload = {k: v for k, v in plan.items() if k not in ("id", "archived")}
assert (
client.post(
"/api/v1/source-images/" + picture["id"] + "/preview", json=payload
).status_code
== 200
)
assert service.db.one("SELECT COUNT(*) AS n FROM library_versions")["n"] == 0
bind(client, "living-room", "family", plan)
version = client.post(
"/api/v1/source-images/" + picture["id"] + "/generate",
json={"plan_id": plan["id"]},
).json()[0]
assert (
client.post(
"/api/v1/library-terminals/living-room/images/"
+ version["asset_id"]
+ "/send"
).status_code
== 200
)
assert client.delete("/api/v1/source-images/" + picture["id"]).status_code == 409
def test_resumed_source_run_reuses_saved_picture_plans_and_delivery(
client, service, library_source, monkeypatch
):
frame = create_plan(client, name="Frame", width=1200, height=1600)
bind(client, "living-room", "family", frame)
root = service.submit(
"library-generate",
{"content": {"type": "photo_source", "source_id": "family"}, "options": {}},
"living-room",
"scheduler",
"resume-test",
)
assert service.process_one()
previous = service.operation(root)["result"]
child = previous["outputs"][0]["operation_id"]
service.db.execute("UPDATE operations SET status='COMPLETED' WHERE id=?", (child,))
update = {k: v for k, v in frame.items() if k not in ("id", "archived")}
assert (
client.put(
"/api/v1/generation-plans/" + frame["id"], json={**update, "quality": 60}
).status_code
== 200
)
service.db.execute("UPDATE operations SET status='QUEUED' WHERE id=?", (root,))
def unexpected_read(*args, **kwargs):
raise AssertionError("Resumed run must not select a new photo")
monkeypatch.setattr(service.photo_sources, "read", unexpected_read)
service.settings.mock = False
service.settings.write_enabled = False
assert service.process_one()
current = service.operation(root)["result"]
assert current["source_image_id"] == previous["source_image_id"]
assert current["outputs"][0]["operation_id"] == child
assert service.db.one("SELECT COUNT(*) AS n FROM library_versions")["n"] == 1
assert (
service.db.one("SELECT COUNT(*) AS n FROM operations WHERE action='display'")[
"n"
]
== 1
)
def test_one_plan_failure_preserves_other_output_and_source(
client, service, library_source, monkeypatch
):
from pigway_frame_flow.errors import ErrorCode, FlowError
frame = create_plan(client, name="Frame", width=1200, height=1600)
watch = create_plan(client)
bind(client, "living-room", "family", frame)
bind(client, "watch", "family", watch)
generate = service.libraries.generate
def fail_frame(picture_id, plan_id, *args, **kwargs):
if plan_id == frame["id"]:
raise FlowError(ErrorCode.INVALID_REQUEST, "Frame render failed")
return generate(picture_id, plan_id, *args, **kwargs)
monkeypatch.setattr(service.libraries, "generate", fail_frame)
root = service.submit(
"library-generate",
{"content": {"type": "photo_source", "source_id": "family"}, "options": {}},
"living-room",
"scheduler",
"partial-test",
)
assert service.process_one()
op = client.get("/api/v1/operations/" + root).json()
assert op["status"] == "FAILED"
assert len(op["result"]["errors"]) == 1 and len(op["result"]["outputs"]) == 1
assert op["result"]["outputs"][0]["terminal_id"] == "watch"
picture_id = op["result"]["source_image_id"]
monkeypatch.setattr(service.libraries, "generate", generate)
assert (
client.post(
"/api/v1/source-images/" + picture_id + "/generate",
json={"plan_id": frame["id"]},
).status_code
== 200
)
assert service.db.one("SELECT COUNT(*) AS n FROM source_images")["n"] == 1
def test_successful_scheduled_frame_delivery_retains_job_provenance(
client, app, service, library_source
):
frame = create_plan(client, name="Frame", width=1200, height=1600)
bind(client, "living-room", "family", frame)
saved = client.post(
"/api/v1/jobs",
json={
"name": "Library job",
"schedule": {"type": "daily", "time": "07:00"},
"source": {"type": "photo_source", "source_id": "family"},
"library_pipeline": True,
},
).json()
now = datetime.now(UTC)
service.db.execute(
"UPDATE jobs SET next_run=? WHERE id=?", (now.isoformat(), saved["id"])
)
app.state.scheduler.tick(now)
assert service.process_one() and service.process_one()
assert (
service.db.one("SELECT job_id FROM managed_cloud_uploads")["job_id"]
== saved["id"]
)
service.sync()
page = client.get("/api/v1/library-terminals/living-room/images").json()
assert page["items"][0]["delivery_status"] == "COMPLETED"
assert page["items"][0]["current"] is True
from pigway_frame_flow.storage.database import encode, now_iso
token = "library-reader-test"
service.db.execute(
"INSERT INTO clients VALUES(?,?,?,?,?,?,?)",
(
"reader",
hashlib.sha256(token.encode()).hexdigest(),
"Reader",
encode(["images:read", "devices:read"]),
now_iso(),
None,
1,
),
)
public = client.get(
"/api/v1/library-terminals/living-room/images",
headers={"Authorization": "Bearer " + token},
)
assert public.status_code == 200
summary = public.json()["items"][0]["operation"]
assert (
"request_id" not in summary
and "client_id" not in summary
and "error" not in summary
)
assert (
client.get(
"/api/v1/operations/" + summary["operation_id"],
headers={"Authorization": "Bearer " + token},
).status_code
== 403
)
def test_caption_plan_respects_full_picture_fit(client, service, library_source):
picture = import_picture(client)
service.db.execute(
"UPDATE source_images SET caption=? WHERE id=?",
('{"title":"Caption"}', picture["id"]),
)
payload = {
"source_id": "family",
"name": "Caption",
"width": 300,
"height": 400,
"caption": True,
"fit": "contain",
}
response = client.post(
"/api/v1/source-images/" + picture["id"] + "/preview", json=payload
)
assert response.status_code == 200
with Image.open(BytesIO(response.content)) as image:
assert image.getpixel((150, 0))[0] > 240
response = client.post(
"/api/v1/source-images/" + picture["id"] + "/preview",
json={**payload, "fit": "cover"},
)
with Image.open(BytesIO(response.content)) as image:
assert image.getpixel((150, 0))[0] < 60
def test_shared_version_keeps_separate_delivery_receipts_for_each_job(
client, app, service, library_source
):
frame = create_plan(client, name="Frame", width=1200, height=1600)
bind(client, "living-room", "family", frame)
job_ids = []
for name in ["Morning", "Evening"]:
job = client.post(
"/api/v1/jobs",
json={
"name": name,
"schedule": {"type": "daily", "time": "07:00"},
"source": {"type": "photo_source", "source_id": "family"},
"library_pipeline": True,
},
).json()
job_ids.append(job["id"])
now = datetime.now(UTC)
service.db.execute("UPDATE jobs SET next_run=?", (now.isoformat(),))
app.state.scheduler.tick(now)
assert service.process_one() and service.process_one()
assert service.db.one("SELECT COUNT(*) AS n FROM library_versions")["n"] == 1
assert service.db.one("SELECT COUNT(*) AS n FROM terminal_deliveries")["n"] == 2
assert service.process_one()
service.sync()
assert service.process_one()
service.sync()
assert {
r["job_id"] for r in service.db.all("SELECT job_id FROM managed_cloud_uploads")
} == set(job_ids)
page = client.get("/api/v1/library-terminals/living-room/images").json()
assert page["total"] == 1 and page["items"][0]["delivery_status"] == "COMPLETED"
def test_watch_only_source_task_does_not_require_a_frame_driver(
client, app, service, library_source
):
plan = create_plan(client)
bind(client, "watch", "family", plan)
service.registry.drivers.clear()
response = client.post(
"/api/v1/jobs",
json={
"name": "Watch only",
"device_id": "watch",
"schedule": {"type": "daily", "time": "07:00"},
"source": {"type": "photo_source", "source_id": "family"},
"library_pipeline": True,
},
)
assert response.status_code == 201
now = datetime.now(UTC)
service.db.execute("UPDATE jobs SET next_run=?", (now.isoformat(),))
app.state.scheduler.tick(now)
assert service.process_one()
operation = service.db.one(
"SELECT id,device_id,status FROM operations WHERE action='library-generate'"
)
assert (
operation["device_id"] == "source:family" and operation["status"] == "COMPLETED"
)
assert (
service.operation(operation["id"])["result"]["outputs"][0]["terminal_id"]
== "watch"
)
def test_interrupted_delivery_link_reuses_queue_idempotency(
client, service, library_source
):
plan = create_plan(client, name="Frame", width=1200, height=1600)
bind(client, "living-room", "family", plan)
root = service.submit(
"library-generate",
{"content": {"type": "photo_source", "source_id": "family"}, "options": {}},
"source:family",
"scheduler",
"receipt-gap",
)
assert service.process_one()
old = service.operation(root)["result"]
child = old["outputs"][0]["operation_id"]
service.db.execute("DELETE FROM terminal_deliveries WHERE operation_id=?", (child,))
service.db.execute("UPDATE operations SET status='QUEUED' WHERE id=?", (root,))
assert service.process_one()
assert service.operation(root)["result"]["outputs"][0]["operation_id"] == child
assert (
service.db.one("SELECT COUNT(*) AS n FROM operations WHERE action='display'")[
"n"
]
== 1
)
assert (
service.db.one("SELECT operation_id FROM terminal_deliveries")["operation_id"]
== child
)
def test_mode_switch_preserves_existing_schedule(client, service, library_source):
plan = create_plan(client)
bind(client, "watch", "family", plan)
original = client.post(
"/api/v1/jobs",
json={
"name": "Preserve timing",
"schedule": {"type": "interval", "interval_minutes": 120},
"source": {"type": "photo_source", "source_id": "family"},
},
).json()
switched = client.put(
"/api/v1/jobs/" + original["id"] + "/library-mode", json={"enabled": True}
)
assert switched.status_code == 200
current = switched.json()
assert current["library_pipeline"] is True
assert (
current["next_run"] == original["next_run"]
and current["schedule"] == original["schedule"]
and current["enabled"] == original["enabled"]
)
assert (
client.put(
"/api/v1/jobs/" + original["id"] + "/library-mode", json={"enabled": False}
).status_code
== 200
)
def test_single_source_migration_preserves_timing_and_generates_without_device_writes(
client, service, library_source, monkeypatch
):
import runpy
from pathlib import Path
migrate = runpy.run_path(
str(Path(__file__).parents[1] / "scripts/migrate-source-libraries.py")
)["migrate"]
job = client.post(
"/api/v1/jobs",
json={
"name": "Existing source task",
"schedule": {"type": "interval", "interval_minutes": 120},
"source": {"type": "photo_source", "source_id": "family"},
},
).json()
def call(method, path, payload=None):
response = client.request(method, path, json=payload)
response.raise_for_status()
return None if response.status_code == 204 else response.json()
with pytest.raises(RuntimeError):
migrate(call, apply=True)
assert client.delete("/api/v1/photo-sources/travel").status_code == 204
source_op = service.submit(
"display",
{
"content": {"type": "photo_source", "source_id": "family"},
"options": {"priority": 0},
},
"living-room",
"scheduler",
"current-photo",
)
assert service.process_one()
service.sync()
assert service.operation(source_op)["status"] == "COMPLETED"
writes = []
monkeypatch.setattr(
service.registry.get("living-room"), "upload", lambda *args: writes.append(args)
)
result = migrate(call, apply=True)
assert result["migrated"] and result["current_source_generated_locally"]
assert writes == []
migrated = client.get("/api/v1/jobs").json()[0]
assert (
migrated["library_pipeline"] is True and migrated["next_run"] == job["next_run"]
)
assert service.db.one("SELECT COUNT(*) AS n FROM terminal_bindings")["n"] == 2
assert service.db.one("SELECT COUNT(*) AS n FROM source_images")["n"] == 1
assert service.db.one("SELECT COUNT(*) AS n FROM library_versions")["n"] == 2
assert migrate(call, apply=True)["already_configured"]
def test_terminal_current_picture_uses_synced_live_asset(
client, service, library_source, monkeypatch
):
from pigway_frame_flow.errors import ErrorCode, FlowError
plan = create_plan(client, name="Frame", width=1200, height=1600)
bind(client, "living-room", "family", plan)
picture = import_picture(client)
version = client.post(
"/api/v1/source-images/" + picture["id"] + "/generate", json={}
).json()[0]
current = {"synced": True, "source_known": True, "asset_id": version["asset_id"]}
monkeypatch.setattr(service, "current_content", lambda _, **kwargs: current)
endpoint = "/api/v1/library-terminals/living-room/images"
assert client.get(endpoint).json()["items"][0]["current"] is True
current["asset_id"] = "another-picture"
assert client.get(endpoint).json()["items"][0]["current"] is False
current.update(asset_id=version["asset_id"], synced=False)
assert client.get(endpoint).json()["items"][0]["current"] is False
current.update(synced=True, source_known=False)
assert client.get(endpoint).json()["items"][0]["current"] is False
def offline(_, **kwargs):
raise FlowError(ErrorCode.CLOUD_UNAVAILABLE, "Offline", 502)
monkeypatch.setattr(service, "current_content", offline)
page = client.get(endpoint)
assert page.status_code == 200 and page.json()["items"][0]["current"] is False
driver = service.registry.get("living-room")
offline_device = driver.device().model_copy(update={"status": "offline"})
monkeypatch.setattr(driver, "device", lambda: offline_device)
monkeypatch.setattr(
service,
"current_content",
lambda _, **kwargs: pytest.fail(
"Offline state must not be treated as a display confirmation"
),
)
assert client.get(endpoint).json()["items"][0]["current"] is False
def test_plan_rename_uses_current_labels_without_rerendering_legacy_versions(
client, service, library_source
):
import json
from pigway_frame_flow.storage.database import encode
plan = create_plan(client, name="Original", width=1200, height=1600)
bind(client, "living-room", "family", plan)
picture = import_picture(client)
endpoint = "/api/v1/source-images/" + picture["id"] + "/generate"
original = client.post(endpoint, json={"plan_id": plan["id"]}).json()[0]
snapshot = original["plan"]
original_bytes = service.assets.path(original["asset_id"]).read_bytes()
# Existing deployed versions used the entire plan, including its old name.
legacy_revision = hashlib.sha256(encode(snapshot).encode()).hexdigest()
service.db.execute(
"UPDATE library_versions SET revision=? WHERE asset_id=?",
(legacy_revision, original["asset_id"]),
)
provenance = json.loads(
service.db.one(
"SELECT payload FROM asset_sources WHERE asset_id=?",
(original["asset_id"],),
)["payload"]
)
provenance["revision"] = legacy_revision
service.db.execute(
"UPDATE asset_sources SET payload=? WHERE asset_id=?",
(encode(provenance), original["asset_id"]),
)
payload = {k: v for k, v in plan.items() if k not in ("id", "archived")}
renamed = {**payload, "name": "Frame v2.1"}
assert (
client.put("/api/v1/generation-plans/" + plan["id"], json=renamed).status_code
== 200
)
repeated = client.post(endpoint, json={"plan_id": plan["id"]}).json()[0]
assert repeated["asset_id"] == original["asset_id"]
assert repeated["revision"] == legacy_revision
assert repeated["plan"] == snapshot
assert repeated["plan_name"] == "Frame v2.1"
assert repeated["name"] == "photo-Frame v2.1.jpg"
assert repeated["stored_name"] == original["stored_name"]
assert repeated["matches_current_plan"] is True
assert service.assets.path(original["asset_id"]).read_bytes() == original_bytes
assert service.db.one("SELECT COUNT(*) AS n FROM library_versions")["n"] == 1
source_version = client.get("/api/v1/source-libraries/family/images").json()[
"items"
][0]["versions"][0]
terminal_version = client.get(
"/api/v1/library-terminals/living-room/images"
).json()["items"][0]
assert source_version["plan_name"] == terminal_version["plan_name"] == "Frame v2.1"
assert source_version["name"] == terminal_version["name"]
assert (
client.put(
"/api/v1/generation-plans/" + plan["id"], json={**renamed, "quality": 70}
).status_code
== 200
)
old = client.get("/api/v1/library-terminals/living-room/images").json()["items"][0]
assert old["matches_current_plan"] is False and old["plan"]["quality"] == 85
updated = client.post(endpoint, json={"plan_id": plan["id"]}).json()[0]
assert (
updated["asset_id"] != original["asset_id"]
and updated["matches_current_plan"] is True
)
assert updated["plan"]["quality"] == 70
client.delete("/api/v1/library-terminals/living-room/binding")
assert client.delete("/api/v1/generation-plans/" + plan["id"]).status_code == 204
versions = client.get("/api/v1/source-libraries/family/images").json()["items"][0][
"versions"
]
assert len(versions) == 2 and all(v["plan_archived"] for v in versions)
def test_source_deletion_removes_unreferenced_archived_plans(
client, service, library_source
):
plan = create_plan(client)
assert client.delete("/api/v1/generation-plans/" + plan["id"]).status_code == 204
assert client.delete("/api/v1/photo-sources/family").status_code == 204
assert (
service.db.one("SELECT id FROM library_plans WHERE source_id='family'") is None
)
assert client.get("/api/v1/generation-plans?source_id=family").status_code == 404