diff --git a/config/prod.exs b/config/prod.exs
index 75c8327..e5b8f9f 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -18,12 +18,20 @@ unless System.get_env("ELIXIR_TORRENT_DESKTOP_BUILD") == "1" do
]
end
-# Do not print debug messages in production
-config :logger,
- level: :info,
- compile_time_purge_matching: [
- [level_lower_than: :info]
- ]
+# Do not print debug messages in production.
+#
+# A diagnostic build (`ELIXIR_TORRENT_DEBUG_BUILD=1 mix mac.dmg`) keeps the
+# `Logger.debug` call sites instead of purging them at compile time, so the
+# level can still be raised back to `:info` over RPC without a rebuild.
+if System.get_env("ELIXIR_TORRENT_DEBUG_BUILD") == "1" do
+ config :logger, level: :debug
+else
+ config :logger,
+ level: :info,
+ compile_time_purge_matching: [
+ [level_lower_than: :info]
+ ]
+end
# Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs.
diff --git a/lib/elixir_torrent_web_ui/engine.ex b/lib/elixir_torrent_web_ui/engine.ex
index 6a36993..508b008 100644
--- a/lib/elixir_torrent_web_ui/engine.ex
+++ b/lib/elixir_torrent_web_ui/engine.ex
@@ -449,6 +449,20 @@ defmodule ElixirTorrentWebUI.Engine do
end)
end
+ # Past this horizon an ETA is noise, and `format_eta/1` renders a concrete figure
+ # for it — reported from the UI as a day count of order 1e39 on a torrent whose
+ # rate had decayed to ~1e-39 KB/s, since `left / (kbps * 1024)` has no upper
+ # bound. qBittorrent caps its own ETA at the same 100 days and shows ∞ beyond,
+ # for the same reason. The rate source was fixed separately (engine `PLAN.md`
+ # #53b); this is the guard that keeps *any* near-zero rate from printing a
+ # nonsense number.
+ @max_eta_seconds 8_640_000
+
+ @doc false
+ @spec compute_eta_for_test(String.t(), non_neg_integer(), number(), non_neg_integer()) ::
+ nil | :infinity | float()
+ def compute_eta_for_test(status, left, kbps, peers), do: compute_eta(status, left, kbps, peers)
+
@spec compute_eta(String.t(), non_neg_integer(), number(), non_neg_integer()) ::
nil | :infinity | float()
defp compute_eta("Seeding", _left, _kbps, _peers), do: nil
@@ -457,7 +471,10 @@ defmodule ElixirTorrentWebUI.Engine do
defp compute_eta(_status, _left, kbps, _peers) when kbps <= 0, do: :infinity
defp compute_eta(_status, left, kbps, _peers) do
- left / (kbps * 1024)
+ case left / (kbps * 1024) do
+ seconds when seconds > @max_eta_seconds -> :infinity
+ seconds -> seconds
+ end
end
@spec hash_from_hex_id(String.t()) :: {:ok, Torrent.hash()} | {:error, :invalid_torrent}
diff --git a/lib/elixir_torrent_web_ui_web/components/core_components.ex b/lib/elixir_torrent_web_ui_web/components/core_components.ex
index 2f1f727..170f245 100644
--- a/lib/elixir_torrent_web_ui_web/components/core_components.ex
+++ b/lib/elixir_torrent_web_ui_web/components/core_components.ex
@@ -69,7 +69,7 @@ defmodule ElixirTorrentWebUIWeb.CoreComponents do
]}>
<.icon :if={@kind == :info} name="hero-information-circle" class="size-5 shrink-0" />
<.icon :if={@kind == :error} name="hero-exclamation-circle" class="size-5 shrink-0" />
-
+
diff --git a/lib/elixir_torrent_web_ui_web/endpoint.ex b/lib/elixir_torrent_web_ui_web/endpoint.ex
index e1fd17b..547d374 100644
--- a/lib/elixir_torrent_web_ui_web/endpoint.ex
+++ b/lib/elixir_torrent_web_ui_web/endpoint.ex
@@ -44,7 +44,7 @@ defmodule ElixirTorrentWebUIWeb.Endpoint do
# The macOS launcher polls `GET /api/torrents` every two seconds to keep the
# Dock menu current. At the default `:info` that is two lines per poll —
# tens of megabytes of chatter in the packaged app's `server.log`. Routine
- # polls log at `:debug`; every other request keeps `:info`.
+ # polls are not logged at all; every other request keeps `:info`.
plug Plug.Telemetry,
event_prefix: [:phoenix, :endpoint],
log: {__MODULE__, :request_log_level, []}
@@ -62,10 +62,10 @@ defmodule ElixirTorrentWebUIWeb.Endpoint do
@doc """
Per-request log level for `Plug.Telemetry`.
- Only the launcher's Dock poll is demoted — `POST /api/torrents` adds a
+ Only the launcher's Dock poll is silenced — `POST /api/torrents` adds a
torrent and stays at `:info`, as does everything else.
"""
- @spec request_log_level(Plug.Conn.t()) :: Logger.level()
- def request_log_level(%Plug.Conn{method: "GET", path_info: ["api", "torrents"]}), do: :debug
+ @spec request_log_level(Plug.Conn.t()) :: Logger.level() | false
+ def request_log_level(%Plug.Conn{method: "GET", path_info: ["api", "torrents"]}), do: false
def request_log_level(%Plug.Conn{}), do: :info
end
diff --git a/mix.exs b/mix.exs
index 2271789..c23b68c 100644
--- a/mix.exs
+++ b/mix.exs
@@ -74,7 +74,7 @@ defmodule ElixirTorrentWebUI.MixProject do
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 1.0"},
{:jason, "~> 1.2"},
- {:dns_cluster, "~> 0.2.0"},
+ {:dns_cluster, "~> 0.3.0"},
{:bandit, "~> 1.5"}
]
end
@@ -86,7 +86,7 @@ defmodule ElixirTorrentWebUI.MixProject do
# ELIXIR_TORRENT_PATH=../ElixirTorrent mix phx.server
defp elixir_torrent_dep do
case System.get_env("ELIXIR_TORRENT_PATH") do
- nil -> {:elixir_torrent, "~> 0.6.5"}
+ nil -> {:elixir_torrent, "~> 0.6.6"}
path -> {:elixir_torrent, path: path}
end
end
diff --git a/mix.lock b/mix.lock
index 4049d81..9dc2c17 100644
--- a/mix.lock
+++ b/mix.lock
@@ -6,7 +6,7 @@
"certifi": {:hex, :certifi, "2.17.0", "835748414307e15e05b17d0e518190228ce648b08d569a5cc93a85a40f3e5c9b", [:rebar3], [], "hexpm", "8122798a17f0293c80daada25d0f81c7f4d708c73fef782c7c9b1950e26e4d21"},
"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"},
"dialyxir": {:hex, :dialyxir, "1.4.8", "7ef671a8aff9948b091d8c30f09467fbb16e77305cda451bce48109a0f5e021c", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "cbd5a851571e5dfeb32aaf2e840bfa98b7864cb3071bf2ef5d95d1276b12e072"},
- "dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
+ "dns_cluster": {:hex, :dns_cluster, "0.3.0", "5064e20cbde3b9a974671d85d996227d6dcc9e570b7a80ed464a2b01fc06a513", [:mix], [], "hexpm", "088428d2d128f8b278ad057d9167fcad221885a5129d0b246c42ed1ae7efc752"},
"elixir_make": {:hex, :elixir_make, "0.10.0", "16577e2583a79bb79237bbff349619ef5d80afffc07eac6e4faf0d00e2ddaf7d", [:mix], [], "hexpm", "dc1f09fb7fa68866b886abd5f0f3c83553b1a19a52359a899e92af1bb3b31982"},
"elixir_torrent": {:hex, :elixir_torrent, "0.6.6", "108fcc95b7003b11fb0d41064cdbc4492eb3a3660ec7c00c761df78e07080167", [:mix], [{:bento, "~> 1.0.0", [hex: :bento, repo: "hexpm", optional: false]}, {:httpoison, "~> 3.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:logger_file_backend, "~> 0.0.14", [hex: :logger_file_backend, repo: "hexpm", optional: false]}, {:recon, "~> 2.5.6", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "7a4719089c8e07d98932d941cd5b0fed9e1c2b6b055cef85918991b66a9dc0fb"},
"erlex": {:hex, :erlex, "0.2.9", "7debbbaa9f4f368b8cd648983e0f1d7963028508e9c59e9d4ed504e94ef52a55", [:mix], [], "hexpm", "8cfffc0ec7159e6d73de2ab28a588064de80f88b2798d5cbe4482cbbc200178b"},
diff --git a/priv/macos/src/AppDelegate+DockMenu.swift b/priv/macos/src/AppDelegate+DockMenu.swift
index 13966a2..6013de5 100644
--- a/priv/macos/src/AppDelegate+DockMenu.swift
+++ b/priv/macos/src/AppDelegate+DockMenu.swift
@@ -62,6 +62,7 @@ extension AppDelegate {
while !Task.isCancelled {
if let torrents = await Self.refreshDockTorrents(endpoint: torrentsEndpoint) {
dockTorrents = torrents
+ sleepPreventer.update(torrents: torrents)
}
try? await Task.sleep(nanoseconds: 2_000_000_000)
}
diff --git a/priv/macos/src/AppDelegate.swift b/priv/macos/src/AppDelegate.swift
index feeb0ab..d0e5dc2 100644
--- a/priv/macos/src/AppDelegate.swift
+++ b/priv/macos/src/AppDelegate.swift
@@ -15,6 +15,7 @@ final class AppDelegate: NSObject {
private let port = Int(ProcessInfo.processInfo.environment["PORT"] ?? "4000") ?? 4000
var dockTorrents: [DockTorrent] = []
var dockRefreshTask: Task
?
+ let sleepPreventer = SleepPreventer()
private var openedFromURL = false
private lazy var server = ServerLifecycle(
dataDirectory: dataDirectory,
@@ -201,6 +202,9 @@ extension AppDelegate: NSApplicationDelegate {
}
func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply {
+ dockRefreshTask?.cancel()
+ sleepPreventer.releaseForShutdown()
+
Task {
await server.shutdown(port: port)
NSApp.reply(toApplicationShouldTerminate: true)
diff --git a/priv/macos/src/ServerLifecycle.swift b/priv/macos/src/ServerLifecycle.swift
index f5c72d3..3f5e633 100644
--- a/priv/macos/src/ServerLifecycle.swift
+++ b/priv/macos/src/ServerLifecycle.swift
@@ -2,6 +2,8 @@ import Darwin
import Foundation
actor ServerLifecycle {
+ private static let maxServerLogBytes: UInt64 = 20 * 1024 * 1024
+
private var ownedProcess: Process?
private let dataDirectory: URL
private let releaseBinary: URL
@@ -170,6 +172,8 @@ actor ServerLifecycle {
private func openServerLog() -> FileHandle? {
let logURL = dataDirectory.appendingPathComponent("server.log")
+ rotateServerLogIfNeeded(at: logURL)
+
do {
if !FileManager.default.fileExists(atPath: logURL.path) {
FileManager.default.createFile(atPath: logURL.path, contents: nil)
@@ -184,6 +188,35 @@ actor ServerLifecycle {
}
}
+ /// The release appends to `server.log` for the life of the install, so
+ /// without this the file grows without bound. Rotating only at launch is
+ /// enough: the running server holds the descriptor, and renaming a file
+ /// out from under an open descriptor would leave the new one empty.
+ private func rotateServerLogIfNeeded(at logURL: URL) {
+ let manager = FileManager.default
+
+ guard
+ let attributes = try? manager.attributesOfItem(atPath: logURL.path),
+ let size = attributes[.size] as? UInt64,
+ size >= Self.maxServerLogBytes
+ else {
+ return
+ }
+
+ let rotatedURL = logURL.appendingPathExtension("1")
+
+ do {
+ if manager.fileExists(atPath: rotatedURL.path) {
+ try manager.removeItem(at: rotatedURL)
+ }
+
+ try manager.moveItem(at: logURL, to: rotatedURL)
+ launcherLog("Rotated server log at \(size) bytes to \(rotatedURL.lastPathComponent)")
+ } catch {
+ launcherLog("Could not rotate server log at \(logURL.path): \(error)")
+ }
+ }
+
private func listenerPIDs(on port: Int) -> [pid_t] {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/sbin/lsof")
diff --git a/priv/macos/src/SleepPreventer.swift b/priv/macos/src/SleepPreventer.swift
new file mode 100644
index 0000000..ba3f9a6
--- /dev/null
+++ b/priv/macos/src/SleepPreventer.swift
@@ -0,0 +1,106 @@
+import Foundation
+import IOKit.ps
+import IOKit.pwr_mgt
+
+/// Holds `kIOPMAssertPreventUserIdleSystemSleep` while a torrent still needs
+/// bytes and the machine is on AC power.
+///
+/// Without it the engine simply stops making progress unattended. Measured on
+/// 2026-09-12: macOS put this Mac into `'Maintenance Sleep':TCPKeepAlive=active`
+/// repeatedly overnight, waking only for 45-second DarkWake slices, so the node
+/// accumulated **64 minutes of awake time in 7 hours** and five incomplete
+/// torrents moved 19 MB between them. Sleeping is worse than just lost time:
+/// every peer TCP connection dies with it, so each wake has to re-dial the swarm
+/// from cold — the `[peer_dial] fail … reason=:etimedout` burst after each wake.
+///
+/// Two deliberate limits, both chosen by the owner:
+/// * **Seeding does not hold the assertion.** Once everything is complete the
+/// laptop is allowed to sleep; otherwise a finished queue would keep it
+/// awake indefinitely.
+/// * **Battery does not hold it either.** The assertion is dropped as soon as
+/// the charger comes out, so this can never flatten the battery.
+@MainActor
+final class SleepPreventer {
+ private var assertionID: IOPMAssertionID = 0
+ private var isHeld = false
+
+ /// Anything that is not seeding still needs the machine awake, which is why
+ /// this is `!= "Seeding"` rather than `== "Downloading"`. The engine derives
+ /// the status from the piece currently being fetched, so an incomplete
+ /// torrent reports `"Connecting"` or `"Idle"` whenever no piece is assigned
+ /// — and behind CGNAT, where a torrent runs on one to three peers, that is a
+ /// state it passes through constantly while hunting for somewhere to ask.
+ /// Letting the Mac sleep there would strand it exactly when re-dialling is
+ /// the only thing that can rescue it. This also matches the Dock menu, which
+ /// files every non-seeding torrent under "Downloading:".
+ ///
+ /// `downKbps` cannot be the trigger even though it reads like the natural
+ /// one: the API reports `0.0` for torrents that are demonstrably
+ /// progressing, because the underlying counter is piece-granular and the
+ /// sample window is shorter than one piece (engine `PLAN.md` open bug #53b).
+ private static func hasIncompleteTorrent(_ torrents: [DockTorrent]) -> Bool {
+ torrents.contains { $0.status != "Seeding" }
+ }
+
+ /// A machine with no battery (desktop) reports AC, which is what we want.
+ /// An unreadable power source is treated as AC too: the failure mode of
+ /// guessing wrong here is a laptop that stays awake, which is recoverable,
+ /// versus a download queue that silently never finishes, which is the bug
+ /// this class exists to fix.
+ private static func isOnACPower() -> Bool {
+ guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue(),
+ let source = IOPSGetProvidingPowerSourceType(snapshot)?.takeUnretainedValue()
+ else {
+ return true
+ }
+
+ return (source as String) == kIOPSACPowerValue
+ }
+
+ /// Re-evaluated on every Dock refresh tick (2 s), so the assertion follows
+ /// both the queue and the power source without a timer of its own.
+ func update(torrents: [DockTorrent]) {
+ if Self.hasIncompleteTorrent(torrents) && Self.isOnACPower() {
+ acquire()
+ } else {
+ release()
+ }
+ }
+
+ /// Released explicitly on quit — an assertion outlives the process that made
+ /// it only until the port closes, and leaving it to chance would leave the
+ /// Mac awake after the app is gone.
+ func releaseForShutdown() {
+ release()
+ }
+
+ private func acquire() {
+ guard !isHeld else { return }
+
+ var id: IOPMAssertionID = 0
+ let result = IOPMAssertionCreateWithName(
+ kIOPMAssertPreventUserIdleSystemSleep as CFString,
+ IOPMAssertionLevel(kIOPMAssertionLevelOn),
+ "ElixirTorrent is downloading" as CFString,
+ &id
+ )
+
+ guard result == kIOReturnSuccess else {
+ launcherLog("Could not create sleep assertion: IOReturn \(result)")
+ return
+ }
+
+ assertionID = id
+ isHeld = true
+ launcherLog("Holding sleep assertion: downloading on AC power")
+ }
+
+ private func release() {
+ guard isHeld else { return }
+
+ IOPMAssertionRelease(assertionID)
+ assertionID = 0
+ isHeld = false
+ launcherLog("Released sleep assertion")
+ }
+}
diff --git a/priv/scripts/macos/build-macos-dmg.sh b/priv/scripts/macos/build-macos-dmg.sh
index 3f1f764..e771818 100755
--- a/priv/scripts/macos/build-macos-dmg.sh
+++ b/priv/scripts/macos/build-macos-dmg.sh
@@ -60,6 +60,7 @@ echo "==> Compiling native Dock launcher…"
swiftc "$ROOT"/priv/macos/src/*.swift \
-o "$APP/Contents/MacOS/${EXECUTABLE_NAME}" \
-framework AppKit \
+ -framework IOKit \
-swift-version 6 \
-O
chmod +x "$APP/Contents/MacOS/${EXECUTABLE_NAME}"
diff --git a/test/elixir_torrent_web_ui/engine_eta_test.exs b/test/elixir_torrent_web_ui/engine_eta_test.exs
new file mode 100644
index 0000000..538a4a3
--- /dev/null
+++ b/test/elixir_torrent_web_ui/engine_eta_test.exs
@@ -0,0 +1,49 @@
+defmodule ElixirTorrentWebUI.EngineETATest do
+ use ExUnit.Case, async: true
+
+ alias ElixirTorrentWebUI.Engine
+
+ # `left / (kbps * 1024)` has no upper bound, and `format_eta/1` prints a concrete
+ # figure for whatever it gets. Reported from the UI as a day count of order 1e39
+ # on a torrent whose rate had decayed to ~1e-39 KB/s.
+ @max_eta_seconds 8_640_000
+ @left 462_422_016
+
+ describe "compute_eta/4" do
+ test "a near-zero rate reports infinity instead of an astronomical number" do
+ # The exact shape that was reported: a rate small enough that the quotient
+ # overflows any sane horizon.
+ assert Engine.compute_eta_for_test("Downloading", @left, 1.0e-39, 3) == :infinity
+ end
+
+ test "anything past the horizon is infinity" do
+ # Chosen so left/(kbps*1024) lands just above the cap.
+ kbps = @left / (@max_eta_seconds * 1024) * 0.99
+
+ assert Engine.compute_eta_for_test("Downloading", @left, kbps, 3) == :infinity
+ end
+
+ test "a realistic slow rate still reports a real estimate" do
+ # 10 KB/s on 462 MB is ~12.5 hours — slow, but a number the user can act on,
+ # so it must not be swallowed by the cap.
+ eta = Engine.compute_eta_for_test("Downloading", @left, 10.0, 3)
+
+ assert is_float(eta)
+ assert eta < @max_eta_seconds
+ assert_in_delta eta, @left / (10.0 * 1024), 1.0
+ end
+
+ test "a fast rate is unaffected" do
+ eta = Engine.compute_eta_for_test("Downloading", @left, 1000.0, 8)
+
+ assert_in_delta eta, @left / (1000.0 * 1024), 1.0
+ end
+
+ test "the pre-existing zero, no-peer, seeding and complete cases are unchanged" do
+ assert Engine.compute_eta_for_test("Downloading", @left, 0.0, 3) == :infinity
+ assert Engine.compute_eta_for_test("Downloading", @left, 50.0, 0) == :infinity
+ assert Engine.compute_eta_for_test("Seeding", 0, 0.0, 3) == nil
+ assert Engine.compute_eta_for_test("Downloading", 0, 50.0, 3) == nil
+ end
+ end
+end
diff --git a/test/elixir_torrent_web_ui_web/components/core_components_test.exs b/test/elixir_torrent_web_ui_web/components/core_components_test.exs
new file mode 100644
index 0000000..8ed6c1a
--- /dev/null
+++ b/test/elixir_torrent_web_ui_web/components/core_components_test.exs
@@ -0,0 +1,54 @@
+defmodule ElixirTorrentWebUIWeb.CoreComponentsTest do
+ use ElixirTorrentWebUIWeb.ConnCase, async: true
+
+ import Phoenix.LiveViewTest
+
+ alias ElixirTorrentWebUIWeb.CoreComponents
+
+ @unbreakable "Some.Release.Name.Without.Any.Spaces.2026.2160p.WEB-DL.DDP5.1.HDR.x265-GROUP"
+
+ setup do
+ ElixirTorrentWebUI.Locale.put("en")
+ :ok
+ end
+
+ for kind <- [:info, :error] do
+ test "#{kind} flash keeps an unbreakable long message inside the toast" do
+ kind = unquote(kind)
+
+ classes =
+ render_component(&CoreComponents.flash/1,
+ kind: kind,
+ flash: %{Atom.to_string(kind) => "Torrent added: #{@unbreakable}"}
+ )
+ |> message_container(kind)
+ |> LazyHTML.attribute("class")
+ |> List.first()
+ |> String.split()
+
+ assert "wrap-anywhere" in classes
+ assert "min-w-0" in classes
+ end
+ end
+
+ test "flash still renders the full message text" do
+ html =
+ render_component(&CoreComponents.flash/1,
+ kind: :info,
+ flash: %{"info" => "Torrent added: #{@unbreakable}"}
+ )
+
+ text =
+ html
+ |> message_container(:info)
+ |> LazyHTML.text()
+
+ assert text =~ @unbreakable
+ end
+
+ defp message_container(html, kind) do
+ html
+ |> LazyHTML.from_fragment()
+ |> LazyHTML.query("#flash-#{kind}-message")
+ end
+end
diff --git a/test/elixir_torrent_web_ui_web/endpoint_log_level_test.exs b/test/elixir_torrent_web_ui_web/endpoint_log_level_test.exs
index 176db36..4df303b 100644
--- a/test/elixir_torrent_web_ui_web/endpoint_log_level_test.exs
+++ b/test/elixir_torrent_web_ui_web/endpoint_log_level_test.exs
@@ -13,10 +13,10 @@ defmodule ElixirTorrentWebUIWeb.EndpointLogLevelTest do
:ok
end
- test "the launcher's Dock poll is demoted below the production log level" do
+ test "the launcher's Dock poll is not logged at any level" do
conn = %Plug.Conn{method: "GET", path_info: ["api", "torrents"]}
- assert Endpoint.request_log_level(conn) == :debug
+ assert Endpoint.request_log_level(conn) == false
end
test "requests that are not the Dock poll keep the default level" do
@@ -40,11 +40,11 @@ defmodule ElixirTorrentWebUIWeb.EndpointLogLevelTest do
refute logs =~ "Sent 200"
end
- test "the same poll is still visible when debugging", %{conn: conn} do
+ test "the same poll stays silent in a debug build", %{conn: conn} do
logs = capture_log([level: :debug], fn -> get(conn, ~p"/api/torrents") end)
- assert logs =~ "GET /api/torrents"
- assert logs =~ "Sent 200"
+ refute logs =~ "GET /api/torrents"
+ refute logs =~ "Sent 200"
end
test "ordinary requests still log at the production level", %{conn: conn} do
diff --git a/test/macos/launcher_integration_test.exs b/test/macos/launcher_integration_test.exs
index cb6019e..7205a2e 100644
--- a/test/macos/launcher_integration_test.exs
+++ b/test/macos/launcher_integration_test.exs
@@ -79,6 +79,8 @@ defmodule ElixirTorrentWebUI.MacOS.LauncherIntegrationTest do
binary,
"-framework",
"AppKit",
+ "-framework",
+ "IOKit",
"-swift-version",
"6",
"-O"