chore: added more @spec

This commit is contained in:
2026-05-01 17:49:50 +02:00
parent abcae1dad7
commit 881056eb61
157 changed files with 6223 additions and 1647 deletions

View File

@@ -197,11 +197,13 @@ defmodule BDS.AITest do
test "put_endpoint, get_endpoint, and delete_endpoint manage encrypted endpoint settings" do
assert {:ok, endpoint} =
BDS.AI.put_endpoint(:online, %{
url: "https://api.example.test/v1",
api_key: "top-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:online,
%{
url: "https://api.example.test/v1",
api_key: "top-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
assert endpoint.kind == :online
assert endpoint.url == "https://api.example.test/v1"
@@ -263,21 +265,26 @@ defmodule BDS.AITest do
assert {:ok, result} = BDS.AI.refresh_model_catalog(http_client: http_client)
assert result.not_modified == true
assert_received {:conditional_headers, %{"accept" => "application/json", "if-none-match" => "W/\"catalog-v1\""}}
assert_received {:conditional_headers,
%{"accept" => "application/json", "if-none-match" => "W/\"catalog-v1\""}}
end
test "list_endpoint_models reads openai-compatible models from the configured endpoint" do
assert {:ok, models} =
BDS.AI.list_endpoint_models(%{url: "https://api.example.test/v1", api_key: "online-secret"},
BDS.AI.list_endpoint_models(
%{url: "https://api.example.test/v1", api_key: "online-secret"},
http_client: FakeEndpointHttpClient
)
assert [%{id: "gpt-4.1", label: "gpt-4.1"}, %{id: "gpt-4.1-mini", label: "gpt-4.1-mini"}] = models
assert [%{id: "gpt-4.1", label: "gpt-4.1"}, %{id: "gpt-4.1-mini", label: "gpt-4.1-mini"}] =
models
end
test "list_endpoint_models returns an error for malformed endpoint JSON" do
assert {:error, %{kind: :invalid_json_response, reason: %Jason.DecodeError{}}} =
BDS.AI.list_endpoint_models(%{url: "https://api.example.test/v1", api_key: "online-secret"},
BDS.AI.list_endpoint_models(
%{url: "https://api.example.test/v1", api_key: "online-secret"},
http_client: BadJsonEndpointHttpClient
)
end
@@ -303,18 +310,22 @@ defmodule BDS.AITest do
test "airplane mode routes title tasks to airplane endpoint and offline title model" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:online, %{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:online,
%{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:airplane, %{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:airplane,
%{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
}, secret_backend: FakeSecretBackend)
assert :ok = BDS.AI.set_airplane_mode(true)
assert :ok = BDS.AI.put_model_preference(:airplane_title, "llama3.1")
@@ -337,18 +348,24 @@ defmodule BDS.AITest do
test "translate_post uses the online title model when airplane mode is disabled" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:online, %{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:online,
%{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
assert :ok = BDS.AI.set_airplane_mode(false)
assert :ok = BDS.AI.put_model_preference(:title, "gpt-4.1-mini")
assert {:ok, translation} =
BDS.AI.translate_post(
%{title: "Hello World", excerpt: "Short summary", content: "# Hello\n\nSource body"},
%{
title: "Hello World",
excerpt: "Short summary",
content: "# Hello\n\nSource body"
},
"de",
runtime: FakeRuntime,
test_pid: self(),
@@ -366,11 +383,13 @@ defmodule BDS.AITest do
test "analyze_import_taxonomy uses the selected model override and returns only valid existing-term mappings" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:online, %{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:online,
%{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
assert :ok = BDS.AI.set_airplane_mode(false)
assert :ok = BDS.AI.put_model_preference(:title, "gpt-4.1-mini")
@@ -396,23 +415,26 @@ defmodule BDS.AITest do
test "analyze_image requires a vision-capable airplane model before sending image input" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:airplane, %{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:airplane,
%{
url: "http://localhost:11434/v1",
api_key: nil,
model: "llama-default"
}, secret_backend: FakeSecretBackend)
assert :ok = BDS.AI.set_airplane_mode(true)
assert :ok = BDS.AI.put_model_preference(:airplane_image_analysis, "llama3.2")
assert {:error, %{kind: :model_capability_missing}} =
BDS.AI.analyze_image(%{
mime_type: "image/png",
title: "Source",
alt: nil,
caption: nil,
image_url: "file:///tmp/test.png"
}, runtime: FakeRuntime, test_pid: self(), secret_backend: FakeSecretBackend)
BDS.AI.analyze_image(
%{
mime_type: "image/png",
title: "Source",
alt: nil,
caption: nil,
image_url: "file:///tmp/test.png"
}, runtime: FakeRuntime, test_pid: self(), secret_backend: FakeSecretBackend)
assert :ok =
BDS.AI.put_model_capabilities("llama3.2", %{
@@ -421,13 +443,14 @@ defmodule BDS.AITest do
})
assert {:ok, analysis} =
BDS.AI.analyze_image(%{
mime_type: "image/png",
title: "Source",
alt: nil,
caption: nil,
image_url: "file:///tmp/test.png"
}, runtime: FakeRuntime, test_pid: self(), secret_backend: FakeSecretBackend)
BDS.AI.analyze_image(
%{
mime_type: "image/png",
title: "Source",
alt: nil,
caption: nil,
image_url: "file:///tmp/test.png"
}, runtime: FakeRuntime, test_pid: self(), secret_backend: FakeSecretBackend)
assert analysis.alt == "Orange sunset over calm water"
@@ -442,11 +465,13 @@ defmodule BDS.AITest do
:ok = seed_project_content(project.id)
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:online, %{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:online,
%{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
assert :ok = BDS.AI.set_airplane_mode(false)
assert {:ok, conversation} = BDS.AI.start_chat(%{model: "gpt-4o-mini"})
@@ -462,13 +487,13 @@ defmodule BDS.AITest do
assert reply.assistant_message.content == "You currently have 1 post and 1 media item."
messages = BDS.AI.list_chat_messages(conversation.id)
assert Enum.map(messages, & &1.role) == [:user, :assistant, :tool, :assistant]
assert Enum.map(messages, & &1.role) == [:user, :assistant, :tool, :assistant]
assistant_tool_call = Enum.at(messages, 1)
tool_message = Enum.at(messages, 2)
assistant_message = Enum.at(messages, 3)
assistant_tool_call = Enum.at(messages, 1)
tool_message = Enum.at(messages, 2)
assistant_message = Enum.at(messages, 3)
assert [%{"id" => "call-blog-stats", "name" => "blog_stats"}] = assistant_tool_call.tool_calls
assert [%{"id" => "call-blog-stats", "name" => "blog_stats"}] = assistant_tool_call.tool_calls
assert tool_message.tool_call_id == "call-blog-stats"
assert tool_message.content =~ "post_count"
assert assistant_message.token_usage_input == 64
@@ -489,11 +514,13 @@ defmodule BDS.AITest do
test "cancel_chat aborts an in-flight chat turn" do
assert {:ok, _endpoint} =
BDS.AI.put_endpoint(:online, %{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
BDS.AI.put_endpoint(
:online,
%{
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}, secret_backend: FakeSecretBackend)
assert {:ok, conversation} = BDS.AI.start_chat(%{model: "gpt-4o-mini"})

View File

@@ -38,7 +38,8 @@ defmodule BDS.CliSyncTest do
:ok = Watcher.poll_now(watcher)
assert_receive {:entity_changed, %{entity: "post", entity_id: "post-1", action: :updated}}, 500
assert_receive {:entity_changed, %{entity: "post", entity_id: "post-1", action: :updated}},
500
seen_notification = Repo.get!(BDS.CliSync.Notification, notification.id)
assert is_integer(seen_notification.seen_at)
@@ -76,7 +77,9 @@ defmodule BDS.CliSyncTest do
assert {:ok, %{processed: 1, unprocessed: 1}} = CliSync.prune_notifications(now)
remaining_ids = Repo.all(from notification in BDS.CliSync.Notification, select: notification.entity_id)
remaining_ids =
Repo.all(from notification in BDS.CliSync.Notification, select: notification.entity_id)
assert remaining_ids == ["fresh"]
end
end

View File

@@ -25,6 +25,7 @@ defmodule BDS.Desktop.AutomationTest do
assert snapshot.assistant_visible == false
assert snapshot.panel_visible == false
assert snapshot.editor_title == "Dashboard"
assert snapshot.activity_labels == [
"Posts",
"Pages",
@@ -37,6 +38,7 @@ defmodule BDS.Desktop.AutomationTest do
"Git",
"Settings"
]
assert "Drafts" in snapshot.sidebar_sections
assert "Status" in snapshot.editor_meta_labels
@@ -155,7 +157,10 @@ defmodule BDS.Desktop.AutomationTest do
end
defp automation_process_counts do
%{app: count_processes("scripts/desktop_automation_app\\.exs"), driver: count_processes("desktop_automation_runner\\.mjs")}
%{
app: count_processes("scripts/desktop_automation_app\\.exs"),
driver: count_processes("desktop_automation_runner\\.mjs")
}
end
defp count_processes(pattern) do

View File

@@ -13,7 +13,9 @@ defmodule BDS.Desktop.ImportShellLiveTest do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
temp_dir = Path.join(System.tmp_dir!(), "bds-import-shell-live-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-import-shell-live-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -23,7 +25,8 @@ defmodule BDS.Desktop.ImportShellLiveTest do
%{project: project, temp_dir: temp_dir}
end
test "opening an import definition renders the dedicated import analysis editor instead of the fallback shell frame", %{project: project, temp_dir: temp_dir} do
test "opening an import definition renders the dedicated import analysis editor instead of the fallback shell frame",
%{project: project, temp_dir: temp_dir} do
uploads_dir = Path.join(temp_dir, "uploads")
wxr_path = Path.join(temp_dir, "legacy.xml")
@@ -89,7 +92,13 @@ defmodule BDS.Desktop.ImportShellLiveTest do
},
post_stats: %{new_count: 1, update_count: 0, conflict_count: 1, duplicate_count: 0},
page_stats: %{new_count: 1, update_count: 0, conflict_count: 0, duplicate_count: 0},
media_stats: %{new_count: 1, update_count: 0, conflict_count: 0, duplicate_count: 0, missing_count: 0},
media_stats: %{
new_count: 1,
update_count: 0,
conflict_count: 0,
duplicate_count: 0,
missing_count: 0
},
category_stats: %{existing_count: 0, mapped_count: 0, new_count: 1},
tag_stats: %{existing_count: 0, mapped_count: 0, new_count: 1},
date_distribution: [%{year: 2024, post_count: 2, media_count: 1}],
@@ -119,7 +128,13 @@ defmodule BDS.Desktop.ImportShellLiveTest do
items: %{
posts: [
%{item_type: "post", title: "Hello World", slug: "hello-world", status: "new"},
%{item_type: "post", title: "Conflict Me", slug: "conflict-me", status: "conflict", resolution: "ignore"}
%{
item_type: "post",
title: "Conflict Me",
slug: "conflict-me",
status: "conflict",
resolution: "ignore"
}
],
pages: [
%{item_type: "page", title: "About", slug: "about", status: "new"}

View File

@@ -4,8 +4,14 @@ defmodule BDS.Desktop.MainWindowTest do
alias BDS.Desktop.MainWindow
setup do
path = Path.join(System.tmp_dir!(), "bds-main-window-state-#{System.unique_integer([:positive])}.json")
path =
Path.join(
System.tmp_dir!(),
"bds-main-window-state-#{System.unique_integer([:positive])}.json"
)
previous = Application.get_env(:bds, :desktop, [])
updated =
previous
|> Keyword.put(:window_state_path, path)
@@ -21,7 +27,9 @@ defmodule BDS.Desktop.MainWindowTest do
%{path: path}
end
test "window options use a smaller safe default and restore persisted size and position", %{path: path} do
test "window options use a smaller safe default and restore persisted size and position", %{
path: path
} do
opts = MainWindow.window_options()
assert opts[:size] == {1280, 780}

View File

@@ -23,7 +23,9 @@ defmodule BDS.Desktop.OverlayTest do
assert language_picker.kind == :language_picker
assert language_picker.source_language == "en"
assert Enum.map(language_picker.available_targets, & &1.code) == ["de", "fr"]
assert Enum.find(language_picker.available_targets, &(&1.code == "de")).has_existing_translation == true
assert Enum.find(language_picker.available_targets, &(&1.code == "de")).has_existing_translation ==
true
gallery = Overlay.open(:post, :gallery, context)
@@ -74,15 +76,59 @@ defmodule BDS.Desktop.OverlayTest do
current_tab: %{type: :post, id: "post-1", title: "Trip Notes", subtitle: "Draft"},
current_post_language: "en",
posts: [
%{id: "post-1", title: "Trip Notes", status: "draft", canonical_url: "/2026/04/26/trip-notes"},
%{id: "post-2", title: "Photo Walk", status: "published", canonical_url: "/2026/04/26/photo-walk"},
%{id: "post-3", title: "Travel Checklist", status: "draft", canonical_url: "/2026/04/20/travel-checklist"},
%{id: "post-4", title: "Packing List", status: "archived", canonical_url: "/2026/03/18/packing-list"}
%{
id: "post-1",
title: "Trip Notes",
status: "draft",
canonical_url: "/2026/04/26/trip-notes"
},
%{
id: "post-2",
title: "Photo Walk",
status: "published",
canonical_url: "/2026/04/26/photo-walk"
},
%{
id: "post-3",
title: "Travel Checklist",
status: "draft",
canonical_url: "/2026/04/20/travel-checklist"
},
%{
id: "post-4",
title: "Packing List",
status: "archived",
canonical_url: "/2026/03/18/packing-list"
}
],
media: [
%{id: "media-1", title: "Cover Shot", original_name: "cover-shot.jpg", is_image: true, thumbnail_url: "/media-thumbnail/media-1", image_url: "/media-thumbnail/media-1?size=large", alt_text: "Cover shot"},
%{id: "media-2", title: "Street Scene", original_name: "street-scene.jpg", is_image: true, thumbnail_url: "/media-thumbnail/media-2", image_url: "/media-thumbnail/media-2?size=large", alt_text: "Street scene"},
%{id: "media-3", title: "Audio Memo", original_name: "memo.m4a", is_image: false, thumbnail_url: nil, image_url: nil, alt_text: nil}
%{
id: "media-1",
title: "Cover Shot",
original_name: "cover-shot.jpg",
is_image: true,
thumbnail_url: "/media-thumbnail/media-1",
image_url: "/media-thumbnail/media-1?size=large",
alt_text: "Cover shot"
},
%{
id: "media-2",
title: "Street Scene",
original_name: "street-scene.jpg",
is_image: true,
thumbnail_url: "/media-thumbnail/media-2",
image_url: "/media-thumbnail/media-2?size=large",
alt_text: "Street scene"
},
%{
id: "media-3",
title: "Audio Memo",
original_name: "memo.m4a",
is_image: false,
thumbnail_url: nil,
image_url: nil,
alt_text: nil
}
],
post_media_ids: ["media-1", "media-2"],
blog_languages: ["en", "de", "fr"],
@@ -90,9 +136,27 @@ defmodule BDS.Desktop.OverlayTest do
language_flags: %{"en" => "GB", "de" => "DE", "fr" => "FR"},
existing_translations: %{"de" => "draft"},
ai_fields: [
%{key: "title", label: "Title", current_value: "Street Scene", suggested_value: "Street Scene at Dusk", locked: false},
%{key: "alt", label: "Alt Text", current_value: "", suggested_value: "Street scene at dusk", locked: false},
%{key: "caption", label: "Caption", current_value: "Busy corner", suggested_value: "A busy corner at dusk", locked: false}
%{
key: "title",
label: "Title",
current_value: "Street Scene",
suggested_value: "Street Scene at Dusk",
locked: false
},
%{
key: "alt",
label: "Alt Text",
current_value: "",
suggested_value: "Street scene at dusk",
locked: false
},
%{
key: "caption",
label: "Caption",
current_value: "Busy corner",
suggested_value: "A busy corner at dusk",
locked: false
}
],
delete_details: %{
entity_name: "Street Scene",

View File

@@ -42,7 +42,9 @@ defmodule BDS.Desktop.ShellCommandsTest do
%{project: project, temp_dir: temp_dir}
end
test "open_in_browser starts preview for the active project and returns a preview url", %{project: project} do
test "open_in_browser starts preview for the active project and returns a preview url", %{
project: project
} do
assert {:ok, result} = ShellCommands.execute("open_in_browser")
assert result.kind == "open_url"
@@ -51,7 +53,10 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert result.project_id == project.id
end
test "validate_translations returns an editor payload with current translation gaps", %{project: project, temp_dir: temp_dir} do
test "validate_translations returns an editor payload with current translation gaps", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
@@ -165,7 +170,8 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert is_map(completed.result.payload.summary)
end
test "rebuild_posts_from_files rebuilds embeddings for published posts when semantic similarity is enabled", %{project: project} do
test "rebuild_posts_from_files rebuilds embeddings for published posts when semantic similarity is enabled",
%{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
@@ -178,7 +184,9 @@ defmodule BDS.Desktop.ShellCommandsTest do
})
assert {:ok, published_post} = BDS.Posts.publish_post(post.id)
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) != nil
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) !=
nil
BDS.Repo.delete_all(BDS.Embeddings.Key)
@@ -186,10 +194,14 @@ defmodule BDS.Desktop.ShellCommandsTest do
completed = wait_for_task(result.task_id, &(&1.status == :completed))
assert completed.group_name == "Maintenance"
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) != nil
assert BDS.Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) !=
nil
end
test "repair_metadata_diff exposes live in-task progress from the repair worker", %{project: project} do
test "repair_metadata_diff exposes live in-task progress from the repair worker", %{
project: project
} do
original = Application.get_env(:bds, :tasks, [])
Application.put_env(
@@ -216,7 +228,8 @@ defmodule BDS.Desktop.ShellCommandsTest do
progressed =
wait_for_task(
result.task_id,
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.2 and &1.progress < 1.0),
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.2 and
&1.progress < 1.0),
5_000
)
@@ -243,7 +256,9 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert is_map(completed.result.payload.summary)
end
test "rebuild_embedding_index exposes live in-task progress while rebuilding posts", %{project: project} do
test "rebuild_embedding_index exposes live in-task progress while rebuilding posts", %{
project: project
} do
original = Application.get_env(:bds, :tasks, [])
original_embeddings = Application.get_env(:bds, :embeddings)
@@ -289,7 +304,8 @@ defmodule BDS.Desktop.ShellCommandsTest do
progressed =
wait_for_task(
result.task_id,
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and &1.progress < 1.0 and
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and
&1.progress < 1.0 and
is_binary(&1.message) and String.contains?(&1.message, "/")),
10_000
)
@@ -297,7 +313,11 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert progressed.group_name == "Embeddings"
assert String.contains?(progressed.message, "/")
assert wait_for_task(result.task_id, &(&1.status == :completed and &1.progress == 1.0), 10_000).status ==
assert wait_for_task(
result.task_id,
&(&1.status == :completed and &1.progress == 1.0),
10_000
).status ==
:completed
end
@@ -307,15 +327,19 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert result.kind == "task_queued"
assert result.action == "rebuild_database"
tasks = wait_for_tasks_by_name([
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
], &(&1.status == :completed))
tasks =
wait_for_tasks_by_name(
[
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
],
&(&1.status == :completed)
)
assert Enum.all?(tasks, &(&1.group_name == "Maintenance"))
assert Enum.all?(tasks, &(&1.status == :completed))
@@ -429,32 +453,40 @@ defmodule BDS.Desktop.ShellCommandsTest do
_posts_task =
wait_for_named_task(
"Rebuild Posts From Files",
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and &1.progress < 1.0),
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and
&1.progress < 1.0),
10_000
)
phase_one_tasks =
BDS.Tasks.list_tasks()
|> Enum.filter(&(&1.name in [
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files"
]))
|> Enum.filter(
&(&1.name in [
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files"
])
)
assert Enum.count(phase_one_tasks, &(&1.status == :running)) == 1
assert Enum.find(phase_one_tasks, &(&1.status == :running)).name == "Rebuild Posts From Files"
tasks = wait_for_tasks_by_name([
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
], &(&1.status == :completed), 20_000)
tasks =
wait_for_tasks_by_name(
[
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
],
&(&1.status == :completed),
20_000
)
assert Enum.all?(tasks, &(&1.status == :completed))
end
@@ -505,7 +537,8 @@ defmodule BDS.Desktop.ShellCommandsTest do
progressed =
wait_for_named_task(
"Rebuild Posts From Files",
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and &1.progress < 1.0),
&(&1.status == :running and is_number(&1.progress) and &1.progress > 0.0 and
&1.progress < 1.0),
5_000
)
@@ -515,15 +548,20 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert wait_for_task(progressed.id, &(&1.status == :completed and &1.progress == 1.0), 5_000).status ==
:completed
tasks = wait_for_tasks_by_name([
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
], &(&1.status == :completed), 20_000)
tasks =
wait_for_tasks_by_name(
[
"Rebuild Posts From Files",
"Rebuild Media From Files",
"Rebuild Scripts From Files",
"Rebuild Templates From Files",
"Rebuild Post Links",
"Regenerate Missing Thumbnails",
"Rebuild Embedding Index"
],
&(&1.status == :completed),
20_000
)
assert Enum.all?(tasks, &(&1.status == :completed))
end
@@ -537,7 +575,11 @@ defmodule BDS.Desktop.ShellCommandsTest do
assert is_binary(result.task_id)
assert is_binary(result.task_group_id)
tasks = wait_for_tasks_by_name(["Reindex Search Text", "Reindex Media Search Text"], &(&1.status == :completed))
tasks =
wait_for_tasks_by_name(
["Reindex Search Text", "Reindex Media Search Text"],
&(&1.status == :completed)
)
assert Enum.all?(tasks, &(&1.group_name == "Search"))
assert Enum.all?(tasks, &(&1.group_id == result.task_group_id))

View File

@@ -8,8 +8,10 @@ defmodule BDS.Desktop.ShellLiveTest do
test "shell live modules use contexts instead of direct Repo.get calls" do
source_files =
[Path.expand("../../../lib/bds/desktop/shell_live.ex", __DIR__) |
Path.wildcard(Path.join(@shell_live_source_root, "**/*.ex"))]
[
Path.expand("../../../lib/bds/desktop/shell_live.ex", __DIR__)
| Path.wildcard(Path.join(@shell_live_source_root, "**/*.ex"))
]
offenders =
source_files
@@ -46,12 +48,20 @@ defmodule BDS.Desktop.ShellLiveTest do
defmodule FakeEndpointModelHttpClient do
def get("https://api.example.test/v1/models", _headers) do
{:ok,
%{status: 200, headers: %{}, body: Jason.encode!(%{"data" => [%{"id" => "gpt-4.1"}, %{"id" => "gpt-4.1-mini"}]})}}
%{
status: 200,
headers: %{},
body: Jason.encode!(%{"data" => [%{"id" => "gpt-4.1"}, %{"id" => "gpt-4.1-mini"}]})
}}
end
def get("http://localhost:11434/v1/models", _headers) do
{:ok,
%{status: 200, headers: %{}, body: Jason.encode!(%{"data" => [%{"id" => "llama3.3"}, %{"id" => "llava:latest"}]})}}
%{
status: 200,
headers: %{},
body: Jason.encode!(%{"data" => [%{"id" => "llama3.3"}, %{"id" => "llava:latest"}]})
}}
end
def get(_url, _headers), do: {:error, :not_found}
@@ -61,8 +71,8 @@ defmodule BDS.Desktop.ShellLiveTest do
use Plug.Router
import Phoenix.ConnTest, except: [post: 2]
plug :match
plug :dispatch
plug(:match)
plug(:dispatch)
post "/v1/chat/completions" do
Process.sleep(300)
@@ -89,7 +99,9 @@ defmodule BDS.Desktop.ShellLiveTest do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, {:shared, self()})
temp_dir = Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-shell-live-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -158,7 +170,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-sidebar-action="import")
end
test "sidebar create actions follow the old-app post, script, template, and import flows", %{project: project} do
test "sidebar create actions follow the old-app post, script, template, and import flows", %{
project: project
} do
{:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
post_count_before = Repo.aggregate(Post, :count, :id)
script_count_before = Repo.aggregate(BDS.Scripts.Script, :count, :id)
@@ -218,7 +232,8 @@ defmodule BDS.Desktop.ShellLiveTest do
|> element("[data-testid='sidebar-create-action'][data-sidebar-action='import']")
|> render_click()
assert Repo.aggregate(ImportDefinitions.ImportDefinition, :count, :id) == import_count_before + 1
assert Repo.aggregate(ImportDefinitions.ImportDefinition, :count, :id) ==
import_count_before + 1
created_definition = Repo.one!(ImportDefinitions.ImportDefinition)
assert created_definition.project_id == project.id
@@ -227,19 +242,27 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-tab-id="#{created_definition.id}")
end
test "shell live refreshes the posts sidebar when the CLI watcher broadcasts an entity change", %{project: project} do
test "shell live refreshes the posts sidebar when the CLI watcher broadcasts an entity change",
%{project: project} do
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
refute html =~ "CLI Added Post"
assert {:ok, post} = Posts.create_post(%{project_id: project.id, title: "CLI Added Post"})
Phoenix.PubSub.broadcast(BDS.PubSub, Watcher.topic(), {:entity_changed, %{entity: "post", entity_id: post.id, action: :created}})
Phoenix.PubSub.broadcast(
BDS.PubSub,
Watcher.topic(),
{:entity_changed, %{entity: "post", entity_id: post.id, action: :created}}
)
assert render(view) =~ "CLI Added Post"
end
test "shell live closes stale post and media tabs when the CLI watcher broadcasts deletions", %{project: project, temp_dir: temp_dir} do
test "shell live closes stale post and media tabs when the CLI watcher broadcasts deletions", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, post} = Posts.create_post(%{project_id: project.id, title: "CLI Delete Post"})
source_path = Path.join(temp_dir, "cli-delete-media.txt")
@@ -264,7 +287,11 @@ defmodule BDS.Desktop.ShellLiveTest do
assert {:ok, :deleted} = Posts.delete_post(post.id)
Phoenix.PubSub.broadcast(BDS.PubSub, Watcher.topic(), {:entity_changed, %{entity: "post", entity_id: post.id, action: :deleted}})
Phoenix.PubSub.broadcast(
BDS.PubSub,
Watcher.topic(),
{:entity_changed, %{entity: "post", entity_id: post.id, action: :deleted}}
)
html = render(view)
refute html =~ ~s(data-tab-type="post")
@@ -285,7 +312,11 @@ defmodule BDS.Desktop.ShellLiveTest do
assert {:ok, :deleted} = Media.delete_media(media.id)
Phoenix.PubSub.broadcast(BDS.PubSub, Watcher.topic(), {:entity_changed, %{entity: "media", entity_id: media.id, action: :deleted}})
Phoenix.PubSub.broadcast(
BDS.PubSub,
Watcher.topic(),
{:entity_changed, %{entity: "media", entity_id: media.id, action: :deleted}}
)
html = render(view)
refute html =~ ~s(data-tab-type="media")
@@ -624,7 +655,14 @@ defmodule BDS.Desktop.ShellLiveTest do
test "shell live renders the legacy git activity badge from remote behind count" do
Application.put_env(:bds, :git_remote_state_provider, fn _project_id, _opts ->
{:ok, %{local_branch: "main", upstream_branch: "origin/main", has_upstream: true, ahead: 0, behind: 7}}
{:ok,
%{
local_branch: "main",
upstream_branch: "origin/main",
has_upstream: true,
ahead: 0,
behind: 7
}}
end)
{:ok, _view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
@@ -906,7 +944,8 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(style="width: 280px;")
html = render_hook(view, "sync_layout", %{"sidebar_width" => 420, "assistant_sidebar_width" => 480})
html =
render_hook(view, "sync_layout", %{"sidebar_width" => 420, "assistant_sidebar_width" => 480})
assert html =~ ~s(data-testid="sidebar-shell")
assert html =~ ~s(style="width: 420px;")
@@ -928,7 +967,8 @@ defmodule BDS.Desktop.ShellLiveTest do
test "sidebar filters and load more are server-driven", %{project: project} do
seed_sidebar_posts(project.id)
assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "tech", color: "#112233"})
assert {:ok, _tag} =
Tags.create_tag(%{project_id: project.id, name: "tech", color: "#112233"})
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
@@ -937,7 +977,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(class="sidebar-section-header")
assert html =~ ~s(class="sidebar-actions")
assert html =~ ~s(data-testid="sidebar-load-more")
assert html_position(html, ~s(data-testid="sidebar-load-more")) > html_position(html, ">Archived<")
assert html_position(html, ~s(data-testid="sidebar-load-more")) >
html_position(html, ">Archived<")
refute html =~ ~s(data-testid="sidebar-filter-tag")
assert html =~ "Alpha Post"
refute html =~ "Overflow Post"
@@ -998,9 +1041,18 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Overflow Post"
end
test "project switcher, ui language, dashboard recents, and output log are wired", %{temp_dir: temp_dir} do
{:ok, other_project} = Projects.create_project(%{name: "Second Blog", data_path: Path.join(temp_dir, "second")})
{:ok, recent_post} = Posts.create_post(%{project_id: other_project.id, title: "Recent Shell Post", content: "body"})
test "project switcher, ui language, dashboard recents, and output log are wired", %{
temp_dir: temp_dir
} do
{:ok, other_project} =
Projects.create_project(%{name: "Second Blog", data_path: Path.join(temp_dir, "second")})
{:ok, recent_post} =
Posts.create_post(%{
project_id: other_project.id,
title: "Recent Shell Post",
content: "body"
})
{:ok, view, html} = live_isolated(build_conn(), BDS.Desktop.ShellLive)
@@ -1044,13 +1096,22 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ "Activated Second Blog"
end
test "task button opens tasks and post panels render real link and git data", %{project: project, temp_dir: temp_dir} do
{:ok, target} = Posts.create_post(%{project_id: project.id, title: "Target Post", content: "target body"})
test "task button opens tasks and post panels render real link and git data", %{
project: project,
temp_dir: temp_dir
} do
{:ok, target} =
Posts.create_post(%{project_id: project.id, title: "Target Post", content: "target body"})
{:ok, target} = Posts.publish_post(target.id)
target_href = canonical_post_href(target)
{:ok, source} =
Posts.create_post(%{project_id: project.id, title: "Linking Source", content: "See [Target](#{target_href})"})
Posts.create_post(%{
project_id: project.id,
title: "Linking Source",
content: "See [Target](#{target_href})"
})
{:ok, source} = Posts.publish_post(source.id)
:ok = Posts.rebuild_post_links(project.id)
@@ -1087,7 +1148,10 @@ defmodule BDS.Desktop.ShellLiveTest do
|> render_click()
refute html =~ ~s(class="panel-shell is-hidden")
assert html =~ ~s(<button class="panel-tab active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">)
assert html =~
~s(<button class="panel-tab active" type="button" phx-click="select_panel_tab" phx-value-tab="tasks">)
assert html =~ ~s(class="task-list") or html =~ "No background tasks running"
end
@@ -1276,7 +1340,9 @@ defmodule BDS.Desktop.ShellLiveTest do
html =
view
|> element("[data-testid='metadata-diff-repair-button'][data-direction='file_to_db'][data-field='title']")
|> element(
"[data-testid='metadata-diff-repair-button'][data-direction='file_to_db'][data-field='title']"
)
|> render_click()
assert html =~ "Repair Metadata Diff"
@@ -1418,7 +1484,9 @@ defmodule BDS.Desktop.ShellLiveTest do
refute orphan_relative_path in Enum.map(diff.orphan_reports, & &1.file_path)
end
test "metadata diff embeddings tab exposes repair actions and clears embedding drift", %{project: project} do
test "metadata diff embeddings tab exposes repair actions and clears embedding drift", %{
project: project
} do
:ok = BDS.Tasks.clear_finished()
assert {:ok, _metadata} =
@@ -1455,7 +1523,9 @@ defmodule BDS.Desktop.ShellLiveTest do
html =
view
|> element("[data-testid='metadata-diff-repair-button'][data-direction='file_to_db'][data-field='content_hash']")
|> element(
"[data-testid='metadata-diff-repair-button'][data-direction='file_to_db'][data-field='content_hash']"
)
|> render_click()
assert html =~ "Repair Metadata Diff"
@@ -1465,15 +1535,24 @@ defmodule BDS.Desktop.ShellLiveTest do
send(view.pid, :refresh_task_status)
_html = render(view)
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) != nil
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: published_post.id) !=
nil
assert {:ok, diff} = BDS.Maintenance.metadata_diff(project.id)
refute Enum.any?(diff.diff_reports, &(&1.entity_type == "embedding" and &1.entity_id == published_post.id))
refute Enum.any?(
diff.diff_reports,
&(&1.entity_type == "embedding" and &1.entity_id == published_post.id)
)
end
test "post tabs render a real editor and drive save publish discard flows", %{project: project} do
assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "alpha", color: "#112233"})
assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "beta", color: "#445566"})
assert {:ok, _tag} =
Tags.create_tag(%{project_id: project.id, name: "alpha", color: "#112233"})
assert {:ok, _tag} =
Tags.create_tag(%{project_id: project.id, name: "beta", color: "#445566"})
assert {:ok, _metadata} = Metadata.add_category(project.id, "notes")
assert {:ok, _metadata} = Metadata.add_category(project.id, "guides")
@@ -1632,7 +1711,10 @@ defmodule BDS.Desktop.ShellLiveTest do
defp new_task!(_existing_ids, _name, 0), do: flunk("new task was not created in time")
defp new_task!(existing_ids, name, attempts) do
case Enum.find(BDS.Tasks.list_tasks(), &(&1.name == name and not MapSet.member?(existing_ids, &1.id))) do
case Enum.find(
BDS.Tasks.list_tasks(),
&(&1.name == name and not MapSet.member?(existing_ids, &1.id))
) do
nil ->
Process.sleep(20)
new_task!(existing_ids, name, attempts - 1)
@@ -1642,7 +1724,9 @@ defmodule BDS.Desktop.ShellLiveTest do
end
end
test "published post editor loads body from file and renders markdown-only editor", %{project: project} do
test "published post editor loads body from file and renders markdown-only editor", %{
project: project
} do
{:ok, post} =
Posts.create_post(%{
project_id: project.id,
@@ -1665,14 +1749,22 @@ defmodule BDS.Desktop.ShellLiveTest do
})
assert html =~ ~s(data-testid="post-editor-content")
assert Regex.match?(~r/name="post_editor\[content\]"[^>]*># Heading\s+```elixir\s+IO\.puts\(:ok\)\s+```/s, html)
assert Regex.match?(
~r/name="post_editor\[content\]"[^>]*># Heading\s+```elixir\s+IO\.puts\(:ok\)\s+```/s,
html
)
assert html =~ ~s(data-monaco-language="markdown-with-macros")
assert html =~ ~s(phx-hook="MonacoEditor")
refute html =~ "post-editor-markdown-highlight"
refute html =~ ~s(phx-value-mode="visual")
end
test "media tabs render a real editor and drive explicit save flows", %{project: project, temp_dir: temp_dir} do
test "media tabs render a real editor and drive explicit save flows", %{
project: project,
temp_dir: temp_dir
} do
{:ok, post} =
Posts.create_post(%{
project_id: project.id,
@@ -1767,7 +1859,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert saved_media.language == "fr"
end
test "media editor follows the old-app translation editing flow", %{project: project, temp_dir: temp_dir} do
test "media editor follows the old-app translation editing flow", %{
project: project,
temp_dir: temp_dir
} do
source_path = Path.join(temp_dir, "hero.txt")
File.write!(source_path, "media body")
@@ -1801,9 +1896,21 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(class="editor-content media-editor")
assert html =~ ~s(class="quick-actions-wrapper")
refute html =~ ~s(class="media-editor-form")
assert has_element?(view, "[data-testid='media-editor'] .editor-content.media-editor .media-details")
assert has_element?(view, "[data-testid='media-editor'] .editor-content.media-editor .media-details .media-translations-section")
assert has_element?(view, "[data-testid='media-editor'] .editor-content.media-editor .media-details .linked-posts-section")
assert has_element?(
view,
"[data-testid='media-editor'] .editor-content.media-editor .media-details"
)
assert has_element?(
view,
"[data-testid='media-editor'] .editor-content.media-editor .media-details .media-translations-section"
)
assert has_element?(
view,
"[data-testid='media-editor'] .editor-content.media-editor .media-details .linked-posts-section"
)
html = render_click(view, "edit_media_translation", %{"id" => media.id, "language" => "de"})
@@ -1814,7 +1921,10 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(name="media_translation[caption]")
end
test "settings and media editors render localized labels when the UI language changes", %{project: project, temp_dir: temp_dir} do
test "settings and media editors render localized labels when the UI language changes", %{
project: project,
temp_dir: temp_dir
} do
source_path = Path.join(temp_dir, "localized-hero.txt")
File.write!(source_path, "media body")
@@ -1862,7 +1972,8 @@ defmodule BDS.Desktop.ShellLiveTest do
refute media_html =~ "Linked Posts"
end
test "remaining step-5 routes render dedicated editors instead of the generic shell placeholder", %{project: project} do
test "remaining step-5 routes render dedicated editors instead of the generic shell placeholder",
%{project: project} do
assert {:ok, script} =
Scripts.create_script(%{
project_id: project.id,
@@ -2134,7 +2245,9 @@ defmodule BDS.Desktop.ShellLiveTest do
refute html =~ ~s(data-testid="chat-input-container")
end
test "chat editor renders assistant markdown and dispatches assistant navigation actions", %{project: project} do
test "chat editor renders assistant markdown and dispatches assistant navigation actions", %{
project: project
} do
assert {:ok, post} =
Posts.create_post(%{
project_id: project.id,
@@ -2315,7 +2428,10 @@ defmodule BDS.Desktop.ShellLiveTest do
refute render(view) =~ "Delayed response"
end
test "translation validation route renders dedicated cards and fix controls", %{project: project, temp_dir: temp_dir} do
test "translation validation route renders dedicated cards and fix controls", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{
main_language: "en",
@@ -2375,7 +2491,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ invalid_file_path
end
test "git diff route renders a structured Monaco diff surface for working tree changes", %{temp_dir: temp_dir} do
test "git diff route renders a structured Monaco diff surface for working tree changes", %{
temp_dir: temp_dir
} do
posts_dir = Path.join(temp_dir, "posts")
File.mkdir_p!(posts_dir)
@@ -2424,7 +2542,9 @@ defmodule BDS.Desktop.ShellLiveTest do
assert html =~ ~s(data-selected-settings-section="ai")
end
test "template sidebar exposes old-app style delete control and removes template rows", %{project: project} do
test "template sidebar exposes old-app style delete control and removes template rows", %{
project: project
} do
assert {:ok, template} =
BDS.Templates.create_template(%{
project_id: project.id,
@@ -2471,9 +2591,20 @@ defmodule BDS.Desktop.ShellLiveTest do
sidebar_post(project_id, "beta-post", "Beta Post", now + 2_000, ["design"], ["guides"])
] ++
Enum.map(1..498, fn index ->
sidebar_post(project_id, "filler-#{index}", "Filler #{index}", now - index, ["filler"], ["archive"])
sidebar_post(
project_id,
"filler-#{index}",
"Filler #{index}",
now - index,
["filler"],
["archive"]
)
end) ++
[sidebar_post(project_id, "overflow-post", "Overflow Post", now - 10_000, ["tech"], ["notes"])]
[
sidebar_post(project_id, "overflow-post", "Overflow Post", now - 10_000, ["tech"], [
"notes"
])
]
{count, _rows} = Repo.insert_all(Post, entries)
assert count == length(entries)

View File

@@ -35,6 +35,7 @@ defmodule BDS.DesktopTest do
test "desktop menu bar exposes the native menu groups for the shell window" do
groups = BDS.Desktop.MenuBar.groups(dev_mode?: false)
item_ids = fn items ->
items
|> Enum.reject(&Map.get(&1, :separator, false))
@@ -86,7 +87,10 @@ defmodule BDS.DesktopTest do
assert menu_item(groups, :view_media).native_label == "Media\tCTRL+2"
assert menu_item(groups, :toggle_sidebar).native_label == "Toggle Sidebar\tCTRL+B"
assert menu_item(groups, :toggle_panel).native_label == "Toggle Panel\tCTRL+J"
assert menu_item(groups, :toggle_assistant_sidebar).native_label == "Toggle Assistant Sidebar\tCTRL+\\"
assert menu_item(groups, :toggle_assistant_sidebar).native_label ==
"Toggle Assistant Sidebar\tCTRL+\\"
assert menu_item(groups, :publish_selected).native_label == "Publish Selected\tCTRL+SHIFT+P"
assert menu_item(groups, :preview_post).native_label == "Preview Post\tCTRL+SHIFT+V"
assert menu_item(groups, :generate_sitemap).native_label == "Generate Site\tCTRL+R"
@@ -135,23 +139,28 @@ defmodule BDS.DesktopTest do
test "desktop endpoint serves active-project media thumbnails for the live sidebar" do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-desktop-thumbnail-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-desktop-thumbnail-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn ->
File.rm_rf(temp_dir)
end)
{:ok, project} = BDS.Projects.create_project(%{name: "Desktop Thumbnails", data_path: temp_dir})
{:ok, project} =
BDS.Projects.create_project(%{name: "Desktop Thumbnails", data_path: temp_dir})
{:ok, _active} = BDS.Projects.set_active_project(project.id)
source_path = Path.join(temp_dir, "sample.jpg")
File.write!(source_path, tiny_jpeg_binary())
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
assert {:ok, media} =
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
conn = conn(:get, "/media-thumbnail/#{media.id}?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
conn = conn(:get, "/media-thumbnail/#{media.id}?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))
assert conn.status == 200
assert [content_type] = Plug.Conn.get_resp_header(conn, "content-type")
@@ -162,7 +171,7 @@ defmodule BDS.DesktopTest do
defp menu_item(groups, id) do
groups
|> Enum.flat_map(& &1.items)
|> Enum.find(&Map.get(&1, :id) == id)
|> Enum.find(&(Map.get(&1, :id) == id))
end
defp tiny_jpeg_binary do

View File

@@ -18,7 +18,9 @@ defmodule BDS.EmbeddingsTest do
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-embeddings-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-embeddings-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -95,12 +97,16 @@ defmodule BDS.EmbeddingsTest do
assert {:ok, alpha} = BDS.Posts.update_post(alpha.id, %{content: "kitchen flour dough loaf"})
assert {:ok, alpha} = BDS.Posts.publish_post(alpha.id)
assert {:ok, updated_scores} = BDS.Embeddings.compute_similarities(alpha.id, [beta.id, gamma.id])
assert {:ok, updated_scores} =
BDS.Embeddings.compute_similarities(alpha.id, [beta.id, gamma.id])
assert updated_scores[gamma.id] > updated_scores[beta.id]
assert {:ok, :deleted} = BDS.Posts.delete_post(gamma.id)
assert {:ok, after_delete} = BDS.Embeddings.compute_similarities(alpha.id, [beta.id, gamma.id])
assert {:ok, after_delete} =
BDS.Embeddings.compute_similarities(alpha.id, [beta.id, gamma.id])
refute Map.has_key?(after_delete, gamma.id)
end
@@ -315,7 +321,6 @@ defmodule BDS.EmbeddingsTest do
assert BDS.Embeddings.index_path(project.id) =~ "/embeddings.usearch"
end
test "reindex_all rebuilds stored embeddings for the whole project", %{project: project} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
@@ -349,7 +354,9 @@ defmodule BDS.EmbeddingsTest do
assert File.exists?(BDS.Embeddings.index_path(project.id))
end
test "sync_post refreshes snapshot drift when the embedding hash is already current", %{project: project} do
test "sync_post refreshes snapshot drift when the embedding hash is already current", %{
project: project
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})

View File

@@ -98,24 +98,25 @@ defmodule BDS.GenerationTest do
assert {:ok, result} = BDS.Generation.generate_site(project.id, [:core])
assert result.sections == [:core]
expected_paths = [
"404.html",
"index.html",
"sitemap.xml",
"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/pagefind/index.json",
"de/pagefind/pagefind-ui.css",
"de/pagefind/pagefind-ui.js"
] ++ Enum.map(PreviewAssets.generated_outputs(), &elem(&1, 0))
expected_paths =
[
"404.html",
"index.html",
"sitemap.xml",
"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/pagefind/index.json",
"de/pagefind/pagefind-ui.css",
"de/pagefind/pagefind-ui.js"
] ++ Enum.map(PreviewAssets.generated_outputs(), &elem(&1, 0))
assert Enum.sort(Enum.map(result.generated_files, & &1.relative_path)) ==
Enum.sort(expected_paths)
@@ -421,6 +422,7 @@ defmodule BDS.GenerationTest do
end)
assert {0.5, "Comparing sitemap to html pages..."} in events
assert Enum.any?(events, fn
{value, message}
when is_number(value) and value > 0.5 and value < 1.0 and is_binary(message) ->
@@ -587,10 +589,11 @@ defmodule BDS.GenerationTest do
assert not_found_html =~ "Back to preview home"
end
test "generation starter templates render localized archive headings in each output language", %{
project: project,
temp_dir: temp_dir
} do
test "generation starter templates render localized archive headings in each output language",
%{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _metadata} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
@@ -646,7 +649,8 @@ defmodule BDS.GenerationTest do
})
media_source_reference =
"/" <> Path.join(Path.dirname(media.file_path), media.original_name) <> "?download=1#preview"
"/" <>
Path.join(Path.dirname(media.file_path), media.original_name) <> "?download=1#preview"
assert {:ok, post} =
Posts.create_post(%{
@@ -956,7 +960,8 @@ defmodule BDS.GenerationTest do
assert {:ok, published_missing_post} = Posts.publish_post(missing_post.id)
assert {:ok, published_updated_post} = Posts.publish_post(updated_post.id)
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
missing_post_path = BDS.Generation.post_output_path(published_missing_post)
updated_post_path = BDS.Generation.post_output_path(published_updated_post)
@@ -1098,7 +1103,10 @@ defmodule BDS.GenerationTest do
})
Repo.update_all(from(p in BDS.Posts.Post, where: p.id == ^translatable_post.id),
set: [created_at: DateTime.to_unix(~U[2026-04-15 12:00:00Z]), updated_at: DateTime.to_unix(~U[2026-04-15 12:00:00Z])]
set: [
created_at: DateTime.to_unix(~U[2026-04-15 12:00:00Z]),
updated_at: DateTime.to_unix(~U[2026-04-15 12:00:00Z])
]
)
assert {:ok, _published_translatable} = Posts.publish_post(translatable_post.id)
@@ -1113,7 +1121,10 @@ defmodule BDS.GenerationTest do
})
Repo.update_all(from(p in BDS.Posts.Post, where: p.id == ^do_not_translate_post.id),
set: [created_at: DateTime.to_unix(~U[2026-04-14 12:00:00Z]), updated_at: DateTime.to_unix(~U[2026-04-14 12:00:00Z])]
set: [
created_at: DateTime.to_unix(~U[2026-04-14 12:00:00Z]),
updated_at: DateTime.to_unix(~U[2026-04-14 12:00:00Z])
]
)
assert {:ok, _published_do_not_translate} = Posts.publish_post(do_not_translate_post.id)
@@ -1247,7 +1258,9 @@ defmodule BDS.GenerationTest do
)
assert {:ok, _published_page} = Posts.publish_post(page_post.id)
assert {:ok, result} = BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
assert {:ok, result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
relative_paths = Enum.map(result.generated_files, & &1.relative_path)
@@ -1260,7 +1273,11 @@ defmodule BDS.GenerationTest do
assert File.exists?(Path.join([temp_dir, "html", "page", "2", "index.html"]))
assert File.exists?(Path.join([temp_dir, "html", "tag", "Elixir", "page", "2", "index.html"]))
assert File.exists?(Path.join([temp_dir, "html", "2026", "04", "15", "index.html"]))
assert File.exists?(Path.join([temp_dir, "html", "2026", "04", "15", "page", "2", "index.html"]))
assert File.exists?(
Path.join([temp_dir, "html", "2026", "04", "15", "page", "2", "index.html"])
)
assert File.exists?(Path.join([temp_dir, "html", "about", "index.html"]))
assert {:ok, report} = BDS.Generation.validate_site(project.id)
@@ -1291,7 +1308,9 @@ defmodule BDS.GenerationTest do
})
assert {:ok, published_post} = Posts.publish_post(post.id)
assert {:ok, _result} = BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
assert {:ok, _result} =
BDS.Generation.generate_site(project.id, [:core, :single, :category, :tag, :date])
post_path = BDS.Generation.post_output_path(published_post)
post_url_path = relative_path_to_url_path(post_path)

View File

@@ -23,13 +23,14 @@ defmodule BDS.GitTest do
project: project,
project_dir: project_dir
} do
runner = fake_runner(fn
"git", ["init", "-b", "master"], _opts -> {"", 0}
"git", ["lfs", "track" | _rest], _opts -> {"Tracking *.png\n", 0}
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts -> {"master\n", 0}
"git", ["remote", "get-url", "origin"], _opts -> {"", 2}
"git", ["lfs", "ls-files"], _opts -> {"", 0}
end)
runner =
fake_runner(fn
"git", ["init", "-b", "master"], _opts -> {"", 0}
"git", ["lfs", "track" | _rest], _opts -> {"Tracking *.png\n", 0}
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts -> {"master\n", 0}
"git", ["remote", "get-url", "origin"], _opts -> {"", 2}
"git", ["lfs", "ls-files"], _opts -> {"", 0}
end)
assert {:ok, repo} = Git.initialize_repo(project.id, runner: runner)
@@ -40,22 +41,41 @@ defmodule BDS.GitTest do
assert File.read!(Path.join(project_dir, ".gitattributes")) =~ "*.png filter=lfs"
end
test "status, diff, history, and provider detection are parsed from git output", %{project: project} do
runner = fake_runner(fn
"git", ["status", "--porcelain=v1", "--untracked-files=all"], _opts ->
{"A posts/new.md\n M meta/project.json\nR old.txt -> new.txt\n?? note.txt\n", 0}
test "status, diff, history, and provider detection are parsed from git output", %{
project: project
} do
runner =
fake_runner(fn
"git", ["status", "--porcelain=v1", "--untracked-files=all"], _opts ->
{"A posts/new.md\n M meta/project.json\nR old.txt -> new.txt\n?? note.txt\n", 0}
"git", ["diff", "--cached", "--no-ext-diff"], _opts -> {"staged diff", 0}
"git", ["diff", "--no-ext-diff"], _opts -> {"unstaged diff", 0}
"git", ["remote", "get-url", "origin"], _opts -> {"git@github.com:owner/repo.git\n", 0}
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts -> {"main\n", 0}
"git", ["log", "--format=%H%x09%s", "main"], _opts -> {"a1\tLocal commit\nb2\tShared commit\n", 0}
"git", ["log", "--format=%H", "origin/main"], _opts -> {"b2\nc3\n", 0}
end)
"git", ["diff", "--cached", "--no-ext-diff"], _opts ->
{"staged diff", 0}
"git", ["diff", "--no-ext-diff"], _opts ->
{"unstaged diff", 0}
"git", ["remote", "get-url", "origin"], _opts ->
{"git@github.com:owner/repo.git\n", 0}
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts ->
{"main\n", 0}
"git", ["log", "--format=%H%x09%s", "main"], _opts ->
{"a1\tLocal commit\nb2\tShared commit\n", 0}
"git", ["log", "--format=%H", "origin/main"], _opts ->
{"b2\nc3\n", 0}
end)
assert {:ok, status} = Git.status(project.id, runner: runner)
assert Enum.any?(status.files, &(&1.path == "posts/new.md" and &1.status == :added))
assert Enum.any?(status.files, &(&1.path == "new.txt" and &1.status == :renamed and &1.old_path == "old.txt"))
assert Enum.any?(
status.files,
&(&1.path == "new.txt" and &1.status == :renamed and &1.old_path == "old.txt")
)
assert Enum.any?(status.files, &(&1.path == "note.txt" and &1.status == :untracked))
assert {:ok, diff} = Git.diff(project.id, runner: runner)
@@ -93,12 +113,20 @@ defmodule BDS.GitTest do
end
test "remote_state reports upstream ahead and behind counts", %{project: project} do
runner = fake_runner(fn
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts -> {"main\n", 0}
"git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], _opts -> {"origin/main\n", 0}
"git", ["rev-list", "--count", "origin/main..HEAD"], _opts -> {"2\n", 0}
"git", ["rev-list", "--count", "HEAD..origin/main"], _opts -> {"5\n", 0}
end)
runner =
fake_runner(fn
"git", ["rev-parse", "--abbrev-ref", "HEAD"], _opts ->
{"main\n", 0}
"git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"], _opts ->
{"origin/main\n", 0}
"git", ["rev-list", "--count", "origin/main..HEAD"], _opts ->
{"2\n", 0}
"git", ["rev-list", "--count", "HEAD..origin/main"], _opts ->
{"5\n", 0}
end)
assert {:ok, remote_state} = Git.remote_state(project.id, runner: runner)
assert remote_state.local_branch == "main"
@@ -117,16 +145,29 @@ defmodule BDS.GitTest do
send(parent, {:git_command, command, args, opts})
case {command, args} do
{"git", ["fetch", "--all", "--prune"]} -> {"", 0}
{"git", ["pull", "--ff-only"]} -> {"", 0}
{"git", ["push"]} -> {"", 0}
{"git", ["add", "-A"]} -> {"", 0}
{"git", ["commit", "-m", "save everything"]} -> {"", 0}
{"git", ["fetch", "--all", "--prune"]} ->
{"", 0}
{"git", ["pull", "--ff-only"]} ->
{"", 0}
{"git", ["push"]} ->
{"", 0}
{"git", ["add", "-A"]} ->
{"", 0}
{"git", ["commit", "-m", "save everything"]} ->
{"", 0}
{"git", ["diff", "--name-status", "old", "new"]} ->
{"A\tposts/2026/04/new-post.md\nM\tscripts/tool.lua\nD\ttemplates/old.liquid\n", 0}
{"git", ["lfs", "prune", "--recent"]} -> {"", 0}
_other -> {"", 0}
{"git", ["lfs", "prune", "--recent"]} ->
{"", 0}
_other ->
{"", 0}
end
end
@@ -151,10 +192,14 @@ defmodule BDS.GitTest do
end
test "fetch returns structured auth errors with provider guidance", %{project: project} do
runner = fake_runner(fn
"git", ["remote", "get-url", "origin"], _opts -> {"git@gitlab.com:owner/repo.git\n", 0}
"git", ["fetch", "--all", "--prune"], _opts -> {"fatal: Authentication failed for 'origin'", 128}
end)
runner =
fake_runner(fn
"git", ["remote", "get-url", "origin"], _opts ->
{"git@gitlab.com:owner/repo.git\n", 0}
"git", ["fetch", "--all", "--prune"], _opts ->
{"fatal: Authentication failed for 'origin'", 128}
end)
assert {:error, error} = Git.fetch(project.id, runner: runner)
assert error.kind == :auth

View File

@@ -9,7 +9,9 @@ defmodule BDS.ImportAnalysisTest do
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-import-analysis-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-import-analysis-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -17,7 +19,10 @@ defmodule BDS.ImportAnalysisTest do
%{project: project, temp_dir: temp_dir}
end
test "analyze_wxr summarizes new items, date distribution, and macros", %{project: project, temp_dir: temp_dir} do
test "analyze_wxr summarizes new items, date distribution, and macros", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")
@@ -32,8 +37,19 @@ defmodule BDS.ImportAnalysisTest do
assert report.site_info.language == "en"
assert report.site_info.source_file == wxr_path
assert report.post_stats == %{new_count: 1, update_count: 0, conflict_count: 0, duplicate_count: 0}
assert report.page_stats == %{new_count: 1, update_count: 0, conflict_count: 0, duplicate_count: 0}
assert report.post_stats == %{
new_count: 1,
update_count: 0,
conflict_count: 0,
duplicate_count: 0
}
assert report.page_stats == %{
new_count: 1,
update_count: 0,
conflict_count: 0,
duplicate_count: 0
}
assert report.media_stats == %{
new_count: 1,
@@ -64,19 +80,39 @@ defmodule BDS.ImportAnalysisTest do
}
]
} = report.macros
assert report.conflicts == []
assert [%{title: "Hello World", slug: "hello-world", status: "new", item_type: "post", post_type: "post"}] =
assert [
%{
title: "Hello World",
slug: "hello-world",
status: "new",
item_type: "post",
post_type: "post"
}
] =
report.items.posts
assert [%{title: "About", slug: "about", status: "new", item_type: "page"} = page_item] = report.items.pages
assert [%{title: "About", slug: "about", status: "new", item_type: "page"} = page_item] =
report.items.pages
assert Map.get(page_item, :post_type) == "page"
assert [%{title: "Import Asset", filename: "import-asset.txt", relative_path: "2024/05/import-asset.txt", status: "new", item_type: "media"}] =
assert [
%{
title: "Import Asset",
filename: "import-asset.txt",
relative_path: "2024/05/import-asset.txt",
status: "new",
item_type: "media"
}
] =
report.items.media
end
test "analyze_wxr detects update, conflict, duplicate, existing taxonomy, and missing uploads", %{project: project, temp_dir: temp_dir} do
test "analyze_wxr detects update, conflict, duplicate, existing taxonomy, and missing uploads",
%{project: project, temp_dir: temp_dir} do
assert {:ok, _category} = Tags.create_tag(%{project_id: project.id, name: "General"})
assert {:ok, _tag} = Tags.create_tag(%{project_id: project.id, name: "News"})
@@ -119,8 +155,19 @@ defmodule BDS.ImportAnalysisTest do
assert {:ok, report} = ImportAnalysis.analyze_wxr(project.id, wxr_path, nil)
assert report.post_stats == %{new_count: 0, update_count: 1, conflict_count: 1, duplicate_count: 1}
assert report.page_stats == %{new_count: 0, update_count: 0, conflict_count: 0, duplicate_count: 0}
assert report.post_stats == %{
new_count: 0,
update_count: 1,
conflict_count: 1,
duplicate_count: 1
}
assert report.page_stats == %{
new_count: 0,
update_count: 0,
conflict_count: 0,
duplicate_count: 0
}
assert report.media_stats == %{
new_count: 0,
@@ -134,16 +181,28 @@ defmodule BDS.ImportAnalysisTest do
assert report.tag_stats == %{existing_count: 1, mapped_count: 0, new_count: 0}
assert Enum.any?(report.conflicts, fn conflict ->
conflict.item_type == "post" and conflict.item_name == "conflict-me" and conflict.resolution == "ignore"
conflict.item_type == "post" and conflict.item_name == "conflict-me" and
conflict.resolution == "ignore"
end)
assert Enum.any?(report.items.posts, &(&1.slug == "update-me" and &1.status == "update"))
assert Enum.any?(report.items.posts, &(&1.slug == "conflict-me" and &1.status == "conflict"))
assert Enum.any?(report.items.posts, &(&1.slug == "duplicate-me" and &1.status == "content-duplicate"))
assert Enum.any?(report.items.media, &(&1.filename == "missing-asset.txt" and &1.status == "missing"))
assert Enum.any?(
report.items.posts,
&(&1.slug == "duplicate-me" and &1.status == "content-duplicate")
)
assert Enum.any?(
report.items.media,
&(&1.filename == "missing-asset.txt" and &1.status == "missing")
)
end
test "analyze_wxr reports legacy progress steps while building the import report", %{project: project, temp_dir: temp_dir} do
test "analyze_wxr reports legacy progress steps while building the import report", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")

View File

@@ -6,15 +6,22 @@ defmodule BDS.ImportDefinitionsTest do
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-import-definitions-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-import-definitions-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
{:ok, project} = BDS.Projects.create_project(%{name: "Import Definitions", data_path: temp_dir})
{:ok, project} =
BDS.Projects.create_project(%{name: "Import Definitions", data_path: temp_dir})
%{project: project, temp_dir: temp_dir}
end
test "get, update, and delete round-trip import definition editor state", %{project: project, temp_dir: temp_dir} do
test "get, update, and delete round-trip import definition editor state", %{
project: project,
temp_dir: temp_dir
} do
uploads_folder_path = Path.join(temp_dir, "uploads")
wxr_file_path = Path.join(temp_dir, "legacy.xml")
@@ -40,15 +47,22 @@ defmodule BDS.ImportDefinitionsTest do
name: "Renamed Import",
wxr_file_path: Path.join(temp_dir, "renamed.xml"),
uploads_folder_path: Path.join(temp_dir, "renamed-uploads"),
last_analysis_result: %{site_info: %{title: "Renamed Blog"}, post_stats: %{new_count: 2}}
last_analysis_result: %{
site_info: %{title: "Renamed Blog"},
post_stats: %{new_count: 2}
}
})
assert updated.name == "Renamed Import"
assert updated.wxr_file_path == Path.join(temp_dir, "renamed.xml")
assert updated.uploads_folder_path == Path.join(temp_dir, "renamed-uploads")
assert updated.last_analysis_result == Jason.encode!(%{site_info: %{title: "Renamed Blog"}, post_stats: %{new_count: 2}})
assert [%{id: listed_id, title: "Renamed Import"}] = ImportDefinitions.list_definitions(project.id)
assert updated.last_analysis_result ==
Jason.encode!(%{site_info: %{title: "Renamed Blog"}, post_stats: %{new_count: 2}})
assert [%{id: listed_id, title: "Renamed Import"}] =
ImportDefinitions.list_definitions(project.id)
assert listed_id == definition.id
assert {:ok, :deleted} = ImportDefinitions.delete_definition(definition.id)

View File

@@ -12,7 +12,9 @@ defmodule BDS.ImportExecutionTest do
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-import-execution-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-import-execution-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -20,7 +22,10 @@ defmodule BDS.ImportExecutionTest do
%{project: project, temp_dir: temp_dir}
end
test "execute_import creates tags, posts, pages, and media from the analysis report", %{project: project, temp_dir: temp_dir} do
test "execute_import creates tags, posts, pages, and media from the analysis report", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")
@@ -46,7 +51,11 @@ defmodule BDS.ImportExecutionTest do
tag_names = project.id |> Tags.list_tags() |> Enum.map(& &1.name) |> Enum.sort()
assert tag_names == ["General", "News"]
posts = Repo.all(from post in Posts.Post, where: post.project_id == ^project.id, order_by: [asc: post.slug])
posts =
Repo.all(
from post in Posts.Post, where: post.project_id == ^project.id, order_by: [asc: post.slug]
)
assert Enum.map(posts, & &1.slug) == ["about", "hello-world"]
hello_world = Enum.find(posts, &(&1.slug == "hello-world"))
@@ -63,12 +72,17 @@ defmodule BDS.ImportExecutionTest do
assert about.content == nil
assert "page" in about.categories
imported_media = Repo.one!(from media in BDS.Media.Media, where: media.project_id == ^project.id)
imported_media =
Repo.one!(from media in BDS.Media.Media, where: media.project_id == ^project.id)
assert imported_media.original_name == "import-asset.txt"
assert File.exists?(Path.join(temp_dir, imported_media.file_path))
end
test "execute_import skips conflicts by default and can import them with a new slug", %{project: project, temp_dir: temp_dir} do
test "execute_import skips conflicts by default and can import them with a new slug", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _existing_post} =
Posts.create_post(%{
project_id: project.id,
@@ -82,21 +96,39 @@ defmodule BDS.ImportExecutionTest do
assert {:ok, report} = ImportAnalysis.analyze_wxr(project.id, wxr_path, nil)
assert {:ok, skipped_result} = ImportExecution.execute_import(project.id, report, default_author: "Imported Author")
assert {:ok, skipped_result} =
ImportExecution.execute_import(project.id, report, default_author: "Imported Author")
assert skipped_result.posts == %{imported: 0, skipped: 1, errors: 0}
assert Repo.aggregate(Posts.Post, :count, :id) == 1
import_report = put_in(report.items.posts, [%{List.first(report.items.posts) | resolution: "import"}])
import_report =
put_in(report.items.posts, [%{List.first(report.items.posts) | resolution: "import"}])
assert {:ok, imported_result} =
ImportExecution.execute_import(project.id, import_report,
default_author: "Imported Author"
)
assert {:ok, imported_result} = ImportExecution.execute_import(project.id, import_report, default_author: "Imported Author")
assert imported_result.posts == %{imported: 1, skipped: 0, errors: 0}
slugs = Repo.all(from post in Posts.Post, where: post.project_id == ^project.id, select: post.slug, order_by: [asc: post.slug])
slugs =
Repo.all(
from post in Posts.Post,
where: post.project_id == ^project.id,
select: post.slug,
order_by: [asc: post.slug]
)
assert length(slugs) == 2
assert "conflict-me" in slugs
assert Enum.any?(slugs, &(&1 != "conflict-me"))
end
test "execute_import reports phase progress while importing", %{project: project, temp_dir: temp_dir} do
test "execute_import reports phase progress while importing", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")

View File

@@ -115,7 +115,9 @@ defmodule BDS.MaintenanceTest do
assert {:ok, posts} = BDS.Maintenance.rebuild_from_filesystem(project.id, "post")
assert length(posts) == 1
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: "dispatch-post") != nil
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: "dispatch-post") !=
nil
assert {:ok, media_items} = BDS.Maintenance.rebuild_from_filesystem(project.id, "media")
assert length(media_items) == 1
@@ -313,17 +315,23 @@ defmodule BDS.MaintenanceTest do
assert_incremental_progress(collect_progress_events())
assert {:ok, _media} =
BDS.Maintenance.rebuild_from_filesystem(project.id, "media", on_progress: on_progress)
BDS.Maintenance.rebuild_from_filesystem(project.id, "media",
on_progress: on_progress
)
assert_incremental_progress(collect_progress_events())
assert {:ok, _scripts} =
BDS.Maintenance.rebuild_from_filesystem(project.id, "script", on_progress: on_progress)
BDS.Maintenance.rebuild_from_filesystem(project.id, "script",
on_progress: on_progress
)
assert_incremental_progress(collect_progress_events())
assert {:ok, _templates} =
BDS.Maintenance.rebuild_from_filesystem(project.id, "template", on_progress: on_progress)
BDS.Maintenance.rebuild_from_filesystem(project.id, "template",
on_progress: on_progress
)
assert_incremental_progress(collect_progress_events())
end
@@ -396,10 +404,16 @@ defmodule BDS.MaintenanceTest do
assert Enum.any?(diff_reports, fn report ->
report.entity_type == "embedding" and report.entity_id == post.id and
Enum.any?(report.differences, &(&1.name == "content_hash" and &1.file_value != "")) and
Enum.any?(report.differences, &(&1.name == "embedding" and &1.db_value == "missing" and &1.file_value == "re-embed required"))
Enum.any?(
report.differences,
&(&1.name == "embedding" and &1.db_value == "missing" and
&1.file_value == "re-embed required")
)
end)
assert {:ok, rebuilt_post_ids} = BDS.Maintenance.rebuild_from_filesystem(project.id, "embedding")
assert {:ok, rebuilt_post_ids} =
BDS.Maintenance.rebuild_from_filesystem(project.id, "embedding")
assert post.id in rebuilt_post_ids
assert Repo.get_by(BDS.Embeddings.Key, project_id: project.id, post_id: post.id) != nil
assert File.exists?(index_path)
@@ -652,7 +666,8 @@ defmodule BDS.MaintenanceTest do
assert Enum.any?(diff_reports, fn report ->
report.entity_type == "post" and report.entity_id == published_post.id and
report.label == "Original Post" and
report.meta_label == BDS.Persistence.timestamp_to_iso8601(published_post.created_at) and
report.meta_label ==
BDS.Persistence.timestamp_to_iso8601(published_post.created_at) and
Enum.any?(
report.differences,
&(&1.name == "title" and &1.db_value == "Original Post" and
@@ -798,10 +813,11 @@ defmodule BDS.MaintenanceTest do
end)
end
test "metadata_diff accepts legacy snake_case post frontmatter keys for status and timestamps", %{
project: project,
temp_dir: temp_dir
} do
test "metadata_diff accepts legacy snake_case post frontmatter keys for status and timestamps",
%{
project: project,
temp_dir: temp_dir
} do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
@@ -839,19 +855,28 @@ defmodule BDS.MaintenanceTest do
refute Enum.any?(diff_reports, fn report ->
report.entity_type == "post" and report.entity_id == published_post.id and
Enum.any?(report.differences, &(&1.name in ["status", "created_at", "updated_at", "published_at"]))
Enum.any?(
report.differences,
&(&1.name in ["status", "created_at", "updated_at", "published_at"])
)
end)
end
test "metadata_diff treats translation status and timestamps as inherited from the canonical post" do
legacy_dir =
Path.join(System.tmp_dir!(), "bds-maintenance-legacy-translation-#{System.unique_integer([:positive])}")
Path.join(
System.tmp_dir!(),
"bds-maintenance-legacy-translation-#{System.unique_integer([:positive])}"
)
File.mkdir_p!(legacy_dir)
on_exit(fn -> File.rm_rf(legacy_dir) end)
assert {:ok, project} =
BDS.Projects.create_project(%{name: "Legacy Translation Diff", data_path: legacy_dir})
BDS.Projects.create_project(%{
name: "Legacy Translation Diff",
data_path: legacy_dir
})
posts_dir = Path.join([BDS.Projects.project_data_dir(project), "posts", "2026", "04"])
File.mkdir_p!(posts_dir)
@@ -896,11 +921,17 @@ defmodule BDS.MaintenanceTest do
refute Enum.any?(diff_reports, fn report ->
report.entity_type == "post_translation" and
Enum.any?(report.differences, &(&1.name in ["status", "created_at", "updated_at", "published_at"]))
Enum.any?(
report.differences,
&(&1.name in ["status", "created_at", "updated_at", "published_at"])
)
end)
end
test "metadata_diff includes project-level metadata drift", %{project: project, temp_dir: temp_dir} do
test "metadata_diff includes project-level metadata drift", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{
name: "Database Blog",
@@ -955,12 +986,18 @@ defmodule BDS.MaintenanceTest do
assert Enum.any?(diff.diff_reports, fn report ->
report.entity_type == "project" and
Enum.any?(report.differences, &(&1.name == "main_language" and &1.db_value == "en" and &1.file_value == "fr"))
Enum.any?(
report.differences,
&(&1.name == "main_language" and &1.db_value == "en" and &1.file_value == "fr")
)
end)
assert Enum.any?(diff.diff_reports, fn report ->
report.entity_type == "publishing" and
Enum.any?(report.differences, &(&1.name == "ssh_mode" and &1.db_value == "scp" and &1.file_value == "rsync"))
Enum.any?(
report.differences,
&(&1.name == "ssh_mode" and &1.db_value == "scp" and &1.file_value == "rsync")
)
end)
end
@@ -986,7 +1023,10 @@ defmodule BDS.MaintenanceTest do
})
)
File.write!(Path.join([temp_dir, "meta", "categories.json"]), Jason.encode!(["notes", "updates"]))
File.write!(
Path.join([temp_dir, "meta", "categories.json"]),
Jason.encode!(["notes", "updates"])
)
File.write!(
Path.join([temp_dir, "meta", "category-meta.json"]),
@@ -1005,34 +1045,42 @@ defmodule BDS.MaintenanceTest do
})
)
write_post_frontmatter(fixture.post_path, %{
"id" => fixture.post.id,
"title" => "Filesystem Post",
"slug" => fixture.post.slug,
"excerpt" => "Filesystem summary",
"status" => "published",
"author" => "Filesystem Writer",
"language" => "fr",
"doNotTranslate" => false,
"templateSlug" => nil,
"createdAt" => fixture.post.created_at,
"updatedAt" => fixture.post.updated_at + 1,
"publishedAt" => fixture.post.published_at,
"tags" => ["beta"],
"categories" => ["updates"]
}, "Filesystem body")
write_post_frontmatter(
fixture.post_path,
%{
"id" => fixture.post.id,
"title" => "Filesystem Post",
"slug" => fixture.post.slug,
"excerpt" => "Filesystem summary",
"status" => "published",
"author" => "Filesystem Writer",
"language" => "fr",
"doNotTranslate" => false,
"templateSlug" => nil,
"createdAt" => fixture.post.created_at,
"updatedAt" => fixture.post.updated_at + 1,
"publishedAt" => fixture.post.published_at,
"tags" => ["beta"],
"categories" => ["updates"]
},
"Filesystem body"
)
write_post_frontmatter(fixture.post_translation_path, %{
"id" => fixture.post_translation.id,
"translationFor" => fixture.post_translation.translation_for,
"language" => fixture.post_translation.language,
"title" => "Datei Beitrag",
"excerpt" => "Datei Zusammenfassung",
"status" => "published",
"createdAt" => fixture.post_translation.created_at,
"updatedAt" => fixture.post_translation.updated_at + 1,
"publishedAt" => fixture.post_translation.published_at
}, "Datei Inhalt")
write_post_frontmatter(
fixture.post_translation_path,
%{
"id" => fixture.post_translation.id,
"translationFor" => fixture.post_translation.translation_for,
"language" => fixture.post_translation.language,
"title" => "Datei Beitrag",
"excerpt" => "Datei Zusammenfassung",
"status" => "published",
"createdAt" => fixture.post_translation.created_at,
"updatedAt" => fixture.post_translation.updated_at + 1,
"publishedAt" => fixture.post_translation.published_at
},
"Datei Inhalt"
)
File.write!(
fixture.media_sidecar_path,
@@ -1068,16 +1116,26 @@ defmodule BDS.MaintenanceTest do
|> Enum.join("\n")
)
write_script_frontmatter(fixture.script_path, fixture.script, %{
"title" => "Filesystem Script",
"entrypoint" => "run",
"enabled" => false
}, "function run() return false end")
write_script_frontmatter(
fixture.script_path,
fixture.script,
%{
"title" => "Filesystem Script",
"entrypoint" => "run",
"enabled" => false
},
"function run() return false end"
)
write_template_frontmatter(fixture.template_path, fixture.template, %{
"title" => "Filesystem Template",
"enabled" => false
}, "<section>Filesystem</section>")
write_template_frontmatter(
fixture.template_path,
fixture.template,
%{
"title" => "Filesystem Template",
"enabled" => false
},
"<section>Filesystem</section>"
)
items = [
%{entity_type: "project", entity_id: project.id},
@@ -1163,41 +1221,73 @@ defmodule BDS.MaintenanceTest do
})
assert {:ok, _metadata} = BDS.Metadata.add_category(project.id, "guides")
assert {:ok, _metadata} = BDS.Metadata.update_category_settings(project.id, "notes", %{title: "DB Notes", show_title: true})
assert {:ok, _metadata} =
BDS.Metadata.update_category_settings(project.id, "notes", %{
title: "DB Notes",
show_title: true
})
from(post in BDS.Posts.Post, where: post.id == ^fixture.post.id)
|> Repo.update_all(set: [
title: "Database Post",
excerpt: "Database summary",
author: "Database Writer",
language: "en",
tags: ["gamma"],
categories: ["guides"],
updated_at: fixture.post.updated_at + 2
])
|> Repo.update_all(
set: [
title: "Database Post",
excerpt: "Database summary",
author: "Database Writer",
language: "en",
tags: ["gamma"],
categories: ["guides"],
updated_at: fixture.post.updated_at + 2
]
)
from(translation in BDS.Posts.Translation, where: translation.id == ^fixture.post_translation.id)
|> Repo.update_all(set: [title: "DB Beitrag", excerpt: "DB Zusammenfassung", updated_at: fixture.post_translation.updated_at + 2])
from(translation in BDS.Posts.Translation,
where: translation.id == ^fixture.post_translation.id
)
|> Repo.update_all(
set: [
title: "DB Beitrag",
excerpt: "DB Zusammenfassung",
updated_at: fixture.post_translation.updated_at + 2
]
)
from(media in BDS.Media.Media, where: media.id == ^fixture.media.id)
|> Repo.update_all(set: [
title: "Database media title",
alt: "Database alt",
caption: "Database caption",
author: "Database Photographer",
language: "en",
tags: ["gamma"],
updated_at: fixture.media.updated_at + 2
])
|> Repo.update_all(
set: [
title: "Database media title",
alt: "Database alt",
caption: "Database caption",
author: "Database Photographer",
language: "en",
tags: ["gamma"],
updated_at: fixture.media.updated_at + 2
]
)
from(translation in BDS.Media.Translation, where: translation.id == ^fixture.media_translation.id)
from(translation in BDS.Media.Translation,
where: translation.id == ^fixture.media_translation.id
)
|> Repo.update_all(set: [title: "DB Medium", alt: "DB Alt", caption: "DB Bildtext"])
from(script in BDS.Scripts.Script, where: script.id == ^fixture.script.id)
|> Repo.update_all(set: [title: "Database Script", entrypoint: "run", enabled: false, updated_at: fixture.script.updated_at + 2])
|> Repo.update_all(
set: [
title: "Database Script",
entrypoint: "run",
enabled: false,
updated_at: fixture.script.updated_at + 2
]
)
from(template in BDS.Templates.Template, where: template.id == ^fixture.template.id)
|> Repo.update_all(set: [title: "Database Template", enabled: false, updated_at: fixture.template.updated_at + 2])
|> Repo.update_all(
set: [
title: "Database Template",
enabled: false,
updated_at: fixture.template.updated_at + 2
]
)
items = [
%{entity_type: "project", entity_id: project.id},
@@ -1222,7 +1312,9 @@ defmodule BDS.MaintenanceTest do
categories_json = Jason.decode!(File.read!(Path.join([temp_dir, "meta", "categories.json"])))
assert "guides" in categories_json
category_meta_json = Jason.decode!(File.read!(Path.join([temp_dir, "meta", "category-meta.json"])))
category_meta_json =
Jason.decode!(File.read!(Path.join([temp_dir, "meta", "category-meta.json"])))
assert category_meta_json["notes"]["title"] == "DB Notes"
publishing_json = Jason.decode!(File.read!(Path.join([temp_dir, "meta", "publishing.json"])))
@@ -1258,96 +1350,120 @@ defmodule BDS.MaintenanceTest do
File.mkdir_p!(Path.join(temp_dir, "scripts"))
File.mkdir_p!(Path.join(temp_dir, "templates"))
File.write!(Path.join(temp_dir, post_orphan_path), [
"---",
"id: orphan-post",
"title: Orphan Post",
"slug: orphan-post",
"status: published",
"createdAt: 1",
"updatedAt: 1",
"publishedAt: 1",
"tags:",
" - orphan",
"categories:",
" - notes",
"---",
"Orphan body",
""
] |> Enum.join("\n"))
File.write!(
Path.join(temp_dir, post_orphan_path),
[
"---",
"id: orphan-post",
"title: Orphan Post",
"slug: orphan-post",
"status: published",
"createdAt: 1",
"updatedAt: 1",
"publishedAt: 1",
"tags:",
" - orphan",
"categories:",
" - notes",
"---",
"Orphan body",
""
]
|> Enum.join("\n")
)
File.write!(Path.join(temp_dir, post_translation_orphan_path), [
"---",
"id: orphan-post-es",
"translationFor: #{fixture.post.id}",
"language: es",
"title: Verwaister Beitrag",
"excerpt: Verwaiste Zusammenfassung",
"status: published",
"createdAt: 1",
"updatedAt: 1",
"publishedAt: 1",
"---",
"Verwaister Inhalt",
""
] |> Enum.join("\n"))
File.write!(
Path.join(temp_dir, post_translation_orphan_path),
[
"---",
"id: orphan-post-es",
"translationFor: #{fixture.post.id}",
"language: es",
"title: Verwaister Beitrag",
"excerpt: Verwaiste Zusammenfassung",
"status: published",
"createdAt: 1",
"updatedAt: 1",
"publishedAt: 1",
"---",
"Verwaister Inhalt",
""
]
|> Enum.join("\n")
)
File.write!(media_orphan_file_path, "orphan media")
File.write!(Path.join(temp_dir, media_orphan_path), [
"id: orphan-media",
"originalName: orphan.txt",
"mimeType: text/plain",
"size: 12",
"title: Orphan Media",
"createdAt: 1",
"updatedAt: 1",
"tags:",
" - orphan",
""
] |> Enum.join("\n"))
File.write!(
Path.join(temp_dir, media_orphan_path),
[
"id: orphan-media",
"originalName: orphan.txt",
"mimeType: text/plain",
"size: 12",
"title: Orphan Media",
"createdAt: 1",
"updatedAt: 1",
"tags:",
" - orphan",
""
]
|> Enum.join("\n")
)
File.write!(Path.join(temp_dir, media_translation_orphan_path), [
"translationFor: orphan-media",
"language: es",
"title: Verwaistes Medium",
"alt: Verwaister Alt",
"caption: Verwaister Bildtext",
""
] |> Enum.join("\n"))
File.write!(
Path.join(temp_dir, media_translation_orphan_path),
[
"translationFor: orphan-media",
"language: es",
"title: Verwaistes Medium",
"alt: Verwaister Alt",
"caption: Verwaister Bildtext",
""
]
|> Enum.join("\n")
)
File.write!(Path.join(temp_dir, script_orphan_path), [
"---",
"id: orphan-script",
"projectId: #{project.id}",
"slug: orphan-script",
"title: Orphan Script",
"kind: utility",
"entrypoint: main",
"enabled: true",
"version: 1",
"createdAt: 1",
"updatedAt: 1",
"---",
"function main() return true end",
""
] |> Enum.join("\n"))
File.write!(
Path.join(temp_dir, script_orphan_path),
[
"---",
"id: orphan-script",
"projectId: #{project.id}",
"slug: orphan-script",
"title: Orphan Script",
"kind: utility",
"entrypoint: main",
"enabled: true",
"version: 1",
"createdAt: 1",
"updatedAt: 1",
"---",
"function main() return true end",
""
]
|> Enum.join("\n")
)
File.write!(Path.join(temp_dir, template_orphan_path), [
"---",
"id: orphan-template",
"projectId: #{project.id}",
"slug: orphan-view",
"title: Orphan View",
"kind: list",
"enabled: true",
"version: 1",
"createdAt: 1",
"updatedAt: 1",
"---",
"<section>Orphan</section>",
""
] |> Enum.join("\n"))
File.write!(
Path.join(temp_dir, template_orphan_path),
[
"---",
"id: orphan-template",
"projectId: #{project.id}",
"slug: orphan-view",
"title: Orphan View",
"kind: list",
"enabled: true",
"version: 1",
"createdAt: 1",
"updatedAt: 1",
"---",
"<section>Orphan</section>",
""
]
|> Enum.join("\n")
)
assert {:ok, %{imported: 6, failed: 0}} =
BDS.Maintenance.import_metadata_diff_orphans(project.id, [
@@ -1360,11 +1476,26 @@ defmodule BDS.MaintenanceTest do
])
assert Repo.get_by(BDS.Posts.Post, project_id: project.id, file_path: post_orphan_path)
assert Repo.get_by(BDS.Posts.Translation, project_id: project.id, file_path: post_translation_orphan_path)
assert Repo.get_by(BDS.Posts.Translation,
project_id: project.id,
file_path: post_translation_orphan_path
)
assert Repo.get_by(BDS.Media.Media, project_id: project.id, sidecar_path: media_orphan_path)
assert Repo.get_by(BDS.Media.Translation, project_id: project.id, translation_for: "orphan-media", language: "es")
assert Repo.get_by(BDS.Media.Translation,
project_id: project.id,
translation_for: "orphan-media",
language: "es"
)
assert Repo.get_by(BDS.Scripts.Script, project_id: project.id, file_path: script_orphan_path)
assert Repo.get_by(BDS.Templates.Template, project_id: project.id, file_path: template_orphan_path)
assert Repo.get_by(BDS.Templates.Template,
project_id: project.id,
file_path: template_orphan_path
)
end
defp collect_progress_events(acc \\ []) do
@@ -1481,7 +1612,8 @@ defmodule BDS.MaintenanceTest do
post_translation_path: Path.join(temp_dir, published_post_translation.file_path),
media: Repo.get!(BDS.Media.Media, media.id),
media_sidecar_path: Path.join(temp_dir, media.sidecar_path),
media_translation: Repo.get_by!(BDS.Media.Translation, translation_for: media.id, language: "de"),
media_translation:
Repo.get_by!(BDS.Media.Translation, translation_for: media.id, language: "de"),
media_translation_sidecar_path: Path.join(temp_dir, "#{media.file_path}.de.meta"),
script: Repo.get!(BDS.Scripts.Script, published_script.id),
script_path: Path.join(temp_dir, published_script.file_path),
@@ -1495,20 +1627,35 @@ defmodule BDS.MaintenanceTest do
[
"---",
"id: #{fields["id"]}",
if(fields["translationFor"], do: "translationFor: #{fields["translationFor"]}", else: nil),
if(fields["translationFor"],
do: "translationFor: #{fields["translationFor"]}",
else: nil
),
if(fields["title"], do: "title: #{fields["title"]}", else: nil),
if(fields["slug"], do: "slug: #{fields["slug"]}", else: nil),
if(fields["excerpt"], do: "excerpt: #{fields["excerpt"]}", else: nil),
if(fields["status"], do: "status: #{fields["status"]}", else: nil),
if(fields["author"], do: "author: #{fields["author"]}", else: nil),
if(fields["language"], do: "language: #{fields["language"]}", else: nil),
if(Map.has_key?(fields, "doNotTranslate"), do: "doNotTranslate: #{fields["doNotTranslate"]}", else: nil),
if(Map.has_key?(fields, "templateSlug"), do: "templateSlug: #{fields["templateSlug"] || ""}", else: nil),
if(Map.has_key?(fields, "doNotTranslate"),
do: "doNotTranslate: #{fields["doNotTranslate"]}",
else: nil
),
if(Map.has_key?(fields, "templateSlug"),
do: "templateSlug: #{fields["templateSlug"] || ""}",
else: nil
),
"createdAt: #{fields["createdAt"]}",
"updatedAt: #{fields["updatedAt"]}",
if(fields["publishedAt"], do: "publishedAt: #{fields["publishedAt"]}", else: nil),
if(Map.has_key?(fields, "tags"), do: ["tags:" | Enum.map(fields["tags"], &" - #{&1}")], else: nil),
if(Map.has_key?(fields, "categories"), do: ["categories:" | Enum.map(fields["categories"], &" - #{&1}")], else: nil),
if(Map.has_key?(fields, "tags"),
do: ["tags:" | Enum.map(fields["tags"], &" - #{&1}")],
else: nil
),
if(Map.has_key?(fields, "categories"),
do: ["categories:" | Enum.map(fields["categories"], &" - #{&1}")],
else: nil
),
"---",
body,
""

View File

@@ -10,7 +10,9 @@ defmodule BDS.MCPAgentConfigTest do
%{home_dir: home_dir}
end
test "github copilot config uses VS Code mcp.json servers format with stdio entry", %{home_dir: home_dir} do
test "github copilot config uses VS Code mcp.json servers format with stdio entry", %{
home_dir: home_dir
} do
install_root = Path.join(home_dir, "bDS2.app/Contents/Resources")
executable_path = Path.join(install_root, "mcp/bin/bds-mcp")
@@ -31,7 +33,9 @@ defmodule BDS.MCPAgentConfigTest do
}
end
test "claude code config uses mcpServers format and preserves other entries", %{home_dir: home_dir} do
test "claude code config uses mcpServers format and preserves other entries", %{
home_dir: home_dir
} do
config_path = Path.join(home_dir, ".claude.json")
install_root = Path.join(home_dir, "dist")
@@ -50,7 +54,11 @@ defmodule BDS.MCPAgentConfigTest do
written = Jason.decode!(File.read!(result.config_path))
assert written["theme"] == "dark"
assert written["mcpServers"]["other"] == %{"command" => "python"}
assert written["mcpServers"]["bDS"] == %{"command" => Path.join(install_root, "mcp/bin/bds-mcp"), "args" => []}
assert written["mcpServers"]["bDS"] == %{
"command" => Path.join(install_root, "mcp/bin/bds-mcp"),
"args" => []
}
end
test "github copilot uninstall removes only the bDS server entry", %{home_dir: home_dir} do
@@ -74,7 +82,12 @@ defmodule BDS.MCPAgentConfigTest do
written = Jason.decode!(File.read!(result.config_path))
assert written["theme"] == "dark"
refute Map.has_key?(written["servers"], "bDS")
assert written["servers"]["other"] == %{"type" => "stdio", "command" => "python", "args" => ["server.py"]}
assert written["servers"]["other"] == %{
"type" => "stdio",
"command" => "python",
"args" => ["server.py"]
}
end
test "claude code uninstall removes only the bDS server entry", %{home_dir: home_dir} do
@@ -122,7 +135,10 @@ defmodule BDS.MCPAgentConfigTest do
end
test "packaged executable path resolves inside the distributable payload" do
assert AgentConfig.packaged_executable_path("/Applications/bDS2.app/Contents/Resources", :macos) ==
assert AgentConfig.packaged_executable_path(
"/Applications/bDS2.app/Contents/Resources",
:macos
) ==
"/Applications/bDS2.app/Contents/Resources/mcp/bin/bds-mcp"
assert AgentConfig.packaged_executable_path("C:/Program Files/bDS2/resources", :windows) ==

View File

@@ -3,7 +3,10 @@ defmodule BDS.MCPServerTest do
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-mcp-server-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-mcp-server-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -36,7 +39,8 @@ defmodule BDS.MCPServerTest do
:httpc.request(
:post,
{to_charlist("http://127.0.0.1:#{server.port}/mcp"),
[{~c"content-type", ~c"application/json"}], ~c"application/json", initialize_body},
[{~c"content-type", ~c"application/json"}], ~c"application/json",
initialize_body},
[],
body_format: :binary
)
@@ -64,8 +68,10 @@ defmodule BDS.MCPServerTest do
:httpc.request(
:post,
{to_charlist("http://127.0.0.1:#{server.port}/mcp"),
[{~c"content-type", ~c"application/json"}, {~c"origin", ~c"https://evil.example"}],
~c"application/json", initialize_body},
[
{~c"content-type", ~c"application/json"},
{~c"origin", ~c"https://evil.example"}
], ~c"application/json", initialize_body},
[],
body_format: :binary
)

View File

@@ -72,15 +72,20 @@ defmodule BDS.MCPTest do
assert read_result["post"]["slug"] == "travel-notes"
end
test "proposal-backed write tools follow the old app lifecycle for scripts, templates, and metadata", %{
project: project,
temp_dir: temp_dir
} do
test "proposal-backed write tools follow the old app lifecycle for scripts, templates, and metadata",
%{
project: project,
temp_dir: temp_dir
} do
source_path = Path.join(temp_dir, "image.txt")
File.write!(source_path, "image body")
assert {:ok, media} =
BDS.Media.import_media(%{project_id: project.id, source_path: source_path, title: "Old"})
BDS.Media.import_media(%{
project_id: project.id,
source_path: source_path,
title: "Old"
})
assert {:ok, post} =
BDS.Posts.create_post(%{
@@ -100,7 +105,10 @@ defmodule BDS.MCPTest do
draft_proposal_id = draft_result["proposal_id"]
draft_post_id = draft_result["post"]["id"]
assert {:ok, _accepted} = BDS.MCP.call_tool("accept_proposal", %{proposalId: draft_proposal_id})
assert {:ok, _accepted} =
BDS.MCP.call_tool("accept_proposal", %{proposalId: draft_proposal_id})
assert BDS.Posts.get_post!(draft_post_id).status == :published
assert {:ok, script_result} =
@@ -111,6 +119,7 @@ defmodule BDS.MCPTest do
})
script_id = script_result["script"]["id"]
assert {:ok, _accepted_script} =
BDS.MCP.call_tool("accept_proposal", %{proposalId: script_result["proposal_id"]})
@@ -124,6 +133,7 @@ defmodule BDS.MCPTest do
})
template_id = template_result["template"]["id"]
assert {:ok, _accepted_template} =
BDS.MCP.call_tool("accept_proposal", %{proposalId: template_result["proposal_id"]})
@@ -138,6 +148,7 @@ defmodule BDS.MCPTest do
assert {:ok, _accepted_media} =
BDS.MCP.call_tool("accept_proposal", %{proposalId: media_proposal["proposal_id"]})
updated_media = Repo.get!(Media, media.id)
assert updated_media.title == "New Title"
assert updated_media.alt == "Alt Text"
@@ -202,7 +213,9 @@ defmodule BDS.MCPTest do
assert ProposalStore.get(expired.id).status == :expired
end
test "resource listing and reads follow old app naming for implemented resources", %{project: project} do
test "resource listing and reads follow old app naming for implemented resources", %{
project: project
} do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,

View File

@@ -53,10 +53,11 @@ defmodule BDS.MenuTest do
assert loaded == menu
end
test "sync_menu_from_filesystem loads canonical bDS OPML and preserves a prepended Home entry", %{
project: project,
temp_dir: temp_dir
} do
test "sync_menu_from_filesystem loads canonical bDS OPML and preserves a prepended Home entry",
%{
project: project,
temp_dir: temp_dir
} do
meta_dir = Path.join(temp_dir, "meta")
File.mkdir_p!(meta_dir)

View File

@@ -6,7 +6,9 @@ defmodule BDS.PostLinksTest do
setup do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
temp_dir = Path.join(System.tmp_dir!(), "bds-post-links-#{System.unique_integer([:positive])}")
temp_dir =
Path.join(System.tmp_dir!(), "bds-post-links-#{System.unique_integer([:positive])}")
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -15,9 +17,10 @@ defmodule BDS.PostLinksTest do
%{project: project, temp_dir: temp_dir}
end
test "publishing and updating posts sync outgoing post links and deleting a post removes them", %{
project: project
} do
test "publishing and updating posts sync outgoing post links and deleting a post removes them",
%{
project: project
} do
assert {:ok, target} =
BDS.Posts.create_post(%{
project_id: project.id,

View File

@@ -23,7 +23,12 @@ defmodule BDS.PostTranslationsTest do
"excerpt" => "Kurze Zusammenfassung",
"content" => "# Hallo Welt\n\nUbersetzter Inhalt"
},
usage: %{input_tokens: 22, output_tokens: 14, cache_read_tokens: 0, cache_write_tokens: 0}
usage: %{
input_tokens: 22,
output_tokens: 14,
cache_read_tokens: 0,
cache_write_tokens: 0
}
}}
:translate_media ->
@@ -34,7 +39,12 @@ defmodule BDS.PostTranslationsTest do
"alt" => "Medien Alt",
"caption" => "Medien Beschriftung"
},
usage: %{input_tokens: 12, output_tokens: 10, cache_read_tokens: 0, cache_write_tokens: 0}
usage: %{
input_tokens: 12,
output_tokens: 10,
cache_read_tokens: 0,
cache_write_tokens: 0
}
}}
end
end
@@ -123,8 +133,9 @@ defmodule BDS.PostTranslationsTest do
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")
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)
@@ -307,7 +318,7 @@ defmodule BDS.PostTranslationsTest do
url: "https://api.example.test/v1",
api_key: "online-secret",
model: "gpt-4o-mini"
}
}
)
assert :ok = AI.set_airplane_mode(false)

View File

@@ -344,7 +344,10 @@ defmodule BDS.PostsTest do
test "rebuild_posts_from_files imports canonical bDS translation files alongside canonical posts" do
temp_dir =
Path.join(System.tmp_dir!(), "bds-post-rebuild-legacy-#{System.unique_integer([:positive])}")
Path.join(
System.tmp_dir!(),
"bds-post-rebuild-legacy-#{System.unique_integer([:positive])}"
)
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -415,7 +418,10 @@ defmodule BDS.PostsTest do
test "rebuild_posts_from_files parses quoted canonical timestamps and inline empty tag arrays" do
temp_dir =
Path.join(System.tmp_dir!(), "bds-post-rebuild-quoted-#{System.unique_integer([:positive])}")
Path.join(
System.tmp_dir!(),
"bds-post-rebuild-quoted-#{System.unique_integer([:positive])}"
)
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -471,7 +477,10 @@ defmodule BDS.PostsTest do
test "rebuild_posts_from_files parses folded multiline title and slug scalars alongside translations" do
temp_dir =
Path.join(System.tmp_dir!(), "bds-post-rebuild-folded-#{System.unique_integer([:positive])}")
Path.join(
System.tmp_dir!(),
"bds-post-rebuild-folded-#{System.unique_integer([:positive])}"
)
File.mkdir_p!(temp_dir)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -548,8 +557,8 @@ 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
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,
@@ -658,7 +667,9 @@ defmodule BDS.PostsTest do
refute BDS.Repo.get(BDS.Posts.Post, stale_post.id)
end
test "rebuild_posts_from_files batches search and embedding refresh after import", %{project: project} do
test "rebuild_posts_from_files batches search and embedding refresh after import", %{
project: project
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{semantic_similarity_enabled: true})
@@ -781,7 +792,11 @@ defmodule BDS.PostsTest do
saved_translation = BDS.Repo.get!(BDS.Posts.Translation, translation.id)
assert saved_translation.content == nil
assert is_binary(saved_translation.file_path)
assert File.exists?(Path.join(BDS.Projects.project_data_dir(project), saved_translation.file_path))
assert File.exists?(
Path.join(BDS.Projects.project_data_dir(project), saved_translation.file_path)
)
refute File.exists?(invalid_file_path)
end

View File

@@ -79,31 +79,31 @@ defmodule BDS.PreviewTest do
BDS.Preview.request(project.id, "/pagefind/pagefind-ui.js")
assert {:ok, %{body: pico_css, content_type: "text/css"}} =
BDS.Preview.request(project.id, "/assets/pico.min.css")
BDS.Preview.request(project.id, "/assets/pico.min.css")
assert pico_css =~ ":root"
assert {:ok, %{body: bds_css, content_type: "text/css"}} =
BDS.Preview.request(project.id, "/assets/bds.css")
BDS.Preview.request(project.id, "/assets/bds.css")
assert bds_css =~ ".blog-menu"
assert {:ok, %{body: calendar_runtime, content_type: "application/javascript"}} =
BDS.Preview.request(project.id, "/assets/calendar-runtime.js")
assert {:ok, %{body: calendar_runtime, content_type: "application/javascript"}} =
BDS.Preview.request(project.id, "/assets/calendar-runtime.js")
assert calendar_runtime =~ "loadCalendarData"
assert calendar_runtime =~ "window.location.assign"
assert calendar_runtime =~ "loadCalendarData"
assert calendar_runtime =~ "window.location.assign"
assert {:ok, %{body: tag_cloud_runtime, content_type: "application/javascript"}} =
BDS.Preview.request(project.id, "/assets/tag-cloud.js")
assert {:ok, %{body: tag_cloud_runtime, content_type: "application/javascript"}} =
BDS.Preview.request(project.id, "/assets/tag-cloud.js")
assert tag_cloud_runtime =~ "data-tag-cloud-words"
assert tag_cloud_runtime =~ "data-tag-cloud-words"
assert {:ok, %{body: _prev_png, content_type: "image/png"}} =
BDS.Preview.request(project.id, "/images/prev.png")
assert {:ok, %{body: _prev_png, content_type: "image/png"}} =
BDS.Preview.request(project.id, "/images/prev.png")
assert {:ok, %{body: _loading_gif, content_type: "image/gif"}} =
BDS.Preview.request(project.id, "/images/loading.gif")
assert {:ok, %{body: _loading_gif, content_type: "image/gif"}} =
BDS.Preview.request(project.id, "/images/loading.gif")
assert {:ok, %{body: "media body", content_type: "text/plain"}} =
BDS.Preview.request(project.id, "/media/2026/04/image.txt")
@@ -152,6 +152,7 @@ defmodule BDS.PreviewTest do
assert {:ok, published_post} = Posts.publish_post(post.id)
published_datetime = DateTime.from_unix!(published_post.created_at, :millisecond)
published_path =
"/#{published_datetime.year}/#{String.pad_leading(Integer.to_string(published_datetime.month), 2, "0")}/#{String.pad_leading(Integer.to_string(published_datetime.day), 2, "0")}/#{published_post.slug}"
@@ -162,7 +163,9 @@ defmodule BDS.PreviewTest do
assert {:ok, {{_version, 200, _reason}, _headers, published_html}} =
:httpc.request(
:get,
{to_charlist("http://#{server.host}:#{server.port}#{published_path}?draft=true&post_id=#{published_post.id}"), []},
{to_charlist(
"http://#{server.host}:#{server.port}#{published_path}?draft=true&post_id=#{published_post.id}"
), []},
[],
body_format: :binary
)
@@ -449,7 +452,11 @@ defmodule BDS.PreviewTest do
assert generated_html =~ ~s(/assets/pico.amber.min.css)
assert {:ok, %{body: draft_html, content_type: "text/html"}} =
BDS.Preview.preview_draft(project.id, "/draft/theme-draft?theme=amber&mode=dark", post.id)
BDS.Preview.preview_draft(
project.id,
"/draft/theme-draft?theme=amber&mode=dark",
post.id
)
assert draft_html =~ ~s(data-theme="dark")
assert draft_html =~ ~s(data-mode="dark")

View File

@@ -135,7 +135,9 @@ defmodule BDS.ProjectsTest do
Repo.delete_all(Project)
assert {:ok, default_project} = BDS.Projects.ensure_default_project()
assert {:error, :cannot_delete_default_project} = BDS.Projects.delete_project(default_project.id)
assert {:error, :cannot_delete_default_project} =
BDS.Projects.delete_project(default_project.id)
temp_dir = Path.join(temp_root, "active-delete")
File.mkdir_p!(temp_dir)
@@ -148,7 +150,9 @@ defmodule BDS.ProjectsTest do
assert %Project{id: ^project_id} = BDS.Projects.get_project(project.id)
end
test "delete_project removes internal project data but preserves external data paths", %{temp_root: temp_root} do
test "delete_project removes internal project data but preserves external data paths", %{
temp_root: temp_root
} do
assert {:ok, internal_project} = BDS.Projects.create_project(%{name: "Internal Project"})
internal_dir = BDS.Projects.project_data_dir(internal_project)
@@ -179,7 +183,9 @@ defmodule BDS.ProjectsTest do
assert File.read!(marker_path) == "preserve me"
end
test "create_project loads project metadata from an existing filesystem-backed blog", %{temp_root: temp_root} do
test "create_project loads project metadata from an existing filesystem-backed blog", %{
temp_root: temp_root
} do
external_dir = Path.join(temp_root, "imported-blog")
meta_dir = Path.join(external_dir, "meta")
File.mkdir_p!(meta_dir)

View File

@@ -53,7 +53,9 @@ defmodule BDS.RealBlogRebuildDiagnosticTest do
assert task.status == :completed
end
test "rebuilds posts from the external real blog path twice in the same project", %{project: project} do
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

View File

@@ -4,7 +4,9 @@ defmodule BDS.ReleasePackagingTest do
alias BDS.ReleasePackaging
setup do
base_dir = Path.join(System.tmp_dir!(), "bds-release-packaging-#{System.unique_integer([:positive])}")
base_dir =
Path.join(System.tmp_dir!(), "bds-release-packaging-#{System.unique_integer([:positive])}")
output_dir = Path.join(base_dir, "dist")
app_release = Path.join(base_dir, "rel/bds")
mcp_release = Path.join(base_dir, "rel/bds_mcp")
@@ -16,7 +18,12 @@ defmodule BDS.ReleasePackagingTest do
on_exit(fn -> File.rm_rf(base_dir) end)
%{base_dir: base_dir, output_dir: output_dir, app_release: app_release, mcp_release: mcp_release}
%{
base_dir: base_dir,
output_dir: output_dir,
app_release: app_release,
mcp_release: mcp_release
}
end
test "build metadata uses a clean future-app payload layout" do
@@ -45,7 +52,8 @@ defmodule BDS.ReleasePackagingTest do
assert File.exists?(Path.join(metadata.mcp_root, "bin/bds-mcp"))
assert File.exists?(Path.join(metadata.payload_root, "manifest.json"))
manifest = metadata.payload_root |> Path.join("manifest.json") |> File.read!() |> Jason.decode!()
manifest =
metadata.payload_root |> Path.join("manifest.json") |> File.read!() |> Jason.decode!()
assert manifest == %{
"platform" => "macos",

View File

@@ -132,8 +132,12 @@ defmodule BDS.RenderingTest do
assert rendered_target =~ "alts=[en=#{canonical_post_href(target)}]"
assert rendered_target =~ "[de=/de#{canonical_post_href(target)}]"
assert rendered_target =~ "backlinks=[linking-source=Linking Source=#{canonical_post_href(source)}]"
assert rendered_target =~ "incoming=[linking-source=Linking Source=#{canonical_post_href(source)}]"
assert rendered_target =~
"backlinks=[linking-source=Linking Source=#{canonical_post_href(source)}]"
assert rendered_target =~
"incoming=[linking-source=Linking Source=#{canonical_post_href(source)}]"
assert {:ok, rendered_source} =
Rendering.render_post_page(project.id, published_template.slug, %{
@@ -145,7 +149,8 @@ defmodule BDS.RenderingTest do
template_slug: published_template.slug
})
assert rendered_source =~ "outgoing=[linked-target=Linked Target=#{canonical_post_href(target)}]"
assert rendered_source =~
"outgoing=[linked-target=Linked Target=#{canonical_post_href(target)}]"
end
test "render_list_page exposes pagination and render_not_found_page localizes default copy", %{
@@ -311,9 +316,10 @@ defmodule BDS.RenderingTest do
assert rendered =~ "range=1711843200-1711929600"
end
test "render_post_page falls back to bundled starter template when the published default template file is missing", %{
project: project
} do
test "render_post_page falls back to bundled starter template when the published default template file is missing",
%{
project: project
} do
assert {:ok, _metadata} =
BDS.Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",

View File

@@ -21,7 +21,8 @@ defmodule BDS.Repo.BootstrapTest do
end
test "ensure_schema creates persistence tables in a blank sqlite database" do
temp_db = Path.join(System.tmp_dir!(), "bds-bootstrap-#{System.unique_integer([:positive])}.db")
temp_db =
Path.join(System.tmp_dir!(), "bds-bootstrap-#{System.unique_integer([:positive])}.db")
Application.put_env(:bds, TempRepo,
database: temp_db,
@@ -39,7 +40,11 @@ defmodule BDS.Repo.BootstrapTest do
assert :ok = BDS.RepoBootstrap.ensure_schema(repo: TempRepo)
tables =
Ecto.Adapters.SQL.query!(TempRepo, "SELECT name FROM sqlite_master WHERE type = 'table'", []).rows
Ecto.Adapters.SQL.query!(
TempRepo,
"SELECT name FROM sqlite_master WHERE type = 'table'",
[]
).rows
|> Enum.map(&hd/1)
assert "projects" in tables
@@ -52,7 +57,8 @@ defmodule BDS.Repo.BootstrapTest do
assert :ok = BDS.RepoBootstrap.ensure_ready(migrate?: false)
assert %Project{id: "default", name: "My Blog", is_active: true} = BDS.Projects.get_active_project()
assert %Project{id: "default", name: "My Blog", is_active: true} =
BDS.Projects.get_active_project()
end
test "dev repo config disables query logging by default" do

View File

@@ -1,8 +1,12 @@
defmodule BDS.Scripting.ApiTest do
use ExUnit.Case, async: false
@tiny_png_1 Base.decode64!("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/a6sAAAAASUVORK5CYII=")
@tiny_png_2 Base.decode64!("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR42mP8z/C/HwAF/gL+qJNmNwAAAABJRU5ErkJggg==")
@tiny_png_1 Base.decode64!(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/a6sAAAAASUVORK5CYII="
)
@tiny_png_2 Base.decode64!(
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADUlEQVR42mP8z/C/HwAF/gL+qJNmNwAAAABJRU5ErkJggg=="
)
alias BDS.Repo
alias BDS.Scripts.Script
@@ -60,34 +64,42 @@ defmodule BDS.Scripting.ApiTest do
assert {:ok, _published_post} = BDS.Posts.publish_post(post.id)
assert {:ok, _tags} = BDS.Tags.sync_tags_from_posts(project.id)
source = [
"function main()",
" local meta = bds.meta.get_project_metadata()",
" local fetched = bds.posts.get_by_slug('capability-post')",
" local tags = bds.tags.get_all()",
" return {",
" project_name = meta.name,",
" post_title = fetched.title,",
" tag_count = #tags",
" }",
"end"
]
|> Enum.join("\n")
source =
[
"function main()",
" local meta = bds.meta.get_project_metadata()",
" local fetched = bds.posts.get_by_slug('capability-post')",
" local tags = bds.tags.get_all()",
" return {",
" project_name = meta.name,",
" post_title = fetched.title,",
" tag_count = #tags",
" }",
"end"
]
|> Enum.join("\n")
assert {:ok, %{"project_name" => "Scripting API", "post_title" => "Capability Post", "tag_count" => 1}} =
assert {:ok,
%{
"project_name" => "Scripting API",
"post_title" => "Capability Post",
"tag_count" => 1
}} =
BDS.Scripting.execute_project_script(project.id, source, "main")
end
test "macro execution uses explicit project capabilities and degrades failures to empty output", %{
project: project
} do
source = [
"function render()",
" local meta = bds.meta.get_project_metadata()",
" return '<strong>' .. meta.name .. '</strong>'",
"end"
]
|> Enum.join("\n")
test "macro execution uses explicit project capabilities and degrades failures to empty output",
%{
project: project
} do
source =
[
"function render()",
" local meta = bds.meta.get_project_metadata()",
" return '<strong>' .. meta.name .. '</strong>'",
"end"
]
|> Enum.join("\n")
assert {:ok, "<strong>Scripting API</strong>"} =
BDS.Scripting.execute_macro(project.id, source, [])
@@ -97,9 +109,10 @@ defmodule BDS.Scripting.ApiTest do
assert {:ok, ""} = BDS.Scripting.execute_macro(project.id, bad_source, [])
end
test "project scripting exposes project, post, script, template, metadata, and task namespaces", %{
project: project
} do
test "project scripting exposes project, post, script, template, metadata, and task namespaces",
%{
project: project
} do
assert {:ok, post} =
BDS.Posts.create_post(%{
project_id: project.id,
@@ -278,7 +291,8 @@ defmodule BDS.Scripting.ApiTest do
source =
[
"function main()",
" local updated = bds.projects.update('" <> project.id <> "', { description = 'Updated through Lua' })",
" local updated = bds.projects.update('" <>
project.id <> "', { description = 'Updated through Lua' })",
" bds.meta.set_publishing_preferences({ ssh_host = 'example.test', ssh_user = 'deploy', ssh_remote_path = '/srv/www', ssh_mode = 'scp' })",
" local prefs = bds.meta.get_publishing_preferences()",
" local categories = bds.meta.get_categories()",
@@ -450,11 +464,14 @@ defmodule BDS.Scripting.ApiTest do
" if not ok then error(name .. ': ' .. tostring(length)) end",
" return length",
" end",
" local imported = step('media.import', function() return bds.media.import({ source_path = '" <> escape_lua_string(media_source_path) <> "', title = 'Imported Image', alt = 'Alt text', caption = 'Caption', tags = { 'gallery', 'cover' }, language = 'en' }) end)",
" local imported = step('media.import', function() return bds.media.import({ source_path = '" <>
escape_lua_string(media_source_path) <>
"', title = 'Imported Image', alt = 'Alt text', caption = 'Caption', tags = { 'gallery', 'cover' }, language = 'en' }) end)",
" local translation = step('media.upsert_translation', function() return bds.media.upsert_translation(imported.id, 'de', { title = 'Bild', alt = 'Alt de', caption = 'Beschriftung' }) end)",
" local fetched_translation = step('media.get_translation', function() return bds.media.get_translation(imported.id, 'de') end)",
" local translation_count = count('media.get_translations.count', step('media.get_translations', function() return bds.media.get_translations(imported.id) end))",
" local media_filter = step('media.filter', function() return bds.media.filter({ year = " <> Integer.to_string(Date.utc_today().year) <> ", tags = { 'gallery' } }) end)",
" local media_filter = step('media.filter', function() return bds.media.filter({ year = " <>
Integer.to_string(Date.utc_today().year) <> ", tags = { 'gallery' } }) end)",
" local media_search = step('media.search', function() return bds.media.search('Imported') end)",
" local media_counts = step('media.get_by_year_month', function() return bds.media.get_by_year_month() end)",
" local media_tags = step('media.get_tags', function() return bds.media.get_tags() end)",
@@ -464,7 +481,8 @@ defmodule BDS.Scripting.ApiTest do
" local thumbnail = step('media.get_thumbnail', function() return bds.media.get_thumbnail(imported.id, 'small') end)",
" local regenerated = step('media.regenerate_thumbnails', function() return bds.media.regenerate_thumbnails(imported.id) end)",
" local missing = step('media.regenerate_missing_thumbnails', function() return bds.media.regenerate_missing_thumbnails() end)",
" local replaced = step('media.replace_file', function() return bds.media.replace_file(imported.id, '" <> escape_lua_string(replacement_source_path) <> "') end)",
" local replaced = step('media.replace_file', function() return bds.media.replace_file(imported.id, '" <>
escape_lua_string(replacement_source_path) <> "') end)",
" local rebuilt_media = step('media.rebuild_from_files', function() return bds.media.rebuild_from_files() end)",
" local media_reindexed = step('media.reindex_text', function() return bds.media.reindex_text() end)",
" local deleted_translation = step('media.delete_translation', function() return bds.media.delete_translation(imported.id, 'de') end)",
@@ -474,13 +492,19 @@ defmodule BDS.Scripting.ApiTest do
" local by_month = step('posts.get_by_year_month', function() return bds.posts.get_by_year_month() end)",
" local dashboard = step('posts.get_dashboard_stats', function() return bds.posts.get_dashboard_stats() end)",
" local filtered = step('posts.filter', function() return bds.posts.filter({ status = 'draft', tags = { 'source' } }) end)",
" local rebuilt_links_before = step('posts.get_links_to.before', function() return bds.posts.get_links_to('" <> source_post.id <> "') end)",
" local rebuilt_links_before = step('posts.get_links_to.before', function() return bds.posts.get_links_to('" <>
source_post.id <> "') end)",
" step('posts.rebuild_links', function() return bds.posts.rebuild_links() end)",
" local links_to = step('posts.get_links_to.after', function() return bds.posts.get_links_to('" <> source_post.id <> "') end)",
" local linked_by = step('posts.get_linked_by', function() return bds.posts.get_linked_by('" <> target_post.id <> "') end)",
" local preview_url = step('posts.get_preview_url', function() return bds.posts.get_preview_url('" <> source_post.id <> "', { draft = true, lang = 'de' }) end)",
" local published_translation = step('posts.publish_translation', function() return bds.posts.publish_translation('" <> source_post.id <> "', 'de') end)",
" local discarded = step('posts.discard', function() return bds.posts.discard('" <> source_post.id <> "') end)",
" local links_to = step('posts.get_links_to.after', function() return bds.posts.get_links_to('" <>
source_post.id <> "') end)",
" local linked_by = step('posts.get_linked_by', function() return bds.posts.get_linked_by('" <>
target_post.id <> "') end)",
" local preview_url = step('posts.get_preview_url', function() return bds.posts.get_preview_url('" <>
source_post.id <> "', { draft = true, lang = 'de' }) end)",
" local published_translation = step('posts.publish_translation', function() return bds.posts.publish_translation('" <>
source_post.id <> "', 'de') end)",
" local discarded = step('posts.discard', function() return bds.posts.discard('" <>
source_post.id <> "') end)",
" return {",
" translation_title = translation and translation.title or nil,",
" fetched_translation_title = fetched_translation and fetched_translation.title or nil,",
@@ -563,7 +587,8 @@ defmodule BDS.Scripting.ApiTest do
" local metrics = bds.app.get_title_bar_metrics()",
" local ready = bds.app.notify_renderer_ready()",
" bds.app.set_preview_post_target(nil)",
" local open_result = bds.app.open_folder('" <> escape_lua_string(project.data_path) <> "')",
" local open_result = bds.app.open_folder('" <>
escape_lua_string(project.data_path) <> "')",
" bds.app.show_item_in_folder('" <> escape_lua_string(sample_file_path) <> "')",
" bds.app.trigger_menu_action('new_post')",
" local startup = bds.meta.sync_on_startup()",

View File

@@ -0,0 +1,82 @@
defmodule BDS.SpecCoverageTest do
use ExUnit.Case, async: true
@section_10_files [
"lib/bds/tags.ex",
"lib/bds/templates.ex",
"lib/bds/scripts.ex",
"lib/bds/post_links.ex"
]
@section_10_editor_globs [
"lib/bds/desktop/shell_live/*editor.ex",
"lib/bds/desktop/shell_live/*_editor/*.ex"
]
describe "CODESMELL Section 10" do
test "smaller contexts have specs for all public functions" do
root = File.cwd!()
offenders =
section_10_files(root)
|> Enum.flat_map(fn relative_path ->
relative_path
|> Path.join("")
|> then(&Path.join(root, &1))
|> public_functions_without_specs(relative_path)
end)
assert offenders == []
end
end
defp section_10_files(root) do
editor_files =
@section_10_editor_globs
|> Enum.flat_map(fn pattern -> Path.wildcard(Path.join(root, pattern)) end)
|> Enum.map(&Path.relative_to(&1, root))
(@section_10_files ++ editor_files)
|> Enum.uniq()
|> Enum.sort()
end
defp public_functions_without_specs(path, relative_path) do
source = File.read!(path)
{:ok, ast} = Code.string_to_quoted(source)
specs = spec_names(source)
ast
|> public_defs()
|> Enum.uniq_by(fn {_line, name, arity} -> {name, arity} end)
|> Enum.reject(fn {_line, name, _arity} -> MapSet.member?(specs, name) end)
|> Enum.map(fn {line, name, arity} -> "#{relative_path}:#{line}:#{name}/#{arity}" end)
end
defp spec_names(source) do
~r/^\s*@spec\s+([a-zA-Z_][a-zA-Z0-9_?!]*)\s*\(/m
|> Regex.scan(source)
|> Enum.map(fn [_match, name] -> String.to_atom(name) end)
|> MapSet.new()
end
defp public_defs(ast) do
{_ast, defs} =
Macro.prewalk(ast, [], fn
{:def, meta, [head | _]} = node, acc ->
{name, arity} = public_def_name_arity(head)
{node, [{Keyword.fetch!(meta, :line), name, arity} | acc]}
node, acc ->
{node, acc}
end)
Enum.reverse(defs)
end
defp public_def_name_arity({:when, _meta, [head | _guards]}), do: public_def_name_arity(head)
defp public_def_name_arity({name, _meta, args}) when is_atom(name),
do: {name, length(args || [])}
end

View File

@@ -121,6 +121,7 @@ defmodule BDS.UI.ShellTest do
test "desktop shell assets persist workbench layout per project" do
live_js = File.read!("/Users/gb/Projects/bDS2/priv/ui/live.js")
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
session_util_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/session_util.ex")
@@ -214,7 +215,10 @@ defmodule BDS.UI.ShellTest do
refute live_js =~ "syncTitlebarMenuAnchor"
refute live_js =~ "handleTitlebarMenuKeyDown"
refute live_js =~ "keyboardMenuIndex"
assert template =~ "phx-window-keydown={if(@titlebar_menu_group, do: \"titlebar_menu_keydown\")}"
assert template =~
"phx-window-keydown={if(@titlebar_menu_group, do: \"titlebar_menu_keydown\")}"
assert template =~ "window-titlebar-menu-group"
assert live_ex =~ ~s(def handle_event("titlebar_menu_keydown")
assert live_ex =~ "titlebar_menu_item_index"
@@ -222,7 +226,11 @@ defmodule BDS.UI.ShellTest do
test "desktop shell css keeps the old media editor layout contract" do
css = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.css")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex")
template =
File.read!(
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor_html/media_editor.html.heex"
)
assert css =~ ".media-preview {"
assert css =~ "min-height: 300px;"
@@ -246,10 +254,20 @@ defmodule BDS.UI.ShellTest do
assert css =~ "padding: 6px 10px;"
assert css =~ ".linked-post-item:hover .unlink-btn {"
assert css =~ "opacity: 1;"
assert Regex.match?(~r/class="secondary quick-actions-btn".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= translated\("Quick Actions"\) %><\/span>/s, template)
assert Regex.match?(
~r/class="secondary quick-actions-btn".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= translated\("Quick Actions"\) %><\/span>/s,
template
)
assert template =~ ~s(class="quick-action-text")
assert template =~ ~s(class="quick-action-icon">🤖</span>)
assert Regex.match?(~r/class="quick-action-text">\s*<strong><%= translated\("AI Suggestions"\) %><\/strong>.*?<\/span>\s*<span class="quick-action-icon">🤖<\/span>/s, template)
assert Regex.match?(
~r/class="quick-action-text">\s*<strong><%= translated\("AI Suggestions"\) %><\/strong>.*?<\/span>\s*<span class="quick-action-icon">🤖<\/span>/s,
template
)
refute template =~ ~s|<span class="quick-action-icon">🤖</span>
<span class="quick-action-text">|
end
@@ -310,8 +328,14 @@ defmodule BDS.UI.ShellTest do
css = File.read!("/Users/gb/Projects/bDS2/priv/ui/app.css")
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
overlay_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/overlay_components.ex")
overlay_template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex")
overlay_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/overlay_components.ex")
overlay_template =
File.read!(
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/overlay_html/shell_overlay.html.heex"
)
assert template =~ "render_editor_toolbar(assigns)"
assert template =~ "<ShellOverlayComponents.shell_overlay"
@@ -339,12 +363,22 @@ defmodule BDS.UI.ShellTest do
test "desktop shell keeps post editor logic in the feature slice" do
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
post_editor_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor.ex")
post_template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex")
post_editor_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor.ex")
post_template =
File.read!(
"/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/post_editor_html/post_editor.html.heex"
)
assert template =~ "<PostEditor.post_editor"
assert post_editor_ex =~ "def build(%{current_tab: %{type: :post, id: post_id}} = assigns)"
assert Regex.match?(~r/class="secondary quick-actions-btn".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= translated\("Quick Actions"\) %><\/span>/s, post_template)
assert Regex.match?(
~r/class="secondary quick-actions-btn".*?<span class="quick-actions-btn-icon">⚡<\/span>\s*<span class="quick-actions-btn-label"><%= translated\("Quick Actions"\) %><\/span>/s,
post_template
)
refute live_ex =~ "defp update_post_editor("
refute live_ex =~ "defp persist_post_editor("
@@ -356,7 +390,9 @@ defmodule BDS.UI.ShellTest do
test "desktop shell keeps media editor logic in the feature slice" do
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
media_editor_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor.ex")
media_editor_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/media_editor.ex")
assert template =~ "<MediaEditor.media_editor"
assert media_editor_ex =~ "def build(%{current_tab: %{type: :media, id: media_id}} = assigns)"
@@ -369,8 +405,12 @@ defmodule BDS.UI.ShellTest do
test "desktop shell keeps sidebar logic in its own slice" do
live_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live.ex")
template = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/index.html.heex")
sidebar_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/sidebar_components.ex")
sidebar_state_ex = File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/sidebar_state.ex")
sidebar_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/sidebar_components.ex")
sidebar_state_ex =
File.read!("/Users/gb/Projects/bDS2/lib/bds/desktop/shell_live/sidebar_state.ex")
assert template =~ "<ShellSidebarComponents.sidebar_content"
assert sidebar_ex =~ "def sidebar_content(assigns)"

View File

@@ -26,13 +26,24 @@ defmodule BDS.UI.SidebarTest do
%{project: project, temp_dir: temp_dir}
end
test "database-backed sidebar views follow the old app ordering keys", %{project: project, temp_dir: temp_dir} do
test "database-backed sidebar views follow the old app ordering keys", %{
project: project,
temp_dir: temp_dir
} do
assert {:ok, draft_old} = Posts.create_post(%{project_id: project.id, title: "Draft Old"})
assert {:ok, draft_new} = Posts.create_post(%{project_id: project.id, title: "Draft New"})
assert {:ok, published_old} = Posts.create_post(%{project_id: project.id, title: "Published Old"})
assert {:ok, published_new} = Posts.create_post(%{project_id: project.id, title: "Published New"})
assert {:ok, archived_old} = Posts.create_post(%{project_id: project.id, title: "Archived Old"})
assert {:ok, archived_new} = Posts.create_post(%{project_id: project.id, title: "Archived New"})
assert {:ok, published_old} =
Posts.create_post(%{project_id: project.id, title: "Published Old"})
assert {:ok, published_new} =
Posts.create_post(%{project_id: project.id, title: "Published New"})
assert {:ok, archived_old} =
Posts.create_post(%{project_id: project.id, title: "Archived Old"})
assert {:ok, archived_new} =
Posts.create_post(%{project_id: project.id, title: "Archived New"})
update_post_sidebar_row(draft_old.id, created_at: 1_000, updated_at: 9_000)
update_post_sidebar_row(draft_new.id, created_at: 2_000, updated_at: 1_000)
@@ -55,26 +66,77 @@ defmodule BDS.UI.SidebarTest do
file_path: "posts/published-new.md"
)
update_post_sidebar_row(archived_old.id, status: :archived, created_at: 5_000, updated_at: 9_000)
update_post_sidebar_row(archived_new.id, status: :archived, created_at: 6_000, updated_at: 1_000)
update_post_sidebar_row(archived_old.id,
status: :archived,
created_at: 5_000,
updated_at: 9_000
)
update_post_sidebar_row(archived_new.id,
status: :archived,
created_at: 6_000,
updated_at: 1_000
)
old_media_path = Path.join(temp_dir, "old-media.txt")
new_media_path = Path.join(temp_dir, "new-media.txt")
File.write!(old_media_path, "old media")
File.write!(new_media_path, "new media")
assert {:ok, old_media} = Media.import_media(%{project_id: project.id, source_path: old_media_path, title: "Old Media"})
assert {:ok, new_media} = Media.import_media(%{project_id: project.id, source_path: new_media_path, title: "New Media"})
assert {:ok, old_media} =
Media.import_media(%{
project_id: project.id,
source_path: old_media_path,
title: "Old Media"
})
assert {:ok, new_media} =
Media.import_media(%{
project_id: project.id,
source_path: new_media_path,
title: "New Media"
})
update_media_sidebar_row(old_media.id, created_at: 7_000, updated_at: 9_000)
update_media_sidebar_row(new_media.id, created_at: 8_000, updated_at: 1_000)
assert {:ok, old_script} = Scripts.create_script(%{project_id: project.id, title: "Old Script", kind: :utility, content: "print('old')", entrypoint: "main"})
assert {:ok, new_script} = Scripts.create_script(%{project_id: project.id, title: "New Script", kind: :utility, content: "print('new')", entrypoint: "main"})
assert {:ok, old_script} =
Scripts.create_script(%{
project_id: project.id,
title: "Old Script",
kind: :utility,
content: "print('old')",
entrypoint: "main"
})
assert {:ok, new_script} =
Scripts.create_script(%{
project_id: project.id,
title: "New Script",
kind: :utility,
content: "print('new')",
entrypoint: "main"
})
update_script_sidebar_row(old_script.id, 9_000)
update_script_sidebar_row(new_script.id, 10_000)
assert {:ok, old_template} = Templates.create_template(%{project_id: project.id, title: "Old Template", kind: :post, content: "old"})
assert {:ok, new_template} = Templates.create_template(%{project_id: project.id, title: "New Template", kind: :post, content: "new"})
assert {:ok, old_template} =
Templates.create_template(%{
project_id: project.id,
title: "Old Template",
kind: :post,
content: "old"
})
assert {:ok, new_template} =
Templates.create_template(%{
project_id: project.id,
title: "New Template",
kind: :post,
content: "new"
})
update_template_sidebar_row(old_template.id, 11_000)
update_template_sidebar_row(new_template.id, 12_000)
@@ -83,8 +145,12 @@ defmodule BDS.UI.SidebarTest do
update_chat_sidebar_row(old_chat.id, 13_000)
update_chat_sidebar_row(new_chat.id, 14_000)
assert {:ok, old_definition} = ImportDefinitions.create_definition(%{project_id: project.id, name: "Old Import"})
assert {:ok, new_definition} = ImportDefinitions.create_definition(%{project_id: project.id, name: "New Import"})
assert {:ok, old_definition} =
ImportDefinitions.create_definition(%{project_id: project.id, name: "Old Import"})
assert {:ok, new_definition} =
ImportDefinitions.create_definition(%{project_id: project.id, name: "New Import"})
update_import_sidebar_row(old_definition.id, 15_000)
update_import_sidebar_row(new_definition.id, 16_000)
@@ -125,18 +191,28 @@ defmodule BDS.UI.SidebarTest do
end
defp update_script_sidebar_row(script_id, updated_at) do
Repo.update_all(from(script in BDS.Scripts.Script, where: script.id == ^script_id), set: [updated_at: updated_at])
Repo.update_all(from(script in BDS.Scripts.Script, where: script.id == ^script_id),
set: [updated_at: updated_at]
)
end
defp update_template_sidebar_row(template_id, updated_at) do
Repo.update_all(from(template in BDS.Templates.Template, where: template.id == ^template_id), set: [updated_at: updated_at])
Repo.update_all(from(template in BDS.Templates.Template, where: template.id == ^template_id),
set: [updated_at: updated_at]
)
end
defp update_chat_sidebar_row(conversation_id, updated_at) do
Repo.update_all(from(conversation in BDS.AI.ChatConversation, where: conversation.id == ^conversation_id), set: [updated_at: updated_at])
Repo.update_all(
from(conversation in BDS.AI.ChatConversation, where: conversation.id == ^conversation_id),
set: [updated_at: updated_at]
)
end
defp update_import_sidebar_row(definition_id, updated_at) do
Repo.update_all(from(definition in BDS.ImportDefinitions.ImportDefinition, where: definition.id == ^definition_id), set: [updated_at: updated_at])
Repo.update_all(
from(definition in BDS.ImportDefinitions.ImportDefinition,
where: definition.id == ^definition_id
), set: [updated_at: updated_at])
end
end

View File

@@ -156,12 +156,18 @@ defmodule BDS.UI.WorkbenchTest do
)
assert status.right.post_status == nil
assert status.right.token_usage == %{input_tokens: 10, output_tokens: 20, cache_read_tokens: 3}
assert status.right.token_usage == %{
input_tokens: 10,
output_tokens: 20,
cache_read_tokens: 3
}
end
test "menu commands expose generic shell controls through a shared command model" do
state = Workbench.new(sidebar_visible: false, panel_visible: false)
groups = MenuBar.default_groups(dev_mode?: false)
item_ids = fn items ->
items
|> Enum.reject(&Map.get(&1, :separator, false))
@@ -221,6 +227,10 @@ defmodule BDS.UI.WorkbenchTest do
assert Enum.any?(final_state.tabs, &(&1.type == :settings and &1.id == "settings"))
assert Enum.any?(final_state.tabs, &(&1.type == :menu_editor and &1.id == "menu_editor"))
assert Enum.any?(final_state.tabs, &(&1.type == :find_duplicates and &1.id == "find_duplicates"))
assert Enum.any?(
final_state.tabs,
&(&1.type == :find_duplicates and &1.id == "find_duplicates")
)
end
end

View File

@@ -14,9 +14,29 @@ defmodule BDS.WxrParserTest do
assert parsed.categories == [%{name: "General", slug: "general", parent: ""}]
assert parsed.tags == [%{name: "News", slug: "news"}]
assert [%{wp_id: 101, title: "Hello World", slug: "hello-world", creator: "Importer", status: "publish", post_type: "post", categories: ["General"], tags: ["News"]}] = parsed.posts
assert [
%{
wp_id: 101,
title: "Hello World",
slug: "hello-world",
creator: "Importer",
status: "publish",
post_type: "post",
categories: ["General"],
tags: ["News"]
}
] = parsed.posts
assert [%{wp_id: 201, title: "About", slug: "about", post_type: "page", categories: ["General"], tags: []}] = parsed.pages
assert [
%{
wp_id: 201,
title: "About",
slug: "about",
post_type: "page",
categories: ["General"],
tags: []
}
] = parsed.pages
assert [media] = parsed.media
assert media.wp_id == 301

View File

@@ -3,8 +3,9 @@ File.mkdir_p!(cache_root)
Application.put_env(:bds, :project_cache_root, cache_root)
ExUnit.start()
ExUnit.after_suite(fn _results ->
File.rm_rf(cache_root)
File.rm_rf(cache_root)
end)
Ecto.Adapters.SQL.Sandbox.mode(BDS.Repo, :manual)