feat(value-sources): add sandboxed-Jinja template combinator
A new `template` value source evaluates a hardened, sandboxed Jinja expression over the live values of other value sources — the system's first float combinator. Backend: - Shared engine (utils/template_expr.py): ImmutableSandboxedEnvironment with filters/tests and auto-injected globals stripped; only min/max/abs/round/ clamp exposed; rejects **, string/collection-literal repetition, attribute access and non-global calls; NaN/inf-safe result coercion. - TemplateValueSource model + TemplateValueStream runtime: compile-once, primitives-only eval context, raw[name] exposure, eval_interval throttle, ref-counted input acquire/release, rename-safe hot-update. - Validation: unbound-variable + reserved-name rejection, reference cycle/depth guards (depth-only at create, full cycle at update), runtime acquire() depth backstop, and delete referential-integrity. - API: Create/Update/Response schemas + discriminated unions, _RESPONSE_MAP, and an advisory POST /value-sources/validate-template endpoint. - Demo seed: a static source plus a template combinator example. Frontend: - Editor modal section: repeatable inputs list (EntitySelect rows), a zero-dependency Jinja syntax highlighter, a hints/reference panel, and a debounced live validator that gates Save (stale-response-safe). - Graph editor: read-only template node with one edge per input. - i18n (en/ru/zh), icon, and card rendering. Tests: engine, stream, factory/cycle, validate endpoint, and demo seed.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""Demo-seed regression tests (value sources, incl. the template combinator)."""
|
||||
|
||||
from ledgrab.core.demo_seed import seed_demo_data
|
||||
from ledgrab.storage.database import Database
|
||||
from ledgrab.storage.value_source import StaticValueSource, TemplateValueSource
|
||||
from ledgrab.storage.value_source_store import ValueSourceStore
|
||||
|
||||
|
||||
def _seed(tmp_path):
|
||||
db = Database(tmp_path / "demo.db")
|
||||
seed_demo_data(db)
|
||||
return db
|
||||
|
||||
|
||||
def test_demo_seeds_template_value_source(tmp_path):
|
||||
db = _seed(tmp_path)
|
||||
try:
|
||||
store = ValueSourceStore(db)
|
||||
by_id = {s.id: s for s in store.get_all_sources()}
|
||||
|
||||
base = by_id["vs_demo0001"]
|
||||
boost = by_id["vs_demo0002"]
|
||||
assert isinstance(base, StaticValueSource)
|
||||
assert isinstance(boost, TemplateValueSource)
|
||||
assert boost.template == "clamp(level * 1.5)"
|
||||
assert boost.inputs == [{"name": "level", "value_source_id": "vs_demo0001"}]
|
||||
|
||||
# The reference graph is intact and consistent.
|
||||
assert store.get_transitive_dependencies("vs_demo0002") == {"vs_demo0001"}
|
||||
assert store.find_referencing_sources("vs_demo0001") == [boost.name]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def test_demo_template_evaluates_through_manager(tmp_path):
|
||||
"""The seeded template must actually evaluate over its seeded input."""
|
||||
from ledgrab.core.processing.value_stream import ValueStreamManager
|
||||
|
||||
db = _seed(tmp_path)
|
||||
try:
|
||||
store = ValueSourceStore(db)
|
||||
vsm = ValueStreamManager(value_source_store=store)
|
||||
stream = vsm.acquire("vs_demo0002")
|
||||
try:
|
||||
# base level 0.5 -> clamp(0.5 * 1.5) = 0.75
|
||||
assert abs(stream.get_value() - 0.75) < 1e-6
|
||||
finally:
|
||||
vsm.release("vs_demo0002")
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user