defmodule BDS.ImageImportPipelineTest do use ExUnit.Case, async: false import Phoenix.ConnTest import Phoenix.LiveViewTest alias BDS.Desktop.{FilePicker, Overlay} alias BDS.{Metadata, AI, Repo} @endpoint BDS.Desktop.Endpoint setup do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo) Ecto.Adapters.SQL.Sandbox.mode(Repo, {:shared, self()}) temp_dir = Path.join(System.tmp_dir!(), "bds-image-import-#{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: "Image Import Test", data_path: temp_dir}) %{project: project, temp_dir: temp_dir} end # ── FilePicker multi-select parsing ──────────────────────────────────────── describe "FilePicker.choose_files/2" do test "single selection returns a single-item list" do # Simulate what osascript returns for regular choose file result = FilePicker.parse_choose_files_result("/Users/test/image.png", false) assert result == {:ok, "/Users/test/image.png"} end test "multi selection parses newline-separated paths" do result = FilePicker.parse_choose_files_result( "/Users/test/photo1.jpg\n/Users/test/photo2.png\n/Users/test/photo3.heic", true ) assert result == {:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]} end test "multi selection filters out empty lines" do result = FilePicker.parse_choose_files_result( "/Users/test/photo1.jpg\n\n/Users/test/photo2.png\n \n/Users/test/photo3.heic\n", true ) assert result == {:ok, ["/Users/test/photo1.jpg", "/Users/test/photo2.png", "/Users/test/photo3.heic"]} end test "multi selection with single file returns list with one element" do result = FilePicker.parse_choose_files_result("/Users/test/photo1.jpg", true) assert result == {:ok, ["/Users/test/photo1.jpg"]} end end # ── Metadata image_import_concurrency ─────────────────────────────────────── describe "Metadata image_import_concurrency" do test "defaults to 4 for new projects", %{project: project} do {:ok, metadata} = Metadata.get_project_metadata(project.id) assert metadata.image_import_concurrency == 4 end test "clamps to minimum 1", %{project: project} do {:ok, _metadata} = Metadata.update_project_metadata(project.id, %{image_import_concurrency: 0}) {:ok, metadata} = Metadata.get_project_metadata(project.id) assert metadata.image_import_concurrency == 1 end test "clamps to maximum 8", %{project: project} do {:ok, _metadata} = Metadata.update_project_metadata(project.id, %{image_import_concurrency: 100}) {:ok, metadata} = Metadata.get_project_metadata(project.id) assert metadata.image_import_concurrency == 8 end test "persists and reads correctly", %{project: project} do {:ok, _metadata} = Metadata.update_project_metadata(project.id, %{image_import_concurrency: 3}) {:ok, metadata} = Metadata.get_project_metadata(project.id) assert metadata.image_import_concurrency == 3 end test "handles string input", %{project: project} do {:ok, _metadata} = Metadata.update_project_metadata(project.id, %{image_import_concurrency: "5"}) {:ok, metadata} = Metadata.get_project_metadata(project.id) assert metadata.image_import_concurrency == 5 end test "handles nil as default", %{project: project} do {:ok, _metadata} = Metadata.update_project_metadata(project.id, %{image_import_concurrency: nil}) {:ok, metadata} = Metadata.get_project_metadata(project.id) assert metadata.image_import_concurrency == 4 end test "reflects in form as string", %{project: project} do {:ok, metadata} = Metadata.get_project_metadata(project.id) form = BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings.project_form(metadata) assert form["image_import_concurrency"] == "4" end end # ── Overlay struct post_id ──────────────────────────────────────────────── describe "Overlay insert_media" do test "open(:post, :insert_media, context) includes post_id" do context = %{ current_tab: %{ type: :post, id: "post-uuid-123", title: "Test Post", subtitle: "draft" }, media: [], insert_media_title: "Insert Media" } overlay = Overlay.open(:post, :insert_media, context) assert overlay.post_id == "post-uuid-123" end test "post_id is nil when opened from non-post context" do context = %{ current_tab: %{ type: :media, id: "media-uuid-456", title: "Test Media", subtitle: "image/png" }, media: [], insert_media_title: "Insert Media" } overlay = Overlay.open(:media, :insert_media, context) assert overlay == nil end test "set_search_query preserves post_id" do overlay = %{ kind: :insert_media, title: "Insert Media", search_query: "", results: [], all_media: [], post_id: "post-uuid-789" } result = Overlay.set_search_query(overlay, "search term") assert result.post_id == "post-uuid-789" end end # ── Airplane mode gating via shell_live event ───────────────────────────── describe "overlay_add_images airplane mode gating" do setup do prev_auto = System.get_env("BDS_DESKTOP_AUTOMATION") System.put_env("BDS_DESKTOP_AUTOMATION", "1") on_exit(fn -> if prev_auto, do: System.put_env("BDS_DESKTOP_AUTOMATION", prev_auto), else: System.delete_env("BDS_DESKTOP_AUTOMATION") end) end test "shows toast in airplane mode when Add Gallery Images is clicked", %{project: project} do {:ok, _project} = BDS.Projects.set_active_project(project.id) assert :ok = AI.set_airplane_mode(true) {:ok, post} = BDS.Posts.create_post(%{ project_id: project.id, title: "Gallery Post", content: "Content" }) {:ok, view, _html} = live_isolated(build_conn(), BDS.Desktop.ShellLive) render_click(view, "pin_sidebar_item", %{ "route" => "post", "id" => post.id, "title" => post.title, "subtitle" => "draft" }) html = view |> element("[phx-click='add_gallery_images']") |> render_click() assert html =~ "Automatic AI actions stay gated by airplane mode" assert :ok = AI.set_airplane_mode(false) end end end