* 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).
77 lines
2.3 KiB
Python
77 lines
2.3 KiB
Python
"""Integration tests for the FAL/Platform utility nodes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
PLATFORM_KEYS = [
|
|
"FalSubmit_fal",
|
|
"FalCollect_fal",
|
|
"FalResultByRequestId_fal",
|
|
"FalCostEstimator_fal",
|
|
"FalSessionCosts_fal",
|
|
"FalSaveMediaURL_fal",
|
|
]
|
|
|
|
|
|
def test_all_platform_nodes_registered(pack):
|
|
for key in PLATFORM_KEYS:
|
|
assert key in pack.NODE_CLASS_MAPPINGS, key
|
|
assert key in pack.NODE_DISPLAY_NAME_MAPPINGS, key
|
|
cls = pack.NODE_CLASS_MAPPINGS[key]
|
|
assert cls.CATEGORY == "FAL/Platform", key
|
|
input_types = cls.INPUT_TYPES()
|
|
assert "required" in input_types or "optional" in input_types
|
|
|
|
|
|
def test_submit_returns_handle_type(pack):
|
|
cls = pack.NODE_CLASS_MAPPINGS["FalSubmit_fal"]
|
|
assert "FAL_HANDLE" in cls.RETURN_TYPES
|
|
|
|
|
|
def test_collect_rejects_bad_handle(pack, errors_mod):
|
|
cls = pack.NODE_CLASS_MAPPINGS["FalCollect_fal"]
|
|
node = cls()
|
|
fn = getattr(node, cls.FUNCTION)
|
|
with pytest.raises(errors_mod.FalApiError):
|
|
fn(handle="not a handle")
|
|
|
|
|
|
def test_cost_estimator_never_raises(pack):
|
|
cls = pack.NODE_CLASS_MAPPINGS["FalCostEstimator_fal"]
|
|
node = cls()
|
|
fn = getattr(node, cls.FUNCTION)
|
|
report, total = fn(endpoint_id="fal-ai/definitely-not-real", runs=5)
|
|
assert isinstance(report, str)
|
|
assert isinstance(total, float)
|
|
|
|
|
|
def test_session_costs_reports(pack):
|
|
cls = pack.NODE_CLASS_MAPPINGS["FalSessionCosts_fal"]
|
|
node = cls()
|
|
fn = getattr(node, cls.FUNCTION)
|
|
out = fn(reset=False)
|
|
report, total = out[0], out[1]
|
|
assert isinstance(report, str)
|
|
assert isinstance(total, float)
|
|
|
|
|
|
def test_save_media_blocks_path_traversal(pack, errors_mod):
|
|
import importlib
|
|
|
|
from conftest import PKG
|
|
|
|
platform = importlib.import_module(f"{PKG}.nodes.platform_node")
|
|
with pytest.raises(errors_mod.FalApiError):
|
|
platform._resolve_save_directory("../../../../tmp/evil")
|
|
directory, basename = platform._resolve_save_directory("fal/media")
|
|
assert basename == "media"
|
|
|
|
|
|
def test_save_media_rejects_empty_url(pack, errors_mod):
|
|
cls = pack.NODE_CLASS_MAPPINGS["FalSaveMediaURL_fal"]
|
|
node = cls()
|
|
fn = getattr(node, cls.FUNCTION)
|
|
with pytest.raises((errors_mod.FalApiError, ValueError)):
|
|
fn(url="", filename_prefix="fal/test")
|