test: D1-14 cover ReplaceMediaFileSideEffects file replace + sync thumbnail regen

This commit is contained in:
2026-05-30 09:18:36 +02:00
parent 56caa653bb
commit 1b37f1fcec
2 changed files with 74 additions and 1 deletions

View File

@@ -128,7 +128,7 @@ All reconciled to follow code. Specs must be self-consistent and match code.
| D1-11 | ~~ChatContextTruncation invariant~~ | ai.allium:375-379 | **Resolved:** test added in `ai_test.exs` — a catalog model with a 2,000-token context window plus 40 large seeded turns forces truncation; the captured chat request keeps the system prompt as the first message, drops the oldest pairs first (surviving markers form a contiguous newest suffix, oldest absent), and always retains the newest user turn |
| D1-12 | ~~BoundedToolLoop enforcement~~ | ai.allium:381-385 | **Resolved:** the round cap is now read from `config.chat_max_tool_rounds` (`config :bds, :chat, max_tool_rounds: 10`) via `chat_max_tool_rounds/0` in chat.ex instead of a hardcoded attribute, matching the spec wording; test added in `ai_test.exs` — a `LoopingToolRuntime` that always returns another tool call (never a final answer) with `max_tool_rounds: 3` ends with `{:error, %{kind: :tool_loop_exhausted}}` after exactly 3 runtime calls (the `rounds_left == 0` round short-circuits before contacting the runtime) |
| D1-13 | ~~DiscardPostChangesSideEffects~~ | engine_side_effects.allium:99-104 | **Resolved:** test added in `posts_test.exs` — a published post is dirtied with unsaved title/content edits (re-indexing the dirty text in FTS), then `discard_post_changes/1` restores the published file version (status=published, content=nil, original title) and re-syncs the FTS index so the published terms are searchable again and the discarded edits are gone |
| D1-14 | ReplaceMediaFileSideEffects | engine_side_effects.allium:128-134 | Write test: file replaced, thumbnails regenerated |
| 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 | Write integration test |
| D1-16 | DebouncedPersistence (5s) | embedding.allium:204-208 | Write test: index persistence debounced |
| D1-17 | Protected categories cannot be deleted | editor_settings.allium:81-84 | Write test: article/aside/page/picture deletion rejected |

View File

@@ -119,6 +119,79 @@ defmodule BDS.MediaTest do
end)
end
test "replace_media_file copies the new file over the path, updates the row, and regenerates thumbnails synchronously",
%{project: project, temp_dir: temp_dir} do
source_path = Path.join(temp_dir, "original.jpg")
File.write!(source_path, Image.new!(4, 4, color: [255, 0, 0]) |> Image.write!(:memory, suffix: ".jpg"))
assert {:ok, media} =
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
assert media.width == 4
assert media.height == 4
thumbnail_paths = BDS.Media.thumbnail_paths(media)
# Delete thumbnails so their presence after the replace proves regeneration ran.
Enum.each(Map.values(thumbnail_paths), fn path ->
File.rm!(Path.join(temp_dir, path))
end)
replacement_path = Path.join(temp_dir, "replacement.jpg")
File.write!(replacement_path, Image.new!(6, 8, color: [0, 0, 255]) |> Image.write!(:memory, suffix: ".jpg"))
replacement_binary = File.read!(replacement_path)
assert {:ok, updated} = BDS.Media.replace_media_file(media.id, replacement_path)
refute updated.checksum == media.checksum
assert updated.width == 6
assert updated.height == 8
assert updated.size == byte_size(replacement_binary)
# New file content is on disk at the same path.
assert File.read!(Path.join(temp_dir, updated.file_path)) == replacement_binary
# Thumbnails were regenerated synchronously (present immediately, no backup left behind).
Enum.each(Map.values(BDS.Media.thumbnail_paths(updated)), fn path ->
assert File.exists?(Path.join(temp_dir, path))
end)
refute File.exists?(Path.join(temp_dir, updated.file_path) <> ".bak")
end
test "replace_media_file is a no-op when the new file matches the current checksum", %{
project: project,
temp_dir: temp_dir
} do
source_path = Path.join(temp_dir, "same.jpg")
File.write!(source_path, Image.new!(4, 4, color: [255, 0, 0]) |> Image.write!(:memory, suffix: ".jpg"))
assert {:ok, media} =
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
# First replace establishes a stored checksum (import does not compute one).
new_path = Path.join(temp_dir, "blue.jpg")
File.write!(new_path, Image.new!(6, 8, color: [0, 0, 255]) |> Image.write!(:memory, suffix: ".jpg"))
assert {:ok, updated} = BDS.Media.replace_media_file(media.id, new_path)
refute is_nil(updated.checksum)
# Replacing with the identical file is a no-op.
identical_path = Path.join(temp_dir, "blue_copy.jpg")
File.cp!(new_path, identical_path)
assert {:ok, nil} = BDS.Media.replace_media_file(media.id, identical_path)
end
test "replace_media_file returns {:error, :not_found} for an unknown media id", %{
temp_dir: temp_dir
} do
replacement_path = Path.join(temp_dir, "orphan.jpg")
File.write!(replacement_path, Image.new!(4, 4, color: [0, 255, 0]) |> Image.write!(:memory, suffix: ".jpg"))
assert {:error, :not_found} = BDS.Media.replace_media_file("does-not-exist", replacement_path)
end
test "deleting a post rewrites linked media sidecars to remove that post id", %{
project: project,
temp_dir: temp_dir