From 64a5eb525ddbcd63a513231b425214b75cfc8f28 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 1 May 2026 22:56:29 +0200 Subject: [PATCH] fix: count_posts paginated before aggregation --- lib/bds/ai/chat_tools.ex | 12 ++++++++++- lib/bds/mcp/tools.ex | 13 ++++++++++-- test/bds/ai_test.exs | 44 ++++++++++++++++++++++++++++++++++++++++ test/bds/mcp_test.exs | 40 ++++++++++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/lib/bds/ai/chat_tools.ex b/lib/bds/ai/chat_tools.ex index 7120c9a..8023fdb 100644 --- a/lib/bds/ai/chat_tools.ex +++ b/lib/bds/ai/chat_tools.ex @@ -220,7 +220,7 @@ defmodule BDS.AI.ChatTools do def execute("count_posts", arguments, project_id) do project_id = project_id || active_project_id() group_by = List.wrap(arguments["groupBy"] || arguments["group_by"]) |> Enum.map(&to_string/1) - {:ok, result} = Search.search_posts(project_id, "", search_filters(arguments)) + result = search_all_counted_posts(project_id, arguments) groups = result.posts @@ -882,6 +882,16 @@ defmodule BDS.AI.ChatTools do |> Map.put(:limit, normalize_limit(arguments["limit"])) end + defp search_all_counted_posts(project_id, arguments) do + filters = search_filters(arguments) |> Map.put(:offset, 0) |> Map.put(:limit, 1) + {:ok, %{total: total}} = Search.search_posts(project_id, "", filters) + + filters = Map.put(filters, :limit, max(total, 1)) + {:ok, result} = Search.search_posts(project_id, "", filters) + + result + end + defp maybe_put(map, _key, nil), do: map defp maybe_put(map, _key, ""), do: map defp maybe_put(map, key, value), do: Map.put(map, key, value) diff --git a/lib/bds/mcp/tools.ex b/lib/bds/mcp/tools.ex index 7e77cbf..24e21ed 100644 --- a/lib/bds/mcp/tools.ex +++ b/lib/bds/mcp/tools.ex @@ -390,8 +390,7 @@ defmodule BDS.MCP.Tools do defp count_posts(params) do project = Queries.active_project!() group_by = map_get(params, :groupBy, []) |> Enum.map(&to_string/1) - filters = Queries.search_filters(params) - {:ok, result} = Search.search_posts(project.id, "", filters) + result = search_all_counted_posts(project.id, params) groups = result.posts @@ -403,6 +402,16 @@ defmodule BDS.MCP.Tools do %{"groups" => groups, "total_posts" => result.total} end + defp search_all_counted_posts(project_id, params) do + filters = Queries.search_filters(params) |> Map.put(:offset, 0) |> Map.put(:limit, 1) + {:ok, %{total: total}} = Search.search_posts(project_id, "", filters) + + filters = Map.put(filters, :limit, max(total, 1)) + {:ok, result} = Search.search_posts(project_id, "", filters) + + result + end + defp read_post_by_slug(%{"slug" => slug} = params), do: read_post_by_slug(Map.put_new(params, :slug, slug)) diff --git a/test/bds/ai_test.exs b/test/bds/ai_test.exs index cdf5293..2f23ef5 100644 --- a/test/bds/ai_test.exs +++ b/test/bds/ai_test.exs @@ -770,6 +770,44 @@ defmodule BDS.AITest do ) end + test "chat count_posts groups every matching post before returning groups" do + {:ok, project} = create_project_fixture("Count Posts") + + month_counts = [{2, 4}, {3, 6}, {4, 3}] + + for {month, count} <- month_counts, + index <- 1..count do + created_at = unix_ms!(NaiveDateTime.new!(Date.new!(2026, month, index), ~T[12:00:00])) + + Repo.insert!( + Post.changeset(%Post{}, %{ + id: Ecto.UUID.generate(), + project_id: project.id, + title: "AI Count #{month}-#{index}", + slug: "ai-count-#{month}-#{index}", + content: "Body", + status: :draft, + created_at: created_at, + updated_at: created_at, + do_not_translate: false + }) + ) + end + + assert %{groups: groups, total_posts: 13} = + BDS.AI.ChatTools.execute( + "count_posts", + %{"groupBy" => ["month"], "year" => 2026}, + project.id + ) + + assert Enum.sort_by(groups, & &1["month"]) == [ + %{"count" => 4, "month" => 2}, + %{"count" => 6, "month" => 3}, + %{"count" => 3, "month" => 4} + ] + end + test "cancel_chat aborts an in-flight chat turn" do assert {:ok, _endpoint} = BDS.AI.put_endpoint( @@ -853,4 +891,10 @@ defmodule BDS.AITest do %{post: post, media: media} end + + defp unix_ms!(%NaiveDateTime{} = naive_datetime) do + naive_datetime + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix(:millisecond) + end end diff --git a/test/bds/mcp_test.exs b/test/bds/mcp_test.exs index 704acdf..dc5b66a 100644 --- a/test/bds/mcp_test.exs +++ b/test/bds/mcp_test.exs @@ -3,6 +3,7 @@ defmodule BDS.MCPTest do alias BDS.Media.Media alias BDS.MCP.ProposalStore + alias BDS.Posts.Post alias BDS.Repo alias BDS.Scripts.Script alias BDS.Templates.Template @@ -88,6 +89,39 @@ defmodule BDS.MCPTest do assert read_result["post"]["slug"] == "travel-notes" end + test "count_posts groups every matching post before returning groups", %{project: project} do + month_counts = [{2, 24}, {3, 26}, {4, 23}] + + for {month, count} <- month_counts, + index <- 1..count do + day = rem(index - 1, 28) + 1 + created_at = unix_ms!(NaiveDateTime.new!(Date.new!(2026, month, day), ~T[12:00:00])) + + Repo.insert!( + Post.changeset(%Post{}, %{ + id: Ecto.UUID.generate(), + project_id: project.id, + title: "MCP Count #{month}-#{index}", + slug: "mcp-count-#{month}-#{index}", + content: "Body", + status: :draft, + created_at: created_at, + updated_at: created_at, + do_not_translate: false + }) + ) + end + + assert {:ok, %{"groups" => groups, "total_posts" => 73}} = + BDS.MCP.call_tool("count_posts", %{groupBy: ["month"], year: 2026}) + + assert Enum.sort_by(groups, & &1["month"]) == [ + %{"count" => 24, "month" => 2}, + %{"count" => 26, "month" => 3}, + %{"count" => 23, "month" => 4} + ] + end + test "translation tools expose post and media translations and upsert media metadata", %{ project: project, temp_dir: temp_dir @@ -437,4 +471,10 @@ defmodule BDS.MCPTest do assert "bds://posts{?cursor}" in template_uris assert "bds://media{?cursor}" in template_uris end + + defp unix_ms!(%NaiveDateTime{} = naive_datetime) do + naive_datetime + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix(:millisecond) + end end