* feat: v2.1.0 — fal platform utilities (async fan-out, cost tools, recovery) Six new nodes under FAL/Platform, built on fal's queue/request-id/pricing primitives: - Fal Submit + Fal Collect: queue jobs without blocking and collect results later — N generations run in parallel inside one graph - Fal Result by Request ID: re-fetch any past generation from fal's queue without re-generating or re-billing - Fal Cost Estimator: per-run cost report parsed from registry pricing before anything is queued - Fal Session Costs: in-memory ledger of every fal call this session (endpoint, duration, request id, est. cost) with running total - Fal Save Media from URL: persist expiring CDN results into the ComfyUI output directory Core: utils/pricing.py (pricing-text parser + registry lookup), utils/ledger.py (thread-safe session ledger), api.py gains submit_only, result_from_request_id, request-id capture via on_enqueue, and automatic ledger recording; any_endpoint helpers extracted for reuse. 16 new tests (61 total). * feat: v2.2.0 — persistent result cache, spend guard, URL passthrough Three fal-platform features on top of the 2.1 platform pack: - Persistent result cache (sqlite, [cache] config): identical fal calls are served free from disk across ComfyUI restarts; uploads are deduplicated by content hash so identical inputs upload once. Bypass per node via force_rerun. Cache hits are never blocked by the spend guard and never recorded as spend. - Spend Guard + billing ([spend_guard] config): preflight refuses to submit once the session's estimated spend reaches session_budget_usd or the account balance (fal Billing API) falls below min_balance_usd. New Fal Account Balance node; ledger reconciliation against the aggregated usage API (fal exposes no per-request billed amounts). - fal-CDN URL passthrough: every dynamic node's media inputs gain optional *_direct_url twins (URL sent directly, no download/re-upload; 1,405 twins across 992 nodes) and image-kind nodes output image_urls alongside IMAGE — fal→fal chains with zero local I/O. Review fixes: path traversal blocked in FalSaveMediaURL (confined to the output dir, atomic O_EXCL filename claim), FalAnyEndpoint force_rerun now bypasses the cache, cache lookup ordered before spend-guard preflight, result recovery no longer counts as new session spend. 14 new tests (76 total).
53 lines
1.5 KiB
Python
53 lines
1.5 KiB
Python
"""Unit tests for the persistent result + upload cache."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
|
|
import pytest
|
|
from conftest import PKG, _load_package
|
|
|
|
|
|
@pytest.fixture()
|
|
def cache():
|
|
_load_package()
|
|
mod = importlib.import_module(f"{PKG}.nodes.utils.result_cache")
|
|
instance = mod.ResultCache()
|
|
instance.clear()
|
|
yield instance
|
|
instance.clear()
|
|
|
|
|
|
def test_round_trip(cache):
|
|
args = {"prompt": "a cat", "seed": 7}
|
|
assert cache.get("fal-ai/test", args) is None
|
|
cache.put("fal-ai/test", args, {"images": [{"url": "https://x/y.png"}]}, "req-1")
|
|
hit = cache.get("fal-ai/test", args)
|
|
assert hit == {"images": [{"url": "https://x/y.png"}]}
|
|
|
|
|
|
def test_key_is_argument_order_independent(cache):
|
|
a = cache.make_key("fal-ai/test", {"a": 1, "b": 2})
|
|
b = cache.make_key("fal-ai/test", {"b": 2, "a": 1})
|
|
assert a == b
|
|
assert a != cache.make_key("fal-ai/other", {"a": 1, "b": 2})
|
|
|
|
|
|
def test_different_args_miss(cache):
|
|
cache.put("fal-ai/test", {"prompt": "a"}, {"ok": 1})
|
|
assert cache.get("fal-ai/test", {"prompt": "b"}) is None
|
|
|
|
|
|
def test_upload_cache_round_trip(cache):
|
|
assert cache.get_upload("hash123") is None
|
|
cache.put_upload("hash123", "https://fal.media/up.png")
|
|
assert cache.get_upload("hash123") == "https://fal.media/up.png"
|
|
|
|
|
|
def test_clear_and_stats(cache):
|
|
cache.put("fal-ai/test", {"p": 1}, {"ok": 1})
|
|
cache.clear()
|
|
assert cache.get("fal-ai/test", {"p": 1}) is None
|
|
stats = cache.stats()
|
|
assert "entries" in stats and "db_path" in stats
|