Compare commits

...

7 Commits

18 changed files with 477 additions and 44 deletions

53
.credo.exs Normal file
View File

@@ -0,0 +1,53 @@
%{
configs: [
%{
name: "default",
files: %{
included: ["lib/", "test/", "config/", "mix.exs"],
excluded: [~r"/deps/", ~r"/_build/", ~r"/priv/static/"]
},
strict: true,
parse_timeout: 10_000,
color: true,
checks: [
{Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.LineEndings},
{Credo.Check.Consistency.SpaceAroundOperators},
{Credo.Check.Consistency.SpaceInParentheses},
{Credo.Check.Consistency.TabsOrSpaces},
{Credo.Check.Design.AliasUsage, false},
{Credo.Check.Readability.BlockPipe, false},
{Credo.Check.Readability.AliasOrder, false},
{Credo.Check.Readability.LargeNumbers, false},
{Credo.Check.Readability.MaxLineLength, false},
{Credo.Check.Readability.ModuleDoc, false},
{Credo.Check.Readability.PreferImplicitTry, false},
{Credo.Check.Readability.Semicolons, false},
{Credo.Check.Readability.StringSigils, false},
{Credo.Check.Readability.TrailingBlankLine, false},
{Credo.Check.Readability.UnnecessaryAliasExpansion, false},
{Credo.Check.Readability.WithSingleClause, false},
{Credo.Check.Refactor.Apply, false},
{Credo.Check.Refactor.CondStatements, false},
{Credo.Check.Refactor.CyclomaticComplexity, false},
{Credo.Check.Refactor.FilterFilter, false},
{Credo.Check.Refactor.FilterReject, false},
{Credo.Check.Refactor.FunctionArity, false},
{Credo.Check.Refactor.MapJoin, false},
{Credo.Check.Refactor.Nesting, false},
{Credo.Check.Refactor.NegatedConditionsWithElse, false},
{Credo.Check.Refactor.RejectFilter, false},
{Credo.Check.Refactor.RejectReject, false},
{Credo.Check.Refactor.RedundantWithClauseResult, false},
{Credo.Check.Warning.ApplicationConfigInModuleAttribute},
{Credo.Check.Warning.BoolOperationOnSameValues},
{Credo.Check.Warning.ExpensiveEmptyEnumCheck},
{Credo.Check.Warning.IExPry},
{Credo.Check.Warning.LazyLogging},
{Credo.Check.Warning.MissedMetadataKeyInLoggerConfig, false},
{Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.RaiseInsideRescue}
]
}
]
}

4
.mix_audit.ignore Normal file
View File

@@ -0,0 +1,4 @@
# GHSA-rhv4-8758-jx7v is pinned transitively through bumblebee -> progress_bar.
# ecto_sqlite3 0.24.x can move to decimal 3.x, but that line is currently
# unsatisfiable alongside the app's Bumblebee dependency.
GHSA-rhv4-8758-jx7v

View File

@@ -28,7 +28,7 @@ This document provides context and best practices for GitHub Copilot when workin
- we have an allium spec in the specs/ folder. you must weed the specs against built code to make sure you follow the spec.
- when changing the spec, validate the spec with the available command line tool.
- you MUST run tests with command line tools at least once to capture compile errors in tests, do not use the integrated testing of vscode, as that blocks on compile errors
- you MUST run build, test and check dialyzer messages and you MUST treet warnings as errors and fix them. we want clean builds, clean tests and clean dialyzer results
- you MUST run build, test, credo, deps.audit and check dialyzer messages and you MUST treet warnings as errors and fix them. we want clean builds, clean tests, clean credo, clean dependency audits and clean dialyzer results
- on a headless Linux machine, you have to run tests with this command (if mix test complains about DISPLAX): xvfb-run mix test
---
@@ -55,7 +55,7 @@ This document provides context and best practices for GitHub Copilot when workin
- Never leave tests failing, even if they appear unrelated to your changes
- If a test failure is pre-existing, fix it as part of your current work
- Run the full test suite (`npm test`) before considering any task complete
- Run the full test suite (`mix test`) before considering any task complete
- If you cannot fix a test, explain why and propose a solution
> **Zero failing tests. No exceptions.**
@@ -106,7 +106,7 @@ This document provides context and best practices for GitHub Copilot when workin
**All user-facing text MUST follow proper i18n patterns.**
- Do not hardcode UI strings directly in React components, menu templates, dialogs, or toasts
- Do not hardcode UI strings directly in LiveView/HEEx components, menu templates, dialogs, or toasts
- Store UI copy in language resources and resolve text through i18n helpers/hooks
- UI language MUST come from the operating system locale
- Rendering/preview/generated-content language MUST come from project preferences (`mainLanguage`), not UI locale

View File

@@ -691,7 +691,19 @@ to `@stemmer_algorithms` keys; default `"en"` unchanged. Delete
**Acceptance.** Misclassification fixtures (umlaut-free German, accented-free
French) detect correctly; stemmer selection unchanged for English.
### TD-18: Evaluate Oban (Lite engine) for durable jobs
### TD-18: Evaluate Oban (Lite engine) for durable jobs ✅ DONE (2026-06-12)
**Status: evaluated; no-go for now.** `BDS.Publishing` already persists
`PublishJob` rows and the suite proves those rows survive a
`BDS.Publishing` restart; what it does **not** provide is durable mid-upload
resume/retry, and the current desktop spec does not require that behavior.
Moving publishing to Oban Lite now would add another coordination layer on top
of `BDS.Tasks`, force a redesign of the in-memory SCP mtime cache in
`BDS.Publishing`, and complicate the existing Ecto sandbox/release split for a
benefit that is not yet part of the product contract. `BDS.Scripting.JobStore`
and `BDS.Tasks` remain intentionally in-memory: they provide UI/runtime job
tracking, not durable work queues. Re-open this only if real crash-recovery
requirements emerge and a spike shows acceptable desktop binary/startup impact.
**Context.** Three overlapping job systems exist: in-memory UI tasks
(`BDS.Tasks`), DB-persisted publish jobs (`Publishing` + `PublishJob` rows),
@@ -712,10 +724,25 @@ check interplay with the test sandbox and with the `bds_mcp` release. Write
up a go/no-go with the spike branch. If no-go, document why in this file and
close.
**Acceptance.** Decision documented; if go: publishing migrated first, with
crash-recovery test (kill app mid-upload, job resumes/retries on restart).
**Acceptance.** Decision documented. Outcome: no-go for now; publishing keeps
its current persisted-status model and the task closes without an Oban
migration.
### TD-19: Add credo, mix_audit (and consider sobelow) to the quality gates
### TD-19: Add credo, mix_audit (and consider sobelow) to the quality gates ✅ DONE (2026-06-12)
**Status: implemented.** `mix.exs` now adds `credo` and `mix_audit` as
dev/test-only tooling, and `mix validate` runs in the test environment with
all four gates wired in: tests, Credo, dependency audit, and dialyzer.
`.credo.exs` deliberately scopes the initial baseline to stable, actionable
checks and explicitly disables the repo-wide legacy style/refactor rules that
would otherwise drown the signal. The dependency audit is clean except for one
documented upstream exception in `.mix_audit.ignore`:
`GHSA-rhv4-8758-jx7v` remains unavoidable while `bumblebee` depends on
`progress_bar ~> 3.0`, which pins `decimal ~> 2.0`; the higher-signal Phoenix
advisory was removed by updating the lockfile to `phoenix 1.8.8`.
Sobelow was evaluated and not adopted: for this desktop app's loopback-only
endpoint it would mostly duplicate checks already covered by compile/test,
Credo, and dependency audit while adding little release signal.
**Context.** The project enforces dialyzer + warnings-as-errors but has no
style/consistency linter and no dependency CVE audit. Cheap, high-leverage
@@ -739,7 +766,16 @@ instructions (AGENTS.md) updated to mention them.
## Phase 5 — Consistency, config & hygiene
### TD-20: Align SQLite pool configuration between dev and prod
### TD-20: Align SQLite pool configuration between dev and prod ✅ DONE (2026-06-12)
**Status: implemented.** The effective mismatch was narrowed to one place:
`config.exs`, `dev.exs`, `prod.exs`, and `test.exs` were already aligned on a
modest pool of `5`, but `config/runtime.exs` still defaulted production to
`POOL_SIZE=1`. The prod runtime default now also uses `5`, with an inline
comment documenting the rationale: keep SQLite concurrency behavior consistent
across dev and prod under the existing WAL + `busy_timeout` setup unless an
operator explicitly overrides `POOL_SIZE`. A regression test reads
`config/runtime.exs` in `:prod` and locks that default to `5`.
**Context.** Dev runs `pool_size: 5` (config.exs), prod runs `pool_size: 1`
(runtime.exs) — so dev and prod have **different concurrency semantics**: dev
@@ -760,7 +796,15 @@ setting.
**Acceptance.** Same pool model in dev and prod; rationale comment in config;
no busy-timeout regressions in tests.
### TD-21: Harden `Persistence.atomic_write`
### TD-21: Harden `Persistence.atomic_write` ✅ DONE (2026-06-12)
**Status: implemented.** `BDS.Persistence.atomic_write/2` now writes through a
per-call temp path (`.tmp.<unique_integer>`) instead of the fixed `.tmp`
suffix, eliminating the concurrent-writer collision that previously produced
`:enoent` races. The new test coverage proves two things: many simultaneous
writers to the same target all complete successfully and the final file is one
intact payload, and the post-rebuild glob still ignores atomic temp files
because the temp suffix no longer matches the `*.md` pattern.
**Context.** `atomic_write/2` uses a fixed `path <> ".tmp"` temp name — two
concurrent writers to the same path corrupt each other's temp file before
@@ -780,7 +824,17 @@ are scanned (check rebuild-from-files glob patterns ignore them).
**Acceptance.** Concurrent-writer test produces two intact outcomes (last
write wins, no corruption); rebuild file globs ignore temp files.
### TD-22: Wrap `delete_chat_conversation` in a transaction
### TD-22: Wrap `delete_chat_conversation` in a transaction ✅ DONE (2026-06-12)
**Status: implemented.** `BDS.AI.Chat.delete_chat_conversation/1` now performs
message deletion and conversation deletion inside a single `Repo.transaction`.
If the second step fails, the whole delete rolls back and both the
conversation and its messages remain intact. The regression test injects a
mid-delete failure through a local test hook and proves the rollback by
asserting both rows still exist afterward. The nearby audit requested by this
TD found no equivalent multi-statement write outlier in `lib/bds/ai/catalog.ex`
or `lib/bds/scripts.ex`: catalog refresh already runs in a transaction and the
script operations are single-row writes.
**Context.** `chat.ex` deletes all messages (`Repo.delete_all`) then the
conversation (`Repo.delete`) without a transaction — a failure in between
@@ -801,7 +855,19 @@ add `ON DELETE CASCADE` via an Ecto migration on the
**Acceptance.** Injected failure between the two deletes leaves both intact;
no orphaned messages possible.
### TD-23: Sweep blanket `rescue`/`catch` blocks for silent failure
### TD-23: Sweep blanket `rescue`/`catch` blocks for silent failure ✅ DONE (2026-06-12)
**Status: implemented.** The named sweep surface was tightened in the intended
way instead of being rewritten wholesale. `ImportAnalysis` and
`ImportExecution` now log contextual warnings when top-level import handling,
repo-transaction wrappers, media linking, or progress callbacks fall back after
exceptions; the tests now prove those progress-callback failures are no longer
silent. In `ImportAnalysis.year_from/1`, the blanket integer-date rescue was
narrowed to `ArgumentError`, which is the actual parsing failure path there.
`OverlayComponents` kept its defensive UI fallbacks, but every rescue now logs
why the fallback path was taken instead of silently swallowing the error.
`MainWindow` needed no change in this pass: its catches are already scoped to
expected `:exit`/wx failures rather than bare rescue-all blocks.
**Context.** Several modules rescue *all* exceptions into `:ok`/fallbacks
(`shutdown.ex` defensibly — it must never block quit; but also
@@ -821,7 +887,11 @@ call, and delete rescues that guard nothing. Shutdown.ex's rescues stay as-is
**Acceptance.** No bare `rescue _ ->` without either a narrow exception match
or a logged justification; suite green.
### TD-24: Fix stale `npm test` reference in AGENTS.md
### TD-24: Fix stale `npm test` reference in AGENTS.md ✅ DONE (2026-06-12)
**Status: implemented.** `AGENTS.md` now tells agents to run `mix test`
instead of the stale `npm test`, and the nearby UI i18n wording was updated to
refer to LiveView/HEEx components rather than leftover React-era terminology.
**Context.** AGENTS.md "Fix All Test Failures" section says "Run the full
test suite (`npm test`)" — stale from the pre-Elixir rewrite. It's an

View File

@@ -7,9 +7,11 @@ if config_env() == :prod do
File.mkdir_p!(Path.dirname(database_path))
# Keep prod on the same modest SQLite pool as dev so WAL + busy_timeout see
# the same concurrency behavior in both environments unless explicitly tuned.
config :bds, BDS.Repo,
database: database_path,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "1")
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "5")
# Persist downloaded embedding model files alongside the database data dir.
config :bumblebee,

View File

@@ -106,12 +106,24 @@ defmodule BDS.AI.Chat do
{:error, :not_found}
%ChatConversation{} = conversation ->
Repo.delete_all(
from message in ChatMessage, where: message.conversation_id == ^conversation_id
)
Repo.transaction(fn ->
Repo.delete_all(
from message in ChatMessage, where: message.conversation_id == ^conversation_id
)
case Repo.delete(conversation) do
{:ok, _conversation} -> {:ok, :deleted}
case delete_chat_conversation_test_hook(conversation_id) do
:ok ->
case Repo.delete(conversation) do
{:ok, _conversation} -> :ok
{:error, reason} -> Repo.rollback(reason)
end
{:error, reason} ->
Repo.rollback(reason)
end
end)
|> case do
{:ok, :ok} -> {:ok, :deleted}
{:error, reason} -> {:error, reason}
end
end
@@ -610,6 +622,13 @@ defmodule BDS.AI.Chat do
end
end
defp delete_chat_conversation_test_hook(conversation_id) do
case Application.get_env(:bds, :chat_delete_conversation_test_hook) do
hook when is_function(hook, 1) -> hook.(conversation_id)
_other -> :ok
end
end
defp persist_assistant_response(conversation_id, response) do
usage = normalize_usage(response.usage)

View File

@@ -5,6 +5,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
use Gettext, backend: BDS.Gettext
import Ecto.Query
require Logger
alias BDS.{I18n, Media, Metadata, Posts, Repo}
alias BDS.Media.Media, as: MediaRecord
@@ -70,7 +71,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
{:ok, metadata} = Metadata.get_project_metadata(project_id)
metadata
rescue
_error -> %{main_language: "en", blog_languages: []}
error ->
log_overlay_warning("project metadata", error)
%{main_language: "en", blog_languages: []}
end
defp posts(nil), do: []
@@ -137,7 +140,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
select: pm.media_id
)
rescue
_error -> []
error ->
log_overlay_warning("post media ids for #{post_id}", error)
[]
end
defp post_media_ids(_tab), do: []
@@ -150,7 +155,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
)
|> Map.new(fn {language, status} -> {language, Atom.to_string(status || :draft)} end)
rescue
_error -> %{}
error ->
log_overlay_warning("post translations for #{post_id}", error)
%{}
end
defp existing_translations(%{type: :media, id: media_id}) do
@@ -161,7 +168,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
)
|> Map.new(fn {language, status} -> {language, status} end)
rescue
_error -> %{}
error ->
log_overlay_warning("media translations for #{media_id}", error)
%{}
end
defp existing_translations(_tab), do: %{}
@@ -179,7 +188,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
_other -> metadata.main_language || "en"
end
rescue
_error -> metadata.main_language || "en"
error ->
log_overlay_warning("post source language for #{post_id}", error)
metadata.main_language || "en"
end
defp source_language(%{type: :media, id: media_id}, metadata) do
@@ -188,7 +199,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
_other -> metadata.main_language || "en"
end
rescue
_error -> metadata.main_language || "en"
error ->
log_overlay_warning("media source language for #{media_id}", error)
metadata.main_language || "en"
end
defp source_language(_tab, metadata), do: metadata.main_language || "en"
@@ -242,7 +255,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
[]
end
rescue
_error -> []
error ->
log_overlay_warning("post AI fields for #{post_id}", error)
[]
end
defp ai_fields(%{type: :media, id: media_id}, title, _subtitle, page_language) do
@@ -279,7 +294,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
[]
end
rescue
_error -> []
error ->
log_overlay_warning("media AI fields for #{media_id}", error)
[]
end
defp ai_fields(_tab, _title, _subtitle, _page_language), do: []
@@ -309,7 +326,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
reference_list: reference_list
}
rescue
_error ->
error ->
log_overlay_warning("delete media details for #{media_id}", error)
%{
title: BDS.Gettext.lgettext(page_language, "ui", "Delete Media"),
entity_name: media_id,
@@ -330,7 +349,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
reference_list: []
}
rescue
_error ->
error ->
log_overlay_warning("delete tag details", error)
%{
title: BDS.Gettext.lgettext(page_language, "ui", "Delete Tag"),
entity_name: "tag",
@@ -367,7 +388,9 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
message: BDS.Gettext.lgettext(page_language, "ui", "Cannot be undone.")
}
rescue
_error ->
error ->
log_overlay_warning("merge tag details for project #{project_id}", error)
%{
target: "tag",
count: 1,
@@ -391,4 +414,8 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do
|> String.replace(~r/[^a-z0-9]+/u, "-")
|> String.trim("-")
end
defp log_overlay_warning(context, error) do
Logger.warning("overlay component fallback for #{context}: #{Exception.message(error)}")
end
end

View File

@@ -2,6 +2,7 @@ defmodule BDS.ImportAnalysis do
@moduledoc false
import Ecto.Query
require Logger
alias BDS.Media.Media
alias BDS.Posts.Post
@@ -25,7 +26,12 @@ defmodule BDS.ImportAnalysis do
wxr_data = WxrParser.parse_file(wxr_file_path)
{:ok, build_report(project_id, wxr_data, wxr_file_path, uploads_folder_path, on_progress)}
rescue
error -> {:error, %{message: Exception.message(error)}}
error ->
Logger.warning(
"import analysis failed project_id=#{project_id} file=#{wxr_file_path}: #{Exception.message(error)}"
)
{:error, %{message: Exception.message(error)}}
end
defp build_report(project_id, wxr_data, wxr_file_path, uploads_folder_path, on_progress) do
@@ -459,7 +465,7 @@ defmodule BDS.ImportAnalysis do
value
end
rescue
_error -> nil
_error in [ArgumentError] -> nil
end
defp year_from(value) when is_binary(value) do
@@ -491,7 +497,12 @@ defmodule BDS.ImportAnalysis do
try do
callback.(step, detail)
rescue
_error -> :ok
error ->
Logger.warning(
"import analysis progress callback failed step=#{inspect(step)}: #{Exception.message(error)}"
)
:ok
end
:ok

View File

@@ -1,6 +1,8 @@
defmodule BDS.ImportExecution do
@moduledoc false
require Logger
alias BDS.Media
alias BDS.Metadata
alias BDS.Posts
@@ -118,7 +120,9 @@ defmodule BDS.ImportExecution do
{:error, reason} -> {:error, %{message: format_import_error(reason)}}
end
rescue
error -> {:error, %{message: Exception.message(error)}}
error ->
Logger.warning("import execution failed project_id=#{project_id}: #{Exception.message(error)}")
{:error, %{message: Exception.message(error)}}
end
defp execute_taxonomies(category_items, tag_items, project_id, result, on_progress, started_at) do
@@ -363,9 +367,19 @@ defmodule BDS.ImportExecution do
try do
Media.link_media_to_post(media_id, post_id)
rescue
_ -> :ok
error ->
Logger.warning(
"import execution failed to link media_id=#{media_id} post_id=#{post_id}: #{Exception.message(error)}"
)
:ok
catch
_, _ -> :ok
kind, reason ->
Logger.warning(
"import execution failed to link media_id=#{media_id} post_id=#{post_id}: #{kind}: #{inspect(reason)}"
)
:ok
end
end)
@@ -586,9 +600,13 @@ defmodule BDS.ImportExecution do
defp run_repo_transaction(fun) when is_function(fun, 0) do
Repo.transaction(fun)
rescue
error -> {:error, error}
error ->
Logger.warning("import execution transaction failed: #{Exception.message(error)}")
{:error, error}
catch
kind, reason -> {:error, {kind, reason}}
kind, reason ->
Logger.warning("import execution transaction failed: #{kind}: #{inspect(reason)}")
{:error, {kind, reason}}
end
defp post_item_label(item) do
@@ -628,11 +646,20 @@ defmodule BDS.ImportExecution do
try do
callback.(phase, current, total, %{detail: detail, eta: eta})
rescue
_error ->
error ->
Logger.warning(
"import execution progress callback failed phase=#{inspect(phase)}: #{Exception.message(error)}"
)
try do
callback.(phase, current, total, detail)
rescue
_error -> :ok
legacy_error ->
Logger.warning(
"import execution legacy progress callback failed phase=#{inspect(phase)}: #{Exception.message(legacy_error)}"
)
:ok
end
end

View File

@@ -68,7 +68,7 @@ defmodule BDS.Persistence do
def parse_timestamp(_value), do: nil
def atomic_write(path, contents) when is_binary(path) and is_binary(contents) do
temp_path = path <> ".tmp"
temp_path = path <> ".tmp." <> Integer.to_string(System.unique_integer([:positive]))
with :ok <- File.mkdir_p(Path.dirname(path)),
:ok <- File.write(temp_path, contents),

View File

@@ -22,6 +22,10 @@ defmodule BDS.MixProject do
]
end
def cli do
[preferred_envs: [validate: :test]]
end
defp deps do
[
{:ecto_sql, "~> 3.13"},
@@ -49,6 +53,8 @@ defmodule BDS.MixProject do
{:tailwind, "~> 0.3", runtime: Mix.env() == :dev},
{:esbuild, "~> 0.10", runtime: Mix.env() == :dev},
{:lazy_html, ">= 0.1.0", only: :test},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
]
end
@@ -62,7 +68,7 @@ defmodule BDS.MixProject do
"assets.build": ["tailwind default", "esbuild default"],
"assets.deploy": ["tailwind default --minify", "esbuild default --minify"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
validate: ["test", "dialyzer"]
validate: ["test", "credo --strict", "deps.audit --ignore-file .mix_audit.ignore", "dialyzer"]
]
end

View File

@@ -1,11 +1,13 @@
%{
"axon": {:hex, :axon, "0.7.0", "2e2c6d93b4afcfa812566b8922204fa022b60081e86ebd411df4db7ea30f5457", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: false]}, {:polaris, "~> 0.1", [hex: :polaris, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "ee9857a143c9486597ceff434e6ca833dc1241be6158b01025b8217757ed1036"},
"bandit": {:hex, :bandit, "1.11.1", "1eb33123cc3c17ae0c3447874eb83399ee530f960c39711ed240342fbd4865fa", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "d4401016df9abbc6dcd325c0b78b2b193e7c7c96bb68f31e576112be025d84a5"},
"bandit": {:hex, :bandit, "1.12.0", "6c5214daa2469644ac4ab0113b98abc24f75e348378e6a974c6343b3e5da22ef", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.5", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "45dac82dc86f45cf4a196dee9cc5a8b791d9c9469d996055f055e6ee36c66e20"},
"bumblebee": {:hex, :bumblebee, "0.6.3", "c0028643c92de93258a9804da1d4d48797eaf7911b702464b3b3dd2cc7f938f1", [:mix], [{:axon, "~> 0.7.0", [hex: :axon, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.9.0 or ~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:nx_signal, "~> 0.2.0", [hex: :nx_signal, repo: "hexpm", optional: false]}, {:progress_bar, "~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:safetensors, "~> 0.1.3", [hex: :safetensors, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.4", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}, {:unzip, "~> 0.12.0", [hex: :unzip, repo: "hexpm", optional: false]}], "hexpm", "c619197787561f8e5fb2ffba269c341654accaec9d591999b7fddd55761dd079"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.19", "6903cabdfd9d1af46454126e7c8385186659dd33ecfb74a885cae52221ad6109", [:mix], [], "hexpm", "3669e6cab13f54c2df26b3e6833745d647f35b6e30d8ddd5975df0d5c842ca98"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
"color": {:hex, :color, "0.13.0", "068110e5397ac5d3c9f97658282e0f4ab9a32468be6d7a2a91a8804e67b228d7", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "de127946869931d418bac2d82dc29feae1a8f5f729f135922fbccf0059a58ab2"},
"complex": {:hex, :complex, "0.7.0", "695632ef9487517aa5d57edd1697801079d622414cb2e1a7cf538b1f9a50f205", [:mix], [], "hexpm", "0ee39c0803129f546e7f3f640da8f021c9e659402bf59da6f7f2c4848f068f8d"},
"credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"},
"date_time_parser": {:hex, :date_time_parser, "1.3.0", "6ba16850b5ab83dd126576451023ab65349e29af2336ca5084aa1e37025b476e", [:mix], [{:kday, "~> 1.0", [hex: :kday, repo: "hexpm", optional: false]}], "hexpm", "93c8203a8ddc66b1f1531fc0e046329bf0b250c75ffa09567ef03d2c09218e8c"},
"db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"},
"dbus": {:hex, :dbus, "0.8.0", "7c800681f35d909c199265e55a8ee4aea9ebe4acccce77a0740f89f29cc57648", [:make], [], "hexpm", "a9784f2d9717ffa1f74169144a226c39633ac0d9c7fe8cb3594aeb89c827cca5"},
@@ -27,6 +29,7 @@
"exla": {:hex, :exla, "0.10.0", "93e7d75a774fbc06ce05b96de20c4b01bda413b315238cb3c727c09a05d2bc3a", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:nx, "~> 0.10.0", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.9.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "16fffdb64667d7f0a3bc683fdcd2792b143a9b345e4b1f1d5cd50330c63d8119"},
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
"exqlite": {:hex, :exqlite, "0.36.0", "07b4f95d61cb82b8d52946d0639497fa7d32117e09b2c8d25e24a38723c295cb", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "cbeca3ce781f9ff07cfa9a87486f3ebd512a143ad6a14ed5c9fca21fe0bf3ae7"},
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
"finch": {:hex, :finch, "0.22.0", "5c48fa6f9706a78eb9036cacb67b8b996b4e66d111c543f4c29bb0f879a6806b", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.8", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b94e83c47780fc6813f746a1f1a34ee65cda42da4c5ea26a68f0acc4498e23dc"},
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
"gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"},
@@ -42,6 +45,7 @@
"luerl": {:hex, :luerl, "1.5.1", "f6700420950fc6889137e7a0c11c4a8467dea04a8c23f707a40d83566d14e786", [:rebar3], [], "hexpm", "abf88d849baa0d5dca93b245a8688d4de2ee3d588159bb2faf51e15946509390"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
@@ -50,7 +54,7 @@
"nx_image": {:hex, :nx_image, "0.1.2", "0c6e3453c1dc30fc80c723a54861204304cebc8a89ed3b806b972c73ee5d119d", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "9161863c42405ddccb6dbbbeae078ad23e30201509cc804b3b3a7c9e98764b81"},
"nx_signal": {:hex, :nx_signal, "0.2.0", "e1ca0318877b17c81ce8906329f5125f1e2361e4c4235a5baac8a95ee88ea98e", [:mix], [{:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "7247e5e18a177a59c4cb5355952900c62fdeadeb2bad02a9a34237b68744e2bb"},
"oncrash": {:hex, :oncrash, "0.1.0", "9cf4ae8eba4ea250b579470172c5e9b8c75418b2264de7dbcf42e408d62e30fb", [:mix], [], "hexpm", "6968e775491cd857f9b6ff940bf2574fd1c2fab84fa7e14d5f56c39174c00018"},
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
"phoenix": {:hex, :phoenix, "1.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
@@ -68,7 +72,7 @@
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
"tokenizers": {:hex, :tokenizers, "0.5.1", "b0975d92b4ee5b18e8f47b5d65b9d5f1e583d9130189b1a2620401af4e7d4b35", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5f08d97cc7f2ed3d71d370d68120da6d3de010948ccf676c9c0eb591ba4bacc9"},
"toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"},
"unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"},
@@ -77,4 +81,6 @@
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"xla": {:hex, :xla, "0.9.1", "cca0040ff94902764007a118871bfc667f1a0085d4a5074533a47d6b58bec61e", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb5e443ae5391b1953f253e051f2307bea183b59acee138053a9300779930daf"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"},
}

View File

@@ -1641,6 +1641,36 @@ defmodule BDS.AITest do
end)
end
test "delete_chat_conversation rolls back message deletion when the conversation delete fails" do
previous_hook = Application.get_env(:bds, :chat_delete_conversation_test_hook)
on_exit(fn ->
if is_nil(previous_hook) do
Application.delete_env(:bds, :chat_delete_conversation_test_hook)
else
Application.put_env(:bds, :chat_delete_conversation_test_hook, previous_hook)
end
end)
assert {:ok, conversation} = BDS.AI.start_chat(%{title: "Rollback Chat", model: "gpt-4o-mini"})
message =
Repo.insert!(%BDS.AI.ChatMessage{
conversation_id: conversation.id,
role: :user,
content: "keep me",
created_at: Persistence.now_ms()
})
Application.put_env(:bds, :chat_delete_conversation_test_hook, fn _conversation_id ->
{:error, :forced_failure}
end)
assert {:error, :forced_failure} = BDS.AI.delete_chat_conversation(conversation.id)
assert Repo.get(BDS.AI.ChatConversation, conversation.id)
assert Repo.get(BDS.AI.ChatMessage, message.id)
end
test "non-stat chat tools expose concrete project data" do
{:ok, project} = create_project_fixture("Concrete Tools")
%{post: post, media: media} = seed_project_content(project.id)

View File

@@ -224,6 +224,30 @@ defmodule BDS.ImportAnalysisTest do
assert_received {:analysis_progress, "Discovering macros...", nil}
end
import ExUnit.CaptureLog
test "analyze_wxr logs and continues when the progress callback raises", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")
wxr_path = Path.join(temp_dir, "legacy.xml")
File.write!(wxr_path, basic_wxr_xml())
log =
capture_log(fn ->
assert {:ok, _report} =
ImportAnalysis.analyze_wxr(project.id, wxr_path, uploads_dir,
on_progress: fn _step, _detail -> raise "boom" end
)
end)
assert log =~ "import analysis progress callback failed"
end
defp sha256(value) do
:sha256
|> :crypto.hash(value)

View File

@@ -112,6 +112,34 @@ defmodule BDS.ImportExecutionTest do
assert File.exists?(Path.join(temp_dir, imported_media.file_path))
end
import ExUnit.CaptureLog
test "execute_import logs and continues when the progress callback raises", %{
project: project,
temp_dir: temp_dir
} do
uploads_dir = Path.join(temp_dir, "uploads")
File.mkdir_p!(Path.join(uploads_dir, "2024/05"))
File.write!(Path.join(uploads_dir, "2024/05/import-asset.txt"), "legacy attachment")
wxr_path = Path.join(temp_dir, "legacy.xml")
File.write!(wxr_path, basic_wxr_xml())
assert {:ok, report} = ImportAnalysis.analyze_wxr(project.id, wxr_path, uploads_dir)
log =
capture_log(fn ->
assert {:ok, _result} =
ImportExecution.execute_import(project.id, report,
default_author: "Imported Author",
uploads_folder_path: uploads_dir,
on_progress: fn _phase, _current, _total, _detail -> raise "boom" end
)
end)
assert log =~ "import execution progress callback failed"
end
test "execute_import skips conflicts by default and can import them with a new slug", %{
project: project,
temp_dir: temp_dir

View File

@@ -0,0 +1,44 @@
defmodule BDS.MixProjectTest do
use ExUnit.Case, async: true
test "validate alias runs all required quality gates" do
aliases = BDS.MixProject.project()[:aliases]
assert aliases[:validate] == [
"test",
"credo --strict",
"deps.audit --ignore-file .mix_audit.ignore",
"dialyzer"
]
end
test "quality tooling deps are available in dev and test" do
deps = BDS.MixProject.project()[:deps]
assert quality_dep(deps, :credo) == {:credo, "~> 1.7", [only: [:dev, :test], runtime: false]}
assert quality_dep(deps, :mix_audit) ==
{:mix_audit, "~> 2.1", [only: [:dev, :test], runtime: false]}
end
test "mix audit exceptions stay explicit and scoped" do
ignore_file = File.read!(Path.expand("../../.mix_audit.ignore", __DIR__))
assert ignore_file =~ "GHSA-rhv4-8758-jx7v"
refute ignore_file =~ "GHSA-628h-q48j-jr6q"
end
test "validate runs in the test environment" do
cli = BDS.MixProject.cli()
assert cli[:preferred_envs][:validate] == :test
end
defp quality_dep(deps, app) do
Enum.find(deps, fn
{^app, _version} -> true
{^app, _version, _opts} -> true
_other -> false
end)
end
end

View File

@@ -0,0 +1,53 @@
defmodule BDS.PersistenceTest do
use ExUnit.Case, async: true
alias BDS.Persistence
alias BDS.Posts.TranslationValidation
test "atomic_write keeps concurrent writers intact" do
temp_dir = Path.join(System.tmp_dir!(), "bds-persistence-#{System.unique_integer([:positive])}")
path = Path.join(temp_dir, "shared.txt")
payload_a = String.duplicate("alpha", 200_000)
payload_b = String.duplicate("bravo", 200_000)
on_exit(fn -> File.rm_rf(temp_dir) end)
parent = self()
tasks =
for index <- 1..12 do
payload = if rem(index, 2) == 0, do: payload_a, else: payload_b
Task.async(fn ->
send(parent, :ready)
receive do
:go -> Persistence.atomic_write(path, payload)
end
end)
end
for _ <- tasks do
assert_receive :ready
end
Enum.each(tasks, fn task -> send(task.pid, :go) end)
assert Enum.map(tasks, &Task.await(&1, 15_000)) == List.duplicate(:ok, length(tasks))
assert File.read!(path) in [payload_a, payload_b]
end
test "post rebuild globs ignore atomic temp files" do
temp_dir = Path.join(System.tmp_dir!(), "bds-persistence-glob-#{System.unique_integer([:positive])}")
posts_dir = Path.join(temp_dir, "posts")
File.mkdir_p!(posts_dir)
File.write!(Path.join(posts_dir, "entry.md"), "---\ntitle: Entry\n---\n")
File.write!(Path.join(posts_dir, "entry.md.tmp.123"), "temp")
on_exit(fn -> File.rm_rf(temp_dir) end)
assert TranslationValidation.list_matching_files(posts_dir, "*.md") ==
[Path.join(posts_dir, "entry.md")]
end
end

View File

@@ -85,6 +85,32 @@ defmodule BDS.Repo.BootstrapTest do
assert repo_config[:busy_timeout] == 15_000
end
test "prod runtime repo config defaults to the same pool size as dev" do
config_path = Path.expand("../../../config/runtime.exs", __DIR__)
database_path = Path.join(System.tmp_dir!(), "bds-runtime-#{System.unique_integer([:positive])}.db")
previous_pool_size = System.get_env("POOL_SIZE")
previous_database_path = System.get_env("BDS_DATABASE_PATH")
System.delete_env("POOL_SIZE")
System.put_env("BDS_DATABASE_PATH", database_path)
on_exit(fn ->
restore_env("POOL_SIZE", previous_pool_size)
restore_env("BDS_DATABASE_PATH", previous_database_path)
File.rm_rf(Path.dirname(database_path))
end)
config = Config.Reader.read!(config_path, env: :prod)
repo_config =
config
|> Keyword.fetch!(:bds)
|> Keyword.fetch!(BDS.Repo)
assert repo_config[:pool_size] == 5
end
defmodule RepoConfigBackup do
def put_env do
Process.put({__MODULE__, :temp_repo_config}, Application.get_env(:bds, TempRepo))
@@ -97,4 +123,7 @@ defmodule BDS.Repo.BootstrapTest do
end
end
end
defp restore_env(key, nil), do: System.delete_env(key)
defp restore_env(key, value), do: System.put_env(key, value)
end