diff --git a/SPECGAPS.md b/SPECGAPS.md index bcf52cf..803d646 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -121,7 +121,7 @@ All reconciled to follow code. Specs must be self-consistent and match code. | D1-5 | ~~LiquidTagSubset (5 tags only)~~ | template.allium:179 | **Resolved:** added `BDS.Rendering.LiquidParser`, a restricted Liquex parser recognizing only the subset (if/for/assign/render + `{{ }}` output); any other tag (`unless`, `case`, `capture`, `tablerow`, `cycle`, `increment`, …) leaves unmatched input and fails `eos/0`. Wired into `validate_liquid` (publish gate), `template_selection.render_template`, `filters.render_macro_source`, and MCP `validate_template` so validation and rendering share the same surface; 6 parametrized tests added asserting unsupported tags are rejected at publish | | D1-6 | ~~LiquidFilterSubset (4 standard + 2 custom)~~ | template.allium:191 | **Resolved:** added `LiquidParser.validate/1`, which parses with the restricted tag grammar then walks the AST to reject any filter outside the allowed set — 4 standard (`escape`, `url_encode`, `default`, `append`) + 3 custom (`i18n`, `markdown`, `slugify`). Wired into `validate_liquid` (publish gate) and MCP `validate_template` so unsupported filters are rejected even though Liquex would otherwise apply them as built-in standard filters. Spec corrected to 3 custom filters (bundled templates use `slugify`); 9 tests added (6 unsupported filters rejected, 3 supported filters accepted). | | D1-7 | ~~LiquidOperatorSubset~~ | template.allium:210 | **Resolved:** `LiquidParser.validate/1` now walks the parsed AST for `{:op, _}` nodes and rejects any comparison operator outside the allowed `==`/`>` subset (`!=`, `<`, `>=`, `<=`, `contains`), sharing the publish gate and MCP `validate_template` surface with the tag/filter checks; spec `LiquidOperatorSubset` annotated with enforcement note; 10 tests added (5 unsupported operators rejected at publish, 5 supported `==`/`>`/`and`/`or`/bare-truthy expressions accepted). | -| D1-8 | MacroTimeout guarantee | script.allium:94-95 | Write test: macro times out within budget | +| D1-8 | ~~MacroTimeout guarantee~~ | script.allium:94-95 | **Resolved:** added test in `api_test.exs` — an infinite-loop `render()` macro run with `max_reductions: :none` (forces the luerl sandbox onto its wall-clock path) and a 150ms `timeout` returns `{:error, :timeout}` and terminates within budget (<2s), proving the macro is killed near its budget rather than the default multi-minute script timeout | | D1-9 | ExecuteTransform rule (pipeline, ordering, toast budget) | script.allium:229-263 | Write test: transform pipeline executes in order, toast budget enforced | | D1-10 | TransformPipelineContinuation | script.allium:247-249 | Write test: error in transform doesn't halt pipeline | | D1-11 | ChatContextTruncation invariant | ai.allium:375-379 | Write test: long chat history trimmed to context window | diff --git a/test/bds/scripting/api_test.exs b/test/bds/scripting/api_test.exs index 2dd38fe..008bf6f 100644 --- a/test/bds/scripting/api_test.exs +++ b/test/bds/scripting/api_test.exs @@ -109,6 +109,33 @@ defmodule BDS.Scripting.ApiTest do assert {:error, _reason} = BDS.Scripting.execute_macro(project.id, bad_source, []) end + test "macro execution is bounded by its timeout budget (MacroTimeout)", %{project: project} do + # An entrypoint that never returns must not run forever: the macro timeout + # budget terminates it and degrades to an error. max_reductions: :none forces + # the luerl sandbox onto its wall-clock path so we exercise the time budget + # itself rather than the reduction limit. + looping_source = "function render() while true do end end" + budget_ms = 150 + + {elapsed_us, result} = + :timer.tc(fn -> + BDS.Scripting.execute_macro(project.id, looping_source, [], + timeout: budget_ms, + max_reductions: :none + ) + end) + + assert {:error, :timeout} = result + + elapsed_ms = div(elapsed_us, 1000) + + # The macro must be killed close to its budget, never allowed to run for the + # default multi-minute script timeout. Generous upper bound to stay stable + # on loaded CI while still proving the budget is enforced. + assert elapsed_ms < 2_000, + "macro ran for #{elapsed_ms}ms, expected termination near the #{budget_ms}ms budget" + end + test "project scripting exposes project, post, script, template, metadata, and task namespaces", %{ project: project