From 56caa653bb7c147eace00e476da76ef7b3337fe6 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Sat, 30 May 2026 09:15:15 +0200 Subject: [PATCH] test: D1-13 cover DiscardPostChangesSideEffects FTS re-sync after discard --- SPECGAPS.md | 2 +- test/bds/posts_test.exs | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/SPECGAPS.md b/SPECGAPS.md index e26d822..ad4208d 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -127,7 +127,7 @@ All reconciled to follow code. Specs must be self-consistent and match code. | D1-10 | ~~TransformPipelineContinuation~~ | script.allium:247-249 | **Resolved:** added focused test in `transforms_test.exs` — a failing *first* transform (no prior valid state) does not halt the pipeline: the original input survives, a later enabled transform still runs against it, and every failure is captured per-script in pipeline order tagged with its slug | | 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 | Write test: FTS updated after discard | +| 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-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 | diff --git a/test/bds/posts_test.exs b/test/bds/posts_test.exs index c6e4683..2e2f58b 100644 --- a/test/bds/posts_test.exs +++ b/test/bds/posts_test.exs @@ -446,6 +446,52 @@ defmodule BDS.PostsTest do assert {:error, :not_found} = BDS.Posts.unarchive_post(Ecto.UUID.generate()) end + test "discard_post_changes restores the published version from file and updates the FTS index" do + temp_dir = + Path.join(System.tmp_dir!(), "bds-post-discard-#{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: "Discard FTS", data_path: temp_dir}) + + assert {:ok, post} = + BDS.Posts.create_post(%{ + project_id: project.id, + title: "Pristine Headline", + content: "pristine meadow body" + }) + + assert {:ok, published} = BDS.Posts.publish_post(post.id) + assert published.status == :published + assert published.content == nil + + # The published file is the source of truth; the FTS index reflects it. + assert {:ok, %{posts: [%{id: post_id}]}} = BDS.Search.search_posts(project.id, "pristine") + assert post_id == post.id + + # Make the DB diverge from the file with unsaved edits, re-indexing the dirty text. + assert {:ok, dirty} = + BDS.Posts.update_post(post.id, %{ + title: "Tampered Headline", + content: "tampered swamp body" + }) + + assert dirty.content == "tampered swamp body" + assert {:ok, %{posts: [%{id: ^post_id}]}} = BDS.Search.search_posts(project.id, "tampered") + assert {:ok, %{posts: []}} = BDS.Search.search_posts(project.id, "pristine") + + # Discarding restores the published file version and re-syncs the FTS index. + assert {:ok, restored} = BDS.Posts.discard_post_changes(post.id) + assert restored.status == :published + assert restored.content == nil + assert restored.title == "Pristine Headline" + + assert {:ok, %{posts: [%{id: ^post_id}]}} = BDS.Search.search_posts(project.id, "pristine") + assert {:ok, %{posts: []}} = BDS.Search.search_posts(project.id, "tampered") + end + test "rebuild_posts_from_files recreates published posts from disk" do temp_dir = Path.join(System.tmp_dir!(), "bds-post-rebuild-#{System.unique_integer([:positive])}")