D1-17: add tests for protected categories deletion rejection (article/aside/page/picture)
This commit is contained in:
@@ -131,7 +131,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
|
|||||||
| D1-14 | ~~ReplaceMediaFileSideEffects~~ | engine_side_effects.allium:128-134 | **Resolved:** 3 tests added in `media_test.exs` — `replace_media_file/2` copies the new image over the existing path, updates the row (checksum/size/width/height), and regenerates all thumbnails synchronously (present immediately after the call, no `.bak` backup left); identical-checksum replace is a no-op (`{:ok, nil}`); unknown media id returns `{:error, :not_found}` |
|
| D1-14 | ~~ReplaceMediaFileSideEffects~~ | engine_side_effects.allium:128-134 | **Resolved:** 3 tests added in `media_test.exs` — `replace_media_file/2` copies the new image over the existing path, updates the row (checksum/size/width/height), and regenerates all thumbnails synchronously (present immediately after the call, no `.bak` backup left); identical-checksum replace is a no-op (`{:ok, nil}`); unknown media id returns `{:error, :not_found}` |
|
||||||
| D1-15 | ~~Drag-and-drop image chain~~ | action_patterns.allium:84-103 | **Resolved:** the chain had no handler — added `BDS.Desktop.ShellLive.EditorImageDrop` (`import_and_link/3` runs steps 1-4: import media + synchronous thumbnails + link to post + return `` markdown; `enrich/3` runs background steps 5-6: AI analysis auto-applied with no modal + auto-translate cascade when `do_not_translate == false`). `PostEditor.handle_event("editor_image_dropped", ...)` runs the synchronous chain (works offline since import isn't AI), pushes the cursor insert, and spawns `enrich` only when airplane mode is off. MonacoEditor JS hook captures image drops on the editor surface and pushes the file path (`phx-target={@myself}` routes the hook event to the component); i18n for de/fr/it/es. 3 tests added (module chain incl. thumbnails+link+markdown, non-image link form, full LiveView drop in airplane mode asserting import/link/insert with no AI metadata). |
|
| D1-15 | ~~Drag-and-drop image chain~~ | action_patterns.allium:84-103 | **Resolved:** the chain had no handler — added `BDS.Desktop.ShellLive.EditorImageDrop` (`import_and_link/3` runs steps 1-4: import media + synchronous thumbnails + link to post + return `` markdown; `enrich/3` runs background steps 5-6: AI analysis auto-applied with no modal + auto-translate cascade when `do_not_translate == false`). `PostEditor.handle_event("editor_image_dropped", ...)` runs the synchronous chain (works offline since import isn't AI), pushes the cursor insert, and spawns `enrich` only when airplane mode is off. MonacoEditor JS hook captures image drops on the editor surface and pushes the file path (`phx-target={@myself}` routes the hook event to the component); i18n for de/fr/it/es. 3 tests added (module chain incl. thumbnails+link+markdown, non-image link form, full LiveView drop in airplane mode asserting import/link/insert with no AI metadata). |
|
||||||
| D1-16 | ~~DebouncedPersistence (5s)~~ | embedding.allium:213-217 | **Resolved:** 3 tests added in `embeddings_test.exs` (DebouncedPersistence describe): `Index.put/3` schedules a per-project save `timer` with ~5s remaining (>4000ms, <=5000ms) instead of writing immediately (no `embeddings.usearch` on disk yet); rapid `put`s coalesce (each reschedules the single timer, the previous timer is cancelled so `Process.read_timer` returns false, and still no file after two writes); when the `{:save, project_id}` debounce message fires the index is persisted to disk and the pending `timer` cleared. The coalescing test exposed a real bug: `handle_call({:put, ...})` replaced the stored entry with `build_entry/2`'s fresh `timer: nil` entry before `schedule_save/2` ran, orphaning the previous debounce timer (left to fire a redundant save) instead of cancelling it; fixed via `cancel_pending_save/2` so bulk `put`s collapse to one deferred write |
|
| D1-16 | ~~DebouncedPersistence (5s)~~ | embedding.allium:213-217 | **Resolved:** 3 tests added in `embeddings_test.exs` (DebouncedPersistence describe): `Index.put/3` schedules a per-project save `timer` with ~5s remaining (>4000ms, <=5000ms) instead of writing immediately (no `embeddings.usearch` on disk yet); rapid `put`s coalesce (each reschedules the single timer, the previous timer is cancelled so `Process.read_timer` returns false, and still no file after two writes); when the `{:save, project_id}` debounce message fires the index is persisted to disk and the pending `timer` cleared. The coalescing test exposed a real bug: `handle_call({:put, ...})` replaced the stored entry with `build_entry/2`'s fresh `timer: nil` entry before `schedule_save/2` ran, orphaning the previous debounce timer (left to fire a redundant save) instead of cancelling it; fixed via `cancel_pending_save/2` so bulk `put`s collapse to one deferred write |
|
||||||
| D1-17 | Protected categories cannot be deleted | editor_settings.allium:81-84 | Write test: article/aside/page/picture deletion rejected |
|
| D1-17 | ~~Protected categories cannot be deleted~~ | editor_settings.allium:81-84 | **Resolved:** 5 tests added in managed_categories_test.exs covering protected_category?/1 classification and remove_category/4 rejection for all 4 protected categories (article, aside, page, picture) plus non-protected deletion allowed |
|
||||||
| D1-18 | HomeItemProtection (menu) | editor_misc.allium:206-209 | Write test: cannot move/reorder/delete Home |
|
| D1-18 | HomeItemProtection (menu) | editor_misc.allium:206-209 | Write test: cannot move/reorder/delete Home |
|
||||||
|
|
||||||
### D2. No Test Coverage (MEDIUM priority — rules/behaviors)
|
### D2. No Test Coverage (MEDIUM priority — rules/behaviors)
|
||||||
|
|||||||
107
test/bds/desktop/managed_categories_test.exs
Normal file
107
test/bds/desktop/managed_categories_test.exs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategoriesTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
alias BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories
|
||||||
|
|
||||||
|
describe "protected_category?/1" do
|
||||||
|
test "returns true for article, aside, page, picture" do
|
||||||
|
assert ManagedCategories.protected_category?("article")
|
||||||
|
assert ManagedCategories.protected_category?("aside")
|
||||||
|
assert ManagedCategories.protected_category?("page")
|
||||||
|
assert ManagedCategories.protected_category?("picture")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "returns false for non-protected categories" do
|
||||||
|
refute ManagedCategories.protected_category?("news")
|
||||||
|
refute ManagedCategories.protected_category?("links")
|
||||||
|
refute ManagedCategories.protected_category?("random")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "remove_category/4" do
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-managed-categories-#{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: "ProtectedCategories", data_path: temp_dir})
|
||||||
|
|
||||||
|
%{project: project, temp_dir: temp_dir}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects deletion of protected category with error output", %{project: project} do
|
||||||
|
socket = %{assigns: %{projects: %{active_project_id: project.id}, workbench: nil}}
|
||||||
|
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), :error_appended)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.remove_category(
|
||||||
|
socket,
|
||||||
|
"article",
|
||||||
|
fn s, _wb -> s end,
|
||||||
|
append_output
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_received :error_appended
|
||||||
|
end
|
||||||
|
|
||||||
|
test "rejects deletion of all protected categories", %{project: project} do
|
||||||
|
socket = %{assigns: %{projects: %{active_project_id: project.id}, workbench: nil}}
|
||||||
|
|
||||||
|
for cat <- ["article", "aside", "page", "picture"] do
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), {:rejected, cat})
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.remove_category(
|
||||||
|
socket,
|
||||||
|
cat,
|
||||||
|
fn s, _wb -> s end,
|
||||||
|
append_output
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_received {:rejected, ^cat}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
test "allows deletion of non-protected category via Metadata.remove_category", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
socket = %{
|
||||||
|
assigns: %{
|
||||||
|
projects: %{active_project_id: project.id},
|
||||||
|
workbench: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BDS.Metadata.add_category(project.id, "test-cat")
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
assert "test-cat" in meta.categories
|
||||||
|
|
||||||
|
append_output = fn _socket, _title, _msg, _nil, _kind ->
|
||||||
|
send(self(), :error_called)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
ManagedCategories.remove_category(
|
||||||
|
socket,
|
||||||
|
"test-cat",
|
||||||
|
fn s, _wb -> s end,
|
||||||
|
append_output
|
||||||
|
)
|
||||||
|
|
||||||
|
refute_received :error_called
|
||||||
|
|
||||||
|
assert {:ok, meta} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
|
refute "test-cat" in meta.categories
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user