test(daemon): route the suite at its chokepoint and cover the protocol directly

Only 25 of the 83 CLI e2e tests actually reached a daemon: the helpers that
build a command bypassed the one place that routed. Routing at
TestServer::args instead brings that to 39 and, more importantly, puts the
streaming tests through a daemon — event.room and event.message delivery to a
remote client's mirror had no end-to-end coverage at all.

Adds what the command surface structurally cannot reach:

- Two clients against one daemon at once, which is the reason the hub exists
  and was proven nowhere in CI.
- tests/daemon_protocol_e2e.rs, speaking the socket by hand: TCP token
  required and socket token not, pre-auth refusal, error ids, notification
  silence, the line cap, the handshake timeout, and that a call shaped by the
  published contract is accepted.
- daemon status/stop/token as commands.
- A TUI unit test for conversation seeding, the part of the attach path that
  had actually been broken.

The protocol suite caught a regression in yesterday's hardening on its first
run: the socket was shut down before the writer flushed a failed handshake's
refusal, so a client with a wrong token saw the connection vanish instead of
being told why.
This commit is contained in:
Michel de Graaf
2026-07-27 12:30:41 +02:00
parent 41307d0510
commit f985850c06
5 changed files with 917 additions and 13 deletions
+6 -2
View File
@@ -181,9 +181,13 @@ fn handle(stream: Stream, daemon: &Daemon) -> std::io::Result<()> {
let writer = std::thread::spawn(move || write_loop(stream, &outbox));
if !authenticate(&mut reader, daemon, trusted, &out) {
closer.shutdown();
// Let the writer flush the refusal before closing: shutting the socket
// first discards the reply, and the caller learns only that the
// connection vanished — which is indistinguishable from the daemon
// having crashed.
drop(out);
let _ = writer.join();
closer.shutdown();
return Ok(());
}
// An established client is expected to idle, holding the connection open
@@ -237,10 +241,10 @@ fn handle(stream: Stream, daemon: &Daemon) -> std::io::Result<()> {
}
daemon.hub.unsubscribe(subscription);
closer.shutdown();
drop(out);
let _ = pump.join();
let _ = writer.join();
closer.shutdown();
Ok(())
}