feat(recipe): send embedded recipe workflow to ComfyUI canvas

This commit is contained in:
Will Miao
2026-08-20 22:18:13 +08:00
parent 0905e2be6e
commit 3ebf256c5d
24 changed files with 1145 additions and 27 deletions
@@ -0,0 +1,293 @@
"""Handler tests for the recipe workflow send endpoint.
Covers ``RecipeWorkflowHandler.send_recipe_workflow`` and the wiring of the
``send_recipe_workflow`` key in ``RecipeHandlerSet.to_route_mapping``.
"""
from __future__ import annotations
import json
import logging
from typing import Any
from unittest.mock import MagicMock
import pytest
from py.routes.handlers.recipe_handlers import (
RecipeHandlerSet,
RecipeWorkflowHandler,
)
from py.routes.handlers import recipe_handlers
async def _noop_ensure() -> None:
return None
@pytest.fixture(autouse=True)
def _clear_standalone_env(monkeypatch: pytest.MonkeyPatch):
"""Clear the standalone env flag leaked by other modules.
``standalone.py`` sets ``LORA_MANAGER_STANDALONE=1`` at import time, and
unrelated tests import that module; without cleanup the flag bleeds into
later tests and flips the handler's standalone branch.
"""
monkeypatch.delenv("LORA_MANAGER_STANDALONE", raising=False)
class FakeRequest:
"""Minimal request double exposing ``match_info``."""
def __init__(self, *, match_info: dict[str, Any] | None = None) -> None:
self.match_info = match_info or {}
class StubRecipeScanner:
"""Scanner double returning a configurable recipe for an id."""
def __init__(self, recipe: dict[str, Any] | None = None) -> None:
self.recipe = recipe
self.lookup_calls: list[str] = []
async def get_recipe_by_id(self, recipe_id: str) -> dict[str, Any] | None:
self.lookup_calls.append(recipe_id)
return self.recipe
def _json_payload(response) -> dict[str, Any]:
"""Decode the JSON body of a web.Response, asserting it is not null."""
text = response.text
assert text is not None
return json.loads(text)
def _make_prompt_server(send_calls: list[tuple[str, Any]], *, send_error: Exception | None = None):
"""Return a PromptServer-like class whose instance records send_sync calls."""
class RecordingPromptServer:
class Instance:
def send_sync(self, event, payload, sid=None):
if send_error is not None:
raise send_error
send_calls.append((event, payload))
instance = Instance()
return RecordingPromptServer
def _make_handler(
scanner: StubRecipeScanner,
prompt_server,
*,
ensure=_noop_ensure,
) -> RecipeWorkflowHandler:
return RecipeWorkflowHandler(
ensure_dependencies_ready=ensure,
recipe_scanner_getter=lambda: scanner,
prompt_server=prompt_server, # pyright: ignore[reportArgumentType]
logger=logging.getLogger(__name__),
)
async def test_send_recipe_workflow_broadcasts_embedded_workflow(monkeypatch: pytest.MonkeyPatch):
recipe = {
"file_path": "/models/recipes/sample.png",
"title": "My Recipe",
}
scanner = StubRecipeScanner(recipe=recipe)
send_calls: list[tuple[str, Any]] = []
prompt_server = _make_prompt_server(send_calls)
monkeypatch.setattr(
recipe_handlers.ExifUtils,
"_load_structured_metadata",
lambda _image_path: {"workflow": '{"nodes":[]}'},
)
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 200
payload = _json_payload(response)
assert payload == {"success": True, "sent": True}
assert send_calls == [
(
"lm_load_workflow",
{
"workflow": {"nodes": []},
"name": "My Recipe",
"recipe_id": "r1",
},
)
]
assert scanner.lookup_calls == ["r1"]
async def test_send_recipe_workflow_defaults_empty_title(monkeypatch: pytest.MonkeyPatch):
recipe = {"file_path": "/models/recipes/sample.png"}
scanner = StubRecipeScanner(recipe=recipe)
send_calls: list[tuple[str, Any]] = []
prompt_server = _make_prompt_server(send_calls)
monkeypatch.setattr(
recipe_handlers.ExifUtils,
"_load_structured_metadata",
lambda _image_path: {"workflow": "{}"},
)
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r2"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 200
assert send_calls[0][1]["workflow"] == {}
assert send_calls[0][1]["name"] == ""
assert send_calls[0][1]["recipe_id"] == "r2"
async def test_send_recipe_workflow_recipe_not_found():
scanner = StubRecipeScanner(recipe=None)
prompt_server = _make_prompt_server([])
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "missing"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 404
assert _json_payload(response) == {"error": "Recipe not found"}
async def test_send_recipe_workflow_missing_file_path():
scanner = StubRecipeScanner(recipe={"title": "No File"})
prompt_server = _make_prompt_server([])
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 404
assert _json_payload(response) == {"error": "no_workflow"}
async def test_send_recipe_workflow_no_embedded_workflow(monkeypatch: pytest.MonkeyPatch):
recipe = {"file_path": "/models/recipes/sample.png", "title": "No Wf"}
scanner = StubRecipeScanner(recipe=recipe)
prompt_server = _make_prompt_server([])
monkeypatch.setattr(
recipe_handlers.ExifUtils,
"_load_structured_metadata",
lambda _image_path: {"parameters": "some params"},
)
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 404
assert _json_payload(response) == {
"error": "no_workflow",
"message": "No embedded workflow found in recipe image",
}
async def test_send_recipe_workflow_invalid_json_payload(monkeypatch: pytest.MonkeyPatch):
recipe = {"file_path": "/models/recipes/sample.png", "title": "Bad Wf"}
scanner = StubRecipeScanner(recipe=recipe)
prompt_server = _make_prompt_server([])
monkeypatch.setattr(
recipe_handlers.ExifUtils,
"_load_structured_metadata",
lambda _image_path: {"workflow": "not-json{"},
)
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 404
assert _json_payload(response)["error"] == "no_workflow"
async def test_send_recipe_workflow_standalone_mode(monkeypatch: pytest.MonkeyPatch):
recipe = {"file_path": "/models/recipes/sample.png", "title": "Recipe"}
scanner = StubRecipeScanner(recipe=recipe)
prompt_server = _make_prompt_server([])
monkeypatch.setenv("LORA_MANAGER_STANDALONE", "1")
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 400
assert _json_payload(response) == {"error": "Standalone Mode Active"}
async def test_send_recipe_workflow_send_sync_error(monkeypatch: pytest.MonkeyPatch):
recipe = {"file_path": "/models/recipes/sample.png", "title": "Recipe"}
scanner = StubRecipeScanner(recipe=recipe)
prompt_server = _make_prompt_server([], send_error=RuntimeError("boom"))
monkeypatch.setattr(
recipe_handlers.ExifUtils,
"_load_structured_metadata",
lambda _image_path: {"workflow": "{}"},
)
handler = _make_handler(scanner, prompt_server)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 500
assert _json_payload(response) == {"error": "boom"}
async def test_send_recipe_workflow_scanner_unavailable():
handler = RecipeWorkflowHandler(
ensure_dependencies_ready=_noop_ensure,
recipe_scanner_getter=lambda: None,
prompt_server=_make_prompt_server([]), # pyright: ignore[reportArgumentType]
logger=logging.getLogger(__name__),
)
response = await handler.send_recipe_workflow(
FakeRequest(match_info={"recipe_id": "r1"}) # pyright: ignore[reportArgumentType]
)
assert response.status == 500
assert _json_payload(response) == {"error": "Recipe scanner unavailable"}
def test_route_mapping_includes_send_recipe_workflow():
workflow = RecipeWorkflowHandler(
ensure_dependencies_ready=_noop_ensure,
recipe_scanner_getter=lambda: None,
prompt_server=_make_prompt_server([]), # pyright: ignore[reportArgumentType]
logger=logging.getLogger(__name__),
)
handler_set = RecipeHandlerSet(
page_view=MagicMock(),
listing=MagicMock(),
query=MagicMock(),
management=MagicMock(),
analysis=MagicMock(),
sharing=MagicMock(),
batch_import=MagicMock(),
workflow=workflow,
)
mapping = handler_set.to_route_mapping()
assert "send_recipe_workflow" in mapping
assert mapping["send_recipe_workflow"] == workflow.send_recipe_workflow
+255 -1
View File
@@ -393,6 +393,214 @@ async def test_load_recipe_upgrades_string_checkpoint(tmp_path: Path, recipe_sca
assert loaded["checkpoint"]["file_name"] == "sd15"
# ---------------------------------------------------------------------------
# has_workflow detection (plan 3.1)
# ---------------------------------------------------------------------------
def _recipe_json(recipes_dir: Path, recipe_id: str, image_path: Path, **extra: Any) -> Path:
recipe_path = recipes_dir / f"{recipe_id}.recipe.json"
data: Dict[str, Any] = {
"id": recipe_id,
"file_path": str(image_path),
"title": recipe_id,
"modified": 0.0,
"created_date": 0.0,
"loras": [],
}
data.update(extra)
recipe_path.write_text(json.dumps(data))
return recipe_path
def _mock_metadata(monkeypatch, workflow=None, raises=False):
def _load_structured_metadata(_image_path):
if raises:
raise RuntimeError("metadata parse failure")
return {
"parameters": None,
"prompt": "a test prompt",
"workflow": workflow,
"comment": None,
}
monkeypatch.setattr(
"py.services.recipe_scanner.ExifUtils._load_structured_metadata",
_load_structured_metadata,
)
@pytest.mark.asyncio
async def test_load_recipe_detects_embedded_workflow(tmp_path: Path, recipe_scanner, monkeypatch):
scanner, _ = recipe_scanner
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "with-workflow.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "with-workflow", image_path)
_mock_metadata(monkeypatch, workflow='{"nodes": []}')
loaded = await scanner._load_recipe_file(str(recipe_path))
assert loaded["has_workflow"] is True
persisted = json.loads(recipe_path.read_text())
assert persisted["has_workflow"] is True
@pytest.mark.asyncio
async def test_load_recipe_detects_no_workflow(tmp_path: Path, recipe_scanner, monkeypatch):
scanner, _ = recipe_scanner
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "without-workflow.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "without-workflow", image_path)
_mock_metadata(monkeypatch, workflow=None)
loaded = await scanner._load_recipe_file(str(recipe_path))
assert loaded["has_workflow"] is False
persisted = json.loads(recipe_path.read_text())
assert persisted["has_workflow"] is False
@pytest.mark.asyncio
async def test_load_recipe_metadata_exception_yields_false(
tmp_path: Path, recipe_scanner, monkeypatch
):
scanner, _ = recipe_scanner
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "broken-meta.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "broken-meta", image_path)
_mock_metadata(monkeypatch, raises=True)
loaded = await scanner._load_recipe_file(str(recipe_path))
assert loaded["has_workflow"] is False
@pytest.mark.asyncio
async def test_load_recipe_missing_image_yields_false(
tmp_path: Path, recipe_scanner, monkeypatch
):
scanner, _ = recipe_scanner
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "missing-image.webp"
recipe_path = _recipe_json(recipes_dir, "missing-image", image_path)
_mock_metadata(monkeypatch, workflow='{"nodes": []}')
loaded = await scanner._load_recipe_file(str(recipe_path))
assert loaded["has_workflow"] is False
@pytest.mark.asyncio
async def test_load_recipe_keeps_existing_has_workflow(
tmp_path: Path, recipe_scanner, monkeypatch
):
scanner, _ = recipe_scanner
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "pre-recorded.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "pre-recorded", image_path, has_workflow=True)
_mock_metadata(monkeypatch, workflow=None)
loaded = await scanner._load_recipe_file(str(recipe_path))
assert loaded["has_workflow"] is True
persisted = json.loads(recipe_path.read_text())
assert persisted["has_workflow"] is True
def test_load_recipe_file_sync_detects_embedded_workflow(tmp_path: Path, monkeypatch):
RecipeScanner._instance = None
settings_manager_module.reset_settings_manager()
monkeypatch.setattr(config, "loras_roots", [str(tmp_path)])
scanner = RecipeScanner(lora_scanner=StubLoraScanner()) # pyright: ignore[reportArgumentType]
try:
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "sync-workflow.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "sync-workflow", image_path)
_mock_metadata(monkeypatch, workflow='{"nodes": []}')
loaded = scanner._load_recipe_file_sync(str(recipe_path))
assert loaded["has_workflow"] is True
persisted = json.loads(recipe_path.read_text())
assert persisted["has_workflow"] is True
finally:
RecipeScanner._instance = None
settings_manager_module.reset_settings_manager()
def test_load_recipe_file_sync_detects_no_workflow(tmp_path: Path, monkeypatch):
RecipeScanner._instance = None
settings_manager_module.reset_settings_manager()
monkeypatch.setattr(config, "loras_roots", [str(tmp_path)])
scanner = RecipeScanner(lora_scanner=StubLoraScanner()) # pyright: ignore[reportArgumentType]
try:
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "sync-no-workflow.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "sync-no-workflow", image_path)
_mock_metadata(monkeypatch, workflow=None)
loaded = scanner._load_recipe_file_sync(str(recipe_path))
assert loaded["has_workflow"] is False
persisted = json.loads(recipe_path.read_text())
assert persisted["has_workflow"] is False
finally:
RecipeScanner._instance = None
settings_manager_module.reset_settings_manager()
def test_load_recipe_file_sync_metadata_exception_yields_false(
tmp_path: Path, monkeypatch
):
RecipeScanner._instance = None
settings_manager_module.reset_settings_manager()
monkeypatch.setattr(config, "loras_roots", [str(tmp_path)])
scanner = RecipeScanner(lora_scanner=StubLoraScanner()) # pyright: ignore[reportArgumentType]
try:
recipes_dir = Path(config.loras_roots[0]) / "recipes"
recipes_dir.mkdir(parents=True, exist_ok=True)
image_path = recipes_dir / "sync-broken-meta.webp"
image_path.write_bytes(b"fake-webp")
recipe_path = _recipe_json(recipes_dir, "sync-broken-meta", image_path)
_mock_metadata(monkeypatch, raises=True)
loaded = scanner._load_recipe_file_sync(str(recipe_path))
assert loaded["has_workflow"] is False
finally:
RecipeScanner._instance = None
settings_manager_module.reset_settings_manager()
@pytest.mark.asyncio
async def test_get_paginated_data_normalizes_legacy_checkpoint(recipe_scanner):
scanner, _ = recipe_scanner
@@ -417,7 +625,6 @@ async def test_get_paginated_data_normalizes_legacy_checkpoint(recipe_scanner):
assert checkpoint["name"] == "legacy.safetensors"
assert checkpoint["file_name"] == "legacy"
@pytest.mark.asyncio
async def test_get_recipe_by_id_handles_non_dict_checkpoint(recipe_scanner):
scanner, _ = recipe_scanner
@@ -440,6 +647,53 @@ async def test_get_recipe_by_id_handles_non_dict_checkpoint(recipe_scanner):
assert recipe["checkpoint"]["file_name"] == "by-id"
@pytest.mark.asyncio
async def test_get_recipe_by_id_detects_workflow_for_legacy_recipes(
recipe_scanner, monkeypatch
):
scanner, _ = recipe_scanner
await scanner.add_recipe(
{
"id": "legacy-no-flag",
"file_path": "/models/recipes/legacy.webp",
"title": "Legacy",
"modified": 0.0,
"created_date": 0.0,
"loras": [],
}
)
monkeypatch.setattr(
RecipeScanner, "_detect_has_workflow", lambda self, path: True
)
recipe = await scanner.get_recipe_by_id("legacy-no-flag")
assert recipe is not None
assert recipe["has_workflow"] is True
@pytest.mark.asyncio
async def test_get_recipe_by_id_keeps_existing_has_workflow_flag(recipe_scanner):
scanner, _ = recipe_scanner
await scanner.add_recipe(
{
"id": "flagged",
"file_path": "/models/recipes/flagged.webp",
"title": "Flagged",
"modified": 0.0,
"created_date": 0.0,
"loras": [],
"has_workflow": False,
}
)
recipe = await scanner.get_recipe_by_id("flagged")
assert recipe is not None
assert recipe["has_workflow"] is False
@pytest.mark.asyncio
async def test_get_recipe_by_id_merges_recipe_json_details(recipe_scanner):
scanner, _ = recipe_scanner
+87
View File
@@ -26,6 +26,7 @@ class DummyExifUtils:
def __init__(self):
self.appended = None
self.optimized_calls = 0
self.workflow_value = None
def optimize_image(self, image_data, target_width, format, quality, preserve_metadata):
self.optimized_calls += 1
@@ -37,6 +38,14 @@ class DummyExifUtils:
def extract_image_metadata(self, path):
return {}
def _load_structured_metadata(self, image_path):
return {
"parameters": None,
"prompt": None,
"workflow": self.workflow_value,
"comment": None,
}
@pytest.mark.asyncio
async def test_save_recipe_video_bypasses_optimization(tmp_path):
@@ -213,6 +222,84 @@ async def test_save_recipe_reports_duplicates(tmp_path):
assert service._exif_utils.appended[0] == expected_image_path
@pytest.mark.asyncio
async def test_save_recipe_records_has_workflow(tmp_path):
exif_utils = DummyExifUtils()
exif_utils.workflow_value = '{"nodes": []}'
class DummyCache:
def __init__(self):
self.raw_data = []
async def resort(self):
pass
class DummyScanner:
def __init__(self, root):
self.recipes_dir = str(root)
self._cache = DummyCache()
async def find_recipes_by_fingerprint(self, fingerprint):
return []
async def add_recipe(self, recipe_data):
self._cache.raw_data.append(recipe_data)
scanner = DummyScanner(tmp_path)
service = RecipePersistenceService(
exif_utils=exif_utils,
card_preview_width=512,
logger=logging.getLogger("test"),
)
result = await service.save_recipe(
recipe_scanner=scanner,
image_bytes=b"image-bytes",
image_base64=None,
name="Workflow Recipe",
tags=[],
metadata={"base_model": "sd", "loras": []},
)
stored = json.loads(Path(result.payload["json_path"]).read_text())
assert stored["has_workflow"] is True
assert scanner._cache.raw_data[0]["has_workflow"] is True
@pytest.mark.asyncio
async def test_save_recipe_records_no_workflow(tmp_path):
exif_utils = DummyExifUtils()
class DummyScanner:
def __init__(self, root):
self.recipes_dir = str(root)
async def find_recipes_by_fingerprint(self, fingerprint):
return []
async def add_recipe(self, recipe_data):
return None
scanner = DummyScanner(tmp_path)
service = RecipePersistenceService(
exif_utils=exif_utils,
card_preview_width=512,
logger=logging.getLogger("test"),
)
result = await service.save_recipe(
recipe_scanner=scanner,
image_bytes=b"image-bytes",
image_base64=None,
name="Plain Recipe",
tags=[],
metadata={"base_model": "sd", "loras": []},
)
stored = json.loads(Path(result.payload["json_path"]).read_text())
assert stored["has_workflow"] is False
@pytest.mark.asyncio
async def test_save_recipe_persists_checkpoint_metadata(tmp_path):
exif_utils = DummyExifUtils()
+83
View File
@@ -551,3 +551,86 @@ class TestPersistentRecipeCache:
loaded = cache.load_cache()
assert loaded is not None
assert loaded.image_id_map == {"222": "new-only"}
class TestHasWorkflowColumn:
"""has_workflow column persistence (plan 3.1)."""
def test_save_and_load_roundtrip(self, temp_db_path):
"""has_workflow must round-trip through save_cache()/load_cache()."""
cache = PersistentRecipeCache(db_path=temp_db_path)
recipes = [
{"id": "wf-1", "title": "Has Workflow", "has_workflow": True},
{"id": "wf-2", "title": "No Workflow", "has_workflow": False},
{"id": "wf-3", "title": "Unset Workflow"},
]
cache.save_cache(recipes)
loaded = cache.load_cache()
assert loaded is not None
by_id = {r["id"]: r for r in loaded.raw_data}
assert by_id["wf-1"]["has_workflow"] is True
assert by_id["wf-2"]["has_workflow"] is False
assert by_id["wf-3"]["has_workflow"] is False
def test_prepare_recipe_row_matches_column_order(self, temp_db_path):
"""The prepared row must append has_workflow in column order."""
cache = PersistentRecipeCache(db_path=temp_db_path)
row_true = cache._prepare_recipe_row({"id": "r1", "has_workflow": True}, "")
row_false = cache._prepare_recipe_row({"id": "r2", "has_workflow": False}, "")
assert row_true[-1] == 1
assert row_false[-1] == 0
assert len(row_true) == len(cache._RECIPE_COLUMNS)
assert cache._RECIPE_COLUMNS[-1] == "has_workflow"
def test_update_recipe_preserves_has_workflow(self, temp_db_path):
"""update_recipe() must write the has_workflow column correctly."""
cache = PersistentRecipeCache(db_path=temp_db_path)
cache.save_cache([{"id": "wf-update", "title": "x", "has_workflow": True}])
cache.update_recipe({"id": "wf-update", "title": "y", "has_workflow": False})
loaded = cache.load_cache()
assert loaded is not None
assert loaded.raw_data[0]["has_workflow"] is False
def test_migrates_legacy_database_adds_has_workflow(self, temp_db_path):
"""A database created before has_workflow existed must still load."""
import sqlite3
conn = sqlite3.connect(temp_db_path)
conn.execute(
"""
CREATE TABLE recipes (
recipe_id TEXT PRIMARY KEY,
file_path TEXT,
json_path TEXT,
title TEXT,
folder TEXT,
source_path TEXT,
base_model TEXT,
fingerprint TEXT,
created_date REAL,
modified REAL,
file_mtime REAL,
file_size INTEGER,
favorite INTEGER DEFAULT 0,
repair_version INTEGER DEFAULT 0,
preview_nsfw_level INTEGER DEFAULT 0,
loras_json TEXT,
checkpoint_json TEXT,
gen_params_json TEXT,
tags_json TEXT
)
"""
)
conn.execute("INSERT INTO recipes (recipe_id, title) VALUES ('legacy-1', 'Legacy')")
conn.commit()
conn.close()
cache = PersistentRecipeCache(db_path=temp_db_path)
loaded = cache.load_cache()
assert loaded is not None
assert loaded.raw_data[0]["id"] == "legacy-1"
assert loaded.raw_data[0]["has_workflow"] is False