From e20913b8e56e018a179a924fdb97bef9676c2210 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 25 Apr 2026 16:58:30 +0200 Subject: [PATCH] fix: hopefully full fix for rebuilding Co-authored-by: Copilot --- lib/bds/posts.ex | 7 +- test/bds/posts_test.exs | 110 ++++++++++++++++++ .../bds/real_blog_rebuild_diagnostic_test.exs | 46 ++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) diff --git a/lib/bds/posts.ex b/lib/bds/posts.ex index 09c39c1..a6ffcad 100644 --- a/lib/bds/posts.ex +++ b/lib/bds/posts.ex @@ -688,7 +688,10 @@ defmodule BDS.Posts do published_excerpt: nil } - post = Repo.get_by(Post, project_id: project_id, slug: attrs.slug) || %Post{} + post = + Repo.get(Post, attrs.id) || + Repo.get_by(Post, project_id: project_id, file_path: rebuild_file.relative_path) || + Repo.get_by(Post, project_id: project_id, slug: attrs.slug) || %Post{} post |> Post.changeset(attrs) @@ -700,7 +703,7 @@ defmodule BDS.Posts do defp upsert_post_translation_from_rebuild_file(project_id, rebuild_file) do fields = rebuild_file.fields source_post_id = Map.fetch!(fields, "translationFor") - source_post = Repo.get!(Post, source_post_id) + source_post = Repo.get_by!(Post, project_id: project_id, id: source_post_id) now = Persistence.now_ms() language = normalize_language(Map.fetch!(fields, "language")) diff --git a/test/bds/posts_test.exs b/test/bds/posts_test.exs index dd732a1..c190f4b 100644 --- a/test/bds/posts_test.exs +++ b/test/bds/posts_test.exs @@ -508,6 +508,116 @@ defmodule BDS.PostsTest do "Introducing Thirty Ten, my guide to creating a Twenty Ten Child Theme | aaron.jorb.inaaron.jorb.in" end + test "rebuild_posts_from_files realigns an existing slug match to the canonical file id before importing translations", + %{project: project} do + assert {:ok, _stale_post} = + BDS.Posts.create_post(%{ + project_id: project.id, + title: "Old Title", + slug: "chimera", + content: "stale body" + }) + + posts_dir = Path.join([BDS.Projects.project_data_dir(project), "posts", "2026", "04"]) + File.mkdir_p!(posts_dir) + + File.write!( + Path.join(posts_dir, "chimera.md"), + [ + "---", + "id: canonical-post-id", + "title: Chimera Source", + "slug: chimera", + "status: published", + "language: de", + "createdAt: 2024-03-30T21:20:00.000Z", + "updatedAt: 2024-03-31T21:20:00.000Z", + "publishedAt: 2024-04-01T21:20:00.000Z", + "---", + "Quelle", + "" + ] + |> Enum.join("\n") + ) + + File.write!( + Path.join(posts_dir, "chimera.en.md"), + [ + "---", + "id: translation-from-old-app", + "translationFor: canonical-post-id", + "language: en", + "title: Chimera", + "excerpt: Imported translation", + "---", + "Translated body", + "" + ] + |> Enum.join("\n") + ) + + assert {:ok, [post]} = BDS.Posts.rebuild_posts_from_files(project.id) + assert post.id == "canonical-post-id" + assert post.slug == "chimera" + + assert {:ok, [translation]} = BDS.Posts.list_post_translations(post.id) + assert translation.translation_for == post.id + end + + test "rebuild_posts_from_files repairs a stale row that matches by file path but has a broken slug", + %{project: project} do + now = BDS.Persistence.now_ms() + relative_path = "posts/2010/11/chimera.md" + + stale_post = + %BDS.Posts.Post{} + |> BDS.Posts.Post.changeset(%{ + id: "stale-post-id", + project_id: project.id, + title: ">-", + slug: ">-", + status: :published, + created_at: now, + updated_at: now, + file_path: relative_path, + do_not_translate: false + }) + |> BDS.Repo.insert!() + + posts_dir = Path.join([BDS.Projects.project_data_dir(project), "posts", "2010", "11"]) + File.mkdir_p!(posts_dir) + + File.write!( + Path.join(posts_dir, "chimera.md"), + [ + "---", + "id: canonical-post-id", + "title: Chimera Source", + "slug: chimera", + "status: published", + "language: de", + "createdAt: 2024-03-30T21:20:00.000Z", + "updatedAt: 2024-03-31T21:20:00.000Z", + "publishedAt: 2024-04-01T21:20:00.000Z", + "---", + "Quelle", + "" + ] + |> Enum.join("\n") + ) + + assert {:ok, [post]} = BDS.Posts.rebuild_posts_from_files(project.id) + assert post.id == "canonical-post-id" + assert post.slug == "chimera" + + posts = + BDS.Repo.all(BDS.Posts.Post) + |> Enum.filter(&(&1.project_id == project.id)) + + assert length(posts) == 1 + refute BDS.Repo.get(BDS.Posts.Post, stale_post.id) + end + defp errors_on(changeset) do Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> Regex.replace(~r"%{(\w+)}", message, fn _, key -> diff --git a/test/bds/real_blog_rebuild_diagnostic_test.exs b/test/bds/real_blog_rebuild_diagnostic_test.exs index 9be0182..f88a475 100644 --- a/test/bds/real_blog_rebuild_diagnostic_test.exs +++ b/test/bds/real_blog_rebuild_diagnostic_test.exs @@ -7,6 +7,7 @@ defmodule BDS.RealBlogRebuildDiagnosticTest do if is_binary(@real_blog_path) and @real_blog_path != "" do setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo, ownership_timeout: 600_000) + Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()}) now = BDS.Persistence.now_ms() unique = Integer.to_string(System.unique_integer([:positive])) @@ -34,6 +35,43 @@ defmodule BDS.RealBlogRebuildDiagnosticTest do test "rebuilds media from the external real blog path", %{project: project} do assert {:ok, _media} = BDS.Maintenance.rebuild_from_filesystem(project.id, "media") end + + test "shell rebuild task rebuilds posts from the external real blog path", %{project: project} do + :ok = BDS.Tasks.clear_finished() + assert {:ok, _active} = BDS.Projects.set_active_project(project.id) + + assert {:ok, result} = BDS.Desktop.ShellCommands.execute("rebuild_database") + assert result.kind == "task_queued" + + task = + wait_for_named_task( + "Rebuild Posts From Files", + &(&1.status in [:completed, :failed]), + 600_000 + ) + + assert task.status == :completed + end + + test "rebuilds posts from the external real blog path twice in the same project", %{project: project} do + assert {:ok, _posts} = BDS.Maintenance.rebuild_from_filesystem(project.id, "post") + assert {:ok, _posts} = BDS.Maintenance.rebuild_from_filesystem(project.id, "post") + end + + defp wait_for_named_task(_name, _matcher, timeout) when timeout <= 0 do + flunk("named task did not reach expected state") + end + + defp wait_for_named_task(name, matcher, timeout) do + task = Enum.find(BDS.Tasks.list_tasks(), &(&1.name == name)) + + if task && matcher.(task) do + task + else + Process.sleep(100) + wait_for_named_task(name, matcher, timeout - 100) + end + end else @tag skip: "BDS_REAL_BLOG_PATH not set" test "rebuilds posts from the external real blog path" do @@ -42,5 +80,13 @@ defmodule BDS.RealBlogRebuildDiagnosticTest do @tag skip: "BDS_REAL_BLOG_PATH not set" test "rebuilds media from the external real blog path" do end + + @tag skip: "BDS_REAL_BLOG_PATH not set" + test "shell rebuild task rebuilds posts from the external real blog path" do + end + + @tag skip: "BDS_REAL_BLOG_PATH not set" + test "rebuilds posts from the external real blog path twice in the same project" do + end end end