diff --git a/SPECGAPS.md b/SPECGAPS.md index ab08bdf..685b12d 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -140,8 +140,8 @@ All reconciled to follow code. Specs must be self-consistent and match code. |---|---|---|---| | ~~D2-1~~ | ~~RemoveCategory rule~~ | ~~metadata.allium:100~~ | **Resolved:** 2 tests added in metadata_test.exs — remove_category removes category+settings from state/files/DB (6 assertions); remove_category is a no-op for non-existent category | | ~~D2-2~~ | ~~CreateAndPublishTemplate rule~~ | template.allium:105 | **Resolved:** added `create_and_publish_template/1` (validates Liquid, creates published template + writs file), 3 tests added (happy path file assertions, invalid liquid rejection, slug dedup on title conflict) | -| D2-3 | CreateAndPublishScript rule | script.allium:160 | Write test: create+publish in one step | -| D2-4 | UniqueScriptSlug dedup | script.allium:115 | Write test: two scripts same title → dedup slug | +| ~~D2-3~~ | ~~CreateAndPublishScript rule~~ | ~~script.allium:160~~ | **Resolved:** `create_and_publish_script/1` implemented in scripts.ex, 4 tests added (happy path with file assertions + macro entrypoint default + invalid Lua rejection + slug dedup on title conflict) | +| ~~D2-4~~ | ~~UniqueScriptSlug dedup~~ | ~~script.allium:115~~ | **Resolved:** test added asserting two scripts with same title produce unique slugs (`dup-slug` → `dup-slug-2`) | | D2-5 | FrontmatterRoundtrip invariant | post.allium:223 | Write test: write file, read back, assert all DB fields match | | D2-6 | SidecarRoundtrip invariant | media.allium:198 | Write test: write sidecar, read back, assert all fields match | | D2-7 | ConditionalPostFields: nil fields absent from frontmatter | frontmatter.allium:398 | Write test: post with nil excerpt/author/language → fields not in file | diff --git a/lib/bds/scripts.ex b/lib/bds/scripts.ex index 6537311..86e8f82 100644 --- a/lib/bds/scripts.ex +++ b/lib/bds/scripts.ex @@ -42,6 +42,56 @@ defmodule BDS.Scripts do |> Repo.insert() end + @spec create_and_publish_script(attrs()) :: script_result() + def create_and_publish_script(attrs) do + project_id = attr(attrs, :project_id) + title = attr(attrs, :title) || "" + kind = attr(attrs, :kind) + content = attr(attrs, :content) || "" + + case validate_script(content) do + :ok -> + slug = unique_slug(project_id, Slug.slugify(title), "script") + entrypoint = attr(attrs, :entrypoint) || default_entrypoint(kind) + file_path = script_file_path(slug) + now = Persistence.now_ms() + + changeset = + %Script{} + |> Script.changeset(%{ + id: Ecto.UUID.generate(), + project_id: project_id, + slug: slug, + title: title, + kind: kind, + entrypoint: entrypoint, + enabled: true, + version: 1, + file_path: file_path, + status: :published, + content: nil, + created_at: now, + updated_at: now + }) + + with {:ok, script} <- Repo.insert(changeset) do + full_path = full_file_path(script.project_id, file_path) + File.mkdir_p!(Path.dirname(full_path)) + + :ok = + Persistence.atomic_write( + full_path, + serialize_script_file(script, content) + ) + + {:ok, script} + end + + {:error, reason} -> + {:error, {:invalid_script, reason}} + end + end + @spec get_script(String.t()) :: Script.t() | nil def get_script(script_id) do case Repo.get(Script, script_id) do diff --git a/test/bds/scripts_test.exs b/test/bds/scripts_test.exs index 68f2962..92c77c7 100644 --- a/test/bds/scripts_test.exs +++ b/test/bds/scripts_test.exs @@ -155,6 +155,114 @@ defmodule BDS.ScriptsTest do assert published.status == :published end + test "create_and_publish_script creates a published script with file in one step", %{ + project: project, + temp_dir: temp_dir + } do + assert {:ok, script} = + BDS.Scripts.create_and_publish_script(%{ + project_id: project.id, + title: "Published Utility", + kind: :utility, + content: "function main() return 'ok' end" + }) + + assert script.status == :published + assert script.content == nil + assert script.slug == "published-utility" + assert script.enabled == true + assert script.version == 1 + assert script.entrypoint == "main" + assert script.file_path == "scripts/published-utility.lua" + + full_path = Path.join(temp_dir, script.file_path) + assert File.exists?(full_path) + + contents = File.read!(full_path) + assert contents =~ "---\nid: #{script.id}\n" + assert contents =~ "projectId: #{project.id}\n" + assert contents =~ "slug: published-utility\n" + assert contents =~ "title: Published Utility\n" + assert contents =~ "kind: utility\n" + assert contents =~ "entrypoint: main\n" + assert contents =~ "enabled: true\n" + assert contents =~ "version: 1\n" + assert contents =~ ~r/createdAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/ + assert contents =~ ~r/updatedAt: '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z'\n/ + assert contents =~ "\n---\nfunction main() return 'ok' end\n" + end + + test "create_and_publish_script uses macro default entrypoint", %{ + project: project + } do + assert {:ok, script} = + BDS.Scripts.create_and_publish_script(%{ + project_id: project.id, + title: "Render Helper", + kind: :macro, + content: "function render() end" + }) + + assert script.entrypoint == "render" + assert script.slug == "render-helper" + end + + test "create_and_publish_script rejects invalid Lua syntax", %{project: project} do + assert {:error, {:invalid_script, _reason}} = + BDS.Scripts.create_and_publish_script(%{ + project_id: project.id, + title: "Bad Script", + kind: :utility, + content: "function main( missing end" + }) + end + + test "create_and_publish_script deduplicates slug on title conflict", %{project: project} do + assert {:ok, _first} = + BDS.Scripts.create_and_publish_script(%{ + project_id: project.id, + title: "Same Title", + kind: :utility, + content: "function main() return 'v1' end" + }) + + assert {:ok, second} = + BDS.Scripts.create_and_publish_script(%{ + project_id: project.id, + title: "Same Title", + kind: :utility, + content: "function main() return 'v2' end" + }) + + assert second.slug == "same-title-2" + assert second.status == :published + end + + test "create_script deduplicates slug on title conflict (UniqueScriptSlug)", %{ + project: project + } do + assert {:ok, first} = + BDS.Scripts.create_script(%{ + project_id: project.id, + title: "Dup Slug", + kind: :utility, + content: "function main() return 'first' end" + }) + + assert first.slug == "dup-slug" + + assert {:ok, second} = + BDS.Scripts.create_script(%{ + project_id: project.id, + title: "Dup Slug", + kind: :utility, + content: "function main() return 'second' end" + }) + + assert second.slug == "dup-slug-2" + refute second.id == first.id + end + test "rebuild_scripts_from_files recreates published scripts from disk", %{ project: project, temp_dir: temp_dir