feat: PLAN step 2 done
This commit is contained in:
@@ -1,17 +1,62 @@
|
||||
defmodule BDS.PostTranslationsTest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
alias BDS.AI
|
||||
alias BDS.Media
|
||||
alias BDS.Metadata
|
||||
alias BDS.Posts
|
||||
alias BDS.Repo
|
||||
|
||||
defmodule FakeRuntime do
|
||||
def generate(endpoint, request, opts) do
|
||||
test_pid = Keyword.fetch!(opts, :test_pid)
|
||||
send(test_pid, {:runtime_request, endpoint, request})
|
||||
|
||||
case request.operation do
|
||||
:translate_post ->
|
||||
{:ok,
|
||||
%{
|
||||
json: %{
|
||||
"title" => "Hallo Welt",
|
||||
"excerpt" => "Kurze Zusammenfassung",
|
||||
"content" => "# Hallo Welt\n\nUbersetzter Inhalt"
|
||||
},
|
||||
usage: %{input_tokens: 22, output_tokens: 14, cache_read_tokens: 0, cache_write_tokens: 0}
|
||||
}}
|
||||
|
||||
:translate_media ->
|
||||
{:ok,
|
||||
%{
|
||||
json: %{
|
||||
"title" => "Medientitel",
|
||||
"alt" => "Medien Alt",
|
||||
"caption" => "Medien Beschriftung"
|
||||
},
|
||||
usage: %{input_tokens: 12, output_tokens: 10, cache_read_tokens: 0, cache_write_tokens: 0}
|
||||
}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
setup do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
|
||||
:ok = BDS.Tasks.clear_finished()
|
||||
|
||||
temp_dir =
|
||||
Path.join(System.tmp_dir!(), "bds-post-translations-#{System.unique_integer([:positive])}")
|
||||
|
||||
File.mkdir_p!(temp_dir)
|
||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||
|
||||
original_posts_config = Application.get_env(:bds, :posts, [])
|
||||
|
||||
on_exit(fn ->
|
||||
File.rm_rf(temp_dir)
|
||||
Application.put_env(:bds, :posts, original_posts_config)
|
||||
_ = BDS.Tasks.clear_finished()
|
||||
end)
|
||||
|
||||
{:ok, project} = BDS.Projects.create_project(%{name: "Translations", data_path: temp_dir})
|
||||
%{project: project, temp_dir: temp_dir}
|
||||
@@ -75,11 +120,119 @@ defmodule BDS.PostTranslationsTest do
|
||||
assert reopened_translation.content == "Neuer Entwurf"
|
||||
assert reopened_translation.updated_at >= published_translation.updated_at
|
||||
|
||||
reopened_source = Posts.get_post!(post.id)
|
||||
assert reopened_source.status == :draft
|
||||
assert reopened_source.content == "Hello world"
|
||||
assert reopened_source.file_path ==
|
||||
String.replace_suffix(published_translation.file_path, ".de.md", ".md")
|
||||
|
||||
assert {:ok, :deleted} = Posts.delete_post_translation(reopened_translation.id)
|
||||
refute File.exists?(translation_path)
|
||||
assert {:ok, []} = Posts.list_post_translations(post.id)
|
||||
end
|
||||
|
||||
test "create_post enqueues and completes auto-translation tasks for missing project languages",
|
||||
%{project: project} do
|
||||
configure_auto_translation_test_runtime()
|
||||
|
||||
assert {:ok, _metadata} =
|
||||
Metadata.update_project_metadata(project.id, %{
|
||||
main_language: "en",
|
||||
blog_languages: ["en", "de", "fr"]
|
||||
})
|
||||
|
||||
assert {:ok, post} =
|
||||
Posts.create_post(%{
|
||||
project_id: project.id,
|
||||
title: "Hello World",
|
||||
excerpt: "Short summary",
|
||||
content: "# Hello\n\nSource body",
|
||||
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"))
|
||||
end
|
||||
|
||||
test "update_post auto-translates missing languages and cascades linked media translations",
|
||||
%{project: project, temp_dir: temp_dir} 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: "Needs Translation",
|
||||
excerpt: "Draft excerpt",
|
||||
content: "Source body",
|
||||
language: "en",
|
||||
do_not_translate: true
|
||||
})
|
||||
|
||||
source_path = Path.join(temp_dir, "linked-media.txt")
|
||||
File.write!(source_path, "linked media")
|
||||
|
||||
assert {:ok, media} =
|
||||
Media.import_media(%{
|
||||
project_id: project.id,
|
||||
source_path: source_path,
|
||||
title: "Source Media",
|
||||
alt: "Source Alt",
|
||||
caption: "Source Caption",
|
||||
language: "en"
|
||||
})
|
||||
|
||||
Repo.insert_all("post_media", [
|
||||
%{
|
||||
id: Ecto.UUID.generate(),
|
||||
project_id: project.id,
|
||||
post_id: post.id,
|
||||
media_id: media.id,
|
||||
sort_order: 0,
|
||||
created_at: BDS.Persistence.now_ms()
|
||||
}
|
||||
])
|
||||
|
||||
assert {:ok, _updated_post} =
|
||||
Posts.update_post(post.id, %{
|
||||
do_not_translate: false,
|
||||
content: "Updated source body"
|
||||
})
|
||||
|
||||
[translation] = wait_for_post_translations(post.id, ["de"])
|
||||
assert translation.language == "de"
|
||||
|
||||
media_translation = wait_for_media_translation(media.id, "de")
|
||||
assert media_translation.title == "Medientitel"
|
||||
assert media_translation.alt == "Medien Alt"
|
||||
assert media_translation.caption == "Medien Beschriftung"
|
||||
|
||||
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, fn task ->
|
||||
task.name == "Auto-translate Media to de" and task.status == :completed
|
||||
end)
|
||||
|
||||
assert File.exists?(Path.join(temp_dir, media.file_path <> ".de.meta"))
|
||||
end
|
||||
|
||||
test "validate_translations reports missing languages, orphan translation files, and do-not-translate posts",
|
||||
%{project: project, temp_dir: temp_dir} do
|
||||
assert {:ok, _metadata} =
|
||||
@@ -145,4 +298,84 @@ defmodule BDS.PostTranslationsTest do
|
||||
assert report.orphan_files == ["posts/2026/04/orphan.fr.md"]
|
||||
assert report.do_not_translate_posts == [ignored_post.id]
|
||||
end
|
||||
|
||||
defp configure_auto_translation_test_runtime do
|
||||
assert {:ok, _endpoint} =
|
||||
AI.put_endpoint(
|
||||
:online,
|
||||
%{
|
||||
url: "https://api.example.test/v1",
|
||||
api_key: "online-secret",
|
||||
model: "gpt-4o-mini"
|
||||
}
|
||||
)
|
||||
|
||||
assert :ok = AI.set_airplane_mode(false)
|
||||
assert :ok = AI.put_model_preference(:title, "gpt-4.1-mini")
|
||||
|
||||
Application.put_env(:bds, :posts,
|
||||
auto_translation_ai_opts: [
|
||||
runtime: FakeRuntime,
|
||||
test_pid: self()
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
defp wait_for_post_translations(post_id, languages, attempts \\ 100)
|
||||
|
||||
defp wait_for_post_translations(_post_id, _languages, 0) do
|
||||
flunk("post translations did not reach expected state")
|
||||
end
|
||||
|
||||
defp wait_for_post_translations(post_id, languages, attempts) do
|
||||
{:ok, translations} = Posts.list_post_translations(post_id)
|
||||
expected = Enum.sort(languages)
|
||||
|
||||
if Enum.sort(Enum.map(translations, & &1.language)) == expected do
|
||||
translations
|
||||
else
|
||||
Process.sleep(20)
|
||||
wait_for_post_translations(post_id, languages, attempts - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_for_media_translation(media_id, language, attempts \\ 100)
|
||||
|
||||
defp wait_for_media_translation(_media_id, _language, 0) do
|
||||
flunk("media translation did not reach expected state")
|
||||
end
|
||||
|
||||
defp wait_for_media_translation(media_id, language, attempts) do
|
||||
translation =
|
||||
Repo.one(
|
||||
from translation in BDS.Media.Translation,
|
||||
where: translation.translation_for == ^media_id and translation.language == ^language
|
||||
)
|
||||
|
||||
if translation do
|
||||
translation
|
||||
else
|
||||
Process.sleep(20)
|
||||
wait_for_media_translation(media_id, language, attempts - 1)
|
||||
end
|
||||
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"))
|
||||
|
||||
if length(tasks) >= count and Enum.all?(tasks, &(&1.status == :completed)) do
|
||||
tasks
|
||||
else
|
||||
Process.sleep(20)
|
||||
wait_for_ai_tasks(count, attempts - 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user