feat: headless server mode with ExRatatui SSH daemon (issue #26, phase 1)

This commit is contained in:
2026-07-14 19:17:06 +02:00
parent 6ec61610f1
commit 700ed79c43
15 changed files with 539 additions and 158 deletions

View File

@@ -127,7 +127,9 @@ defmodule BDS.Scripting.JobTest do
_running_job = wait_for_job(job.id, &(&1.status == :running))
assert :ok = BDS.Scripting.cancel_job(job.id)
assert_receive :job_cleanup_ran, 1_000
# Generous timeout: cleanup runs in a separate shutdown path and 1s
# flakes when the full suite loads the machine.
assert_receive :job_cleanup_ran, 5_000
cancelled_job = wait_for_job(job.id, &(&1.status == :cancelled))
assert cancelled_job.finished_at != nil

111
test/bds/server_test.exs Normal file
View File

@@ -0,0 +1,111 @@
defmodule BDS.ServerTest do
use ExUnit.Case, async: false
alias BDS.Server
describe "mode/1" do
test "defaults to :desktop" do
assert Server.mode(nil) == :desktop
assert Server.mode("") == :desktop
end
test "recognizes server mode" do
assert Server.mode("server") == :server
assert Server.mode("SERVER") == :server
end
test "unknown values fall back to :desktop" do
assert Server.mode("garbage") == :desktop
end
end
describe "ensure_ssh_dir!/1" do
setup do
dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}")
on_exit(fn -> File.rm_rf!(dir) end)
{:ok, dir: dir}
end
test "creates the directory and an empty authorized_keys file", %{dir: dir} do
assert Server.ensure_ssh_dir!(dir) == dir
assert File.dir?(dir)
keys_path = Path.join(dir, "authorized_keys")
assert File.exists?(keys_path)
assert File.read!(keys_path) == ""
%File.Stat{mode: mode} = File.stat!(keys_path)
assert Bitwise.band(mode, 0o777) == 0o600
end
test "leaves an existing authorized_keys file untouched", %{dir: dir} do
File.mkdir_p!(dir)
keys_path = Path.join(dir, "authorized_keys")
File.write!(keys_path, "ssh-ed25519 AAAA... user@host\n")
Server.ensure_ssh_dir!(dir)
assert File.read!(keys_path) == "ssh-ed25519 AAAA... user@host\n"
end
end
describe "daemon_opts/1" do
setup do
dir = Path.join(System.tmp_dir!(), "bds-ssh-test-#{System.unique_integer([:positive])}")
on_exit(fn -> File.rm_rf!(dir) end)
{:ok, dir: dir}
end
test "serves the TUI app over public-key auth only, with GUI tunneling", %{dir: dir} do
opts = Server.daemon_opts(dir)
assert opts[:mod] == BDS.TUI
assert opts[:auth_methods] == ~c"publickey"
assert opts[:tcpip_tunnel_out] == true
refute Keyword.has_key?(opts, :pwdfun)
assert opts[:system_dir] == String.to_charlist(dir)
assert opts[:user_dir] == String.to_charlist(dir)
end
test "generates a host key on first use and reuses it", %{dir: dir} do
Server.daemon_opts(dir)
key_path = Path.join(dir, "ssh_host_rsa_key")
assert File.exists?(key_path)
first = File.read!(key_path)
Server.daemon_opts(dir)
assert File.read!(key_path) == first
end
test "uses the configured SSH port", %{dir: dir} do
assert Server.daemon_opts(dir)[:port] == Application.get_env(:bds, :server)[:ssh_port]
end
end
describe "BDS.Application.mode_children/2" do
test "server mode starts endpoint, cli sync watcher, and the SSH daemon" do
children = BDS.Application.mode_children(:server, :prod)
assert {BDS.Desktop.Server, []} in children
assert BDS.CliSync.Watcher in children
# BDS.Server wraps ExRatatui.SSH.Daemon, deferring key generation to
# process start so building this list never touches the data dir.
assert BDS.Server in children
end
test "server mode starts no wx window children" do
children = BDS.Application.mode_children(:server, :prod)
refute Enum.any?(children, fn
{Desktop.Window, _} -> true
_other -> false
end)
end
test "desktop mode delegates to desktop_children" do
assert BDS.Application.mode_children(:desktop, :test) ==
BDS.Application.desktop_children(:test)
end
end
end