fix: D1-9 implement ExecuteTransform pipeline with ordering and toast budget
This commit is contained in:
240
test/bds/scripts/transforms_test.exs
Normal file
240
test/bds/scripts/transforms_test.exs
Normal file
@@ -0,0 +1,240 @@
|
||||
defmodule BDS.Scripts.TransformsTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
alias BDS.Scripts
|
||||
alias BDS.Scripts.Transforms
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
|
||||
|
||||
temp_dir =
|
||||
Path.join(System.tmp_dir!(), "bds-transforms-#{System.unique_integer([:positive])}")
|
||||
|
||||
File.mkdir_p!(temp_dir)
|
||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||
|
||||
{:ok, project} = BDS.Projects.create_project(%{name: "Transforms", data_path: temp_dir})
|
||||
|
||||
%{project: project}
|
||||
end
|
||||
|
||||
defp transform(project_id, title, content, opts \\ []) do
|
||||
{:ok, script} =
|
||||
Scripts.create_script(%{
|
||||
project_id: project_id,
|
||||
title: title,
|
||||
kind: :transform,
|
||||
content: content,
|
||||
entrypoint: Keyword.get(opts, :entrypoint, "main")
|
||||
})
|
||||
|
||||
script =
|
||||
case Keyword.get(opts, :enabled, true) do
|
||||
true ->
|
||||
script
|
||||
|
||||
false ->
|
||||
{:ok, s} = Scripts.update_script(script.id, %{enabled: false})
|
||||
s
|
||||
end
|
||||
|
||||
script
|
||||
end
|
||||
|
||||
test "runs enabled transforms in deterministic order (updated_at, slug, id)", %{
|
||||
project: project
|
||||
} do
|
||||
# Each transform appends its marker to content so we can read execution order.
|
||||
transform(project.id, "Bravo", """
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "B"
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
# Ensure distinct updated_at ordering by spacing out creation.
|
||||
Process.sleep(5)
|
||||
|
||||
transform(project.id, "Alpha", """
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "A"
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
data = %{
|
||||
"title" => "t",
|
||||
"content" => "",
|
||||
"tags" => [],
|
||||
"categories" => [],
|
||||
"url" => "http://x"
|
||||
}
|
||||
|
||||
assert {:ok, result} = Transforms.run(project.id, data)
|
||||
# Bravo created first (earlier updated_at) so runs before Alpha.
|
||||
assert result.data["content"] == "BA"
|
||||
assert result.errors == []
|
||||
end
|
||||
|
||||
test "disabled transforms and transforms from other projects are skipped", %{project: project} do
|
||||
{:ok, other} =
|
||||
BDS.Projects.create_project(%{
|
||||
name: "Other",
|
||||
data_path:
|
||||
Path.join(
|
||||
System.tmp_dir!(),
|
||||
"bds-transforms-other-#{System.unique_integer([:positive])}"
|
||||
)
|
||||
})
|
||||
|
||||
transform(
|
||||
project.id,
|
||||
"Disabled",
|
||||
"""
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "D"
|
||||
return data
|
||||
end
|
||||
""", enabled: false)
|
||||
|
||||
transform(other.id, "Foreign", """
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "F"
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
transform(project.id, "Enabled", """
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "E"
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
data = %{
|
||||
"title" => "t",
|
||||
"content" => "",
|
||||
"tags" => [],
|
||||
"categories" => [],
|
||||
"url" => "http://x"
|
||||
}
|
||||
|
||||
assert {:ok, result} = Transforms.run(project.id, data)
|
||||
assert result.data["content"] == "E"
|
||||
end
|
||||
|
||||
test "pipeline continues after a failing transform, keeping last valid state", %{
|
||||
project: project
|
||||
} do
|
||||
transform(project.id, "First", """
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "1"
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
Process.sleep(5)
|
||||
|
||||
transform(project.id, "Boom", """
|
||||
function main(_data, _ctx)
|
||||
error("boom")
|
||||
end
|
||||
""")
|
||||
|
||||
Process.sleep(5)
|
||||
|
||||
transform(project.id, "Third", """
|
||||
function main(data, _ctx)
|
||||
data.content = data.content .. "3"
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
data = %{
|
||||
"title" => "t",
|
||||
"content" => "",
|
||||
"tags" => [],
|
||||
"categories" => [],
|
||||
"url" => "http://x"
|
||||
}
|
||||
|
||||
assert {:ok, result} = Transforms.run(project.id, data)
|
||||
# Boom's failure does not roll back "1" and does not stop "3".
|
||||
assert result.data["content"] == "13"
|
||||
assert [%{reason: _}] = result.errors
|
||||
end
|
||||
|
||||
test "receives blogmark context with source and originating url", %{project: project} do
|
||||
transform(project.id, "Ctx", """
|
||||
function main(data, ctx)
|
||||
data.content = ctx.source .. "|" .. ctx.url
|
||||
return data
|
||||
end
|
||||
""")
|
||||
|
||||
data = %{
|
||||
"title" => "t",
|
||||
"content" => "",
|
||||
"tags" => [],
|
||||
"categories" => [],
|
||||
"url" => "http://example.com/a"
|
||||
}
|
||||
|
||||
assert {:ok, result} = Transforms.run(project.id, data)
|
||||
assert result.data["content"] == "blogmark|http://example.com/a"
|
||||
end
|
||||
|
||||
test "per-script toast budget caps and truncates toasts", %{project: project} do
|
||||
long = String.duplicate("x", 500)
|
||||
|
||||
transform(project.id, "Noisy", """
|
||||
function main(data, _ctx)
|
||||
local toasts = {}
|
||||
for i = 1, 10 do toasts[i] = "#{long}" end
|
||||
return { data = data, toasts = toasts }
|
||||
end
|
||||
""")
|
||||
|
||||
data = %{
|
||||
"title" => "t",
|
||||
"content" => "",
|
||||
"tags" => [],
|
||||
"categories" => [],
|
||||
"url" => "http://x"
|
||||
}
|
||||
|
||||
assert {:ok, result} = Transforms.run(project.id, data)
|
||||
# max 5 per script
|
||||
assert length(result.toasts) == 5
|
||||
# truncated to 300 chars
|
||||
assert Enum.all?(result.toasts, &(String.length(&1) == 300))
|
||||
end
|
||||
|
||||
test "total toast budget caps across the whole pipeline", %{project: project} do
|
||||
body = """
|
||||
function main(data, _ctx)
|
||||
local toasts = {}
|
||||
for i = 1, 5 do toasts[i] = "msg" end
|
||||
return { data = data, toasts = toasts }
|
||||
end
|
||||
"""
|
||||
|
||||
# 5 transforms x 5 toasts each = 25 emitted, total budget is 20.
|
||||
for i <- 1..5 do
|
||||
transform(project.id, "T#{i}", body)
|
||||
Process.sleep(3)
|
||||
end
|
||||
|
||||
data = %{
|
||||
"title" => "t",
|
||||
"content" => "",
|
||||
"tags" => [],
|
||||
"categories" => [],
|
||||
"url" => "http://x"
|
||||
}
|
||||
|
||||
assert {:ok, result} = Transforms.run(project.id, data)
|
||||
assert length(result.toasts) == 20
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user