From c735c1afa7287ca3b5b822f677d0ece483c4028a Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sun, 5 Jul 2026 19:22:44 +0200 Subject: [PATCH] fix: issue #2 auto-translate only on manual save now --- lib/bds/desktop/shell_live/post_editor.ex | 4 +- .../shell_live/post_editor/persistence.ex | 30 +++-- lib/bds/posts.ex | 13 +- specs/editor_post.allium | 2 + specs/translation.allium | 9 +- test/bds/post_translations_test.exs | 111 ++++++++++++++---- 6 files changed, 125 insertions(+), 44 deletions(-) diff --git a/lib/bds/desktop/shell_live/post_editor.ex b/lib/bds/desktop/shell_live/post_editor.ex index fd153bf..b0aef77 100644 --- a/lib/bds/desktop/shell_live/post_editor.ex +++ b/lib/bds/desktop/shell_live/post_editor.ex @@ -503,7 +503,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor do active_language = socket.assigns.active_language draft = component_current_draft(socket, post, metadata, active_language) - case persist(post, draft, active_language, metadata, :save) do + action = if keep_draft?, do: :auto_save, else: :save + + case persist(post, draft, active_language, metadata, action) do {:ok, record} -> refreshed_post = Posts.get_post!(post.id) _refreshed_form = persisted_form(refreshed_post, metadata, active_language) diff --git a/lib/bds/desktop/shell_live/post_editor/persistence.ex b/lib/bds/desktop/shell_live/post_editor/persistence.ex index 52447ef..28fa9c2 100644 --- a/lib/bds/desktop/shell_live/post_editor/persistence.ex +++ b/lib/bds/desktop/shell_live/post_editor/persistence.ex @@ -17,7 +17,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do canonical_language ) do post - |> save_canonical_draft(draft) + |> save_canonical_draft(draft, action) |> maybe_publish_post(post.id, action) else post.id @@ -65,18 +65,22 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do else: dgettext("ui", "Delete this unpublished draft") end - defp save_canonical_draft(%Post{id: post_id}, draft) do - Posts.update_post(post_id, %{ - title: blank_to_nil(Map.get(draft, "title")), - excerpt: blank_to_nil(Map.get(draft, "excerpt")), - content: blank_to_nil(Map.get(draft, "content")), - tags: csv_to_list(Map.get(draft, "tags")), - categories: csv_to_list(Map.get(draft, "categories")), - author: blank_to_nil(Map.get(draft, "author")), - language: blank_to_nil(Map.get(draft, "language")), - do_not_translate: Map.get(draft, "do_not_translate", false), - template_slug: blank_to_nil(Map.get(draft, "template_slug")) - }) + defp save_canonical_draft(%Post{id: post_id}, draft, action) do + Posts.update_post( + post_id, + %{ + title: blank_to_nil(Map.get(draft, "title")), + excerpt: blank_to_nil(Map.get(draft, "excerpt")), + content: blank_to_nil(Map.get(draft, "content")), + tags: csv_to_list(Map.get(draft, "tags")), + categories: csv_to_list(Map.get(draft, "categories")), + author: blank_to_nil(Map.get(draft, "author")), + language: blank_to_nil(Map.get(draft, "language")), + do_not_translate: Map.get(draft, "do_not_translate", false), + template_slug: blank_to_nil(Map.get(draft, "template_slug")) + }, + auto_translate: action != :auto_save + ) end defp save_translation_draft(post_id, language, draft) do diff --git a/lib/bds/posts.ex b/lib/bds/posts.ex index a200f22..f22e563 100644 --- a/lib/bds/posts.ex +++ b/lib/bds/posts.ex @@ -106,7 +106,6 @@ defmodule BDS.Posts do {:ok, post} -> :ok = Embeddings.sync_post(post) :ok = Search.sync_post(post) - :ok = AutoTranslation.maybe_schedule(post) {:ok, post} error -> @@ -114,9 +113,9 @@ defmodule BDS.Posts do end end - @spec update_post(String.t(), attrs()) :: + @spec update_post(String.t(), attrs(), keyword()) :: {:ok, Post.t()} | {:error, :not_found | Ecto.Changeset.t()} - def update_post(post_id, attrs) do + def update_post(post_id, attrs, opts \\ []) do case Repo.get(Post, post_id) do nil -> {:error, :not_found} @@ -145,7 +144,13 @@ defmodule BDS.Posts do :ok = Embeddings.sync_post(updated_post) :ok = PostLinks.sync_post_links(updated_post) :ok = Search.sync_post(updated_post) - :ok = AutoTranslation.maybe_schedule(updated_post) + + # Auto-translation only runs on an explicit manual save from the + # editor, never on post creation or autosave. + if Keyword.get(opts, :auto_translate, false) do + :ok = AutoTranslation.maybe_schedule(updated_post) + end + {:ok, updated_post} error -> diff --git a/specs/editor_post.allium b/specs/editor_post.allium index c28dcc1..d6fd39b 100644 --- a/specs/editor_post.allium +++ b/specs/editor_post.allium @@ -231,6 +231,8 @@ rule PostTranslateAction { rule PostAutoTranslateOnSave { when: PostSaved(post_id) + -- Only an explicit manual save (or publish) triggers this. Auto-saves and + -- post creation (sidebar, deeplink/bookmarklet, import, scripting) never do. -- Gate: airplane mode check + auto_translate not disabled (doNotTranslate=false) -- For each configured blog language missing a translation: -- Enqueue background translation task (title model) diff --git a/specs/translation.allium b/specs/translation.allium index a21b0f4..6f96ba3 100644 --- a/specs/translation.allium +++ b/specs/translation.allium @@ -192,8 +192,9 @@ invariant FtsIncludesTranslations { -- Distilled from: lib/bds/posts/auto_translation.ex -- -- Two entry points share one translation primitive: --- 1. ScheduleAutoTranslation - reactive, fired after a post is created or --- updated. One background task per missing language produces a DRAFT +-- 1. ScheduleAutoTranslation - reactive, fired only by an explicit manual +-- save (or publish) in the post editor — never by post creation or +-- auto-save. One background task per missing language produces a DRAFT -- translation, then cascades to the post's linked media. -- 2. FillMissingTranslations - batch maintenance action. Scans every -- published post, AUTO-PUBLISHES the generated translations, fills linked @@ -210,7 +211,9 @@ surface AutoTranslationControlSurface { facing _: TranslationOperator provides: - -- Reactive trigger: emitted by post create/update side effects. + -- Reactive trigger: emitted only when a post is updated with + -- auto_translate enabled (the editor's manual save/publish path); + -- post creation and auto-save never emit it. PostSavedForAutoTranslation(post) -- Batch trigger: "fill missing translations" maintenance action. FillMissingTranslationsRequested(project) diff --git a/test/bds/post_translations_test.exs b/test/bds/post_translations_test.exs index e5885c5..3b9298a 100644 --- a/test/bds/post_translations_test.exs +++ b/test/bds/post_translations_test.exs @@ -143,7 +143,7 @@ defmodule BDS.PostTranslationsTest do assert {:ok, []} = Posts.list_post_translations(post.id) end - test "create_post enqueues and completes auto-translation tasks for missing project languages", + test "create_post does not enqueue auto-translation tasks", %{project: project} do configure_auto_translation_test_runtime() @@ -162,21 +162,79 @@ defmodule BDS.PostTranslationsTest do language: "en" }) - translations = wait_for_post_translations(post.id, ["de", "fr"]) - - assert Enum.map(translations, & &1.language) == ["de", "fr"] - assert Enum.all?(translations, &(&1.title == "Hallo Welt")) - assert Enum.all?(translations, &(&1.excerpt == "Kurze Zusammenfassung")) - assert Enum.all?(translations, &(&1.content == "# Hallo Welt\n\nUbersetzter Inhalt")) - - tasks = wait_for_ai_tasks(2) - - assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to de" and &1.status == :completed)) - assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to fr" and &1.status == :completed)) - assert Enum.all?(tasks, &(&1.group_name == "AI")) + assert ai_tasks() == [] + assert {:ok, []} = Posts.list_post_translations(post.id) end - test "update_post auto-translates missing languages and cascades linked media translations", + test "update_post without auto_translate does not enqueue auto-translation tasks", + %{project: project} do + configure_auto_translation_test_runtime() + + assert {:ok, _metadata} = + Metadata.update_project_metadata(project.id, %{ + main_language: "en", + blog_languages: ["en", "de"] + }) + + assert {:ok, post} = + Posts.create_post(%{ + project_id: project.id, + title: "Autosaved", + content: "Source body", + language: "en" + }) + + assert {:ok, _updated_post} = Posts.update_post(post.id, %{content: "Autosaved body"}) + + assert ai_tasks() == [] + assert {:ok, []} = Posts.list_post_translations(post.id) + end + + test "editor persist schedules auto-translation on manual save but not on auto-save", + %{project: project} do + configure_auto_translation_test_runtime() + + assert {:ok, metadata} = + Metadata.update_project_metadata(project.id, %{ + main_language: "en", + blog_languages: ["en", "de"] + }) + + assert {:ok, post} = + Posts.create_post(%{ + project_id: project.id, + title: "Editor Post", + content: "Source body", + language: "en" + }) + + draft = %{ + "title" => "Editor Post", + "excerpt" => "", + "content" => "Edited body", + "tags" => "", + "categories" => "", + "author" => "", + "language" => "en", + "do_not_translate" => false, + "template_slug" => "" + } + + alias BDS.Desktop.ShellLive.PostEditor.Persistence + + assert {:ok, _post} = Persistence.persist(post, draft, "en", metadata, :auto_save) + assert ai_tasks() == [] + + assert {:ok, _post} = Persistence.persist(post, draft, "en", metadata, :save) + + [translation] = wait_for_post_translations(post.id, ["de"]) + assert translation.language == "de" + + tasks = wait_for_ai_tasks(1) + assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to de" and &1.status == :completed)) + end + + test "update_post with auto_translate translates missing languages and cascades linked media translations", %{project: project, temp_dir: temp_dir} do configure_auto_translation_test_runtime() @@ -221,10 +279,14 @@ defmodule BDS.PostTranslationsTest do ]) assert {:ok, _updated_post} = - Posts.update_post(post.id, %{ - do_not_translate: false, - content: "Updated source body" - }) + Posts.update_post( + post.id, + %{ + do_not_translate: false, + content: "Updated source body" + }, + auto_translate: true + ) [translation] = wait_for_post_translations(post.id, ["de"]) assert translation.language == "de" @@ -427,8 +489,6 @@ defmodule BDS.PostTranslationsTest do end end - defp wait_for_ai_tasks(count, attempts \\ 100) - test "do_not_translate guard prevents translation upsert via the API", %{ project: project } do @@ -453,14 +513,19 @@ defmodule BDS.PostTranslationsTest do {"cannot add translations when do_not_translate is true", []} end + defp ai_tasks do + BDS.Tasks.list_tasks() + |> Enum.filter(&(&1.group_name == "AI")) + end + + defp wait_for_ai_tasks(count, attempts \\ 100) + defp wait_for_ai_tasks(_count, 0) do flunk("AI tasks did not reach expected state") end defp wait_for_ai_tasks(count, attempts) do - tasks = - BDS.Tasks.list_tasks() - |> Enum.filter(&(&1.group_name == "AI")) + tasks = ai_tasks() if length(tasks) >= count and Enum.all?(tasks, &(&1.status == :completed)) do tasks