fix: working on rebuild from filesystem and analysis warnings
This commit is contained in:
@@ -130,9 +130,12 @@ The scripts do the standard sequence:
|
|||||||
|
|
||||||
1. `mix deps.get`
|
1. `mix deps.get`
|
||||||
2. `mix test` unless `BDS_SKIP_TESTS=1`
|
2. `mix test` unless `BDS_SKIP_TESTS=1`
|
||||||
3. `MIX_ENV=prod mix release bds`
|
3. `mix dialyzer` unless `BDS_SKIP_DIALYZER=1`
|
||||||
4. `MIX_ENV=prod mix release bds_mcp`
|
4. `MIX_ENV=prod mix release bds`
|
||||||
5. `MIX_ENV=prod mix bds.package <platform>`
|
5. `MIX_ENV=prod mix release bds_mcp`
|
||||||
|
6. `MIX_ENV=prod mix bds.package <platform>`
|
||||||
|
|
||||||
|
For local CLI validation without packaging, use `mix validate`.
|
||||||
|
|
||||||
The packaging task creates a clean redistributable payload under `dist/<platform>/` with this layout:
|
The packaging task creates a clean redistributable payload under `dist/<platform>/` with this layout:
|
||||||
|
|
||||||
|
|||||||
@@ -991,12 +991,14 @@ defmodule BDS.AI do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp await_chat_task(task) do
|
defp await_chat_task(task) do
|
||||||
|
ref = task.ref
|
||||||
|
|
||||||
receive do
|
receive do
|
||||||
{ref, result} when ref == task.ref ->
|
{^ref, result} ->
|
||||||
Process.demonitor(task.ref, [:flush])
|
Process.demonitor(task.ref, [:flush])
|
||||||
result
|
result
|
||||||
|
|
||||||
{:DOWN, ref, :process, _pid, reason} when ref == task.ref ->
|
{:DOWN, ^ref, :process, _pid, reason} ->
|
||||||
case reason do
|
case reason do
|
||||||
:normal ->
|
:normal ->
|
||||||
receive do
|
receive do
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defmodule BDS.Application do
|
|||||||
|
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
|
@compiled_env Application.compile_env(:bds, :current_env, Mix.env())
|
||||||
|
|
||||||
def desktop_children(env \\ nil)
|
def desktop_children(env \\ nil)
|
||||||
|
|
||||||
def desktop_children(:test) do
|
def desktop_children(:test) do
|
||||||
@@ -41,8 +43,7 @@ defmodule BDS.Application do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp current_env do
|
defp current_env do
|
||||||
Application.get_env(:bds, :current_env_override) ||
|
Application.get_env(:bds, :current_env_override) || @compiled_env
|
||||||
if(Code.ensure_loaded?(Mix), do: Mix.env(), else: :prod)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp desktop_window_children do
|
defp desktop_window_children do
|
||||||
|
|||||||
@@ -286,8 +286,6 @@ defmodule BDS.Desktop.Automation do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp port_os_pid(nil), do: nil
|
|
||||||
|
|
||||||
defp port_os_pid(port) do
|
defp port_os_pid(port) do
|
||||||
case Port.info(port, :os_pid) do
|
case Port.info(port, :os_pid) do
|
||||||
{:os_pid, pid} when is_integer(pid) -> pid
|
{:os_pid, pid} when is_integer(pid) -> pid
|
||||||
|
|||||||
@@ -213,10 +213,8 @@ defmodule BDS.Desktop.MainWindow do
|
|||||||
|
|
||||||
defp wx_env_undefined? do
|
defp wx_env_undefined? do
|
||||||
try do
|
try do
|
||||||
case :wx.get_env() do
|
_ = :wx.get_env()
|
||||||
:undefined -> true
|
false
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
rescue
|
rescue
|
||||||
ErlangError -> true
|
ErlangError -> true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -117,7 +117,6 @@ defmodule BDS.Git do
|
|||||||
case run_git(project_dir, ["fetch", "--all", "--prune"], opts) do
|
case run_git(project_dir, ["fetch", "--all", "--prune"], opts) do
|
||||||
{:ok, output} -> {:ok, %{updated: true, output: output}}
|
{:ok, output} -> {:ok, %{updated: true, output: output}}
|
||||||
{:error, {:git_failed, message}} -> structured_git_error(project_dir, :fetch, message, opts)
|
{:error, {:git_failed, message}} -> structured_git_error(project_dir, :fetch, message, opts)
|
||||||
other -> other
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -209,7 +208,6 @@ defmodule BDS.Git do
|
|||||||
case run_git(project_dir, ["remote", "get-url", "origin"], opts) do
|
case run_git(project_dir, ["remote", "get-url", "origin"], opts) do
|
||||||
{:ok, output} -> {:ok, blank_to_nil(output)}
|
{:ok, output} -> {:ok, blank_to_nil(output)}
|
||||||
{:error, {:git_failed, _message}} -> {:ok, nil}
|
{:error, {:git_failed, _message}} -> {:ok, nil}
|
||||||
other -> other
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -217,7 +215,6 @@ defmodule BDS.Git do
|
|||||||
case run_git(project_dir, ["lfs", "ls-files"], opts) do
|
case run_git(project_dir, ["lfs", "ls-files"], opts) do
|
||||||
{:ok, _output} -> {:ok, true}
|
{:ok, _output} -> {:ok, true}
|
||||||
{:error, {:git_failed, _message}} -> {:ok, false}
|
{:error, {:git_failed, _message}} -> {:ok, false}
|
||||||
other -> other
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -335,7 +332,6 @@ defmodule BDS.Git do
|
|||||||
provider =
|
provider =
|
||||||
case remote_url(project_dir, opts) do
|
case remote_url(project_dir, opts) do
|
||||||
{:ok, remote} -> provider_info(remote)
|
{:ok, remote} -> provider_info(remote)
|
||||||
_other -> nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if auth_error?(message) do
|
if auth_error?(message) do
|
||||||
@@ -370,9 +366,8 @@ defmodule BDS.Git do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_optional_string({:ok, value}), do: {:ok, blank_to_nil(value)}
|
defp normalize_optional_string({:ok, value}), do: {:ok, blank_to_nil(value)}
|
||||||
defp normalize_optional_string(other), do: other
|
defp normalize_optional_string({:error, _reason} = error), do: error
|
||||||
|
|
||||||
defp blank_to_nil(nil), do: nil
|
|
||||||
defp blank_to_nil(value) when value in ["", "\n"], do: nil
|
defp blank_to_nil(value) when value in ["", "\n"], do: nil
|
||||||
defp blank_to_nil(value), do: String.trim(value)
|
defp blank_to_nil(value), do: String.trim(value)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -217,9 +217,6 @@ defmodule BDS.MCP.Server do
|
|||||||
|
|
||||||
{:error, :not_found} ->
|
{:error, :not_found} ->
|
||||||
{:error, error_response(id, -32004, "Not found")}
|
{:error, error_response(id, -32004, "Not found")}
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
{:error, error_response(id, -32000, inspect(reason))}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -292,9 +289,7 @@ defmodule BDS.MCP.Server do
|
|||||||
defp http_error_response(status, headers \\ %{}), do: http_response(status, reason_body(status), "text/plain", headers)
|
defp http_error_response(status, headers \\ %{}), do: http_response(status, reason_body(status), "text/plain", headers)
|
||||||
|
|
||||||
defp reason_body(400), do: "Bad Request"
|
defp reason_body(400), do: "Bad Request"
|
||||||
defp reason_body(403), do: "Forbidden"
|
|
||||||
defp reason_body(404), do: "Not Found"
|
defp reason_body(404), do: "Not Found"
|
||||||
defp reason_body(_status), do: "Internal Server Error"
|
|
||||||
|
|
||||||
defp maybe_allow_repo(owner_pid) do
|
defp maybe_allow_repo(owner_pid) do
|
||||||
try do
|
try do
|
||||||
|
|||||||
101
lib/bds/posts.ex
101
lib/bds/posts.ex
@@ -139,12 +139,21 @@ defmodule BDS.Posts do
|
|||||||
def rebuild_posts_from_files(project_id) do
|
def rebuild_posts_from_files(project_id) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
|
|
||||||
posts =
|
rebuild_files =
|
||||||
project
|
project
|
||||||
|> Projects.project_data_dir()
|
|> Projects.project_data_dir()
|
||||||
|> Path.join("posts")
|
|> Path.join("posts")
|
||||||
|> list_matching_files("*.md")
|
|> list_matching_files("*.md")
|
||||||
|> Enum.map(&upsert_post_from_file(project_id, project, &1))
|
|> Enum.map(&parse_rebuild_file(project, &1))
|
||||||
|
|
||||||
|
{translation_files, post_files} = Enum.split_with(rebuild_files, &translation_rebuild_file?/1)
|
||||||
|
|
||||||
|
posts =
|
||||||
|
post_files
|
||||||
|
|> Enum.map(&upsert_post_from_rebuild_file(project_id, &1))
|
||||||
|
|
||||||
|
translation_files
|
||||||
|
|> Enum.map(&upsert_post_translation_from_rebuild_file(project_id, &1))
|
||||||
|
|
||||||
{:ok, posts}
|
{:ok, posts}
|
||||||
end
|
end
|
||||||
@@ -632,9 +641,13 @@ defmodule BDS.Posts do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp upsert_post_from_file(project_id, project, path) do
|
defp upsert_post_from_file(project_id, project, path) do
|
||||||
contents = File.read!(path)
|
project
|
||||||
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
|> parse_rebuild_file(path)
|
||||||
relative_path = Path.relative_to(path, Projects.project_data_dir(project))
|
|> upsert_post_from_rebuild_file(project_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp upsert_post_from_rebuild_file(project_id, rebuild_file) do
|
||||||
|
fields = rebuild_file.fields
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
|
|
||||||
attrs = %{
|
attrs = %{
|
||||||
@@ -649,7 +662,7 @@ defmodule BDS.Posts do
|
|||||||
created_at: Map.get(fields, "created_at", now),
|
created_at: Map.get(fields, "created_at", now),
|
||||||
updated_at: Map.get(fields, "updated_at", now),
|
updated_at: Map.get(fields, "updated_at", now),
|
||||||
published_at: Map.get(fields, "published_at"),
|
published_at: Map.get(fields, "published_at"),
|
||||||
file_path: relative_path,
|
file_path: rebuild_file.relative_path,
|
||||||
checksum: nil,
|
checksum: nil,
|
||||||
tags: Map.get(fields, "tags", []),
|
tags: Map.get(fields, "tags", []),
|
||||||
categories: Map.get(fields, "categories", []),
|
categories: Map.get(fields, "categories", []),
|
||||||
@@ -672,9 +685,85 @@ defmodule BDS.Posts do
|
|||||||
|> tap(&Embeddings.sync_post/1)
|
|> tap(&Embeddings.sync_post/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp upsert_post_translation_from_rebuild_file(project_id, rebuild_file) do
|
||||||
|
fields = rebuild_file.fields
|
||||||
|
source_post_id = Map.fetch!(fields, "translation_for")
|
||||||
|
source_post = Repo.get!(Post, source_post_id)
|
||||||
|
now = Persistence.now_ms()
|
||||||
|
language = normalize_language(Map.fetch!(fields, "language"))
|
||||||
|
|
||||||
|
translation =
|
||||||
|
Repo.get_by(Translation, translation_for: source_post_id, language: language) || %Translation{}
|
||||||
|
|
||||||
|
attrs = %{
|
||||||
|
id: Map.get(fields, "id") || Ecto.UUID.generate(),
|
||||||
|
project_id: project_id,
|
||||||
|
translation_for: source_post_id,
|
||||||
|
language: language,
|
||||||
|
title: Map.get(fields, "title") || "",
|
||||||
|
excerpt: Map.get(fields, "excerpt"),
|
||||||
|
content: nil,
|
||||||
|
status: parse_translation_status(Map.get(fields, "status", "published")),
|
||||||
|
created_at: Map.get(fields, "created_at", source_post.created_at || now),
|
||||||
|
updated_at: Map.get(fields, "updated_at", source_post.updated_at || source_post.created_at || now),
|
||||||
|
published_at: Map.get(fields, "published_at", source_post.published_at),
|
||||||
|
file_path: rebuild_file.relative_path,
|
||||||
|
checksum: nil
|
||||||
|
}
|
||||||
|
|
||||||
|
translation
|
||||||
|
|> Translation.changeset(attrs)
|
||||||
|
|> Repo.insert_or_update!()
|
||||||
|
|> tap(fn _translation ->
|
||||||
|
:ok = Search.sync_post(source_post_id)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp parse_post_status(status) when is_atom(status), do: status
|
defp parse_post_status(status) when is_atom(status), do: status
|
||||||
defp parse_post_status(status), do: String.to_existing_atom(status)
|
defp parse_post_status(status), do: String.to_existing_atom(status)
|
||||||
|
|
||||||
|
defp parse_translation_status(status) when is_atom(status), do: status
|
||||||
|
defp parse_translation_status(status), do: String.to_existing_atom(status)
|
||||||
|
|
||||||
|
defp parse_rebuild_file(project, path) do
|
||||||
|
contents = File.read!(path)
|
||||||
|
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
||||||
|
|
||||||
|
%{
|
||||||
|
path: path,
|
||||||
|
relative_path: Path.relative_to(path, Projects.project_data_dir(project)),
|
||||||
|
fields: normalize_rebuild_fields(fields)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translation_rebuild_file?(%{fields: fields}) do
|
||||||
|
Map.has_key?(fields, "translation_for") and not Map.has_key?(fields, "slug")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_rebuild_fields(fields) when is_map(fields) do
|
||||||
|
[
|
||||||
|
{"translationFor", "translation_for"},
|
||||||
|
{"doNotTranslate", "do_not_translate"},
|
||||||
|
{"templateSlug", "template_slug"},
|
||||||
|
{"createdAt", "created_at"},
|
||||||
|
{"updatedAt", "updated_at"},
|
||||||
|
{"publishedAt", "published_at"}
|
||||||
|
]
|
||||||
|
|> Enum.reduce(fields, fn {legacy_key, current_key}, acc ->
|
||||||
|
case Map.fetch(acc, legacy_key) do
|
||||||
|
{:ok, value} -> Map.put_new(acc, current_key, normalize_rebuild_field_value(current_key, value))
|
||||||
|
:error -> acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_rebuild_field_value(key, value)
|
||||||
|
when key in ["created_at", "updated_at", "published_at"] do
|
||||||
|
Persistence.parse_timestamp(value) || value
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_rebuild_field_value(_key, value), do: value
|
||||||
|
|
||||||
defp list_matching_files(dir, pattern) do
|
defp list_matching_files(dir, pattern) do
|
||||||
if File.dir?(dir) do
|
if File.dir?(dir) do
|
||||||
Path.join([dir, "**", pattern])
|
Path.join([dir, "**", pattern])
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ defmodule BDS.Rendering do
|
|||||||
case Frontmatter.parse_document(source) do
|
case Frontmatter.parse_document(source) do
|
||||||
{:ok, %{body: body}} -> {:ok, body}
|
{:ok, %{body: body}} -> {:ok, body}
|
||||||
{:error, :invalid_frontmatter} -> {:ok, source}
|
{:error, :invalid_frontmatter} -> {:ok, source}
|
||||||
{:error, reason} -> {:error, reason}
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:error, :template_not_found}
|
{:error, :template_not_found}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
defmodule BDS.Scripting.Capabilities do
|
defmodule BDS.Scripting.Capabilities do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
@mix_env Mix.env()
|
|
||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
@@ -495,7 +494,6 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
defp rebuild_post_links(project_id) do
|
defp rebuild_post_links(project_id) do
|
||||||
case Posts.rebuild_post_links(project_id) do
|
case Posts.rebuild_post_links(project_id) do
|
||||||
:ok -> true
|
:ok -> true
|
||||||
_other -> false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -508,7 +506,6 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
defp reindex_project_search(project_id) do
|
defp reindex_project_search(project_id) do
|
||||||
case Search.reindex_project(project_id) do
|
case Search.reindex_project(project_id) do
|
||||||
:ok -> true
|
:ok -> true
|
||||||
_other -> false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1018,10 +1015,10 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp do_copy_to_clipboard(text) do
|
defp do_copy_to_clipboard(text) do
|
||||||
if @mix_env == :test do
|
if test_mode?() do
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
command = string_or_nil(text) || ""
|
command = string_or_nil(text)
|
||||||
|
|
||||||
case :os.type() do
|
case :os.type() do
|
||||||
{:unix, :darwin} -> match?({_output, 0}, System.cmd("pbcopy", [], input: command, stderr_to_stdout: true))
|
{:unix, :darwin} -> match?({_output, 0}, System.cmd("pbcopy", [], input: command, stderr_to_stdout: true))
|
||||||
@@ -1060,16 +1057,16 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
|
|
||||||
defp open_folder(folder_path, opts) do
|
defp open_folder(folder_path, opts) do
|
||||||
case Keyword.get(opts, :open_folder) do
|
case Keyword.get(opts, :open_folder) do
|
||||||
callback when is_function(callback, 1) -> callback.(string_or_nil(folder_path) || "")
|
callback when is_function(callback, 1) -> callback.(string_or_nil(folder_path))
|
||||||
_other -> do_open_folder(folder_path)
|
_other -> do_open_folder(folder_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_open_folder(folder_path) do
|
defp do_open_folder(folder_path) do
|
||||||
if @mix_env == :test do
|
if test_mode?() do
|
||||||
""
|
""
|
||||||
else
|
else
|
||||||
case open_system_path(string_or_nil(folder_path) || "") do
|
case shell_open_system_path(string_or_nil(folder_path)) do
|
||||||
:ok -> ""
|
:ok -> ""
|
||||||
{:error, reason} -> inspect(reason)
|
{:error, reason} -> inspect(reason)
|
||||||
end
|
end
|
||||||
@@ -1084,7 +1081,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp do_select_folder(title) do
|
defp do_select_folder(title) do
|
||||||
if @mix_env == :test do
|
if test_mode?() do
|
||||||
nil
|
nil
|
||||||
else
|
else
|
||||||
case FolderPicker.choose_directory(string_or_nil(title) || "Select Folder") do
|
case FolderPicker.choose_directory(string_or_nil(title) || "Select Folder") do
|
||||||
@@ -1104,9 +1101,9 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
callback = Keyword.get(opts, :show_item_in_folder)
|
callback = Keyword.get(opts, :show_item_in_folder)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
is_function(callback, 1) -> callback.(string_or_nil(item_path) || "")
|
is_function(callback, 1) -> callback.(string_or_nil(item_path))
|
||||||
@mix_env == :test -> :ok
|
test_mode?() -> :ok
|
||||||
true -> _ = reveal_system_path(string_or_nil(item_path) || "")
|
true -> _ = shell_reveal_system_path(string_or_nil(item_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
@@ -1116,9 +1113,9 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
callback = Keyword.get(opts, :trigger_menu_action)
|
callback = Keyword.get(opts, :trigger_menu_action)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
is_function(callback, 1) -> callback.(string_or_nil(action) || "")
|
is_function(callback, 1) -> callback.(string_or_nil(action))
|
||||||
@mix_env == :test -> :ok
|
test_mode?() -> :ok
|
||||||
true -> _ = MenuBar.handle_event(string_or_nil(action) || "", nil)
|
true -> _ = MenuBar.handle_event(string_or_nil(action), nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
@@ -1674,7 +1671,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
end
|
end
|
||||||
defp parse_datetime(_value), do: nil
|
defp parse_datetime(_value), do: nil
|
||||||
|
|
||||||
defp open_system_path(path) do
|
defp shell_open_system_path(path) do
|
||||||
{command, args} =
|
{command, args} =
|
||||||
case :os.type() do
|
case :os.type() do
|
||||||
{:unix, :darwin} -> {"open", [path]}
|
{:unix, :darwin} -> {"open", [path]}
|
||||||
@@ -1690,7 +1687,7 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
error -> {:error, error}
|
error -> {:error, error}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp reveal_system_path(path) do
|
defp shell_reveal_system_path(path) do
|
||||||
{command, args} =
|
{command, args} =
|
||||||
case :os.type() do
|
case :os.type() do
|
||||||
{:unix, :darwin} -> {"open", ["-R", path]}
|
{:unix, :darwin} -> {"open", ["-R", path]}
|
||||||
@@ -1705,4 +1702,8 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
rescue
|
rescue
|
||||||
error -> {:error, error}
|
error -> {:error, error}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp test_mode? do
|
||||||
|
Application.get_env(:bds, :test_mode, false)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -195,9 +195,7 @@ defmodule BDS.Scripting.Lua do
|
|||||||
case values do
|
case values do
|
||||||
[] -> nil
|
[] -> nil
|
||||||
[value] -> value
|
[value] -> value
|
||||||
_other -> values
|
_values -> values
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unwrap_result(values), do: values
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -350,7 +350,10 @@ defmodule BDS.Tasks do
|
|||||||
defp public_task(nil), do: nil
|
defp public_task(nil), do: nil
|
||||||
|
|
||||||
defp public_task(task) do
|
defp public_task(task) do
|
||||||
Map.drop(task, [:last_reported_at])
|
task
|
||||||
|
|> Map.drop([:last_reported_at])
|
||||||
|
|> Map.update(:error, nil, &json_safe_value/1)
|
||||||
|
|> Map.update(:result, nil, &json_safe_value/1)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_status_snapshot(state) do
|
defp build_status_snapshot(state) do
|
||||||
@@ -419,6 +422,29 @@ defmodule BDS.Tasks do
|
|||||||
defp normalize_result({:error, _reason} = result), do: result
|
defp normalize_result({:error, _reason} = result), do: result
|
||||||
defp normalize_result(value), do: {:ok, value}
|
defp normalize_result(value), do: {:ok, value}
|
||||||
|
|
||||||
|
defp json_safe_value(value) when is_nil(value), do: nil
|
||||||
|
defp json_safe_value(value) when is_binary(value), do: value
|
||||||
|
defp json_safe_value(value) when is_boolean(value), do: value
|
||||||
|
defp json_safe_value(value) when is_number(value), do: value
|
||||||
|
defp json_safe_value(value) when is_atom(value), do: value
|
||||||
|
|
||||||
|
defp json_safe_value(value) when is_list(value) do
|
||||||
|
Enum.map(value, &json_safe_value/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp json_safe_value(value) when is_struct(value), do: inspect(value)
|
||||||
|
|
||||||
|
defp json_safe_value(value) when is_map(value) do
|
||||||
|
Map.new(value, fn {key, item} -> {json_safe_key(key), json_safe_value(item)} end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp json_safe_value(value) when is_tuple(value), do: inspect(value)
|
||||||
|
defp json_safe_value(value), do: inspect(value)
|
||||||
|
|
||||||
|
defp json_safe_key(key) when is_binary(key), do: key
|
||||||
|
defp json_safe_key(key) when is_atom(key), do: key
|
||||||
|
defp json_safe_key(key), do: inspect(key)
|
||||||
|
|
||||||
defp max_concurrent do
|
defp max_concurrent do
|
||||||
Application.get_env(:bds, :tasks, [])
|
Application.get_env(:bds, :tasks, [])
|
||||||
|> Keyword.get(:max_concurrent, @default_max_concurrent)
|
|> Keyword.get(:max_concurrent, @default_max_concurrent)
|
||||||
|
|||||||
18
mix.exs
18
mix.exs
@@ -9,6 +9,7 @@ defmodule BDS.MixProject do
|
|||||||
default_release: :bds,
|
default_release: :bds,
|
||||||
releases: releases(),
|
releases: releases(),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
|
dialyzer: dialyzer(),
|
||||||
aliases: aliases(),
|
aliases: aliases(),
|
||||||
deps: deps()
|
deps: deps()
|
||||||
]
|
]
|
||||||
@@ -16,7 +17,7 @@ defmodule BDS.MixProject do
|
|||||||
|
|
||||||
def application do
|
def application do
|
||||||
[
|
[
|
||||||
extra_applications: [:logger, :wx],
|
extra_applications: [:logger, :wx, :inets, :ssl],
|
||||||
mod: {BDS.Application, []}
|
mod: {BDS.Application, []}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
@@ -33,7 +34,8 @@ defmodule BDS.MixProject do
|
|||||||
{:bandit, "~> 1.5"},
|
{:bandit, "~> 1.5"},
|
||||||
{:desktop, "~> 1.5"},
|
{:desktop, "~> 1.5"},
|
||||||
{:image, "~> 0.65"},
|
{:image, "~> 0.65"},
|
||||||
{:stemex, "~> 0.2.1"}
|
{:stemex, "~> 0.2.1"},
|
||||||
|
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -42,7 +44,17 @@ defmodule BDS.MixProject do
|
|||||||
setup: ["deps.get", "ecto.setup"],
|
setup: ["deps.get", "ecto.setup"],
|
||||||
"ecto.setup": ["ecto.create", "ecto.migrate"],
|
"ecto.setup": ["ecto.create", "ecto.migrate"],
|
||||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||||
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
|
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
|
||||||
|
validate: ["test", "dialyzer"]
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp dialyzer do
|
||||||
|
env = Mix.env()
|
||||||
|
|
||||||
|
[
|
||||||
|
plt_add_apps: [:mix, :inets, :ssl],
|
||||||
|
paths: ["_build/#{env}/lib/bds/ebin"]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
2
mix.lock
2
mix.lock
@@ -8,11 +8,13 @@
|
|||||||
"debouncer": {:hex, :debouncer, "0.1.13", "af5906b231c196943ac8386b5b5f45a2f36d54a8bcd7e1b29eef2671de33d287", [:mix], [], "hexpm", "a14f57420c7d4a287f8f08e715fc8759b5d28dcd1032f9585d57c45d22123382"},
|
"debouncer": {:hex, :debouncer, "0.1.13", "af5906b231c196943ac8386b5b5f45a2f36d54a8bcd7e1b29eef2671de33d287", [:mix], [], "hexpm", "a14f57420c7d4a287f8f08e715fc8759b5d28dcd1032f9585d57c45d22123382"},
|
||||||
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||||
"desktop": {:hex, :desktop, "1.5.3", "dcf875dcff5b49a54646b4e6964acb079545c8c9c3790799aa5f1ccdcd314d15", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3750aabb8ed8aaf09b33f3cad5bda20f8ce4dfa65b026c019baed99c5264e2aa"},
|
"desktop": {:hex, :desktop, "1.5.3", "dcf875dcff5b49a54646b4e6964acb079545c8c9c3790799aa5f1ccdcd314d15", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_sni, "~> 0.2", [hex: :ex_sni, repo: "hexpm", optional: false]}, {:gettext, "> 0.10.0", [hex: :gettext, repo: "hexpm", optional: false]}, {:oncrash, "~> 0.1", [hex: :oncrash, repo: "hexpm", optional: false]}, {:phoenix, "> 1.0.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "> 0.15.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "> 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3750aabb8ed8aaf09b33f3cad5bda20f8ce4dfa65b026c019baed99c5264e2aa"},
|
||||||
|
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
|
||||||
"earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
|
"earmark": {:hex, :earmark, "1.4.48", "5f41e579d85ef812351211842b6e005f6e0cef111216dea7d4b9d58af4608434", [:mix], [], "hexpm", "a461a0ddfdc5432381c876af1c86c411fd78a25790c75023c7a4c035fdc858f9"},
|
||||||
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"},
|
"ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"},
|
||||||
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"},
|
"ecto_sqlite3": {:hex, :ecto_sqlite3, "0.22.0", "edab2d0f701b7dd05dcf7e2d97769c106aff62b5cfddc000d1dd6f46b9cbd8c3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "5af9e031bffcc5da0b7bca90c271a7b1e7c04a93fecf7f6cd35bc1b1921a64bd"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||||
|
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
|
||||||
"ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"},
|
"ex_dbus": {:hex, :ex_dbus, "0.1.4", "053df83d45b27ba0b9b6ef55a47253922069a3ace12a2a7dd30d3aff58301e17", [:mix], [{:dbus, "~> 0.8.0", [hex: :dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "d8baeaf465eab57b70a47b70e29fdfef6eb09ba110fc37176eebe6ac7874d6d5"},
|
||||||
"ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"},
|
"ex_sni": {:hex, :ex_sni, "0.2.9", "81f9421035dd3edb6d69f1a4dd5f53c7071b41628130d32ba5ab7bb4bfdc2da0", [:mix], [{:debouncer, "~> 0.1", [hex: :debouncer, repo: "hexpm", optional: false]}, {:ex_dbus, "~> 0.1", [hex: :ex_dbus, repo: "hexpm", optional: false]}, {:saxy, "~> 1.4.0", [hex: :saxy, repo: "hexpm", optional: false]}], "hexpm", "921d67d913765ed20ea8354fd1798dabc957bf66990a6842d6aaa7cd5ee5bc06"},
|
||||||
"ex_stemmers": {:hex, :ex_stemmers, "0.1.0", "63a84ae3a6f0c28a1d75768411f0ae15cfe8462fb70589b60977aa1b04c9372d", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "498826e2188e502f41d1a15f3d90e7738f0d94747e197367f03a2a44c09167c0"},
|
"ex_stemmers": {:hex, :ex_stemmers, "0.1.0", "63a84ae3a6f0c28a1d75768411f0ae15cfe8462fb70589b60977aa1b04c9372d", [:mix], [{:rustler, "~> 0.32.1", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "498826e2188e502f41d1a15f3d90e7738f0d94747e197367f03a2a44c09167c0"},
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ if not "%BDS_SKIP_TESTS%"=="1" (
|
|||||||
if errorlevel 1 goto :error
|
if errorlevel 1 goto :error
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not "%BDS_SKIP_DIALYZER%"=="1" (
|
||||||
|
call mix dialyzer
|
||||||
|
if errorlevel 1 goto :error
|
||||||
|
)
|
||||||
|
|
||||||
call set MIX_ENV=prod
|
call set MIX_ENV=prod
|
||||||
call mix release --overwrite bds
|
call mix release --overwrite bds
|
||||||
if errorlevel 1 goto :error
|
if errorlevel 1 goto :error
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ if [[ "${BDS_SKIP_TESTS:-0}" != "1" ]]; then
|
|||||||
mix test
|
mix test
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${BDS_SKIP_DIALYZER:-0}" != "1" ]]; then
|
||||||
|
mix dialyzer
|
||||||
|
fi
|
||||||
|
|
||||||
MIX_ENV=prod mix release --overwrite bds
|
MIX_ENV=prod mix release --overwrite bds
|
||||||
MIX_ENV=prod mix release --overwrite bds_mcp
|
MIX_ENV=prod mix release --overwrite bds_mcp
|
||||||
MIX_ENV=prod mix bds.package "$PLATFORM"
|
MIX_ENV=prod mix bds.package "$PLATFORM"
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ defmodule BDS.DesktopTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "desktop router exposes live task status for shell polling" do
|
test "desktop router exposes live task status for shell polling" do
|
||||||
|
:ok = BDS.Tasks.clear_finished()
|
||||||
|
|
||||||
assert {:ok, task} =
|
assert {:ok, task} =
|
||||||
BDS.Tasks.register_external_task("preview build", %{
|
BDS.Tasks.register_external_task("preview build", %{
|
||||||
group_id: "generation",
|
group_id: "generation",
|
||||||
@@ -129,6 +131,7 @@ defmodule BDS.DesktopTest do
|
|||||||
|
|
||||||
on_exit(fn ->
|
on_exit(fn ->
|
||||||
_ = BDS.Tasks.complete_task(task.id)
|
_ = BDS.Tasks.complete_task(task.id)
|
||||||
|
_ = BDS.Tasks.clear_finished()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert :ok = BDS.Tasks.report_progress(task.id, 0.5, "halfway")
|
assert :ok = BDS.Tasks.report_progress(task.id, 0.5, "halfway")
|
||||||
@@ -149,6 +152,39 @@ defmodule BDS.DesktopTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "desktop router encodes failed task snapshots even when the task error is a tuple" do
|
||||||
|
:ok = BDS.Tasks.clear_finished()
|
||||||
|
|
||||||
|
assert {:ok, task} =
|
||||||
|
BDS.Tasks.submit_task(
|
||||||
|
"broken rebuild",
|
||||||
|
fn _report ->
|
||||||
|
{:error, {{:badkey, "slug"}, [{BDS.Posts, :upsert_post_from_file, 3, [line: 644]}]}}
|
||||||
|
end,
|
||||||
|
%{group_id: "maintenance", group_name: "Maintenance"}
|
||||||
|
)
|
||||||
|
|
||||||
|
on_exit(fn ->
|
||||||
|
_ = BDS.Tasks.clear_finished()
|
||||||
|
end)
|
||||||
|
|
||||||
|
failed = wait_for_task(task.id, &(&1.status == :failed and &1.error != nil))
|
||||||
|
|
||||||
|
conn = conn(:get, "/api/tasks?k=#{Desktop.Auth.login_key()}")
|
||||||
|
conn = BDS.Desktop.Router.call(conn, BDS.Desktop.Router.init([]))
|
||||||
|
|
||||||
|
assert conn.status == 200
|
||||||
|
payload = Jason.decode!(conn.resp_body)
|
||||||
|
|
||||||
|
assert Enum.any?(payload["tasks"], fn item ->
|
||||||
|
item["id"] == failed.id and item["status"] == "failed" and is_binary(item["error"])
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert Enum.any?(payload["tasks"], fn item ->
|
||||||
|
item["id"] == failed.id and String.contains?(item["error"], "badkey")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
test "desktop router exposes projects for shell project selection and creation" do
|
test "desktop router exposes projects for shell project selection and creation" do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
BDS.Repo.delete_all(BDS.Projects.Project)
|
BDS.Repo.delete_all(BDS.Projects.Project)
|
||||||
@@ -281,4 +317,21 @@ defmodule BDS.DesktopTest do
|
|||||||
|> Enum.flat_map(& &1.items)
|
|> Enum.flat_map(& &1.items)
|
||||||
|> Enum.find(&Map.get(&1, :id) == id)
|
|> Enum.find(&Map.get(&1, :id) == id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp wait_for_task(task_id, matcher, timeout \\ 2_000)
|
||||||
|
|
||||||
|
defp wait_for_task(task_id, _matcher, timeout) when timeout <= 0 do
|
||||||
|
BDS.Tasks.get_task(task_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_task(task_id, matcher, timeout) do
|
||||||
|
task = BDS.Tasks.get_task(task_id)
|
||||||
|
|
||||||
|
if task && matcher.(task) do
|
||||||
|
task
|
||||||
|
else
|
||||||
|
Process.sleep(50)
|
||||||
|
wait_for_task(task_id, matcher, timeout - 50)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -313,6 +313,77 @@ defmodule BDS.PostsTest do
|
|||||||
assert post.content == nil
|
assert post.content == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "rebuild_posts_from_files imports legacy old-app translation files alongside canonical posts" do
|
||||||
|
temp_dir =
|
||||||
|
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)
|
||||||
|
|
||||||
|
assert {:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Legacy Rebuild", data_path: temp_dir})
|
||||||
|
|
||||||
|
posts_dir = Path.join([BDS.Projects.project_data_dir(project), "posts", "2026", "04"])
|
||||||
|
File.mkdir_p!(posts_dir)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(posts_dir, "chimera.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"id: post-from-old-app",
|
||||||
|
"title: Chimera Source",
|
||||||
|
"slug: chimera",
|
||||||
|
"status: published",
|
||||||
|
"language: de",
|
||||||
|
"createdAt: 2024-03-30T21:20:00.000Z",
|
||||||
|
"updatedAt: 2024-03-31T21:20:00.000Z",
|
||||||
|
"publishedAt: 2024-04-01T21:20:00.000Z",
|
||||||
|
"---",
|
||||||
|
"Quelle",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|> Enum.join("\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(posts_dir, "chimera.en.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"id: translation-from-old-app",
|
||||||
|
"translationFor: post-from-old-app",
|
||||||
|
"language: en",
|
||||||
|
"title: Chimera",
|
||||||
|
"excerpt: Imported translation",
|
||||||
|
"---",
|
||||||
|
"Translated body",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|> Enum.join("\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, posts} = BDS.Posts.rebuild_posts_from_files(project.id)
|
||||||
|
assert length(posts) == 1
|
||||||
|
|
||||||
|
[post] = posts
|
||||||
|
assert post.id == "post-from-old-app"
|
||||||
|
assert post.slug == "chimera"
|
||||||
|
assert post.language == "de"
|
||||||
|
|
||||||
|
assert {:ok, translations} = BDS.Posts.list_post_translations(post.id)
|
||||||
|
assert length(translations) == 1
|
||||||
|
|
||||||
|
[translation] = translations
|
||||||
|
assert translation.id == "translation-from-old-app"
|
||||||
|
assert translation.translation_for == post.id
|
||||||
|
assert translation.project_id == project.id
|
||||||
|
assert translation.language == "en"
|
||||||
|
assert translation.title == "Chimera"
|
||||||
|
assert translation.excerpt == "Imported translation"
|
||||||
|
assert translation.status == :published
|
||||||
|
assert translation.file_path == "posts/2026/04/chimera.en.md"
|
||||||
|
assert translation.content == nil
|
||||||
|
end
|
||||||
|
|
||||||
defp errors_on(changeset) do
|
defp errors_on(changeset) do
|
||||||
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
||||||
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
||||||
|
|||||||
Reference in New Issue
Block a user