test(daemon): drive the whole CLI suite against a daemon, and fix what it caught

SOULSEEK_E2E_MODE=daemon makes the existing e2e harness serve every session
from a daemon instead of the command's own login, so the same 83 tests assert
the same records and exit codes both ways. CI runs the suite twice.

Two defects it found:

serve judged whether anything was shared from this run's configuration
rather than from the session, so routed to a daemon it would start with
nothing to serve and report success.

The TUI showed an empty conversation when attached, because the daemon's
message history was never asked for. It is now a SessionApi method, empty
locally, which puts the difference behind the seam.

main.rs is now a thin entry point over the library rather than a second copy
of every module, so the program compiles once instead of twice.
This commit is contained in:
Michel de Graaf
2026-07-27 10:58:27 +02:00
parent 834fbcd35b
commit a69f52c657
12 changed files with 663 additions and 578 deletions
+28 -9
View File
@@ -106,18 +106,27 @@ pub fn serve(
args: &ServeArgs,
mut sweeper: Option<crate::commands::wish::Sweeper>,
) -> CliResult {
if ctx.settings.shared_directories.is_empty() {
return Err(CliError::usage(
"nothing to share: pass --shared-dir, or add one with `shares add`",
));
}
if !ctx.settings.enable_listen {
return Err(CliError::usage(
"serving needs the listener: drop --no-listener",
));
// Cheap check first, so a misconfigured local run is rejected before it
// bothers the server with a login.
if ctx.daemon.is_none() {
if ctx.settings.shared_directories.is_empty() {
return Err(nothing_to_share());
}
if !ctx.settings.enable_listen {
return Err(needs_a_listener());
}
}
let session = Session::open(ctx)?;
// What the session actually offers is what decides. Routed to a daemon
// that is what the daemon shares, which is the only thing peers can see —
// this run's own configuration has no bearing on it.
if session.client.shared_directories().is_empty() {
return Err(nothing_to_share());
}
if session.client.listen_port().is_none() {
return Err(needs_a_listener());
}
if let Some(slots) = args.upload_slots {
session.client.set_upload_slots(usize::from(slots));
ctx.out.status(&format!("{slots} upload slots"));
@@ -169,6 +178,16 @@ pub fn serve(
Ok(())
}
fn nothing_to_share() -> CliError {
CliError::usage(
"nothing to share: pass --shared-dir, or add one with `shares add`",
)
}
fn needs_a_listener() -> CliError {
CliError::usage("serving needs the listener: drop --no-listener")
}
/// Flatten a live upload into the record `serve` reports.
fn upload_record(upload: &UploadInfo) -> UploadRecord {
let (status, reason) = match &upload.status {