feat: closing last gaps in backend functions we have available

This commit is contained in:
2026-04-24 12:15:56 +02:00
parent 13ace08a41
commit f96759ab2f
16 changed files with 610 additions and 62 deletions

View File

@@ -0,0 +1,62 @@
defmodule BDS.CliSyncTest do
use ExUnit.Case, async: false
import Ecto.Query
alias BDS.CliSync
alias BDS.Repo
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Repo.delete_all(BDS.CliSync.Notification)
:ok
end
test "cli mutations are written to db_notifications, processed on file change, and marked seen" do
assert {:ok, notification} = CliSync.cli_mutation_performed("post", "post-1", :updated)
assert notification.from_cli == true
assert notification.seen_at == nil
assert {:ok, processed} = CliSync.db_file_change_detected()
assert [%{entity_type: "post", entity_id: "post-1", action: :updated}] = processed
seen_notification = Repo.get!(BDS.CliSync.Notification, notification.id)
assert is_integer(seen_notification.seen_at)
end
test "processed notifications are pruned after one hour and unprocessed notifications after one day" do
now = BDS.Persistence.now_ms()
Repo.insert!(%BDS.CliSync.Notification{
entity_type: "post",
entity_id: "processed-old",
action: :updated,
from_cli: true,
seen_at: now - 10,
created_at: now - 3_600_001
})
Repo.insert!(%BDS.CliSync.Notification{
entity_type: "media",
entity_id: "unprocessed-old",
action: :deleted,
from_cli: true,
seen_at: nil,
created_at: now - 86_400_001
})
Repo.insert!(%BDS.CliSync.Notification{
entity_type: "script",
entity_id: "fresh",
action: :created,
from_cli: true,
seen_at: nil,
created_at: now
})
assert {:ok, %{processed: 1, unprocessed: 1}} = CliSync.prune_notifications(now)
remaining_ids = Repo.all(from notification in BDS.CliSync.Notification, select: notification.entity_id)
assert remaining_ids == ["fresh"]
end
end

View File

@@ -103,10 +103,16 @@ defmodule BDS.GenerationTest do
"feed.xml",
"atom.xml",
"calendar.json",
"pagefind/index.json",
"pagefind/pagefind-ui.css",
"pagefind/pagefind-ui.js",
"de/404.html",
"de/index.html",
"de/feed.xml",
"de/atom.xml"
"de/atom.xml",
"de/pagefind/index.json",
"de/pagefind/pagefind-ui.css",
"de/pagefind/pagefind-ui.js"
]
assert Enum.sort(Enum.map(result.generated_files, & &1.relative_path)) ==
@@ -127,7 +133,7 @@ defmodule BDS.GenerationTest do
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
main_language: "en",
blog_languages: ["en"]
blog_languages: ["en", "de"]
})
assert {:ok, list_template} =
@@ -178,6 +184,25 @@ defmodule BDS.GenerationTest do
post_html = File.read!(Path.join([temp_dir, "html", post_path]))
assert post_html =~ "post-template"
assert post_html =~ "Rendered body"
assert "pagefind/index.json" in relative_paths
assert "pagefind/pagefind-ui.js" in relative_paths
assert "de/pagefind/index.json" in relative_paths
pagefind_index =
Path.join([temp_dir, "html", "pagefind", "index.json"])
|> File.read!()
|> Jason.decode!()
assert pagefind_index["language"] == "en"
assert Enum.any?(pagefind_index["pages"], &(&1["url"] == "/#{post_path}"))
de_pagefind_index =
Path.join([temp_dir, "html", "de", "pagefind", "index.json"])
|> File.read!()
|> Jason.decode!()
assert de_pagefind_index["language"] == "de"
end
test "generation renders copied starter templates with partials, i18n, and markdown", %{

View File

@@ -2,6 +2,7 @@ defmodule BDS.MCPTest do
use ExUnit.Case, async: false
alias BDS.Media.Media
alias BDS.MCP.ProposalStore
alias BDS.Repo
alias BDS.Scripts.Script
alias BDS.Templates.Template
@@ -168,6 +169,39 @@ defmodule BDS.MCPTest do
assert_raise Ecto.NoResultsError, fn -> BDS.Posts.get_post!(draft_post_id) end
end
test "proposal lifecycle is persisted with pending, accepted, discarded, and expired statuses" do
assert {:ok, accepted_result} =
BDS.MCP.call_tool("draft_post", %{title: "Accept Me", content: "Body"})
accepted_id = accepted_result["proposal_id"]
assert ProposalStore.get(accepted_id).status == :pending
assert {:ok, _accepted} = BDS.MCP.call_tool("accept_proposal", %{proposalId: accepted_id})
accepted_proposal = ProposalStore.get(accepted_id)
assert accepted_proposal.status == :accepted
assert accepted_proposal.entity_id == accepted_result["post"]["id"]
assert {:ok, discarded_result} =
BDS.MCP.call_tool("draft_post", %{title: "Discard Me Later", content: "Body"})
discarded_id = discarded_result["proposal_id"]
assert {:ok, _discarded} = BDS.MCP.call_tool("discard_proposal", %{proposalId: discarded_id})
discarded_proposal = ProposalStore.get(discarded_id)
assert discarded_proposal.status == :discarded
expired =
ProposalStore.create("draft_post", %{"post_id" => "expired-post"},
entity_id: "expired-post",
ttl_ms: -1
)
expired_proposals = ProposalStore.cleanup_expired()
assert Enum.any?(expired_proposals, &(&1.id == expired.id and &1.status == :expired))
assert ProposalStore.get(expired.id).status == :expired
end
test "resource listing and reads follow old app naming for implemented resources", %{project: project} do
assert {:ok, post} =
BDS.Posts.create_post(%{

View File

@@ -1,8 +1,11 @@
defmodule BDS.PublishingTest do
use ExUnit.Case, async: false
alias BDS.Repo
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
:ok = Ecto.Adapters.SQL.Sandbox.allow(BDS.Repo, self(), Process.whereis(BDS.Publishing))
temp_dir =
Path.join(System.tmp_dir!(), "bds-publishing-#{System.unique_integer([:positive])}")
@@ -277,6 +280,43 @@ defmodule BDS.PublishingTest do
assert elem(html_upload, 1) == ["-q", html_index, "deploy@example.com:/srv/blog/index.html"]
end
test "publish jobs survive a publishing server restart because they are persisted", %{
project: project,
temp_dir: temp_dir
} do
File.mkdir_p!(Path.join([temp_dir, "html"]))
File.write!(Path.join([temp_dir, "html", "index.html"]), "<html />")
credentials = %{
ssh_host: "example.com",
ssh_user: "deploy",
ssh_remote_path: "/srv/blog",
ssh_mode: :rsync
}
assert {:ok, job} =
BDS.Publishing.upload_site(project.id, credentials, uploader: fn _, _, _ -> :ok end)
assert wait_for_publish_job(job.id, &(&1.status == :completed)).status == :completed
persisted_before_restart = Repo.get!(BDS.Publishing.PublishJob, job.id)
assert persisted_before_restart.status == :completed
publishing_pid = Process.whereis(BDS.Publishing)
ref = Process.monitor(publishing_pid)
:ok = GenServer.stop(BDS.Publishing, :normal)
assert_receive {:DOWN, ^ref, :process, ^publishing_pid, _reason}
restarted_pid = wait_for_process(BDS.Publishing)
refute restarted_pid == publishing_pid
:ok = Ecto.Adapters.SQL.Sandbox.allow(BDS.Repo, self(), restarted_pid)
persisted_after_restart = BDS.Publishing.get_job(job.id)
assert persisted_after_restart.id == job.id
assert persisted_after_restart.status == :completed
end
defp collect_command_runs(acc \\ []) do
receive do
{:command_run, command, args, _opts} -> collect_command_runs([{command, args} | acc])
@@ -301,4 +341,21 @@ defmodule BDS.PublishingTest do
defp wait_for_publish_job(_job_id, _predicate, 0) do
flunk("publish job did not reach expected state")
end
defp wait_for_process(name, attempts \\ 100)
defp wait_for_process(name, attempts) when attempts > 0 do
case Process.whereis(name) do
nil ->
Process.sleep(20)
wait_for_process(name, attempts - 1)
pid ->
pid
end
end
defp wait_for_process(name, 0) do
flunk("process #{inspect(name)} did not restart")
end
end

View File

@@ -197,6 +197,29 @@ defmodule BDS.Repo.SchemaMigrationTest do
"created_at",
"updated_at"
],
"publish_jobs" => [
"id",
"project_id",
"ssh_host",
"ssh_user",
"ssh_remote_path",
"ssh_mode",
"status",
"task_id",
"targets",
"error",
"inserted_at",
"updated_at"
],
"mcp_proposals" => [
"id",
"kind",
"status",
"entity_id",
"data",
"created_at",
"expires_at"
],
"db_notifications" => [
"id",
"entity_type",
@@ -251,6 +274,12 @@ defmodule BDS.Repo.SchemaMigrationTest do
"generated_file_hashes_project_path_idx"
) == ["project_id", "relative_path"]
assert unique_index_columns("mcp_proposals", "mcp_proposals_entity_idx") == [
"kind",
"entity_id",
"status"
]
assert unique_index_columns("dismissed_duplicate_pairs", "dismissed_pairs_idx") == [
"project_id",
"post_id_a",
@@ -296,6 +325,7 @@ defmodule BDS.Repo.SchemaMigrationTest do
assert column_metadata("posts", "file_path")[:default] == ""
assert column_metadata("posts", "do_not_translate")[:default] == "false"
assert column_metadata("post_media", "sort_order")[:default] == "0"
assert column_metadata("publish_jobs", "status")[:default] == "pending"
assert column_metadata("db_notifications", "from_cli")[:default] == "true"
assert foreign_keys("posts") == [%{from: "project_id", table: "projects", to: "id"}]
@@ -316,6 +346,10 @@ defmodule BDS.Repo.SchemaMigrationTest do
%{from: "translation_for", table: "media", to: "id"}
]
assert foreign_keys("publish_jobs") == [
%{from: "project_id", table: "projects", to: "id"}
]
assert foreign_keys("chat_messages") == [
%{from: "conversation_id", table: "chat_conversations", to: "id"}
]

View File

@@ -275,11 +275,17 @@ defmodule BDS.SearchTest do
languages = BDS.Search.list_stemmer_languages()
assert is_list(languages)
assert length(languages) == 24
assert "en" in languages
assert "de" in languages
assert "fr" in languages
assert "it" in languages
assert "es" in languages
assert "ar" in languages
assert "ca" in languages
assert "el" in languages
assert "ga" in languages
assert "hi" in languages
assert Enum.uniq(languages) == languages
end
end